Files
TurboHR/frontend/src/pages/portal/EmployeeHome.tsx
T
freedakgmail e8cd0f472b feat: 电子签署导航调整+员工手机端电子签署功能
- 管理端导航:电子签署移到首页分组(工作日历下)
- 员工端新增电子签署页面(MyEsign.tsx):
  - 查看自己的签署记录列表,支持待签/已签/已取消状态
  - 查看签署详情,含文件内容、关联合同标识
  - 确认签署操作(框架阶段,对接易签宝后跳转签署页面)
- 员工端导航(PortalNav)增加电子签署入口
- 员工首页(EmployeeHome)增加电子签署快捷入口
- 员工首页待办事项增加待签署文件提醒
- 后端portal路由新增3个接口:GET /esign、GET /esign/:id、POST /esign/:id/sign
2026-08-05 07:39:11 +08:00

239 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 员工 Hub 首页 — 员工端统一入口
* 展示个人概览、待办事项、快捷入口、公司公告
*/
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import {
DollarSign, FileText, CalendarCheck, ScrollText, CalendarClock,
AlertCircle, ChevronRight, UserX, PenTool,
} from 'lucide-react'
import { portalApi } from '../../lib/api-services'
import Card from '../../components/ui/Card'
import { InlineAlert } from '../../components/ui/InlineAlert'
/** 金额格式化 */
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/esign', label: '电子签署', icon: PenTool, color: 'bg-violet-50 text-violet-600' },
{ path: '/portal/attendance', label: '我的考勤', icon: CalendarCheck, color: 'bg-purple-50 text-purple-600' },
{ path: '/portal/leave', label: '休假申请', icon: CalendarClock, color: 'bg-cyan-50 text-cyan-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 () => {
return await portalApi.homeOverview()
},
})
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>
)}
{/* 辞职申请入口 */}
<Card>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserX className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium"></span>
</div>
<Link to="/portal/resignation" className="text-xs text-primary flex items-center hover:underline">
<ChevronRight className="w-3 h-3" />
</Link>
</div>
</Card>
</div>
)
}