import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus } from 'lucide-react' 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 Signal from '../components/ui/Signal' import Pagination from '../components/ui/Pagination' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) type DetailTab = 'basic' | 'contract' | 'payslip' | 'overtime' | 'disciplinary' | 'attendance' | 'training' | 'performance' | 'termination' | 'attachment' | 'evidence' export default function Roster() { const queryClient = useQueryClient() const [selectedId, setSelectedId] = useState(null) const [search, setSearch] = useState('') const [showAddModal, setShowAddModal] = useState(false) const [showResignModal, setShowResignModal] = useState(false) const [resignEmployee, setResignEmployee] = useState(null) const [showRehireModal, setShowRehireModal] = useState(false) const [rehireEmployee, setRehireEmployee] = useState(null) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const { data: employees, isLoading } = useQuery({ queryKey: ['roster'], queryFn: async () => { const res = await api.get('/roster') as any return res.data }, }) const addMutation = useMutation({ mutationFn: (data: any) => api.post('/employees', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setShowAddModal(false) }, }) const resignMutation = useMutation({ mutationFn: (data: any) => api.post('/termination/resignation', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setShowResignModal(false) setResignEmployee(null) }, }) const revokeMutation = useMutation({ mutationFn: (recordId: string) => api.delete(`/termination/${recordId}/revoke`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) }, }) const rehireMutation = useMutation({ mutationFn: (data: any) => api.post(`/employees/${rehireEmployee?.id}/rehire`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setShowRehireModal(false) setRehireEmployee(null) }, }) const filtered = employees?.filter((e: any) => !search || e.name.includes(search) || e.department.includes(search) ) || [] const paged = filtered.slice((page - 1) * pageSize, page * pageSize) if (selectedId) { return setSelectedId(null)} /> } return (

花名册

setSearch(e.target.value)} className="!w-64 shrink-0" />
{isLoading ? (
加载中...
) : filtered.length === 0 ? (
暂无员工
) : ( { setPageSize(s); setPage(1) }} />
{paged.map((e: any) => ( setSelectedId(e.id)} > ))}
姓名 部门 状态 入职日期 离职日期 月薪 合同状态 违纪 考勤 培训 绩效 工资条 操作
{e.name} {e.department} {e.status === 'ACTIVE' ? '在职' : '离职'} {e.hireDate?.toString().slice(0, 10)} {e.hasTermination && e.latestTerminationDate ? ( {e.latestTerminationDate.toString().slice(0, 10)} {e.status === 'ACTIVE' && ' (预计)'} ) : ( )} ¥{fmt(e.monthlySalary)} {(() => { const tagStyles: Record = { expired: 'bg-red-50 text-danger', unsigned_over_year: 'bg-red-50 text-danger', unsigned_over_30: 'bg-red-50 text-danger', unsigned: 'bg-yellow-50 text-yellow-700', expiring: 'bg-yellow-50 text-yellow-700', active: 'bg-green-50 text-safe', unfixed: 'bg-blue-50 text-blue-700', } const style = tagStyles[e.contractStatus] || 'bg-gray-100 text-gray-500' return {e.contractStatusText || '无合同'} })()} {e.counts?.disciplinaryRecords ? ( {e.counts.disciplinaryRecords} ) : 0} {e.counts?.attendanceRecords || 0} {e.counts?.trainingRecords || 0} {e.counts?.performanceRecords || 0} {e.counts?.payslips || 0} {e.status === 'ACTIVE' && !e.hasTermination && ( )} {e.hasTermination && e.status === 'ACTIVE' && (
{e.latestTerminationType === 'RESIGNATION' ? '待离职' : '待解聘'}
)} {e.status === 'RESIGNED' && ( )}
)} {showAddModal && ( setShowAddModal(false)} onSubmit={(data) => addMutation.mutate(data)} loading={addMutation.isPending} error={addMutation.error as any} /> )} {showResignModal && resignEmployee && ( { setShowResignModal(false); setResignEmployee(null) }} onSubmit={(data) => resignMutation.mutate(data)} loading={resignMutation.isPending} error={resignMutation.error as any} /> )} {showRehireModal && rehireEmployee && ( { setShowRehireModal(false); setRehireEmployee(null) }} onSubmit={(data) => rehireMutation.mutate(data)} loading={rehireMutation.isPending} error={rehireMutation.error as any} /> )}
) } function EmployeeProfile({ employeeId, onBack }: { employeeId: string; onBack: () => void }) { const [tab, setTab] = useState('basic') const [showEvidence, setShowEvidence] = useState(false) const { data: profile, isLoading } = useQuery({ queryKey: ['roster-profile', employeeId], queryFn: async () => { const res = await api.get(`/roster/${employeeId}/profile`) as any return res.data }, }) const tabs: { key: DetailTab; label: string; icon: any }[] = [ { key: 'basic', label: '基本信息', icon: Users }, { key: 'contract', label: '劳动合同', icon: FileText }, { key: 'payslip', label: '工资条', icon: FileText }, { key: 'overtime', label: '加班记录', icon: Calendar }, { key: 'disciplinary', label: '违纪记录', icon: AlertTriangle }, { key: 'attendance', label: '考勤记录', icon: Calendar }, { key: 'training', label: '培训签收', icon: GraduationCap }, { key: 'performance', label: '绩效考核', icon: TrendingUp }, { key: 'termination', label: '离职/解聘记录', icon: FileText }, { key: 'attachment', label: '附件管理', icon: Paperclip }, { key: 'evidence', label: '仲裁证据链', icon: Scale }, ] if (isLoading) return
加载中...
if (!profile) return
员工不存在
return (

{profile.name} - 完整档案

{profile.status === 'ACTIVE' ? '在职' : '离职'}
{tabs.map((t) => { const Icon = t.icon return ( ) })}
{tab === 'basic' && } {tab === 'contract' && } {tab === 'payslip' && } {tab === 'overtime' && } {tab === 'disciplinary' && } {tab === 'attendance' && } {tab === 'training' && } {tab === 'performance' && } {tab === 'termination' && } {tab === 'attachment' && } {tab === 'evidence' && }
) } function BasicInfo({ profile }: { profile: any }) { const queryClient = useQueryClient() const [editing, setEditing] = useState(false) const [form, setForm] = useState({ department: profile.department || '', gender: profile.gender || '男', phone: profile.phone || '', hireDate: profile.hireDate?.toString().slice(0, 10) || '', monthlySalary: profile.monthlySalary || '', emergencyContact: profile.emergencyContact || '', emergencyPhone: profile.emergencyPhone || '', address: profile.address || '', bankName: profile.bankName || '', bankAccount: profile.bankAccount || '', isPregnant: profile.isPregnant || false, isInMedicalPeriod: profile.isInMedicalPeriod || false, isWorkInjured: profile.isWorkInjured || false, socialInsBase: profile.socialInsBase ?? '', housingFundBase: profile.housingFundBase ?? '', specialDeduction: profile.specialDeduction ?? 0, }) const updateMutation = useMutation({ mutationFn: (data: any) => api.put(`/employees/${profile.id}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) setEditing(false) }, }) const handleSave = () => { const data: any = { department: form.department, gender: form.gender, phone: form.phone || undefined, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: String(form.monthlySalary), emergencyContact: form.emergencyContact || undefined, emergencyPhone: form.emergencyPhone || undefined, address: form.address || undefined, bankName: form.bankName || undefined, bankAccount: form.bankAccount || undefined, isPregnant: form.isPregnant, isInMedicalPeriod: form.isInMedicalPeriod, isWorkInjured: form.isWorkInjured, socialInsBase: form.socialInsBase === '' ? null : Number(form.socialInsBase), housingFundBase: form.housingFundBase === '' ? null : Number(form.housingFundBase), specialDeduction: Number(form.specialDeduction) || 0, } updateMutation.mutate(data) } const fields = [ { label: '姓名', value: profile.name }, { label: '部门', value: profile.department }, { label: '性别', value: profile.gender || '未填写' }, { label: '身份证号', value: profile.idCardNumber || '未填写' }, { label: '手机号', value: profile.phone || '未填写' }, { label: '入职日期', value: profile.hireDate?.toString().slice(0, 10) }, { label: '月工资', value: `¥${fmt(profile.monthlySalary)}` }, { label: '紧急联系人', value: profile.emergencyContact || '未填写' }, { label: '紧急联系电话', value: profile.emergencyPhone || '未填写' }, { label: '住址', value: profile.address || '未填写' }, { label: '开户行', value: profile.bankName || '未填写' }, { label: '银行账号', value: profile.bankAccount || '未填写' }, { label: '状态', value: profile.status === 'ACTIVE' ? '在职' : '离职' }, ] const special = [ { label: '孕期', value: profile.isPregnant }, { label: '医疗期', value: profile.isInMedicalPeriod }, { label: '工伤', value: profile.isWorkInjured }, ] return (

基本信息

{!editing ? ( ) : (
)}
{!editing ? (
{fields.map((f) => (
{f.label} {f.value}
))}
) : (
setForm({ ...form, department: e.target.value })} />
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
setForm({ ...form, hireDate: e.target.value })} />
setForm({ ...form, monthlySalary: Number(e.target.value) })} />
setForm({ ...form, emergencyContact: e.target.value })} placeholder="选填" />
setForm({ ...form, emergencyPhone: e.target.value })} placeholder="选填" />
setForm({ ...form, address: e.target.value })} placeholder="选填" />
setForm({ ...form, bankName: e.target.value })} placeholder="选填" />
setForm({ ...form, bankAccount: e.target.value })} placeholder="选填" />
)} {/* 薪税信息 */}

薪税信息

{!editing ? (
社保缴费基数 {profile.socialInsBase ? `¥${fmt(profile.socialInsBase)}` : '未设置'}
公积金缴费基数 {profile.housingFundBase ? `¥${fmt(profile.housingFundBase)}` : '未设置'}
专项附加扣除 {profile.specialDeduction ? `¥${fmt(profile.specialDeduction)}/月` : '¥0.00/月'}
) : (
setForm({ ...form, socialInsBase: e.target.value })} />
setForm({ ...form, housingFundBase: e.target.value })} />
setForm({ ...form, specialDeduction: Number(e.target.value) || 0 })} />
)}

社保/公积金基数按上年度月均工资核定,每年7月调整。专项附加扣除由员工在portal端填报,无则为0。

{/* 特殊状态 */}

特殊状态

{!editing ? (
{special.map((s) => ( {s.label}:{s.value ? '是' : '否'} ))}
) : (
)} {(profile.isPregnant || profile.isInMedicalPeriod || profile.isWorkInjured) && !editing && (

⚠️ 该员工处于特殊保护期,解聘操作将触发法律风险预警

)}
) } function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', electronicContractNo: '', electronicContractUrl: '' }) const contractFileRef = useRef(null) const addContractMutation = useMutation({ mutationFn: (data: any) => api.post('/employees/contracts', { ...data, employeeId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const handleContractFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = (event) => { setForm({ ...form, attachmentUrl: event.target?.result as string }) } reader.readAsDataURL(file) } const typeMap: Record = { FIXED: '固定期限', UNFIXED: '无固定期限', UNSIGNED: '未签订' } // 按劳动合同法自动判断续签和合同类型建议 const contractAdvice = (() => { if (!contracts?.length) return null const fixedContracts = contracts.filter((c: any) => c.contractType === 'FIXED') const latestContract = contracts[0] const isRenewal = !!latestContract?.endDate const renewalCount = (latestContract?.renewalCount || 0) // 连续订立二次固定期限劳动合同,第三次应订立无固定期限 const shouldUnfixed = fixedContracts.length >= 2 // 连续工作满十年 const yearsSinceHire = hireDate ? (Date.now() - new Date(hireDate).getTime()) / (365.25 * 24 * 60 * 60 * 1000) : 0 const shouldUnfixedByTenure = yearsSinceHire >= 10 if (shouldUnfixed || shouldUnfixedByTenure) { return { isRenewal, renewalCount: isRenewal ? renewalCount + 1 : renewalCount, suggestedType: 'UNFIXED', reason: shouldUnfixed ? `已连续签订${fixedContracts.length}次固定期限合同,按《劳动合同法》第十四条应订立无固定期限合同` : `连续工作满${Math.floor(yearsSinceHire)}年,按《劳动合同法》第十四条应订立无固定期限合同`, } } if (isRenewal) { return { isRenewal: true, renewalCount: renewalCount + 1, suggestedType: 'FIXED', reason: `本次为第${renewalCount + 1}次续签`, } } return null })() const handleShowForm = () => { if (contractAdvice?.suggestedType) { setForm({ ...form, contractType: contractAdvice.suggestedType, signDate: new Date().toISOString().slice(0, 10), startDate: contractAdvice.isRenewal && contracts[0]?.endDate ? contracts[0].endDate.toString().slice(0, 10) : new Date().toISOString().slice(0, 10), endDate: '', probationMonths: 0, probationSalary: 0, signMethod: 'PAPER', attachmentUrl: '', electronicContractNo: '', electronicContractUrl: '', }) } setShowForm(!showForm) } return (

劳动合同({contracts?.length || 0}份)

{contractAdvice && !showForm && (
{contractAdvice.reason},建议选择「{contractAdvice.suggestedType === 'UNFIXED' ? '无固定期限' : '固定期限'}」
)} {showForm && ( {contractAdvice && (
{contractAdvice.reason}
)}
setForm({ ...form, signDate: e.target.value })} />
setForm({ ...form, startDate: e.target.value })} />
{form.contractType === 'FIXED' && (
setForm({ ...form, endDate: e.target.value })} />
)} {form.contractType === 'FIXED' && !contractAdvice?.isRenewal && ( <>
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} />
setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} />
)}
{form.signMethod === 'PAPER' && (
{form.attachmentUrl && ✓ 已上传}
)} {form.signMethod === 'ELECTRONIC' && ( <>
setForm({ ...form, electronicContractNo: e.target.value })} placeholder="如 E-2026-001" />
setForm({ ...form, electronicContractUrl: e.target.value })} placeholder="https://..." />
)}
)} {!contracts?.length ? (
暂无合同记录
) : contracts.map((c) => (
合同类型{typeMap[c.contractType] || c.contractType}
签订日期{c.signDate ? c.signDate.toString().slice(0, 10) : '未签订'}
合同开始{c.startDate?.toString().slice(0, 10)}
合同结束{c.endDate ? c.endDate.toString().slice(0, 10) : '无固定期限'}
合同期限{c.contractYears}年
试用期{c.probationMonths}个月(¥{c.probationSalary})
签订方式{c.signMethod === 'PAPER' ? '纸质' : '电子'}
续签次数{c.renewalCount}
{c.signMethod === 'PAPER' && (
合同扫描件 {c.attachmentUrl ? ( 查看扫描件 ) : ( 未上传 )}
)} {c.signMethod === 'ELECTRONIC' && ( <> {c.electronicContractNo &&
电子合同编号{c.electronicContractNo}
} {c.electronicContractUrl && ( )} )}
))}
) } function ResignModal({ employee, onClose, onSubmit, loading, error }: { employee: any onClose: () => void onSubmit: (data: any) => void loading: boolean error: any }) { const [form, setForm] = useState({ terminationDate: new Date().toISOString().slice(0, 10), resignationReason: '个人原因', remark: '', }) const reasons = ['个人原因', '职业发展', '薪资不满意', '家庭原因', '身体原因', '其他'] const handleSubmit = () => { onSubmit({ employeeId: employee.id, terminationDate: new Date(form.terminationDate).toISOString(), resignationReason: form.resignationReason, remark: form.remark || undefined, }) } return (
员工主动离职,不涉及经济补偿金。离职日期可在未来(提前办理),到日期后状态自动变为离职。
{employee.name} - {employee.department}
setForm({ ...form, terminationDate: e.target.value })} />
setForm({ ...form, remark: e.target.value })} placeholder="补充说明" />
{error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } function RehireModal({ employee, onClose, onSubmit, loading, error }: { employee: any onClose: () => void onSubmit: (data: any) => void loading: boolean error: any }) { const todayStr = new Date().toISOString().slice(0, 10) const defaultEndDate = (() => { const d = new Date() d.setFullYear(d.getFullYear() + 3) d.setDate(d.getDate() - 1) return d.toISOString().slice(0, 10) })() const [form, setForm] = useState({ hireDate: todayStr, contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED', signDate: '', startDate: todayStr, endDate: defaultEndDate, contractYears: 3, probationMonths: 0, probationSalary: 0, }) // 计算合同月数 const contractMonths = (() => { if (form.contractType !== 'FIXED' || !form.startDate) return 0 if (form.endDate) { const start = new Date(form.startDate) const end = new Date(form.endDate) return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44)) } return form.contractYears * 12 })() // 试用期上限(劳动合同法第19条) const probationMax = (() => { if (contractMonths >= 36) return 6 if (contractMonths >= 12) return 2 if (contractMonths >= 3) return 1 return 0 })() const probationError = (() => { if (form.probationMonths <= 0) return '' if (contractMonths > 0 && contractMonths < 3) return '合同不足3个月,不得约定试用期' if (form.probationMonths > probationMax) return `合同${contractMonths}个月,试用期最多${probationMax}个月` return '' })() const monthlySalaryNum = employee?.monthlySalary || 0 const probationSalaryError = (() => { if (form.probationMonths <= 0) return '' if (form.probationSalary <= 0) return '有试用期时试用期工资必填' if (monthlySalaryNum > 0 && form.probationSalary < monthlySalaryNum * 0.8) { return `试用期工资不得低于转正工资的80%(最低¥${(monthlySalaryNum * 0.8).toFixed(0)})` } return '' })() // 合同结束日期自动计算 const handleContractYearsChange = (years: number) => { if (!form.startDate || years <= 0) { setForm({ ...form, contractYears: years, endDate: '' }) return } const start = new Date(form.startDate) const end = new Date(start) end.setFullYear(end.getFullYear() + years) end.setDate(end.getDate() - 1) setForm({ ...form, contractYears: years, endDate: end.toISOString().slice(0, 10) }) } // 合同结束日期变更 → 自动计算签约年限 const handleEndDateChange = (endDate: string) => { if (!form.startDate || !endDate) { setForm({ ...form, endDate, contractYears: 0 }) return } const start = new Date(form.startDate) const end = new Date(endDate) const months = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44)) setForm({ ...form, endDate, contractYears: Math.max(1, Math.round(months / 12)) }) } // 入职日期变更 → 同步合同开始日期 + 重算结束日期 const handleHireDateChange = (hireDate: string) => { if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) { const start = new Date(hireDate) const end = new Date(start) end.setFullYear(end.getFullYear() + form.contractYears) end.setDate(end.getDate() - 1) setForm({ ...form, hireDate, startDate: hireDate, endDate: end.toISOString().slice(0, 10) }) } else { setForm({ ...form, hireDate, startDate: hireDate }) } } // 开始日期变更 → 重新计算结束日期 const handleStartDateChange = (startDate: string) => { if (form.contractType === 'FIXED' && form.contractYears > 0 && startDate) { const start = new Date(startDate) const end = new Date(start) end.setFullYear(end.getFullYear() + form.contractYears) end.setDate(end.getDate() - 1) setForm({ ...form, startDate, endDate: end.toISOString().slice(0, 10) }) } else { setForm({ ...form, startDate }) } } const handleSubmit = () => { const data: any = { hireDate: new Date(form.hireDate).toISOString(), } if (form.contractType !== 'UNSIGNED' && form.startDate) { data.contract = { signDate: form.signDate ? new Date(form.signDate).toISOString() : null, startDate: new Date(form.startDate).toISOString(), endDate: form.endDate ? new Date(form.endDate).toISOString() : null, contractType: form.contractType, contractYears: form.contractYears, probationMonths: form.probationMonths, probationSalary: form.probationSalary, } } onSubmit(data) } const canSubmit = form.hireDate && (form.contractType === 'UNSIGNED' || form.startDate) && !probationError && !probationSalaryError return (
复用员工已有基本信息(姓名、部门、身份证号等),只需填写新入职日期和劳动合同。
{employee.name}
{employee.department}
handleHireDateChange(e.target.value)} />
{form.contractType !== 'UNSIGNED' && (
setForm({ ...form, signDate: e.target.value })} />
留空表示尚未签订
{form.startDate || '随入职日期'}
{form.contractType === 'FIXED' && ( <>
handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
handleEndDateChange(e.target.value)} />
修改签约时长自动计算结束日期,修改结束日期自动计算签约时长
)}
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /> {contractMonths > 0 && (
法定上限:{probationMax}个月
)} {probationError &&
{probationError}
}
setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} /> {monthlySalaryNum > 0 && form.probationMonths > 0 && (
不低于转正工资80%(≥¥{(monthlySalaryNum * 0.8).toFixed(0)})
)} {probationSalaryError &&
{probationSalaryError}
}
)} {error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } function AddEmployeeModal({ onClose, onSubmit, loading, error }: { onClose: () => void onSubmit: (data: any) => void loading: boolean error: any }) { const todayStr = new Date().toISOString().slice(0, 10) const defaultEndDate = (() => { const d = new Date() d.setFullYear(d.getFullYear() + 3) d.setDate(d.getDate() - 1) return d.toISOString().slice(0, 10) })() const [form, setForm] = useState({ name: '', department: '', hireDate: todayStr, monthlySalary: '', idCardNumber: '', gender: '男' as '男' | '女', phone: '', contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED', signDate: '', startDate: todayStr, endDate: defaultEndDate, contractYears: 3, probationMonths: 0, probationSalary: 0, }) // 入职日期变更 → 同步合同开始日期 + 重算结束日期 const handleHireDateChange = (hireDate: string) => { if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) { const start = new Date(hireDate) const end = new Date(start) end.setFullYear(end.getFullYear() + form.contractYears) end.setDate(end.getDate() - 1) setForm({ ...form, hireDate, startDate: hireDate, endDate: end.toISOString().slice(0, 10) }) } else { setForm({ ...form, hireDate, startDate: hireDate }) } } // 根据身份证号自动计算性别(第17位:奇数=男,偶数=女) const handleIdCardChange = (idCard: string) => { let gender = form.gender if (idCard.length >= 17) { const digit = parseInt(idCard[16]) if (!isNaN(digit)) gender = digit % 2 === 1 ? '男' : '女' } setForm({ ...form, idCardNumber: idCard, gender }) } // 计算合同月数 const contractMonths = (() => { if (form.contractType !== 'FIXED' || !form.startDate) return 0 if (form.endDate) { const start = new Date(form.startDate) const end = new Date(form.endDate) return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44)) } return form.contractYears * 12 })() // 试用期上限(劳动合同法第19条) const probationMax = (() => { if (contractMonths >= 36) return 6 if (contractMonths >= 12) return 2 if (contractMonths >= 3) return 1 return 0 })() const probationError = (() => { if (form.probationMonths <= 0) return '' if (contractMonths > 0 && contractMonths < 3) return '合同不足3个月,不得约定试用期' if (form.probationMonths > probationMax) return `合同${contractMonths}个月,试用期最多${probationMax}个月` return '' })() const monthlySalaryNum = parseFloat(form.monthlySalary) || 0 const probationSalaryError = (() => { if (form.probationMonths <= 0) return '' if (form.probationSalary <= 0) return '有试用期时试用期工资必填' if (monthlySalaryNum > 0 && form.probationSalary < monthlySalaryNum * 0.8) { return `试用期工资不得低于转正工资的80%(最低¥${(monthlySalaryNum * 0.8).toFixed(0)})` } return '' })() // 合同结束日期自动计算 const handleContractYearsChange = (years: number) => { if (!form.startDate || years <= 0) { setForm({ ...form, contractYears: years, endDate: '' }) return } const start = new Date(form.startDate) const end = new Date(start) end.setFullYear(end.getFullYear() + years) end.setDate(end.getDate() - 1) setForm({ ...form, contractYears: years, endDate: end.toISOString().slice(0, 10) }) } // 合同结束日期变更 → 自动计算签约年限 const handleEndDateChange = (endDate: string) => { if (!form.startDate || !endDate) { setForm({ ...form, endDate, contractYears: 0 }) return } const start = new Date(form.startDate) const end = new Date(endDate) const months = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44)) setForm({ ...form, endDate, contractYears: Math.max(1, Math.round(months / 12)) }) } // 开始日期变更 → 重新计算结束日期 const handleStartDateChange = (startDate: string) => { if (form.contractType === 'FIXED' && form.contractYears > 0 && startDate) { const start = new Date(startDate) const end = new Date(start) end.setFullYear(end.getFullYear() + form.contractYears) end.setDate(end.getDate() - 1) setForm({ ...form, startDate, endDate: end.toISOString().slice(0, 10) }) } else { setForm({ ...form, startDate }) } } const handleSubmit = () => { const data: any = { name: form.name, department: form.department, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: form.monthlySalary, gender: form.gender, idCardNumber: form.idCardNumber || undefined, phone: form.phone || undefined, } if (form.contractType !== 'UNSIGNED' && form.startDate) { data.contract = { signDate: form.signDate ? new Date(form.signDate).toISOString() : null, startDate: new Date(form.startDate).toISOString(), endDate: form.endDate ? new Date(form.endDate).toISOString() : null, contractType: form.contractType, contractYears: form.contractYears, probationMonths: form.probationMonths, probationSalary: form.probationSalary, } } onSubmit(data) } const canSubmit = form.name && form.department && form.hireDate && form.monthlySalary && form.idCardNumber.length >= 18 && (form.contractType === 'UNSIGNED' || form.startDate) && !probationError && !probationSalaryError return (
{error && (
{error.response?.data?.error?.message || '操作失败'}
)}
setForm({ ...form, name: e.target.value })} placeholder="员工姓名" />
setForm({ ...form, department: e.target.value })} placeholder="如:技术部" />
handleIdCardChange(e.target.value)} placeholder="18位身份证号" maxLength={18} />
{form.idCardNumber.length >= 17 ? form.gender : '由身份证号自动识别'}
handleHireDateChange(e.target.value)} />
setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" />
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
{form.contractType !== 'UNSIGNED' && (
setForm({ ...form, signDate: e.target.value })} />
留空表示尚未签订
{form.startDate || '随入职日期'}
{form.contractType === 'FIXED' && ( <>
handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
handleEndDateChange(e.target.value)} />
修改签约时长自动计算结束日期,修改结束日期自动计算签约时长
)}
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /> {contractMonths > 0 && (
法定上限:{probationMax}个月
)} {probationError &&
{probationError}
}
setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} /> {monthlySalaryNum > 0 && form.probationMonths > 0 && (
不低于转正工资80%(≥¥{(monthlySalaryNum * 0.8).toFixed(0)})
)} {probationSalaryError &&
{probationSalaryError}
}
)}
) } function AttachmentInfo({ employeeId, attachments }: { employeeId: string; attachments: any[] }) { const queryClient = useQueryClient() const fileInputRef = useRef(null) const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'OTHER'>('ID_CARD') const addAttachmentMutation = useMutation({ mutationFn: (data: any) => api.post('/attachments', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const deleteAttachmentMutation = useMutation({ mutationFn: (id: string) => api.delete(`/attachments/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = (event) => { const fileUrl = event.target?.result as string addAttachmentMutation.mutate({ employeeId, fileName: file.name, fileType, fileUrl, fileSize: file.size }) } reader.readAsDataURL(file) } const fileTypeLabels: Record = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', OTHER: '其他' } const fileTypeColors: Record = { ID_CARD: 'bg-blue-50 text-blue-600', BANK_CARD: 'bg-green-50 text-safe', EDUCATION: 'bg-amber-50 text-amber-600', OTHER: 'bg-gray-100 text-gray-500' } const formatSize = (bytes: number) => { if (!bytes) return '-' if (bytes < 1024) return `${bytes}B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB` return `${(bytes / 1024 / 1024).toFixed(1)}MB` } return (

附件管理({attachments?.length || 0}个)

{attachments?.length ? (
{attachments.map((att) => (
{att.fileName}
{fileTypeLabels[att.fileType] || att.fileType} {formatSize(att.fileSize)} {new Date(att.createdAt).toLocaleDateString('zh-CN')}
))}
) :
暂无附件
}
) } function PayslipInfo({ payslips }: { payslips: any[] }) { if (!payslips?.length) return
暂无工资条记录
const totalBase = payslips.reduce((s, p) => s + (p.baseSalary || 0), 0) const totalOT = payslips.reduce((s, p) => s + (p.overtimePay || 0), 0) const totalAllow = payslips.reduce((s, p) => s + (p.allowance || 0), 0) const totalDed = payslips.reduce((s, p) => s + (p.deduction || 0), 0) const totalPay = payslips.reduce((s, p) => s + (p.totalPay || 0), 0) return ( {payslips.map((p) => ( ))}
月份 基本工资 加班费 津贴 扣款 应发合计 确认状态
{p.month} ¥{fmt(p.baseSalary)} {p.overtimePay > 0 ? `¥${fmt(p.overtimePay)}` : '-'} {p.allowance > 0 ? `¥${fmt(p.allowance)}` : '-'} {p.deduction > 0 ? `-¥${fmt(p.deduction)}` : '-'} ¥{fmt(p.totalPay)} {p.confirmedAt ? ( 已确认 ) : ( 未确认 )}
合计 ¥{fmt(totalBase)} {totalOT > 0 ? `¥${fmt(totalOT)}` : '-'} {totalAllow > 0 ? `¥${fmt(totalAllow)}` : '-'} {totalDed > 0 ? `-¥${fmt(totalDed)}` : '-'} ¥{fmt(totalPay)}
) } function OvertimeInfo({ records }: { records: any[] }) { if (!records?.length) return
暂无加班记录
const totalPay = records.reduce((sum, o) => sum + (o.totalPay || 0), 0) const totalWeekday = records.reduce((sum, o) => sum + (o.weekdayHours || 0), 0) const totalWeekend = records.reduce((sum, o) => sum + (o.weekendHours || 0), 0) const totalHoliday = records.reduce((sum, o) => sum + (o.holidayHours || 0), 0) return ( {records.map((o) => ( ))}
月份 工作日(h) 休息日(h) 节假日(h) 加班费
{o.month} {o.weekdayHours || '-'} {o.weekendHours || '-'} {o.holidayHours || '-'} ¥{fmt(o.totalPay)}
合计 {totalWeekday} {totalWeekend} {totalHoliday} ¥{fmt(totalPay)}
) } function TerminationInfo({ employeeId, profile, records }: { employeeId: string; profile: any; records: any[] }) { const [printRecord, setPrintRecord] = useState(null) const reasonMap: Record = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', ILLEGAL: '违法解除', RESIGNATION: '员工主动离职', } const legalBasisMap: Record = { NEGOTIATED: '《劳动合同法》第36条', FAULT: '《劳动合同法》第39条', NONFAULT: '《劳动合同法》第40条', LAYOFF: '《劳动合同法》第41条', EXPIRED: '《劳动合同法》第44条、第46条', ILLEGAL: '《劳动合同法》第87条', } const { data: evidenceChain } = useQuery({ queryKey: ['evidence-chain', employeeId], queryFn: async () => { const res = await api.get(`/roster/${employeeId}/evidence-chain`) as any return res.data }, enabled: !!printRecord, }) if (!records?.length) return
暂无离职/解聘记录
if (printRecord) { return (
{/* 1. 解聘通知书 */}

解除劳动合同通知书

{profile.name} 先生/女士:

您于 {profile.hireDate?.toString().slice(0, 10)} 入职我公司{profile.department}部门。 因 {reasonMap[printRecord.reason] || printRecord.reason} 原因,公司决定于 {printRecord.terminationDate?.toString().slice(0, 10)} 起解除与您的劳动合同。

解除依据:{legalBasisMap[printRecord.reason] || ''}

经济补偿金:¥{fmt(printRecord.compensation)}

{printRecord.remark &&

备注:{printRecord.remark}

}

请于解除日期前办理工作交接手续,结清相关费用。

公司(盖章)

{printRecord.terminationDate?.toString().slice(0, 10)}

{/* 2. 费用结算明细 */}

费用结算明细

员工{profile.name}({profile.department})
入职日期{profile.hireDate?.toString().slice(0, 10)}
月工资¥{fmt(profile.monthlySalary)}/月
解聘日期{printRecord.terminationDate?.toString().slice(0, 10)}
解聘原因{reasonMap[printRecord.reason] || printRecord.reason}
经济补偿金¥{fmt(printRecord.compensation)}
{/* 3. 合规检查清单 */} {printRecord.checklist && Object.keys(printRecord.checklist).length > 0 && (

合规检查清单

{Object.entries(printRecord.checklist).map(([key, passed]: [string, any]) => (
{passed ? '✓' : '✗'} {key}
))}
)} {/* 4. 风险评估 */} {printRecord.riskLevel && printRecord.riskLevel !== 'SAFE' && (

风险评估

风险等级:{printRecord.riskLevel === 'DANGER' ? '高风险' : '注意'}
)} {/* 5. 仲裁证据链 */}

仲裁证据链

{evidenceChain ? ( <>
共 {evidenceChain.summary?.total || 0} 条证据, 已签确认 {evidenceChain.summary?.signed || 0} 条, 未签 {evidenceChain.summary?.unsigned || 0} 条
{(() => { const grouped = (evidenceChain.evidence || []).reduce((acc: Record, e: any) => { (acc[e.category] = acc[e.category] || []).push(e) return acc }, {}) return Object.entries(grouped).map(([category, items]) => (
{category}
{(items as any[]).map((e: any, i: number) => (
{e.title} {e.acknowledged === true && ✓已签} {e.acknowledged === false && ✗未签}
{e.description}
))}
)) })()} ) : (
加载证据链中...
)}
) } return (
{records.map((t) => (
{t.terminationDate?.toString().slice(0, 10)} {t.type === 'RESIGNATION' ? '主动离职' : '公司解聘'} {reasonMap[t.reason] || t.reason} {t.type !== 'RESIGNATION' && ( {t.riskLevel === 'SAFE' ? '风险低' : t.riskLevel === 'WARNING' ? '注意' : '高风险'} )}
{t.type === 'RESIGNATION' ? ( <>
离职原因 {t.resignationReason || '-'}
) : ( <>
经济补偿金 ¥{fmt(t.compensation)}
法律依据 {legalBasisMap[t.reason] || '-'}
)}
{t.remark &&
{t.remark}
}
{t.type !== 'RESIGNATION' && ( )}
))}
) } // ========== 违纪记录管理 ========== function DisciplinaryInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ violationDate: '', violationType: 'LATE', description: '', severity: 'WARNING', action: 'ORAL_WARNING', actionDetail: '', employeeAck: false, ackDate: '', ackMethod: 'SIGN', witness: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/disciplinary`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/disciplinary/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const typeMap: Record = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' } const actionMap: Record = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' } const severityMap: Record = { WARNING: '警告', SERIOUS: '严重', SEVERE: '重度' } return (

违纪记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, violationDate: e.target.value })} />
setForm({ ...form, description: e.target.value })} placeholder="详细描述违纪事实" />
setForm({ ...form, actionDetail: e.target.value })} placeholder="扣款金额/降职说明等" />
setForm({ ...form, witness: e.target.value })} />
setForm({ ...form, employeeAck: e.target.checked })} />
{form.employeeAck &&
setForm({ ...form, ackDate: e.target.value })} />
}
)} {records?.length === 0 ? (
暂无违纪记录
) : records?.map((r) => (
{r.violationDate?.toString().slice(0, 10)} {severityMap[r.severity] || r.severity} {typeMap[r.violationType] || r.violationType}
{r.description}
处理方式 {actionMap[r.action] || r.action} {r.actionDetail && {r.actionDetail}}
{r.employeeAck ? ( 已签字({r.ackDate?.toString().slice(0, 10)}) ) : ( 未签字 )} {r.witness && 见证人:{r.witness}} {r.ackMethod && 确认方式:{r.ackMethod === 'SIGN' ? '签字' : r.ackMethod === 'ELECTRONIC' ? '电子' : '拒绝'}}
))}
) } // ========== 考勤记录管理 ========== function AttendanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ date: '', checkInTime: '', checkOutTime: '', status: 'NORMAL', lateMinutes: 0, earlyMinutes: 0, workHours: 8, overtimeHours: 0, remark: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/attendance`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/attendance/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const statusMap: Record = { NORMAL: '正常', LATE: '迟到', EARLY_LEAVE: '早退', ABSENT: '旷工', LEAVE: '请假', BUSINESS_TRIP: '出差' } const statusColor: Record = { NORMAL: 'bg-green-50 text-safe', LATE: 'bg-amber-50 text-warning', EARLY_LEAVE: 'bg-amber-50 text-warning', ABSENT: 'bg-red-50 text-danger', LEAVE: 'bg-blue-50 text-blue-600', BUSINESS_TRIP: 'bg-blue-50 text-blue-600' } return (

