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:
@@ -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')
|
||||
}
|
||||
Reference in New Issue
Block a user