Files
TurboHR/frontend/src/pages/Templates.tsx
T
selfrelease d79e3baa34 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
2026-07-26 20:32:38 +08:00

209 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}