考勤记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, date: e.target.value })} />
setForm({ ...form, checkInTime: e.target.value })} />
setForm({ ...form, checkOutTime: e.target.value })} />
setForm({ ...form, lateMinutes: Number(e.target.value) })} />
setForm({ ...form, earlyMinutes: Number(e.target.value) })} />
setForm({ ...form, workHours: Number(e.target.value) })} />
setForm({ ...form, overtimeHours: Number(e.target.value) })} />
setForm({ ...form, remark: e.target.value })} />
)} {records?.length === 0 ? (
暂无考勤记录
) : ( {records?.map((a) => ( ))}
日期 签到 签退 状态 工时 加班
{a.date?.toString().slice(0, 10)} {a.checkInTime || '-'} {a.checkOutTime || '-'} {statusMap[a.status] || a.status} {a.workHours}h {a.overtimeHours > 0 ? `${a.overtimeHours}h` : '-'}
)}
) } // ========== 培训签收记录管理 ========== function TrainingInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ trainingDate: '', topic: '', content: '', trainer: '', duration: 1, ackStatus: 'PENDING', ackDate: '', remark: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/training`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/training/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const ackMap: Record = { PENDING: '待签收', SIGNED: '已签收', REFUSED: '拒绝签收' } const ackColor: Record = { PENDING: 'bg-amber-50 text-warning', SIGNED: 'bg-green-50 text-safe', REFUSED: 'bg-red-50 text-danger' } return (

