import { useState, useRef } from 'react' import { usePageSize } from '../hooks/usePageSize' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { FileText, Copy, X, Download, BookOpen, HelpCircle, Plus, Edit, Trash2, Building2, Upload } from 'lucide-react' import { toast } from 'sonner' import { templatesApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' import mammoth from 'mammoth' import Modal from '../components/ui/Modal' import EmptyState from '../components/ui/EmptyState' import Pagination from '../components/ui/Pagination' import { useConfirm } from '../hooks/useConfirm' import PageGuide from '../components/ui/PageGuide' const CATEGORY_LABELS: Record = { CONTRACT: '合同', RULES: '规章制度', NOTICE: '通知', AGREEMENT: '协议', OTHER: '其他', } const VARIABLE_LABELS: Record = { 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 [tab, setTab] = useState<'system' | 'enterprise'>('system') return (
模板管理,维护各类法律文书模板,包括劳动合同、离职证明、收入证明等。支持新增、编辑、预览模板,模板变量自动填充员工信息。关联:开具证明和签订合同时使用此处模板。

用工文本模板库

{/* Tab 切换 */}
{tab === 'system' ? : }
) } function SystemTemplates() { const [category, setCategory] = useState('') const [selected, setSelected] = useState(null) const [rendered, setRendered] = useState('') const [variables, setVariables] = useState>({}) const { data: list, isLoading } = useQuery({ queryKey: ['templates', category], queryFn: async () => { return await templatesApi.list(category || undefined) }, }) const { data: detail } = useQuery({ queryKey: ['template-detail', selected?.id], queryFn: async () => { return await templatesApi.detail(selected.id) }, enabled: !!selected, }) const handleRender = async () => { if (!selected) return try { const res = await templatesApi.render(selected.id, variables) as any setRendered(res.content) } catch (err: any) { toast.error('渲染失败') } } const [showHelp, setShowHelp] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(rendered) toast.success('已复制到剪贴板') } const handleDownloadWord = () => { if (!selected) return const token = useAuthStore.getState().accessToken const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1' const query = new URLSearchParams() query.set('token', token || '') for (const [k, v] of Object.entries(variables)) { if (v) query.append(k, v) } const a = document.createElement('a') a.href = `${baseURL}/templates/${selected.id}/download?${query.toString()}` a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) } const handleCopyAsNew = () => { const text = rendered || detail?.content || '' navigator.clipboard.writeText(text) toast.success('模板内容已复制,可粘贴到 Word 中编辑使用') } return (

合同、制度、通知等常用文本模板,支持变量替换

{showHelp && (
使用说明
1. 选择需要的模板分类(合同/制度/通知/协议),点击模板卡片打开详情
2. 在弹窗中填写变量字段(如公司名称、员工姓名等),点击「渲染模板」生成完整文本
3. 渲染后可「复制」到剪贴板,或「下载 Word」保存为 .doc 文件
4. 「复制为新模板」将渲染结果复制到剪贴板,可粘贴到 Word 中进一步编辑
5. 变量字段为选填,未填写的变量将保留 {'{{变量名}}'} 占位符
)}
{['', 'CONTRACT', 'RULES', 'NOTICE', 'AGREEMENT', 'OTHER'].map(c => ( ))}
{isLoading ? (
加载中...
) : !list || list.length === 0 ? ( ) : (
{list.map((t: any) => (
{ setSelected(t); setRendered(''); setVariables({}) }}>
{CATEGORY_LABELS[t.category]} {t.name}

{t.description}

{t.variables?.slice(0, 4).map((v: string) => ( {VARIABLE_LABELS[v] || v} ))} {t.variables?.length > 4 && +{t.variables.length - 4}}
))}
)} {/* 模板详情弹窗 */} {selected && (
setSelected(null)}>
e.stopPropagation()}>

{selected.name}

