Sprint 4-5: 员工自助+考勤+合规+AI+搜索
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 员工 Hub 首页 — 员工端统一入口
|
||||
* 展示个人概览、待办事项、快捷入口、公司公告
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
DollarSign, FileText, CalendarCheck, ScrollText,
|
||||
TrendingUp, Clock, AlertCircle, ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import Card from '../../components/ui/Card'
|
||||
import { InlineAlert } from '../../components/ui/InlineAlert'
|
||||
|
||||
/** 员工端 API 实例(自动携带 portalToken) */
|
||||
const portalApi = (await import('../../lib/api')).default.create({ baseURL: '/api/v1/portal' })
|
||||
portalApi.interceptors.request.use((config: any) => {
|
||||
const token = localStorage.getItem('portalToken')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
/** 金额格式化 */
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
/** 快捷入口配置 */
|
||||
const QUICK_ACTIONS = [
|
||||
{ path: '/portal/payslip', label: '工资条', icon: DollarSign, color: 'bg-emerald-50 text-emerald-600' },
|
||||
{ path: '/portal/contract', label: '我的合同', icon: FileText, color: 'bg-blue-50 text-blue-600' },
|
||||
{ path: '/portal/attendance', label: '我的考勤', icon: CalendarCheck, color: 'bg-purple-50 text-purple-600' },
|
||||
{ path: '/portal/policies', label: '规章制度', icon: ScrollText, color: 'bg-amber-50 text-amber-600' },
|
||||
]
|
||||
|
||||
export default function EmployeeHome() {
|
||||
const employee = (() => {
|
||||
try { return JSON.parse(localStorage.getItem('portalEmployee') || '{}') } catch { return {} }
|
||||
})()
|
||||
|
||||
/** 获取员工首页概览数据 */
|
||||
const { data: overview, isLoading } = useQuery<any>({
|
||||
queryKey: ['portal-home-overview'],
|
||||
queryFn: async () => {
|
||||
const res = await portalApi.get('/home/overview') as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="space-y-4">
|
||||
<div className="h-28 bg-gray-100 rounded-xl animate-pulse" />
|
||||
<div className="h-32 bg-gray-100 rounded-xl animate-pulse" />
|
||||
<div className="h-48 bg-gray-100 rounded-xl animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
|
||||
const latestPayslip = overview?.latestPayslip
|
||||
const contractInfo = overview?.contract
|
||||
const pendingTasks = overview?.pendingTasks || []
|
||||
const announcements = overview?.announcements || []
|
||||
const attendanceSummary = overview?.attendance
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 欢迎卡片 */}
|
||||
<Card className="bg-gradient-to-br from-indigo-600 to-indigo-700 text-white border-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">你好,{employee.name || '同事'}</h1>
|
||||
<p className="text-sm text-indigo-100 mt-1">{employee.department || ''} · {employee.position || ''}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-indigo-100">本月实发</div>
|
||||
<div className="text-xl font-bold">¥{fmt(latestPayslip?.netPay || 0)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 待办提醒 */}
|
||||
{pendingTasks.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-sm font-medium text-gray-700 flex items-center gap-1">
|
||||
<AlertCircle className="w-4 h-4 text-amber-500" />
|
||||
待办事项({pendingTasks.length})
|
||||
</h2>
|
||||
{pendingTasks.map((task: any, i: number) => (
|
||||
<InlineAlert key={i} type={task.severity === 'high' ? 'error' : 'warning'} className="text-xs">
|
||||
{task.message}
|
||||
</InlineAlert>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷入口 */}
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-700 mb-2">快捷入口</h2>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{QUICK_ACTIONS.map((action) => {
|
||||
const Icon = action.icon
|
||||
return (
|
||||
<Link
|
||||
key={action.path}
|
||||
to={action.path}
|
||||
className="flex flex-col items-center gap-1.5 p-3 rounded-xl bg-white border border-gray-100 hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${action.color}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-xs text-gray-600">{action.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最新工资条 */}
|
||||
{latestPayslip && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||||
<DollarSign className="w-4 h-4 text-emerald-500" />
|
||||
最新工资条
|
||||
</h2>
|
||||
<Link to="/portal/payslip" className="text-xs text-primary flex items-center hover:underline">
|
||||
查看全部 <ChevronRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-gray-400">月份</div>
|
||||
<div className="font-medium">{latestPayslip.month}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400">应发合计</div>
|
||||
<div className="font-medium">¥{fmt(latestPayslip.grossPay)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400">实发合计</div>
|
||||
<div className="font-medium text-emerald-600">¥{fmt(latestPayslip.netPay)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 合同状态 */}
|
||||
{contractInfo && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||||
<FileText className="w-4 h-4 text-blue-500" />
|
||||
合同信息
|
||||
</h2>
|
||||
<Link to="/portal/contract" className="text-xs text-primary flex items-center hover:underline">
|
||||
详情 <ChevronRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">合同类型</span>
|
||||
<span className="font-medium">{contractInfo.typeLabel || '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">合同期间</span>
|
||||
<span className="font-medium text-xs">{contractInfo.startDate} ~ {contractInfo.endDate || '无固定期'}</span>
|
||||
</div>
|
||||
{contractInfo.daysToExpire !== null && contractInfo.daysToExpire !== undefined && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">到期天数</span>
|
||||
<span className={`font-medium ${contractInfo.daysToExpire < 30 ? 'text-amber-600' : 'text-gray-700'}`}>
|
||||
{contractInfo.daysToExpire > 0 ? `${contractInfo.daysToExpire}天后到期` : '已到期'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 考勤概览 */}
|
||||
{attendanceSummary && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||||
<CalendarCheck className="w-4 h-4 text-purple-500" />
|
||||
本月考勤
|
||||
</h2>
|
||||
<Link to="/portal/attendance" className="text-xs text-primary flex items-center hover:underline">
|
||||
详情 <ChevronRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 text-center">
|
||||
<div className="p-2 rounded-lg bg-green-50">
|
||||
<div className="text-lg font-bold text-emerald-600">{attendanceSummary.normalDays || 0}</div>
|
||||
<div className="text-xs text-gray-400">正常</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-amber-50">
|
||||
<div className="text-lg font-bold text-amber-600">{attendanceSummary.lateCount || 0}</div>
|
||||
<div className="text-xs text-gray-400">迟到</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-blue-50">
|
||||
<div className="text-lg font-bold text-blue-600">{attendanceSummary.leaveDays || 0}</div>
|
||||
<div className="text-xs text-gray-400">请假</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-600">{attendanceSummary.absentDays || 0}</div>
|
||||
<div className="text-xs text-gray-400">缺勤</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 公司公告 */}
|
||||
{announcements.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="text-sm font-medium mb-3 flex items-center gap-1">
|
||||
<ScrollText className="w-4 h-4 text-gray-400" />
|
||||
公司公告
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{announcements.slice(0, 3).map((ann: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-md hover:bg-gray-50">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-700 truncate">{ann.title}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{ann.date} · {ann.author}</div>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-300 shrink-0 mt-1" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user