|
@@ -0,0 +1,290 @@
|
|
|
|
|
+import React, { useEffect, useState } from 'react'
|
|
|
|
|
+import { View, Text, ScrollView } from '@tarojs/components'
|
|
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
|
|
+import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
+import { YongrenTabBarLayout } from '@d8d/mini-shared-ui-components'
|
|
|
|
|
+import { enterpriseCompanyClient } from './api'
|
|
|
|
|
+import { useAuth } from '@d8d/mini-shared-ui-components' // TODO: 需要迁移认证hooks
|
|
|
|
|
+import { useRequireAuth } from '@d8d/mini-shared-ui-components' // TODO: 需要迁移认证hooks
|
|
|
|
|
+import './Dashboard.css'
|
|
|
|
|
+
|
|
|
|
|
+// 类型定义
|
|
|
|
|
+interface OverviewData {
|
|
|
|
|
+ totalEmployees: number
|
|
|
|
|
+ pendingAssignments: number
|
|
|
|
|
+ monthlyOrders: number
|
|
|
|
|
+ companyName: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface AllocationData {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ name: string
|
|
|
|
|
+ avatarColor: 'blue' | 'green' | 'purple' | 'orange'
|
|
|
|
|
+ disabilityType: string
|
|
|
|
|
+ disabilityLevel: string
|
|
|
|
|
+ status: '在职' | '待入职' | '离职'
|
|
|
|
|
+ joinDate: string
|
|
|
|
|
+ salary: number
|
|
|
|
|
+ progress: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const Dashboard: React.FC = () => {
|
|
|
|
|
+ const { user } = useAuth()
|
|
|
|
|
+ const [refreshing, setRefreshing] = useState(false)
|
|
|
|
|
+ const queryClient = useQueryClient()
|
|
|
|
|
+
|
|
|
|
|
+ // 检查登录状态,未登录则重定向
|
|
|
|
|
+ useRequireAuth()
|
|
|
|
|
+
|
|
|
|
|
+ // 获取企业概览数据
|
|
|
|
|
+ const { data: overview, isLoading: _overviewLoading } = useQuery({
|
|
|
|
|
+ queryKey: ['enterpriseOverview'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseCompanyClient.overview.$get()
|
|
|
|
|
+ if (response.status !== 200) {
|
|
|
|
|
+ throw new Error('获取企业概览数据失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ const data = await response.json()
|
|
|
|
|
+ // 转换为OverviewData接口
|
|
|
|
|
+ return {
|
|
|
|
|
+ totalEmployees: data.在职人员数,
|
|
|
|
|
+ pendingAssignments: data.进行中订单数,
|
|
|
|
|
+ monthlyOrders: data.已完成订单数,
|
|
|
|
|
+ companyName: data.companyName || '企业名称'
|
|
|
|
|
+ } as OverviewData
|
|
|
|
|
+ },
|
|
|
|
|
+ refetchOnWindowFocus: false
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 将API人才数据转换为前端AllocationData接口
|
|
|
|
|
+ const convertTalentToAllocation = (talent: any): AllocationData => {
|
|
|
|
|
+ // 根据personId生成稳定的颜色
|
|
|
|
|
+ const colors: Array<'blue' | 'green' | 'purple' | 'orange'> = ['blue', 'green', 'purple', 'orange']
|
|
|
|
|
+ const colorIndex = talent.personId ? talent.personId % colors.length : 0
|
|
|
|
|
+ const avatarColor = colors[colorIndex]
|
|
|
|
|
+
|
|
|
|
|
+ // 工作状态映射:working -> 在职, on_leave -> 待入职, left -> 离职
|
|
|
|
|
+ const statusMap: Record<string, '在职' | '待入职' | '离职'> = {
|
|
|
|
|
+ 'working': '在职',
|
|
|
|
|
+ 'on_leave': '待入职',
|
|
|
|
|
+ 'left': '离职'
|
|
|
|
|
+ }
|
|
|
|
|
+ const status = statusMap[talent.workStatus] || '在职'
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化日期
|
|
|
|
|
+ const joinDate = talent.joinDate ? new Date(talent.joinDate).toLocaleDateString('zh-CN') : '未知'
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: talent.personId?.toString() || '0',
|
|
|
|
|
+ name: talent.personName || '未知姓名',
|
|
|
|
|
+ avatarColor,
|
|
|
|
|
+ disabilityType: '肢体残疾', // 暂时使用默认值,后续可从其他API获取
|
|
|
|
|
+ disabilityLevel: '三级', // 暂时使用默认值
|
|
|
|
|
+ status,
|
|
|
|
|
+ joinDate,
|
|
|
|
|
+ salary: 4500, // 暂时使用默认薪资
|
|
|
|
|
+ progress: 75 // 暂时使用默认进度
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取近期分配人才列表
|
|
|
|
|
+ const { data: allocations, isLoading: allocationsLoading } = useQuery({
|
|
|
|
|
+ queryKey: ['recentAllocations'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseCompanyClient.allocations.recent.$get({
|
|
|
|
|
+ query: { limit: 5 } // 获取5条记录,dashboard只显示前2条
|
|
|
|
|
+ })
|
|
|
|
|
+ if (response.status !== 200) {
|
|
|
|
|
+ throw new Error('获取分配人才列表失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ const data = await response.json()
|
|
|
|
|
+ // 转换数据格式
|
|
|
|
|
+ return data.人才列表.map(convertTalentToAllocation)
|
|
|
|
|
+ },
|
|
|
|
|
+ refetchOnWindowFocus: false
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 下拉刷新
|
|
|
|
|
+ const onRefresh = async () => {
|
|
|
|
|
+ setRefreshing(true)
|
|
|
|
|
+ try {
|
|
|
|
|
+ await Promise.all([
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['enterpriseOverview'] }),
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['recentAllocations'] })
|
|
|
|
|
+ ])
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setTimeout(() => setRefreshing(false), 1000)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 页面加载时设置标题
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ Taro.setNavigationBarTitle({
|
|
|
|
|
+ title: '企业仪表板'
|
|
|
|
|
+ })
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
|
|
+ // const isLoading = overviewLoading || allocationsLoading // 未使用
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <YongrenTabBarLayout activeKey="dashboard">
|
|
|
|
|
+ <ScrollView
|
|
|
|
|
+ className="h-[calc(100%-60px)] overflow-y-auto p-4"
|
|
|
|
|
+ scrollY
|
|
|
|
|
+ refresherEnabled
|
|
|
|
|
+ refresherTriggered={refreshing}
|
|
|
|
|
+ onRefresherRefresh={onRefresh}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 顶部信息栏 - 对照原型第276-300行 */}
|
|
|
|
|
+ <View className="bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-2xl p-5 mb-4">
|
|
|
|
|
+ <View className="flex justify-between items-center">
|
|
|
|
|
+ <View>
|
|
|
|
|
+ <Text className="text-sm opacity-80">欢迎回来</Text>
|
|
|
|
|
+ <Text className="text-xl font-bold">
|
|
|
|
|
+ {overview?.companyName || user?.name || '企业名称'}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
|
|
|
|
|
+ <View className="i-heroicons-building-office-20-solid text-white text-xl" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="mt-4 flex justify-between">
|
|
|
|
|
+ <View className="text-center">
|
|
|
|
|
+ <Text className="text-2xl font-bold">{overview?.totalEmployees || 0}</Text>
|
|
|
|
|
+ <Text className="text-xs opacity-80">在职人员</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="text-center">
|
|
|
|
|
+ <Text className="text-2xl font-bold">{overview?.pendingAssignments || 0}</Text>
|
|
|
|
|
+ <Text className="text-xs opacity-80">待入职</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="text-center">
|
|
|
|
|
+ <Text className="text-2xl font-bold">{overview?.monthlyOrders || 0}</Text>
|
|
|
|
|
+ <Text className="text-xs opacity-80">本月新增</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 快速操作网格 - 对照原型第303-320行 */}
|
|
|
|
|
+ <View className="grid grid-cols-4 gap-3 mb-4">
|
|
|
|
|
+ <View className="bg-blue-50 rounded-xl p-3 text-center">
|
|
|
|
|
+ <View className="i-heroicons-user-group-20-solid text-blue-500 text-lg mb-1" />
|
|
|
|
|
+ <Text className="text-xs text-gray-700">人才库</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="bg-green-50 rounded-xl p-3 text-center">
|
|
|
|
|
+ <View className="i-heroicons-chart-bar-20-solid text-green-500 text-lg mb-1" />
|
|
|
|
|
+ <Text className="text-xs text-gray-700">数据统计</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="bg-purple-50 rounded-xl p-3 text-center">
|
|
|
|
|
+ <View className="i-heroicons-document-text-20-solid text-purple-500 text-lg mb-1" />
|
|
|
|
|
+ <Text className="text-xs text-gray-700">订单管理</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="bg-yellow-50 rounded-xl p-3 text-center">
|
|
|
|
|
+ <View className="i-heroicons-cog-6-tooth-20-solid text-yellow-500 text-lg mb-1" />
|
|
|
|
|
+ <Text className="text-xs text-gray-700">设置</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 人才列表区域 - 对照原型第323-376行 */}
|
|
|
|
|
+ <View className="mb-4">
|
|
|
|
|
+ <View className="flex justify-between items-center mb-3">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700">分配人才</Text>
|
|
|
|
|
+ <Text className="text-xs text-blue-500">查看全部</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {allocationsLoading ? (
|
|
|
|
|
+ <View className="space-y-3">
|
|
|
|
|
+ {[1, 2].map((i) => (
|
|
|
|
|
+ <View key={i} className="bg-white p-4 rounded-lg animate-pulse">
|
|
|
|
|
+ <View className="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>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : allocations && allocations.length > 0 ? (
|
|
|
|
|
+ <View className="space-y-3">
|
|
|
|
|
+ {allocations.slice(0, 2).map((allocation) => (
|
|
|
|
|
+ <View key={allocation.id} className="bg-white p-4 rounded-lg flex items-center">
|
|
|
|
|
+ {/* 头像区域 */}
|
|
|
|
|
+ <View className={`name-avatar ${allocation.avatarColor} w-10 h-10 rounded-full flex items-center justify-center`}>
|
|
|
|
|
+ <Text className="text-white font-semibold">
|
|
|
|
|
+ {allocation.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">{allocation.name}</Text>
|
|
|
|
|
+ <Text className="text-xs text-gray-500">
|
|
|
|
|
+ {allocation.disabilityType} · {allocation.disabilityLevel}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <Text className={`text-xs px-2 py-1 rounded-full ${
|
|
|
|
|
+ allocation.status === '在职'
|
|
|
|
|
+ ? 'bg-green-100 text-green-800'
|
|
|
|
|
+ : allocation.status === '待入职'
|
|
|
|
|
+ ? 'bg-yellow-100 text-yellow-800'
|
|
|
|
|
+ : 'bg-gray-100 text-gray-800'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {allocation.status}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View className="mt-2">
|
|
|
|
|
+ <View className="flex justify-between text-xs text-gray-500 mb-1">
|
|
|
|
|
+ <Text>{allocation.status === '在职' ? '入职时间:' : '预计入职:'} {allocation.joinDate}</Text>
|
|
|
|
|
+ <Text>薪资: ¥{allocation.salary.toLocaleString()}</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="progress-bar">
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="progress-fill"
|
|
|
|
|
+ style={{ width: `${allocation.progress}%` }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <View className="bg-white p-4 rounded-lg text-center">
|
|
|
|
|
+ <Text className="text-gray-500 text-sm">暂无分配人才</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 数据统计卡片 - 对照原型第379-394行 */}
|
|
|
|
|
+ <View className="mb-4">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">数据统计</Text>
|
|
|
|
|
+ <View className="grid grid-cols-2 gap-3">
|
|
|
|
|
+ <View className="stat-card bg-white p-4 rounded-lg">
|
|
|
|
|
+ <View className="flex items-center mb-2">
|
|
|
|
|
+ <View className="pulse-dot mr-2" />
|
|
|
|
|
+ <Text className="text-sm text-gray-600">在职率</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">
|
|
|
|
|
+ {overview?.totalEmployees ? '92%' : '--'}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="stat-card bg-white p-4 rounded-lg">
|
|
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">平均薪资</Text>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">
|
|
|
|
|
+ {allocations && allocations.length > 0
|
|
|
|
|
+ ? `¥${Math.round(allocations.reduce((sum, a) => sum + a.salary, 0) / allocations.length).toLocaleString()}`
|
|
|
|
|
+ : '¥0'}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </ScrollView>
|
|
|
|
|
+ </YongrenTabBarLayout>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default Dashboard
|