{/* 变量输入 */} {detail?.variables && detail.variables.length > 0 && (
填写变量
{detail.variables.map((v: string) => (
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}`} />
))}
)} {/* 渲染结果 */} {rendered ? (
渲染结果
{rendered}
) : detail?.content ? (
模板原文
{detail.content}
) : null}
)}
) } function EnterpriseTemplates() { const queryClient = useQueryClient() const confirm = useConfirm() const fileInputRef = useRef(null) const [category, setCategory] = useState('') const pageSize = usePageSize() const [page, setPage] = useState(1) const [showEdit, setShowEdit] = useState(false) const [editItem, setEditItem] = useState(null) const [form, setForm] = useState({ name: '', category: 'CONTRACT', description: '', content: '' }) const [selected, setSelected] = useState(null) const [rendered, setRendered] = useState('') const [variables, setVariables] = useState>({}) const { data: listData, isLoading } = useQuery({ queryKey: ['enterprise-templates', category, page, pageSize], queryFn: async () => { const params: any = { page, pageSize } if (category) params.category = category return await templatesApi.enterpriseList({ page, pageSize, category: category || undefined } as any) }, }) const list = listData?.items || [] const total = listData?.total || 0 const { data: detail } = useQuery({ queryKey: ['enterprise-template-detail', selected?.id], queryFn: async () => { return await templatesApi.enterpriseDetail(selected.id) }, enabled: !!selected, }) const saveMutation = useMutation({ mutationFn: async (data: any) => { if (editItem) { return await templatesApi.saveEnterprise(data, editItem.id) } else { return await templatesApi.saveEnterprise(data) } }, onSuccess: () => { toast.success(editItem ? '已更新' : '已创建') queryClient.invalidateQueries({ queryKey: ['enterprise-templates'] }) setShowEdit(false) setEditItem(null) setForm({ name: '', category: 'CONTRACT', description: '', content: '' }) }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '操作失败'), }) const deleteMutation = useMutation({ mutationFn: async (id: string) => { await templatesApi.removeEnterprise(id) }, onSuccess: () => { toast.success('已删除') queryClient.invalidateQueries({ queryKey: ['enterprise-templates'] }) }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '删除失败'), }) const handleRender = async () => { if (!selected) return try { const res = await templatesApi.renderEnterprise(selected.id, variables) as any setRendered(res.content) } catch { toast.error('渲染失败') } } const handleDownloadWord = () => { if (!selected) return const token = useAuthStore.getState().accessToken const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1' const query = new URLSearchParams() query.set('token', token || '') for (const [k, v] of Object.entries(variables)) { if (v) query.append(k, v) } const a = document.createElement('a') a.href = `${baseURL}/enterprise-templates/${selected.id}/download?${query.toString()}` a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) } const handleEdit = (item: any) => { setEditItem(item) setForm({ name: item.name, category: item.category, description: item.description || '', content: item.content }) setShowEdit(true) } const handleAdd = () => { setEditItem(null) setForm({ name: '', category: 'CONTRACT', description: '', content: '' }) setShowEdit(true) } return (

企业自建文本模板,支持变量替换和 Word 下载

{['', 'CONTRACT', 'RULES', 'NOTICE', 'AGREEMENT', 'OTHER'].map(c => ( ))}
{isLoading ? (
加载中...
) : !list || list.length === 0 ? ( ) : (
{list.map((t: any) => (
{ setSelected(t); setRendered(''); setVariables({}) }} className="cursor-pointer">
{CATEGORY_LABELS[t.category]} {t.name}

{t.description}

{t.variables?.slice(0, 4).map((v: string) => ( {VARIABLE_LABELS[v] || v} ))} {t.variables?.length > 4 && +{t.variables.length - 4}}
))}
)} setPage(1)} /> {/* 编辑弹窗 */} setShowEdit(false)} title={editItem ? '编辑模板' : '新建模板'} size="lg">
setForm({ ...form, name: e.target.value })} placeholder="如:员工保密协议" />
setForm({ ...form, description: e.target.value })} placeholder="简要描述模板用途" />
{ const file = e.target.files?.[0] if (!file) return try { const arrayBuffer = await file.arrayBuffer() const result = await mammoth.convertToHtml({ arrayBuffer }) setForm({ ...form, content: result.value }) toast.success('文档导入成功') } catch { toast.error('文档解析失败,请确保为 .docx 格式') } e.target.value = '' }} /> 支持 .docx 格式,导入后转为 HTML