feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全

- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
This commit is contained in:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
+119
View File
@@ -0,0 +1,119 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { CalendarCheck, CheckCircle, Clock, AlertCircle, Upload } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import EmptyState from '../components/ui/EmptyState'
const STATUS_CONFIG: Record<string, { label: string; color: string; bg: string; icon: typeof CheckCircle }> = {
PENDING: { label: '待确认', color: 'text-amber-700', bg: 'bg-amber-100', icon: Clock },
CONFIRMED: { label: '已确认', color: 'text-green-700', bg: 'bg-green-100', icon: CheckCircle },
DISPUTED: { label: '有异议', color: 'text-red-700', bg: 'bg-red-100', icon: AlertCircle },
}
/**
* 考勤确认管理页面
*/
export default function Attendance() {
const queryClient = useQueryClient()
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
const { data: list, isLoading } = useQuery<any>({
queryKey: ['attendance', month],
queryFn: async () => {
const res = await api.get(`/attendance?month=${month}`) as any
return res.data
},
})
const { data: stats } = useQuery<any>({
queryKey: ['attendance-stats', month],
queryFn: async () => {
const res = await api.get(`/attendance/stats?month=${month}`) as any
return res.data
},
})
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<CalendarCheck className="h-5 w-5 text-primary" />
<h1 className="text-base font-semibold"></h1>
</div>
<p className="mt-1 text-sm text-gray-500"></p>
</div>
<input
type="month"
value={month}
onChange={e => setMonth(e.target.value)}
className="px-3 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
{/* 统计卡片 */}
{stats && (
<div className="grid grid-cols-4 gap-2">
{[
{ label: '总计', value: stats.total, color: 'text-gray-700' },
{ label: '待确认', value: stats.pending, color: 'text-amber-600' },
{ label: '已确认', value: stats.confirmed, color: 'text-green-600' },
{ label: '有异议', value: stats.disputed, color: 'text-red-600' },
].map(s => (
<Card key={s.label} className="text-center">
<div className={`text-lg font-bold ${s.color}`}>{s.value}</div>
<div className="text-xs text-gray-500">{s.label}</div>
</Card>
))}
</div>
)}
{isLoading ? (
<div className="text-center py-8 text-gray-500">...</div>
) : !list || list.length === 0 ? (
<EmptyState title="本月暂无考勤确认记录" description="请先批量导入考勤数据" />
) : (
<div className="space-y-2">
{list.map((item: any) => {
const config = STATUS_CONFIG[item.status] || STATUS_CONFIG.PENDING
const StatusIcon = config.icon
return (
<Card key={item.id}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-50 flex-shrink-0">
<CalendarCheck className="w-4 h-4 text-gray-600" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{item.employee?.name}</span>
<span className="text-xs text-gray-500">{item.employee?.department}</span>
</div>
<div className="flex items-center gap-3 text-xs text-gray-500 mt-0.5">
<span> {item.workDays} </span>
<span> {item.weekdayHours}h</span>
<span> {item.weekendHours}h</span>
<span> {item.holidayHours}h</span>
<span className="text-gray-700"> ¥{item.overtimePay?.toFixed(2)}</span>
</div>
{item.disputeNote && (
<div className="text-xs text-red-600 mt-1">{item.disputeNote}</div>
)}
</div>
</div>
<div className={`flex items-center gap-1 px-2 py-1 rounded-lg ${config.bg} ${config.color} flex-shrink-0`}>
<StatusIcon className="w-3.5 h-3.5" />
<span className="text-xs font-medium">{config.label}</span>
</div>
</div>
</Card>
)
})}
</div>
)}
</div>
)
}