diff --git a/frontend/src/app/templates/page.tsx b/frontend/src/app/templates/page.tsx index d924175c..cc5e4281 100644 --- a/frontend/src/app/templates/page.tsx +++ b/frontend/src/app/templates/page.tsx @@ -3,8 +3,13 @@ import { CaretDownOutlined, CaretRightOutlined, + CopyOutlined, + EditOutlined, + FileTextOutlined, + HistoryOutlined, MinusCircleOutlined, PlusOutlined, + RollbackOutlined, SearchOutlined, } from "@ant-design/icons"; import { @@ -14,15 +19,16 @@ import { Form, Input, List, + message, Popconfirm, Select, Space, Spin, Switch, Tag, + Tooltip, Tree, Typography, - message, } from "antd"; import type { DataNode } from "antd/es/tree"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -124,21 +130,18 @@ export default function TemplatesPage() { const [rollbackingId, setRollbackingId] = useState(null); const [items, setItems] = useState([]); + const [selectedId, setSelectedId] = useState(null); const [editingId, setEditingId] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(ALL_KEY); const [expandedKeys, setExpandedKeys] = useState([ALL_KEY]); - const [versionDrawerOpen, setVersionDrawerOpen] = useState(false); const [versionLoading, setVersionLoading] = useState(false); const [versionHistory, setVersionHistory] = useState([]); - const [versionTemplate, setVersionTemplate] = useState(null); - const [rollbackingVersion, setRollbackingVersion] = useState(null); - const editingTemplate = useMemo(() => items.find((item) => item.id === editingId) ?? null, [editingId, items]); - const watchedVariables = Form.useWatch("variables", form); + const selectedTemplate = useMemo(() => items.find((item) => item.id === selectedId) ?? null, [items, selectedId]); + const editingTemplate = useMemo(() => items.find((item) => item.id === editingId) ?? null, [items, editingId]); const loadTemplates = useCallback(async () => { if (currentUser?.role !== "admin") { @@ -154,18 +157,45 @@ export default function TemplatesPage() { return; } setItems(result.data.items); + if (!selectedId && result.data.items.length > 0) { + setSelectedId(result.data.items[0].id); + } } catch { messageApi.error("模板列表加载失败,请稍后重试"); } finally { setLoading(false); } - }, [currentUser?.role, messageApi]); + }, [currentUser?.role, messageApi, selectedId]); + + const loadVersions = useCallback(async (templateId: string) => { + setVersionLoading(true); + try { + const result = await getTemplateVersions(templateId); + if (result.code !== 0) { + messageApi.error(result.message || "获取版本历史失败"); + return; + } + setVersionHistory(result.data.items); + } catch { + messageApi.error("版本历史加载失败"); + } finally { + setVersionLoading(false); + } + }, [messageApi]); useEffect(() => { const timer = window.setTimeout(() => void loadTemplates(), 0); return () => window.clearTimeout(timer); }, [loadTemplates]); + useEffect(() => { + if (selectedId) { + void loadVersions(selectedId); + } else { + setVersionHistory([]); + } + }, [selectedId, loadVersions]); + useEffect(() => { if (!editingTemplate) { form.resetFields(); @@ -186,30 +216,15 @@ export default function TemplatesPage() { }, [editingTemplate, form]); const filteredItems = useMemo(() => { - let result = items; - if (selectedCategory !== ALL_KEY) { - if (selectedCategory.startsWith("contract:")) { - const contractType = selectedCategory.replace("contract:", ""); - result = result.filter((item) => item.contract_type === contractType); - } else if (selectedCategory.startsWith("template:")) { - const templateCategory = selectedCategory.replace("template:", ""); - result = result.filter((item) => item.template_category === templateCategory); - } else if (selectedCategory.startsWith("industry:")) { - const industryCategory = selectedCategory.replace("industry:", ""); - result = result.filter((item) => item.industry_category === industryCategory); - } - } - if (searchQuery.trim()) { - const q = searchQuery.toLowerCase(); - result = result.filter( - (item) => - item.name.toLowerCase().includes(q) || - (item.template_code ?? "").toLowerCase().includes(q) || - (item.description ?? "").toLowerCase().includes(q), - ); - } - return result; - }, [items, selectedCategory, searchQuery]); + if (!searchQuery.trim()) return items; + const q = searchQuery.toLowerCase(); + return items.filter( + (item) => + item.name.toLowerCase().includes(q) || + (item.template_code ?? "").toLowerCase().includes(q) || + (item.description ?? "").toLowerCase().includes(q), + ); + }, [items, searchQuery]); const treeData = useMemo((): DataNode[] => { const contractCounts: Record = {}; @@ -230,39 +245,81 @@ export default function TemplatesPage() { const contractChildren: DataNode[] = Object.entries(contractTypeLabelMap) .filter(([key]) => contractCounts[key]) - .map(([key, label]) => ({ - key: `contract:${key}`, - title: ( - - {label} - {contractCounts[key] ?? 0} - - ), - })); + .map(([key, label]) => { + const templatesInCategory = items.filter((item) => item.contract_type === key); + return { + key: `group-contract:${key}`, + title: ( + + {label} + {contractCounts[key] ?? 0} + + ), + selectable: false, + children: templatesInCategory.map((t) => ({ + key: t.id, + title: ( +
+ {t.name} + {t.enabled ? null : 停} +
+ ), + isLeaf: true, + })), + }; + }); const templateChildren: DataNode[] = templateCategoryOptions .filter((opt) => templateCounts[opt.value]) - .map((opt) => ({ - key: `template:${opt.value}`, - title: ( - - {opt.label} - {templateCounts[opt.value] ?? 0} - - ), - })); + .map((opt) => { + const templatesInCategory = items.filter((item) => item.template_category === opt.value); + return { + key: `group-template:${opt.value}`, + title: ( + + {opt.label} + {templateCounts[opt.value] ?? 0} + + ), + selectable: false, + children: templatesInCategory.map((t) => ({ + key: t.id, + title: ( +
+ {t.name} + {t.enabled ? null : 停} +
+ ), + isLeaf: true, + })), + }; + }); const industryChildren: DataNode[] = industryCategoryOptions .filter((opt) => industryCounts[opt.value]) - .map((opt) => ({ - key: `industry:${opt.value}`, - title: ( - - {opt.label} - {industryCounts[opt.value] ?? 0} - - ), - })); + .map((opt) => { + const templatesInCategory = items.filter((item) => item.industry_category === opt.value); + return { + key: `group-industry:${opt.value}`, + title: ( + + {opt.label} + {industryCounts[opt.value] ?? 0} + + ), + selectable: false, + children: templatesInCategory.map((t) => ({ + key: t.id, + title: ( +
+ {t.name} + {t.enabled ? null : 停} +
+ ), + isLeaf: true, + })), + }; + }); return [ { @@ -270,13 +327,14 @@ export default function TemplatesPage() { title: ( 全部模板 - {items.length} + {items.length} ), + selectable: false, children: [ - { key: "group-contract", title: 按合同类型, children: contractChildren, selectable: false }, - { key: "group-template", title: 按模板分类, children: templateChildren, selectable: false }, - { key: "group-industry", title: 按行业入口, children: industryChildren, selectable: false }, + { key: "group-contract", title: 按合同类型, selectable: false, children: contractChildren }, + { key: "group-template", title: 按模板分类, selectable: false, children: templateChildren }, + { key: "group-industry", title: 按行业入口, selectable: false, children: industryChildren }, ], }, ]; @@ -284,10 +342,8 @@ export default function TemplatesPage() { const handleTreeSelect = (_selectedKeys: React.Key[], info: { node: DataNode }) => { const key = info.node.key as string; - if (!key.startsWith("group-")) { - setSelectedCategory(key); - setExpandedKeys((prev) => (prev.includes(key) ? prev : [...prev, key])); - } + if (key.startsWith("group-") || key === ALL_KEY) return; + setSelectedId(key); }; const handleOpenDrawer = (template?: TemplateItem) => { @@ -330,6 +386,11 @@ export default function TemplatesPage() { messageApi.success(editingTemplate ? "模板已更新并生成新版本" : "模板已创建"); handleCloseDrawer(); await loadTemplates(); + if (editingTemplate) { + setSelectedId(editingTemplate.id); + } else if (result.data?.id) { + setSelectedId(result.data.id); + } } catch { messageApi.error("保存模板失败,请稍后重试"); } finally { @@ -342,6 +403,9 @@ export default function TemplatesPage() { try { await deleteTemplate(templateId); messageApi.success("模板已删除"); + if (selectedId === templateId) { + setSelectedId(items.find((item) => item.id !== templateId)?.id ?? null); + } await loadTemplates(); } catch { messageApi.error("删除模板失败,请稍后重试"); @@ -418,30 +482,11 @@ export default function TemplatesPage() { } }; - const handleViewVersions = async (item: TemplateItem) => { - setVersionTemplate(item); - setVersionDrawerOpen(true); - setVersionLoading(true); - try { - const result = await getTemplateVersions(item.id); - if (result.code !== 0) { - messageApi.error(result.message || "获取模板版本历史失败"); - return; - } - setVersionHistory(result.data.items); - } catch { - messageApi.error("模板版本历史加载失败,请稍后重试"); - } finally { - setVersionLoading(false); - } - }; - const handleRollback = async (versionLabel: string) => { - if (!versionTemplate) return; - const rollbackKey = `${versionTemplate.id}:${versionLabel}`; - setRollbackingVersion(rollbackKey); + if (!selectedTemplate) return; + setRollbackingId(versionLabel); try { - const result = await rollbackTemplate(versionTemplate.id, { + const result = await rollbackTemplate(selectedTemplate.id, { target_version_label: versionLabel, change_summary: `Rollback to ${versionLabel}`, }); @@ -451,28 +496,14 @@ export default function TemplatesPage() { } messageApi.success(`模板已回滚为新版本,来源 ${versionLabel}`); await loadTemplates(); - await handleViewVersions(versionTemplate); + await loadVersions(selectedTemplate.id); } catch (error) { messageApi.error(error instanceof Error ? error.message : "模板回滚失败,请稍后重试"); } finally { - setRollbackingVersion(null); + setRollbackingId(null); } }; - const getCategoryLabel = () => { - if (selectedCategory === ALL_KEY) return "全部模板"; - if (selectedCategory.startsWith("contract:")) { - return contractTypeLabelMap[selectedCategory.replace("contract:", "")] ?? "合同类型"; - } - if (selectedCategory.startsWith("template:")) { - return formatTemplateCategory(selectedCategory.replace("template:", "")); - } - if (selectedCategory.startsWith("industry:")) { - return formatIndustryCategory(selectedCategory.replace("industry:", "")); - } - return "模板列表"; - }; - if (currentUser?.role !== "admin") { return (
@@ -487,117 +518,202 @@ export default function TemplatesPage() { return (
{contextHolder} -
+
{/* 左侧树形菜单 */} -
); }