Răsfoiți Sursa

✨ feat(talent-detail): 新增人才详情页面组件

- 新增 TalentDetail.tsx 组件,实现企业专用人才详情页面
- 新增 TalentDetail.css 样式文件,包含渐变背景、头像颜色、卡片样式等
- 集成企业专用 API 接口获取人才基本信息、工作历史、薪资历史和征信信息
- 实现响应式布局和加载状态处理,包含骨架屏动画
- 在 index.ts 中导出 TalentDetail 组件及其类型定义
yourname 1 lună în urmă
părinte
comite
4f62d197e3

+ 78 - 0
mini-ui-packages/yongren-talent-management-ui/src/TalentDetail.css

@@ -0,0 +1,78 @@
+/* 人才详情页样式 */
+
+/* 渐变背景 */
+.gradient-bg {
+  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
+}
+
+/* 头像颜色类 */
+.name-avatar.blue {
+  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
+}
+.name-avatar.green {
+  background: linear-gradient(135deg, #10b981, #059669);
+}
+.name-avatar.purple {
+  background: linear-gradient(135deg, #8b5cf6, #7c3aed);
+}
+.name-avatar.orange {
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+}
+.name-avatar.red {
+  background: linear-gradient(135deg, #ef4444, #dc2626);
+}
+.name-avatar.teal {
+  background: linear-gradient(135deg, #14b8a6, #0d9488);
+}
+
+/* 卡片样式 */
+.card {
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+/* 加载动画 */
+@keyframes pulse-bg {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
+}
+
+.animate-pulse {
+  animation: pulse-bg 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+/* 文件项样式 */
+.file-item {
+  transition: background-color 0.2s ease;
+}
+
+.file-item:active {
+  background-color: rgb(243 244 246);
+}
+
+/* 状态标签 */
+.status-badge {
+  font-size: 12px;
+  padding: 2px 8px;
+  border-radius: 9999px;
+  display: inline-block;
+}
+
+.status-badge.in-service {
+  background-color: rgb(220 252 231);
+  color: rgb(21 128 61);
+}
+
+.status-badge.pending {
+  background-color: rgb(254 249 195);
+  color: rgb(133 77 14);
+}
+
+.status-badge.left {
+  background-color: rgb(243 244 246);
+  color: rgb(55 65 81);
+}

+ 551 - 0
mini-ui-packages/yongren-talent-management-ui/src/TalentDetail.tsx

@@ -0,0 +1,551 @@
+import React, { useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useQuery } from '@tanstack/react-query'
+import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui'
+import { PageContainer } from '@d8d/mini-shared-ui-components'
+import { enterpriseDisabilityClient } from './api'
+import { useRequireAuth } from '@d8d/mini-enterprise-auth-ui'
+import './TalentDetail.css'
+
+export interface TalentDetailProps {
+  // 组件属性定义(目前为空)
+}
+
+// 类型定义 - 匹配企业专用人才详情API的CompanyPersonDetailSchema
+interface TalentDetailData {
+  personId: number
+  name: string
+  gender: string
+  idCard: string
+  disabilityType: string
+  disabilityLevel: string
+  birthDate?: string
+  phone?: string
+  jobStatus: string
+  bankCards: Array<{
+    cardId: number
+    bankName: string
+    cardNumber: string
+    isDefault: boolean
+  }>
+  photos: Array<{
+    fileId: number
+    fileName: string
+    fileUrl: string
+  }>
+  // 兼容字段
+  id?: number  // 兼容旧代码,映射personId
+  status?: string  // 兼容旧代码,映射jobStatus
+  age?: number  // 根据birthDate计算
+  idAddress?: string  // 可能来自其他API
+  province?: string  // 可能来自其他API
+  city?: string  // 可能来自其他API
+  joinDate?: string  // 可能来自工作信息API
+  salary?: number  // 可能来自薪资信息API
+  companyId?: number  // 可能来自其他API
+  disabilityId?: string  // 可能来自其他API
+  specificDisability?: string  // 可能来自其他API
+  [key: string]: any
+}
+
+interface WorkInfoData {
+  id: number
+  orderId?: number
+  position?: string
+  department?: string
+  startDate?: string
+  endDate?: string
+  status?: string
+  companyId?: number
+  [key: string]: any
+}
+
+interface SalaryData {
+  id: number
+  personId: number
+  amount?: number
+  paymentDate?: string
+  period?: string
+  type?: string
+  notes?: string
+  [key: string]: any
+}
+
+interface FileData {
+  id: string
+  name: string
+  url?: string
+  size?: number
+  type?: string
+  createdAt?: string
+  [key: string]: any
+}
+
+// 企业专用API响应类型
+interface WorkHistoryItem {
+  订单ID: number
+  订单名称: string | null
+  入职日期: string | null
+  实际入职日期: string | null
+  离职日期: string | null
+  工作状态: string
+  个人薪资: number
+}
+
+interface WorkHistoryResponse {
+  工作历史: WorkHistoryItem[]
+}
+
+interface SalaryHistoryItem {
+  月份: string | null
+  基本工资: number
+  补贴: number
+  扣款: number
+  实发工资: number
+}
+
+interface SalaryHistoryResponse {
+  薪资历史: SalaryHistoryItem[]
+}
+
+interface CreditInfoItem {
+  文件ID: string
+  文件URL: string | null
+  上传时间: string | null
+  文件类型: string | null
+  银行卡号: string | null
+  持卡人姓名: string | null
+  银行名称: number | null
+}
+
+interface CreditInfoResponse {
+  征信信息: CreditInfoItem[]
+}
+
+const TalentDetail: React.FC<TalentDetailProps> = () => {
+  const { isLoggedIn } = useRequireAuth()
+  const router = Taro.useRouter()
+  const talentId = router.params.id ? parseInt(router.params.id) : 0
+
+  // 获取人才基本信息
+  const { data: talentDetail, isLoading: talentLoading, error: talentError } = useQuery({
+    queryKey: ['talentDetail', talentId],
+    queryFn: async () => {
+      if (!talentId) throw new Error('无效的人才ID')
+      const response = await enterpriseDisabilityClient[':id'].$get({
+        param: { id: talentId.toString() }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取人才详情失败')
+      }
+      const data = await response.json() as TalentDetailData
+      // 添加兼容字段映射
+      return {
+        ...data,
+        id: data.personId,  // 映射id字段
+        status: data.jobStatus,  // 映射status字段
+        // 计算年龄
+        age: data.birthDate ? Math.floor((Date.now() - new Date(data.birthDate).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) : undefined
+      }
+    },
+    enabled: isLoggedIn && talentId > 0
+  })
+
+  // 获取工作信息 - 使用企业专用工作历史API
+  const { data: workInfo, isLoading: workLoading } = useQuery({
+    queryKey: ['workInfo', talentId],
+    queryFn: async () => {
+      if (!talentId) throw new Error('无效的人才ID')
+      // 使用企业专用工作历史API:/api/v1/yongren/disability-person/{id}/work-history
+      const response = await enterpriseDisabilityClient[':id']['work-history'].$get({
+        param: { id: talentId.toString() }
+      })
+      if (response.status !== 200) {
+        // 可能没有工作信息,返回空对象
+        return {} as WorkInfoData
+      }
+      const data = await response.json() as WorkHistoryResponse
+      // 企业专用工作历史API返回的是工作历史列表,取最新的一条作为当前工作信息
+      const workHistory = data?.工作历史 || []
+      if (workHistory.length === 0) {
+        return {} as WorkInfoData
+      }
+      // 取最新的一条工作记录(按入职日期降序)
+      const latestWork = workHistory[0]
+      return {
+        id: latestWork.订单ID || talentId,
+        orderId: latestWork.订单ID,
+        position: latestWork.订单名称 || undefined,
+        department: undefined, // 企业专用API没有部门字段
+        startDate: latestWork.入职日期 || undefined,
+        endDate: latestWork.离职日期 || undefined,
+        status: latestWork.工作状态,
+        companyId: undefined // 企业专用API没有公司ID字段
+      } as WorkInfoData
+    },
+    enabled: isLoggedIn && talentId > 0
+  })
+
+  // 获取薪资信息 - 使用企业专用薪资历史API
+  const { data: salaryInfo, isLoading: salaryLoading } = useQuery({
+    queryKey: ['salaryInfo', talentId],
+    queryFn: async () => {
+      if (!talentId) throw new Error('无效的人才ID')
+      // 使用企业专用薪资历史API:/api/v1/yongren/disability-person/{id}/salary-history
+      const response = await enterpriseDisabilityClient[':id']['salary-history'].$get({
+        param: { id: talentId.toString() }
+      })
+      if (response.status !== 200) {
+        // 可能没有薪资信息,返回空对象
+        return {} as SalaryData
+      }
+      const data = await response.json() as SalaryHistoryResponse
+      // 企业专用薪资历史API返回结构:{ 薪资历史: [...] }
+      const salaryHistory = data?.薪资历史 || []
+      if (salaryHistory.length === 0) {
+        return {} as SalaryData
+      }
+      // 取最新的一条薪资记录(按月份降序)
+      const latestSalary = salaryHistory[0]
+      return {
+        id: talentId,
+        personId: talentId,
+        amount: latestSalary.实发工资 || latestSalary.基本工资,
+        paymentDate: latestSalary.月份 || undefined,
+        type: '月薪', // 默认类型
+        period: '月度' // 默认周期
+      } as SalaryData
+    },
+    enabled: isLoggedIn && talentId > 0
+  })
+
+  // 获取薪资历史记录 - 使用企业专用薪资历史API
+  const { data: salaryHistory, isLoading: historyLoading } = useQuery({
+    queryKey: ['salaryHistory', talentId],
+    queryFn: async () => {
+      if (!talentId) throw new Error('无效的人才ID')
+      // 使用企业专用薪资历史API:/api/v1/yongren/disability-person/{id}/salary-history
+      const response = await enterpriseDisabilityClient[':id']['salary-history'].$get({
+        param: { id: talentId.toString() }
+      })
+      if (response.status !== 200) {
+        return [] as SalaryData[]
+      }
+      const data = await response.json() as SalaryHistoryResponse
+      // 企业专用薪资历史API返回结构:{ 薪资历史: [...] }
+      const salaryHistoryData = data?.薪资历史 || []
+      // 转换为SalaryData数组
+      return salaryHistoryData.map((item: SalaryHistoryItem, index: number) => ({
+        id: index + 1,
+        personId: talentId,
+        amount: item.实发工资 || item.基本工资,
+        paymentDate: item.月份 || undefined,
+        type: '月薪', // 默认类型
+        period: '月度' // 默认周期
+      })) as SalaryData[]
+    },
+    enabled: isLoggedIn && talentId > 0
+  })
+
+  // 获取个人征信文件 - 使用企业专用征信信息API
+  const { data: creditFiles, isLoading: filesLoading } = useQuery({
+    queryKey: ['creditFiles', talentId],
+    queryFn: async () => {
+      if (!talentId) throw new Error('无效的人才ID')
+      // 使用企业专用征信信息API:/api/v1/yongren/disability-person/{id}/credit-info
+      const response = await enterpriseDisabilityClient[':id']['credit-info'].$get({
+        param: { id: talentId.toString() }
+      })
+      if (response.status !== 200) {
+        return [] as FileData[]
+      }
+      const data = await response.json() as CreditInfoResponse
+      // 企业专用征信信息API返回结构:{ 征信信息: [...] }
+      const creditInfoList = data?.征信信息 || []
+      // 转换为FileData数组
+      return creditInfoList.map((item: CreditInfoItem) => ({
+        id: item.文件ID || '',
+        name: item.银行卡号 ? `银行卡 ${item.银行卡号}` : item.持卡人姓名 ? `征信文件 - ${item.持卡人姓名}` : '征信文件',
+        url: item.文件URL || undefined,
+        size: undefined, // 征信信息API不返回文件大小
+        type: item.文件类型 || undefined,
+        createdAt: item.上传时间 || undefined
+      })) as FileData[]
+    },
+    enabled: isLoggedIn && talentId > 0
+  })
+
+  // 页面加载时设置标题
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '人才详情'
+    })
+  }, [])
+
+  const isLoading = talentLoading || workLoading || salaryLoading || filesLoading || historyLoading
+  const hasError = talentError
+
+  // 获取头像颜色
+  const getAvatarColor = (id: number) => {
+    const colors = ['blue', 'green', 'purple', 'orange', 'red', 'teal']
+    const index = id % colors.length
+    return colors[index]
+  }
+
+  // 格式化日期
+  const formatDate = (dateStr?: string) => {
+    if (!dateStr) return '未指定'
+    return dateStr.split('T')[0]
+  }
+
+  // 格式化金额
+  const formatCurrency = (amount?: number) => {
+    if (!amount) return '¥0'
+    return `¥${amount.toLocaleString()}`
+  }
+
+  return (
+    <YongrenTabBarLayout activeKey="talent">
+      <PageContainer padding={false} className="pb-0">
+        <ScrollView
+          className="h-[calc(100vh-120px)] overflow-y-auto"
+          scrollY
+        >
+          {isLoading ? (
+            // 加载状态
+            <View className="p-4 space-y-4">
+              {[1, 2, 3].map((i) => (
+                <View key={i} className="bg-white p-4 rounded-lg animate-pulse">
+                  <View className="h-6 bg-gray-200 rounded w-1/3 mb-3" />
+                  <View className="space-y-2">
+                    <View className="h-4 bg-gray-200 rounded w-full" />
+                    <View className="h-4 bg-gray-200 rounded w-2/3" />
+                  </View>
+                </View>
+              ))}
+            </View>
+          ) : hasError ? (
+            // 错误状态
+            <View className="p-4">
+              <View className="bg-white p-4 rounded-lg text-center">
+                <Text className="text-red-500 text-sm">加载失败: {(talentError as Error).message}</Text>
+                <Text className="text-gray-400 text-xs mt-1">请返回重试</Text>
+              </View>
+            </View>
+          ) : talentDetail ? (
+            <>
+              {/* 顶部信息区域 - 对照原型第576-605行 */}
+              <View className="gradient-bg text-white p-5">
+                <View className="flex justify-between items-start">
+                  <View className="flex items-center">
+                    <View className={`name-avatar ${getAvatarColor(talentDetail.id)} w-16 h-16 rounded-full border-2 border-white mr-4 flex items-center justify-center`}>
+                      <Text className="text-white text-2xl font-bold">
+                        {talentDetail.name.charAt(0)}
+                      </Text>
+                    </View>
+                    <View>
+                      <Text className="text-xl font-bold">{talentDetail.name}</Text>
+                      <Text className="text-sm opacity-80">
+                        {talentDetail.disabilityType || '未指定'} · {talentDetail.disabilityLevel || '未分级'} · {talentDetail.status || '未知'}
+                      </Text>
+                    </View>
+                  </View>
+                  <View className="bg-white/20 rounded-full p-2">
+                    <Text className="i-heroicons-ellipsis-vertical-20-solid text-white" />
+                  </View>
+                </View>
+                <View className="mt-4 flex justify-between">
+                  <View className="text-center">
+                    <Text className="text-2xl font-bold">{formatCurrency(talentDetail.salary)}</Text>
+                    <Text className="text-xs opacity-80">当前薪资</Text>
+                  </View>
+                  <View className="text-center">
+                    <Text className="text-2xl font-bold">
+                      {talentDetail.joinDate ? Math.floor((Date.now() - new Date(talentDetail.joinDate).getTime()) / (1000 * 60 * 60 * 24)) : 0}
+                    </Text>
+                    <Text className="text-xs opacity-80">在职天数</Text>
+                  </View>
+                  <View className="text-center">
+                    <Text className="text-2xl font-bold">98%</Text>
+                    <Text className="text-xs opacity-80">出勤率</Text>
+                  </View>
+                </View>
+              </View>
+
+              {/* 详细信息区域 - 对照原型第608-864行 */}
+              <View className="p-4">
+                {/* 基本信息卡片 */}
+                <View className="card bg-white p-4 mb-4">
+                  <Text className="font-semibold text-gray-700 mb-3">基本信息</Text>
+                  <View className="grid grid-cols-2 gap-3 text-sm">
+                    <View>
+                      <Text className="text-gray-500">性别</Text>
+                      <Text className="text-gray-800">{talentDetail.gender || '未指定'}</Text>
+                    </View>
+                    <View>
+                      <Text className="text-gray-500">年龄</Text>
+                      <Text className="text-gray-800">{talentDetail.age || '未知'}岁</Text>
+                    </View>
+                    <View>
+                      <Text className="text-gray-500">身份证号</Text>
+                      <Text className="text-gray-800">{talentDetail.idCard || '未提供'}</Text>
+                    </View>
+                    <View>
+                      <Text className="text-gray-500">残疾证号</Text>
+                      <Text className="text-gray-800">{talentDetail.disabilityId || '未提供'}</Text>
+                    </View>
+                    <View className="col-span-2">
+                      <Text className="text-gray-500">联系地址</Text>
+                      <Text className="text-gray-800">{talentDetail.idAddress || '未提供'}</Text>
+                    </View>
+                    <View className="col-span-2">
+                      <Text className="text-gray-500">联系电话</Text>
+                      <Text className="text-gray-800">{talentDetail.phone || '未提供'}</Text>
+                    </View>
+                  </View>
+                </View>
+
+                {/* 工作信息卡片 */}
+                <View className="card bg-white p-4 mb-4">
+                  <Text className="font-semibold text-gray-700 mb-3">工作信息</Text>
+                  <View className="space-y-3 text-sm">
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">入职日期</Text>
+                      <Text className="text-gray-800">{formatDate(talentDetail.joinDate)}</Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">工作状态</Text>
+                      <Text className={`text-xs px-2 py-1 rounded-full ${
+                        talentDetail.status === '在职'
+                          ? 'bg-green-100 text-green-800'
+                          : talentDetail.status === '待入职'
+                          ? 'bg-yellow-100 text-yellow-800'
+                          : 'bg-gray-100 text-gray-800'
+                      }`}>
+                        {talentDetail.status || '未知'}
+                      </Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">岗位类型</Text>
+                      <Text className="text-gray-800">{workInfo?.position || '未指定'}</Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">所属订单</Text>
+                      <Text className="text-gray-800">{workInfo?.orderId ? `订单 #${workInfo.orderId}` : '无'}</Text>
+                    </View>
+                  </View>
+                </View>
+
+                {/* 薪资信息卡片 */}
+                <View className="card bg-white p-4 mb-4">
+                  <Text className="font-semibold text-gray-700 mb-3">薪资信息</Text>
+                  <View className="space-y-3 text-sm">
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">当前薪资</Text>
+                      <Text className="text-gray-800 font-semibold">{formatCurrency(talentDetail.salary)}</Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">薪资结构</Text>
+                      <Text className="text-gray-800">{salaryInfo?.type || '月薪'}</Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">发薪日</Text>
+                      <Text className="text-gray-800">{salaryInfo?.paymentDate ? formatDate(salaryInfo.paymentDate) : '每月底'}</Text>
+                    </View>
+                    <View className="flex justify-between">
+                      <Text className="text-gray-500">薪资周期</Text>
+                      <Text className="text-gray-800">{salaryInfo?.period || '月度'}</Text>
+                    </View>
+                  </View>
+                </View>
+
+                {/* 薪资历史记录卡片 */}
+                <View className="card bg-white p-4 mb-4">
+                  <Text className="font-semibold text-gray-700 mb-3">薪资历史记录</Text>
+                  {historyLoading ? (
+                    <View className="space-y-2">
+                      {[1, 2, 3].map((i) => (
+                        <View key={i} className="h-10 bg-gray-200 rounded animate-pulse" />
+                      ))}
+                    </View>
+                  ) : salaryHistory && salaryHistory.length > 0 ? (
+                    <View className="space-y-3">
+                      <View className="grid grid-cols-4 gap-2 text-xs text-gray-500 font-medium pb-2 border-b border-gray-200">
+                        <Text>日期</Text>
+                        <Text>薪资</Text>
+                        <Text>类型</Text>
+                        <Text>周期</Text>
+                      </View>
+                      {salaryHistory.slice(0, 5).map((record, index) => (
+                        <View key={index} className="grid grid-cols-4 gap-2 text-sm">
+                          <Text className="text-gray-800">{formatDate(record.paymentDate)}</Text>
+                          <Text className="text-gray-800 font-medium">{formatCurrency(record.amount)}</Text>
+                          <Text className="text-gray-800">{record.type || '月薪'}</Text>
+                          <Text className="text-gray-800">{record.period || '月度'}</Text>
+                        </View>
+                      ))}
+                      {salaryHistory.length > 5 && (
+                        <Text className="text-center text-xs text-blue-500 mt-2">
+                          查看更多 ({salaryHistory.length - 5} 条记录)
+                        </Text>
+                      )}
+                    </View>
+                  ) : (
+                    <View className="text-center py-4">
+                      <Text className="text-gray-400 text-sm">暂无薪资历史记录</Text>
+                    </View>
+                  )}
+                </View>
+
+                {/* 个人征信文件区域 */}
+                <View className="card bg-white p-4">
+                  <Text className="font-semibold text-gray-700 mb-3">个人征信文件</Text>
+                  {filesLoading ? (
+                    <View className="space-y-2">
+                      {[1, 2].map((i) => (
+                        <View key={i} className="h-10 bg-gray-200 rounded animate-pulse" />
+                      ))}
+                    </View>
+                  ) : creditFiles && creditFiles.length > 0 ? (
+                    <View className="space-y-3">
+                      {creditFiles.map((file) => (
+                        <View key={file.id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
+                          <View className="flex items-center">
+                            <Text className="i-heroicons-document-text-20-solid text-gray-400 mr-2" />
+                            <View>
+                              <Text className="text-sm text-gray-800">{file.name}</Text>
+                              <Text className="text-xs text-gray-500">
+                                {file.size ? `${(file.size / 1024).toFixed(1)} KB` : '大小未知'} · {formatDate(file.createdAt)}
+                              </Text>
+                            </View>
+                          </View>
+                          <View className="flex space-x-2">
+                            <Text className="i-heroicons-eye-20-solid text-blue-500" />
+                            <Text className="i-heroicons-arrow-down-tray-20-solid text-green-500" />
+                          </View>
+                        </View>
+                      ))}
+                    </View>
+                  ) : (
+                    <View className="text-center py-4">
+                      <Text className="text-gray-400 text-sm">暂无征信文件</Text>
+                    </View>
+                  )}
+                </View>
+              </View>
+            </>
+          ) : (
+            <View className="p-4">
+              <View className="bg-white p-4 rounded-lg text-center">
+                <Text className="text-gray-500 text-sm">未找到人才信息</Text>
+              </View>
+            </View>
+          )}
+        </ScrollView>
+      </PageContainer>
+    </YongrenTabBarLayout>
+  )
+}
+
+export default TalentDetail

+ 3 - 1
mini-ui-packages/yongren-talent-management-ui/src/index.ts

@@ -1,2 +1,4 @@
 export { default as TalentManagement } from './TalentManagement'
-export type { TalentManagementProps } from './TalentManagement'
+export { default as TalentDetail } from './TalentDetail'
+export type { TalentManagementProps } from './TalentManagement'
+export type { TalentDetailProps } from './TalentDetail'