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
+173
View File
@@ -0,0 +1,173 @@
import { useState, useRef } from "react"
import { toast } from "sonner"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import api from "../../lib/api"
import Card from "../../components/ui/Card"
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import Modal from "../../components/ui/Modal"
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, Briefcase, FileSignature, DollarSign, Building2, RotateCcw, History } from "lucide-react"
import { fmt } from "./shared"
// ========== 仲裁证据链 ==========
export default function EvidenceChain({ employeeId }: { employeeId: string }) {
const { data, isLoading } = useQuery<any>({
queryKey: ['evidence-chain', employeeId],
queryFn: async () => {
const res = await api.get(`/roster/${employeeId}/evidence-chain`) as any
return res.data
},
})
if (isLoading) return <div className="text-center py-8 text-gray-400">...</div>
if (!data) return <div className="text-center py-8 text-gray-400"></div>
const categoryColor: Record<string, string> = {
'劳动关系': 'bg-blue-50 text-blue-700 border-blue-200',
'薪酬发放': 'bg-green-50 text-green-700 border-green-200',
'考勤记录': 'bg-amber-50 text-amber-700 border-amber-200',
'违纪处理': 'bg-red-50 text-red-700 border-red-200',
'培训签收': 'bg-purple-50 text-purple-700 border-purple-200',
'绩效考核': 'bg-indigo-50 text-indigo-700 border-indigo-200',
'解聘记录': 'bg-gray-100 text-gray-700 border-gray-300',
}
const handleExport = () => {
const text = generateEvidenceText(data)
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `仲裁证据链_${data.employee.name}_${new Date().toISOString().slice(0, 10)}.txt`
a.click()
URL.revokeObjectURL(url)
}
const riskStyle: Record<string, string> = {
DANGER: 'bg-red-50 border-red-300 text-red-700',
HIGH: 'bg-orange-50 border-orange-300 text-orange-700',
MEDIUM: 'bg-amber-50 border-amber-300 text-amber-700',
}
const riskIcon: Record<string, string> = {
DANGER: '🔴',
HIGH: '🟠',
MEDIUM: '🟡',
}
return (
<div className="space-y-3">
<Card>
<div className="flex items-center justify-between">
<div>
<h2 className="text-xs font-medium flex items-center gap-2"><Scale className="w-4 h-4" /></h2>
<div className="text-xs text-gray-500 mt-1">
{data.employee.name} · {data.employee.department} · {data.employee.hireDate}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-xs text-center">
<div className="text-gray-500"></div>
<div className="text-xl font-bold">{data.summary.total}</div>
</div>
<div className="text-xs text-center">
<div className="text-gray-500"></div>
<div className="text-xl font-bold text-safe">{data.summary.signed}</div>
</div>
<div className="text-xs text-center">
<div className="text-gray-500"></div>
<div className="text-xl font-bold text-warning">{data.summary.unsigned}</div>
</div>
{data.summary.riskCount > 0 && (
<div className="text-xs text-center">
<div className="text-gray-500"></div>
<div className="text-xl font-bold text-danger">{data.summary.riskCount}</div>
</div>
)}
<Button onClick={handleExport}></Button>
</div>
</div>
</Card>
{data.risks && data.risks.length > 0 && (
<Card>
<h3 className="text-xs font-medium mb-3 flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-danger" />
{data.risks.length}
</h3>
<div className="space-y-2">
{data.risks.map((r: any, i: number) => (
<div key={i} className={`border rounded-lg p-3 ${riskStyle[r.level] || 'bg-gray-50 border-gray-200 text-gray-600'}`}>
<div className="flex items-center gap-2">
<span>{riskIcon[r.level] || '⚠'}</span>
<span className="font-medium text-xs">{r.title}</span>
<span className="text-xs opacity-70">{r.category}</span>
</div>
<div className="text-xs mt-1 opacity-90">{r.description}</div>
</div>
))}
</div>
</Card>
)}
<div className="space-y-2">
{data.evidence.map((e: any, i: number) => (
<Card key={i} className={e.riskLevel === 'HIGH' ? 'border-orange-300' : ''}>
<div className="flex items-start gap-3">
<span className={`px-2 py-0.5 rounded border text-xs shrink-0 ${categoryColor[e.category] || 'bg-gray-50 text-gray-600 border-gray-200'}`}>
{e.category}
</span>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-xs">{e.title}</span>
<span className="text-xs text-gray-400">{e.date}</span>
{e.acknowledged === true && <span className="text-xs text-safe"> </span>}
{e.acknowledged === false && <span className="text-xs text-warning"> </span>}
{e.riskLevel === 'HIGH' && <span className="text-xs text-danger"> </span>}
</div>
<div className="text-xs text-gray-600 mt-1">{e.description}</div>
</div>
</div>
</Card>
))}
</div>
</div>
)
}
export function generateEvidenceText(data: any): string {
const lines: string[] = []
lines.push('========================================')
lines.push(' 劳动仲裁证据链')
lines.push('========================================')
lines.push('')
lines.push(`员工姓名:${data.employee.name}`)
lines.push(`部门:${data.employee.department}`)
lines.push(`入职日期:${data.employee.hireDate}`)
lines.push(`状态:${data.employee.status === 'ACTIVE' ? '在职' : '离职'}`)
lines.push('')
lines.push(`证据总数:${data.summary.total}`)
lines.push(`已签字:${data.summary.signed}`)
lines.push(`未签字:${data.summary.unsigned}`)
lines.push('')
lines.push('----------------------------------------')
lines.push('')
let currentCategory = ''
data.evidence.forEach((e: any, i: number) => {
if (e.category !== currentCategory) {
currentCategory = e.category
lines.push(`${currentCategory}`)
lines.push('')
}
lines.push(`${i + 1}. ${e.title}${e.date}`)
lines.push(` ${e.description}`)
if (e.acknowledged === true) lines.push(' [已签字确认]')
if (e.acknowledged === false) lines.push(' [未签字]')
lines.push('')
})
lines.push('----------------------------------------')
lines.push(`导出时间:${new Date().toLocaleString('zh-CN')}`)
return lines.join('\n')
}