培训签收记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, trainingDate: e.target.value })} />
setForm({ ...form, topic: e.target.value })} placeholder="如《员工手册》培训" />
setForm({ ...form, content: e.target.value })} />
setForm({ ...form, trainer: e.target.value })} />
setForm({ ...form, duration: Number(e.target.value) })} />
{form.ackStatus === 'SIGNED' &&
setForm({ ...form, ackDate: e.target.value })} />
}
setForm({ ...form, remark: e.target.value })} />
)} {records?.length === 0 ? (
暂无培训签收记录
) : records?.map((r) => (
{r.trainingDate?.toString().slice(0, 10)} {r.ackStatus === 'SIGNED' ? '已签收' : r.ackStatus === 'REFUSED' ? '拒绝签收' : '待签收'}
{r.topic}
{r.content &&
{r.content}
}
时长 {r.duration}h {r.trainer && 培训人:{r.trainer}} {r.ackDate && 签收日期:{r.ackDate.toString().slice(0, 10)}} {r.remark && 备注:{r.remark}}
))}
) } // ========== 绩效记录管理 ========== function PerformanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ period: '', score: 80, grade: 'B', result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/performance`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/performance/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const resultMap: Record = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' } const resultColor: Record = { EXCELLENT: 'bg-green-50 text-safe', QUALIFIED: 'bg-blue-50 text-blue-600', NEED_IMPROVE: 'bg-amber-50 text-warning', UNQUALIFIED: 'bg-red-50 text-danger' } return (

