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
+208
View File
@@ -0,0 +1,208 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { FileText, Copy, X, ChevronRight } from 'lucide-react'
import { toast } from 'sonner'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import EmptyState from '../components/ui/EmptyState'
const CATEGORY_LABELS: Record<string, string> = {
CONTRACT: '合同',
RULES: '规章制度',
NOTICE: '通知',
AGREEMENT: '协议',
OTHER: '其他',
}
const VARIABLE_LABELS: Record<string, string> = {
companyName: '公司名称',
employeeName: '员工姓名',
idCard: '身份证号',
address: '地址',
startDate: '开始日期',
position: '岗位',
endDate: '结束日期',
terminationDate: '解除日期',
publishDate: '公示日期',
effectiveDate: '生效日期',
meetingDate: '会议日期',
meetingLocation: '会议地点',
policyTitle: '制度名称',
department: '部门',
violationDate: '违纪日期',
violationDescription: '违纪描述',
probationEndDate: '试用期截止',
regularDate: '转正日期',
contractEndDate: '合同到期日',
salary: '工资',
phone: '手机号',
hireDate: '入职日期',
gender: '性别',
birthDate: '出生日期',
bankAccount: '银行账号',
bankName: '开户银行',
emergencyContact: '紧急联系人',
emergencyPhone: '紧急联系电话',
workYears: '工作年限',
compAmount: '补偿金额',
noticeDate: '通知日期',
reason: '原因',
overtimeHours: '加班时长',
overtimePay: '加班费',
socialInsBase: '社保基数',
housingFundBase: '公积金基数',
compensation: '经济补偿',
lastWorkDay: '最后工作日',
socialInsEndMonth: '社保截止月',
housingFundEndMonth: '公积金截止月',
probationMonths: '试用期月数',
monthlySalary: '月薪',
workplace: '工作地点',
disciplineType: '处分类型',
policyBasis: '制度依据',
}
/**
* 用工文本模板库页面
*/
export default function Templates() {
const [category, setCategory] = useState<string>('')
const [selected, setSelected] = useState<any>(null)
const [rendered, setRendered] = useState<string>('')
const [variables, setVariables] = useState<Record<string, string>>({})
const { data: list, isLoading } = useQuery<any>({
queryKey: ['templates', category],
queryFn: async () => {
const params = category ? `?category=${category}` : ''
const res = await api.get(`/templates${params}`) as any
return res.data
},
})
const { data: detail } = useQuery<any>({
queryKey: ['template-detail', selected?.id],
queryFn: async () => {
const res = await api.get(`/templates/${selected.id}`) as any
return res.data
},
enabled: !!selected,
})
const handleRender = async () => {
if (!selected) return
try {
const res = await api.post(`/templates/${selected.id}/render`, { variables }) as any
setRendered(res.data.content)
} catch (err: any) {
toast.error('渲染失败')
}
}
const handleCopy = () => {
navigator.clipboard.writeText(rendered)
toast.success('已复制到剪贴板')
}
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-primary" />
<h1 className="text-base font-semibold"></h1>
</div>
<p className="text-sm text-gray-500"></p>
<div className="flex gap-2">
{['', 'CONTRACT', 'RULES', 'NOTICE', 'AGREEMENT', 'OTHER'].map(c => (
<button
key={c}
onClick={() => setCategory(c)}
className={`px-3 py-1 text-xs rounded-lg ${category === c ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
>
{c === '' ? '全部' : CATEGORY_LABELS[c]}
</button>
))}
</div>
{isLoading ? (
<div className="text-center py-8 text-gray-500">...</div>
) : !list || list.length === 0 ? (
<EmptyState title="暂无模板" />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{list.map((t: any) => (
<Card key={t.id} className="hover:shadow-md transition-shadow cursor-pointer" >
<div onClick={() => { setSelected(t); setRendered(''); setVariables({}) }}>
<div className="flex items-center gap-2">
<span className="px-1.5 py-0.5 rounded text-xs bg-primary/10 text-primary">{CATEGORY_LABELS[t.category]}</span>
<span className="text-sm font-medium truncate">{t.name}</span>
</div>
<p className="text-xs text-gray-500 mt-1">{t.description}</p>
<div className="flex items-center gap-1 mt-2 text-xs text-gray-400">
{t.variables?.slice(0, 4).map((v: string) => (
<span key={v} className="px-1 py-0.5 rounded bg-gray-100">{VARIABLE_LABELS[v] || v}</span>
))}
{t.variables?.length > 4 && <span>+{t.variables.length - 4}</span>}
</div>
</div>
</Card>
))}
</div>
)}
{/* 模板详情弹窗 */}
{selected && (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 p-4" onClick={() => setSelected(null)}>
<Card className="max-w-3xl w-full max-h-[85vh] overflow-y-auto" >
<div onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium">{selected.name}</h2>
<button onClick={() => setSelected(null)} className="text-gray-400 hover:text-gray-600"><X className="w-5 h-5" /></button>
</div>
{/* 变量输入 */}
{detail?.variables && detail.variables.length > 0 && (
<div className="mb-3 space-y-2">
<div className="text-xs font-medium text-gray-600"></div>
<div className="grid grid-cols-2 gap-2">
{detail.variables.map((v: string) => (
<div key={v}>
<label className="text-xs text-gray-500">{VARIABLE_LABELS[v] || v}</label>
<input
value={variables[v] || ''}
onChange={e => setVariables(prev => ({ ...prev, [v]: e.target.value }))}
className="w-full px-2 py-1 text-sm border rounded focus:outline-none focus:ring-1 focus:ring-primary"
placeholder={`输入${VARIABLE_LABELS[v] || v}`}
/>
</div>
))}
</div>
<Button size="sm" onClick={handleRender}></Button>
</div>
)}
{/* 渲染结果 */}
{rendered ? (
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-600"></span>
<button onClick={handleCopy} className="flex items-center gap-1 text-xs text-primary hover:underline">
<Copy className="w-3 h-3" />
</button>
</div>
<pre className="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-[50vh] overflow-y-auto">{rendered}</pre>
</div>
) : detail?.content ? (
<div>
<div className="text-xs font-medium text-gray-600 mb-2"></div>
<pre className="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-[50vh] overflow-y-auto">{detail.content}</pre>
</div>
) : null}
</div>
</Card>
</div>
)}
</div>
)
}