|
|
@@ -0,0 +1,355 @@
|
|
|
+import React, { useEffect, useState } from 'react'
|
|
|
+import { View, Text, Input, ScrollView } from '@tarojs/components'
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
+import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
+import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui'
|
|
|
+import { PageContainer } from '@d8d/mini-shared-ui-components'
|
|
|
+import { enterpriseDisabilityClient } from './api'
|
|
|
+import { useAuth, useRequireAuth } from '@d8d/mini-enterprise-auth-ui'
|
|
|
+import './TalentManagement.css'
|
|
|
+
|
|
|
+export interface TalentManagementProps {
|
|
|
+ // 组件属性定义(目前为空)
|
|
|
+}
|
|
|
+
|
|
|
+// 企业专用人才列表项类型 - 匹配CompanyPersonListItemSchema
|
|
|
+interface CompanyPersonListItem {
|
|
|
+ personId: number
|
|
|
+ name: string
|
|
|
+ gender: string
|
|
|
+ idCard: string
|
|
|
+ disabilityType: string
|
|
|
+ disabilityLevel: string
|
|
|
+ phone: string | null
|
|
|
+ jobStatus: string
|
|
|
+ latestJoinDate: string | null
|
|
|
+ orderName: string | null
|
|
|
+}
|
|
|
+
|
|
|
+// 企业专用人才列表API响应类型 - 匹配CompanyPersonListResponseSchema
|
|
|
+interface CompanyPersonListResponse {
|
|
|
+ data: CompanyPersonListItem[]
|
|
|
+ pagination: {
|
|
|
+ page: number
|
|
|
+ limit: number
|
|
|
+ total: number
|
|
|
+ totalPages: number
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// useQuery返回的数据结构
|
|
|
+interface TalentListData {
|
|
|
+ data: CompanyPersonListItem[]
|
|
|
+ total: number
|
|
|
+ page: number
|
|
|
+ limit: number
|
|
|
+ totalPages: number
|
|
|
+}
|
|
|
+
|
|
|
+const TalentManagement: React.FC<TalentManagementProps> = () => {
|
|
|
+ const { user: _user } = useAuth()
|
|
|
+ const { isLoggedIn } = useRequireAuth()
|
|
|
+ const queryClient = useQueryClient()
|
|
|
+
|
|
|
+ // 搜索和筛选状态
|
|
|
+ const [searchText, setSearchText] = useState('')
|
|
|
+ const [activeStatus, setActiveStatus] = useState<string>('全部')
|
|
|
+ const [activeDisabilityType, setActiveDisabilityType] = useState<string>('')
|
|
|
+ const [page, setPage] = useState(1)
|
|
|
+ const limit = 20
|
|
|
+
|
|
|
+ // 搜索参数防抖
|
|
|
+ const [debouncedSearchText, setDebouncedSearchText] = useState('')
|
|
|
+ useEffect(() => {
|
|
|
+ const timer = setTimeout(() => {
|
|
|
+ setDebouncedSearchText(searchText)
|
|
|
+ setPage(1) // 搜索时重置到第一页
|
|
|
+ }, 500)
|
|
|
+ return () => clearTimeout(timer)
|
|
|
+ }, [searchText])
|
|
|
+
|
|
|
+ // 构建查询参数(企业专用API使用camelCase参数名)
|
|
|
+ const queryParams = {
|
|
|
+ search: debouncedSearchText || undefined,
|
|
|
+ jobStatus: activeStatus !== '全部' ? activeStatus : undefined,
|
|
|
+ disabilityType: activeDisabilityType || undefined,
|
|
|
+ page,
|
|
|
+ limit
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取人才列表数据(使用企业专用API)
|
|
|
+ const { data: talentList, isLoading, error, refetch } = useQuery({
|
|
|
+ queryKey: ['talentList', queryParams],
|
|
|
+ queryFn: async () => {
|
|
|
+ const response = await enterpriseDisabilityClient.$get({
|
|
|
+ query: queryParams
|
|
|
+ })
|
|
|
+ if (response.status !== 200) {
|
|
|
+ throw new Error('获取人才列表失败')
|
|
|
+ }
|
|
|
+ const result = await response.json() as CompanyPersonListResponse
|
|
|
+ // API返回结构:{ data: [...], pagination: { total, page, limit, totalPages } }
|
|
|
+ const data = result?.data || []
|
|
|
+ const pagination = result?.pagination || { total: 0, page: 1, limit: 20, totalPages: 0 }
|
|
|
+ // 转换为扁平结构以便使用
|
|
|
+ return {
|
|
|
+ data,
|
|
|
+ total: pagination.total,
|
|
|
+ page: pagination.page,
|
|
|
+ limit: pagination.limit,
|
|
|
+ totalPages: pagination.totalPages
|
|
|
+ }
|
|
|
+ },
|
|
|
+ enabled: isLoggedIn, // 只有登录后才获取数据
|
|
|
+ refetchOnWindowFocus: false
|
|
|
+ })
|
|
|
+
|
|
|
+ // 下拉刷新
|
|
|
+ const [refreshing, setRefreshing] = useState(false)
|
|
|
+ const onRefresh = async () => {
|
|
|
+ setRefreshing(true)
|
|
|
+ try {
|
|
|
+ await queryClient.invalidateQueries({ queryKey: ['talentList'] })
|
|
|
+ await refetch()
|
|
|
+ } finally {
|
|
|
+ setTimeout(() => setRefreshing(false), 1000)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载时设置标题
|
|
|
+ useEffect(() => {
|
|
|
+ Taro.setNavigationBarTitle({
|
|
|
+ title: '人才管理'
|
|
|
+ })
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 状态标签列表
|
|
|
+ const statusTags = ['全部', '在职', '待入职', '离职']
|
|
|
+ const disabilityTypeTags = ['肢体残疾', '听力残疾', '视力残疾', '言语残疾', '智力残疾', '精神残疾']
|
|
|
+
|
|
|
+ // 处理状态筛选点击
|
|
|
+ const handleStatusClick = (status: string) => {
|
|
|
+ setActiveStatus(status)
|
|
|
+ setPage(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理残疾类型筛选点击
|
|
|
+ const handleDisabilityTypeClick = (type: string) => {
|
|
|
+ setActiveDisabilityType(activeDisabilityType === type ? '' : type)
|
|
|
+ setPage(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理搜索输入
|
|
|
+ const handleSearchChange = (e: any) => {
|
|
|
+ setSearchText(e.detail.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分页处理
|
|
|
+ const handlePrevPage = () => {
|
|
|
+ if (page > 1) {
|
|
|
+ setPage(page - 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleNextPage = () => {
|
|
|
+ if (talentList && page < Math.ceil(talentList.total / limit)) {
|
|
|
+ setPage(page + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理人才卡片点击跳转
|
|
|
+ const handleTalentClick = (talentId: number) => {
|
|
|
+ Taro.navigateTo({
|
|
|
+ url: `/pages/yongren/talent/detail/index?id=${talentId}`
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取头像颜色
|
|
|
+ const getAvatarColor = (id: number) => {
|
|
|
+ const colors = ['blue', 'green', 'purple', 'orange', 'red', 'teal']
|
|
|
+ const index = id % colors.length
|
|
|
+ return colors[index]
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <YongrenTabBarLayout activeKey="talent">
|
|
|
+ <PageContainer padding={false} className="pb-0">
|
|
|
+ <ScrollView
|
|
|
+ className="h-[calc(100vh-120px)] overflow-y-auto"
|
|
|
+ scrollY
|
|
|
+ refresherEnabled
|
|
|
+ refresherTriggered={refreshing}
|
|
|
+ onRefresherRefresh={onRefresh}
|
|
|
+ >
|
|
|
+ {/* 搜索和筛选区域 - 对照原型第434-447行 */}
|
|
|
+ <View className="p-4 border-b border-gray-200">
|
|
|
+ <View className="flex items-center bg-gray-100 rounded-lg px-4 py-2 mb-3">
|
|
|
+ <Text className="i-heroicons-magnifying-glass-20-solid text-gray-400 mr-2" />
|
|
|
+ <Input
|
|
|
+ type="text"
|
|
|
+ placeholder="搜索姓名、残疾证号..."
|
|
|
+ className="w-full bg-transparent outline-none text-sm"
|
|
|
+ value={searchText}
|
|
|
+ onInput={handleSearchChange}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ <ScrollView className="flex space-x-2 pb-2" scrollX>
|
|
|
+ {statusTags.map((status) => (
|
|
|
+ <View
|
|
|
+ key={status}
|
|
|
+ className={`text-xs px-3 py-1 rounded-full whitespace-nowrap ${
|
|
|
+ activeStatus === status
|
|
|
+ ? 'bg-blue-100 text-blue-800'
|
|
|
+ : 'bg-gray-100 text-gray-800'
|
|
|
+ }`}
|
|
|
+ onClick={() => handleStatusClick(status)}
|
|
|
+ >
|
|
|
+ {status}
|
|
|
+ </View>
|
|
|
+ ))}
|
|
|
+ </ScrollView>
|
|
|
+ <ScrollView className="flex space-x-2 mt-2" scrollX>
|
|
|
+ {disabilityTypeTags.map((type) => (
|
|
|
+ <View
|
|
|
+ key={type}
|
|
|
+ className={`text-xs px-3 py-1 rounded-full whitespace-nowrap ${
|
|
|
+ activeDisabilityType === type
|
|
|
+ ? 'bg-blue-100 text-blue-800'
|
|
|
+ : 'bg-gray-100 text-gray-800'
|
|
|
+ }`}
|
|
|
+ onClick={() => handleDisabilityTypeClick(type)}
|
|
|
+ >
|
|
|
+ {type}
|
|
|
+ </View>
|
|
|
+ ))}
|
|
|
+ </ScrollView>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 人才列表区域 - 对照原型第451-560行 */}
|
|
|
+ <View className="p-4">
|
|
|
+ <View className="flex justify-between items-center mb-4">
|
|
|
+ <Text className="font-semibold text-gray-700">
|
|
|
+ 全部人才 ({talentList?.total || 0})
|
|
|
+ </Text>
|
|
|
+ <View className="flex space-x-2">
|
|
|
+ <View className="text-gray-500">
|
|
|
+ <Text className="i-heroicons-funnel-20-solid" />
|
|
|
+ </View>
|
|
|
+ <View className="text-gray-500">
|
|
|
+ <Text className="i-heroicons-arrows-up-down-20-solid" />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {isLoading ? (
|
|
|
+ // 加载状态
|
|
|
+ <View className="space-y-3">
|
|
|
+ {[1, 2, 3].map((i) => (
|
|
|
+ <View key={i} className="bg-white p-4 rounded-lg animate-pulse flex items-center">
|
|
|
+ <View className="w-10 h-10 bg-gray-200 rounded-full" />
|
|
|
+ <View className="flex-1 ml-3">
|
|
|
+ <View className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
|
|
|
+ <View className="h-3 bg-gray-200 rounded w-1/2" />
|
|
|
+ <View className="flex justify-between mt-2">
|
|
|
+ <View className="h-3 bg-gray-200 rounded w-1/4" />
|
|
|
+ <View className="h-3 bg-gray-200 rounded w-1/4" />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ ))}
|
|
|
+ </View>
|
|
|
+ ) : error ? (
|
|
|
+ // 错误状态
|
|
|
+ <View className="bg-white p-4 rounded-lg text-center">
|
|
|
+ <Text className="text-red-500 text-sm">加载失败: {(error as Error).message}</Text>
|
|
|
+ <View
|
|
|
+ className="mt-2 bg-blue-500 text-white text-xs px-3 py-1 rounded-full inline-block"
|
|
|
+ onClick={() => refetch()}
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ ) : talentList && talentList.data.length > 0 ? (
|
|
|
+ // 人才列表
|
|
|
+ <View className="space-y-3">
|
|
|
+ {talentList.data.map((talent) => (
|
|
|
+ <View
|
|
|
+ key={talent.personId}
|
|
|
+ className="card bg-white p-4 flex items-center cursor-pointer active:bg-gray-50"
|
|
|
+ onClick={() => handleTalentClick(talent.personId)}
|
|
|
+ >
|
|
|
+ <View className={`name-avatar ${getAvatarColor(talent.personId)} w-10 h-10 rounded-full flex items-center justify-center`}>
|
|
|
+ <Text className="text-white font-semibold">
|
|
|
+ {talent.name.charAt(0)}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View className="flex-1 ml-3">
|
|
|
+ <View className="flex justify-between items-start">
|
|
|
+ <View>
|
|
|
+ <Text className="font-semibold text-gray-800">{talent.name}</Text>
|
|
|
+ <Text className="text-xs text-gray-500">
|
|
|
+ {talent.disabilityType || '未指定'} · {talent.disabilityLevel || '未分级'} · {talent.gender} · 未知岁
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <Text className={`text-xs px-2 py-1 rounded-full ${
|
|
|
+ talent.jobStatus === '在职'
|
|
|
+ ? 'bg-green-100 text-green-800'
|
|
|
+ : talent.jobStatus === '待入职'
|
|
|
+ ? 'bg-yellow-100 text-yellow-800'
|
|
|
+ : 'bg-gray-100 text-gray-800'
|
|
|
+ }`}>
|
|
|
+ {talent.jobStatus}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View className="mt-2 flex justify-between text-xs text-gray-500">
|
|
|
+ <Text>入职: {talent.latestJoinDate ? new Date(talent.latestJoinDate).toLocaleDateString() : '未入职'}</Text>
|
|
|
+ <Text>薪资: 待定</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 分页控件 */}
|
|
|
+ {talentList.total > limit && (
|
|
|
+ <View className="flex justify-between items-center mt-4 pt-4 border-t border-gray-200">
|
|
|
+ <View
|
|
|
+ className={`text-xs px-3 py-1 rounded ${page === 1 ? 'text-gray-400' : 'text-blue-600'}`}
|
|
|
+ onClick={handlePrevPage}
|
|
|
+ >
|
|
|
+ 上一页
|
|
|
+ </View>
|
|
|
+ <Text className="text-xs text-gray-600">
|
|
|
+ 第 {page} 页 / 共 {Math.ceil(talentList.total / limit)} 页
|
|
|
+ </Text>
|
|
|
+ <View
|
|
|
+ className={`text-xs px-3 py-1 rounded ${
|
|
|
+ talentList && page >= Math.ceil(talentList.total / limit)
|
|
|
+ ? 'text-gray-400'
|
|
|
+ : 'text-blue-600'
|
|
|
+ }`}
|
|
|
+ onClick={handleNextPage}
|
|
|
+ >
|
|
|
+ 下一页
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ ) : (
|
|
|
+ // 空状态
|
|
|
+ <View className="bg-white p-4 rounded-lg text-center">
|
|
|
+ <Text className="text-gray-500 text-sm">暂无人才数据</Text>
|
|
|
+ {debouncedSearchText && (
|
|
|
+ <Text className="text-gray-400 text-xs mt-1">
|
|
|
+ 尝试调整搜索条件
|
|
|
+ </Text>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ </ScrollView>
|
|
|
+ </PageContainer>
|
|
|
+ </YongrenTabBarLayout>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default TalentManagement
|