|
|
@@ -1,14 +1,440 @@
|
|
|
-import React from 'react'
|
|
|
-import { View, Text } from '@tarojs/components'
|
|
|
+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 '@/layouts/yongren-tab-bar-layout'
|
|
|
+import PageContainer from '@/components/ui/page-container'
|
|
|
+import { disabilityClient, orderClient, salaryClient, fileClient } from '@/api'
|
|
|
+import { useRequireAuth } from '@/hooks/useRequireAuth'
|
|
|
+import './index.css'
|
|
|
+
|
|
|
+// 类型定义
|
|
|
+interface TalentDetailData {
|
|
|
+ id: number
|
|
|
+ name: string
|
|
|
+ gender: string
|
|
|
+ idCard?: string
|
|
|
+ disabilityId?: string
|
|
|
+ disabilityType?: string
|
|
|
+ disabilityLevel?: string
|
|
|
+ idAddress?: string
|
|
|
+ phone?: string
|
|
|
+ province?: string
|
|
|
+ city?: string
|
|
|
+ age?: number
|
|
|
+ status?: '在职' | '待入职' | '离职'
|
|
|
+ joinDate?: string
|
|
|
+ salary?: number
|
|
|
+ companyId?: number
|
|
|
+ birthDate?: string
|
|
|
+ specificDisability?: string
|
|
|
+ [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
|
|
|
+}
|
|
|
|
|
|
const YongrenTalentDetailPage: React.FC = () => {
|
|
|
+ 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 disabilityClient['{id}'].$get({
|
|
|
+ param: { id: talentId.toString() }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ throw new Error('获取人才详情失败')
|
|
|
+ }
|
|
|
+ const data = await response.json() as TalentDetailData
|
|
|
+ return data
|
|
|
+ },
|
|
|
+ enabled: isLoggedIn && talentId > 0
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取工作信息
|
|
|
+ const { data: workInfo, isLoading: workLoading } = useQuery({
|
|
|
+ queryKey: ['workInfo', talentId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!talentId) throw new Error('无效的人才ID')
|
|
|
+ // 根据API规范,路径为:/api/v1/order/person/{person_id}
|
|
|
+ const response = await orderClient['person/{person_id}'].$get({
|
|
|
+ param: { person_id: talentId.toString() }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ // 可能没有工作信息,返回空对象
|
|
|
+ return {} as WorkInfoData
|
|
|
+ }
|
|
|
+ const data = await response.json() as WorkInfoData
|
|
|
+ return data
|
|
|
+ },
|
|
|
+ enabled: isLoggedIn && talentId > 0
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取薪资信息
|
|
|
+ const { data: salaryInfo, isLoading: salaryLoading } = useQuery({
|
|
|
+ queryKey: ['salaryInfo', talentId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!talentId) throw new Error('无效的人才ID')
|
|
|
+ // 根据API规范,路径为:/api/v1/salary/person/{person_id}
|
|
|
+ const response = await salaryClient['person/{person_id}'].$get({
|
|
|
+ param: { person_id: talentId.toString() }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ // 可能没有薪资信息,返回空对象
|
|
|
+ return {} as SalaryData
|
|
|
+ }
|
|
|
+ const data = await response.json() as SalaryData
|
|
|
+ return data
|
|
|
+ },
|
|
|
+ enabled: isLoggedIn && talentId > 0
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取薪资历史记录
|
|
|
+ const { data: salaryHistory, isLoading: historyLoading } = useQuery({
|
|
|
+ queryKey: ['salaryHistory', talentId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!talentId) throw new Error('无效的人才ID')
|
|
|
+ // 根据API规范,路径为:/api/v1/salary/history/{person_id}
|
|
|
+ const response = await salaryClient['history/{person_id}'].$get({
|
|
|
+ param: { person_id: talentId.toString() },
|
|
|
+ query: {
|
|
|
+ start_date: '2024-01-01', // 默认开始日期
|
|
|
+ end_date: new Date().toISOString().split('T')[0] // 今天
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ return [] as SalaryData[]
|
|
|
+ }
|
|
|
+ const result = await response.json() as any
|
|
|
+ return (result?.data || []) as SalaryData[]
|
|
|
+ },
|
|
|
+ enabled: isLoggedIn && talentId > 0
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取个人征信文件
|
|
|
+ const { data: creditFiles, isLoading: filesLoading } = useQuery({
|
|
|
+ queryKey: ['creditFiles', talentId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!talentId) throw new Error('无效的人才ID')
|
|
|
+ // 根据API规范,路径为:/api/v1/file/list
|
|
|
+ const response = await fileClient.$get({
|
|
|
+ query: {
|
|
|
+ relation_type: 'disabled_person',
|
|
|
+ relation_id: talentId.toString()
|
|
|
+ } as any // 类型断言,因为TypeScript类型定义可能不完整
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ return [] as FileData[]
|
|
|
+ }
|
|
|
+ const result = await response.json() as any
|
|
|
+ return (result?.data || []) 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">
|
|
|
- <View className="p-4">
|
|
|
- <Text className="text-xl font-bold">人才详情</Text>
|
|
|
- <Text className="text-gray-600 mt-2">人才详细信息页面(待实现)</Text>
|
|
|
- </View>
|
|
|
+ <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>
|
|
|
)
|
|
|
}
|