绩效考核记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, period: e.target.value })} placeholder="如 2026-07 或 2026-Q3" />
setForm({ ...form, score: Number(e.target.value) })} />
setForm({ ...form, summary: e.target.value })} />
setForm({ ...form, improvementPlan: e.target.value })} placeholder="如:调岗至XX岗位,培训XX技能" />
setForm({ ...form, reviewer: e.target.value })} />
setForm({ ...form, employeeAck: e.target.checked })} />
{form.employeeAck &&
setForm({ ...form, ackDate: e.target.value })} />
}
)} {records?.length === 0 ? (
暂无绩效记录
) : records?.map((r) => (
{r.period} {resultMap[r.result] || r.result} 得分 {r.score} · 等级 {r.grade}
{r.summary &&
{r.summary}
} {r.improvementPlan && (
改进计划:{r.improvementPlan}
)}
{r.employeeAck ? ( 已签字({r.ackDate?.toString().slice(0, 10)}) ) : ( 未签字 )} {r.reviewer && 考核人:{r.reviewer}}
))}
) } // ========== 仲裁证据链 ========== function EvidenceChain({ employeeId }: { employeeId: string }) { const { data, isLoading } = useQuery({ queryKey: ['evidence-chain', employeeId], queryFn: async () => { const res = await api.get(`/roster/${employeeId}/evidence-chain`) as any return res.data }, }) if (isLoading) return
生成证据链中...
if (!data) return
无数据
const categoryColor: Record = { '劳动关系': '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) } return (

仲裁证据链

{data.employee.name} · {data.employee.department} · 入职{data.employee.hireDate}
证据总数
{data.summary.total}
已签字
{data.summary.signed}
未签字
{data.summary.unsigned}
{data.evidence.map((e: any, i: number) => (
{e.category}
{e.title} {e.date} {e.acknowledged === true && ✓ 已签字} {e.acknowledged === false && ⚠ 未签字}
{e.description}
))}
) } 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') }