diff --git a/frontend/src/app/contracts/page.tsx b/frontend/src/app/contracts/page.tsx index 81203adc..e1b1dbcf 100644 --- a/frontend/src/app/contracts/page.tsx +++ b/frontend/src/app/contracts/page.tsx @@ -1,27 +1,36 @@ "use client"; +import { + CaretDownOutlined, + CaretRightOutlined, + DownloadOutlined, + EditOutlined, + FileTextOutlined, + HistoryOutlined, + PlusOutlined, + SearchOutlined, + WarningOutlined, +} from "@ant-design/icons"; import { Alert, Button, - Card, + Drawer, Empty, Form, Input, - Popover, + message, Select, Space, + Spin, Steps, - Table, Tag, + Tree, Typography, - message, } from "antd"; -import type { ColumnsType } from "antd/es/table"; -import type { TablePaginationConfig } from "antd/es/table/interface"; +import type { DataNode } from "antd/es/tree"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useAuthStore } from "@/stores/auth.store"; -import { useContractGeneration } from "@/hooks/use-contract-generation"; import { formatAutoReviewStatus, @@ -32,7 +41,6 @@ import { getAutoReviewStatusColor, getDueStatusColor, getErrorMessage, - getGenerationModeMeta, getGenerationQualityColor, getLifecycleNextStatuses, getLifecycleStatusColor, @@ -40,8 +48,6 @@ import { hasRiskReviewSuggestions, isLifecycleTerminal, lifecycleStatusMeta, - lifecycleStatusOptions, - type StatusMeta, } from "@/lib/format"; import { downloadContract, @@ -52,17 +58,14 @@ import { updateGeneratedContractLifecycle, type ContractPreview, type ContractTemplate, - type ContractVariable, type AutoReviewSummary, - type ContractLifecycle, type GeneratedContract, type GeneratedContractListItem, } from "@/services/contract"; import { downloadReviewReport } from "@/services/review"; -import { formatIndustryCategory, formatTemplateCategory, industryCategoryOptions, templateCategoryOptions } from "@/lib/template-taxonomy"; -const { Search, TextArea } = Input; -const { Title, Text } = Typography; +const { TextArea } = Input; +const { Title, Text, Paragraph } = Typography; type ContractFormValues = { template_id: string; @@ -70,24 +73,6 @@ type ContractFormValues = { export_format: string; }; -type RefillPayload = { - template_id: string; - variable_values: Record; - export_format: string; - options?: Record; -}; - -type ReviewContextPayload = { - reviewId?: string | null; - reviewTitle?: string | null; - contractType?: string | null; - overallRisk?: string | null; - reviewOpinion?: string | null; - recommendation?: string | null; - riskItems?: string[]; - suggestions?: string[]; -}; - const exportFormatOptions = [ { value: "docx", label: "Word文档 (.docx)" }, { value: "pdf", label: "PDF文档 (.pdf)" }, @@ -102,488 +87,241 @@ function withTimeout(promise: Promise, timeoutMs: number, timeoutMessage: const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); }); - return Promise.race([promise, timeout]).finally(() => { - if (timeoutId) { - clearTimeout(timeoutId); - } + if (timeoutId) clearTimeout(timeoutId); }); } function getRequiredVariableValidationMessage(template: ContractTemplate, variableValues: Record | undefined) { const missing = template.variables - .filter((variable) => variable.required) - .filter((variable) => !(variableValues?.[variable.name] ?? "").trim()) - .map((variable) => variable.label || variable.name); - - if (!missing.length) { - return null; - } - + .filter((v) => v.required) + .filter((v) => !(variableValues?.[v.name] ?? "").trim()) + .map((v) => v.label || v.name); + if (!missing.length) return null; const visible = missing.slice(0, 5).join("、"); - const suffix = missing.length > 5 ? ` 等 ${missing.length} 项` : ""; - return `请先填写必填变量:${visible}${suffix}`; + return `请先填写必填变量:${visible}${missing.length > 5 ? ` 等 ${missing.length} 项` : ""}`; } -function parseRestorePayload(value: string | null): RefillPayload | null { - if (!value) { - return null; - } - - try { - const parsed = JSON.parse(value) as Partial; - if (!parsed.template_id || !parsed.variable_values || !parsed.export_format) { - return null; - } - - return { - template_id: parsed.template_id, - variable_values: parsed.variable_values, - export_format: parsed.export_format, - options: parsed.options ?? {}, - }; - } catch { - return null; - } -} - -function parseReviewContextPayload(value: string | null): ReviewContextPayload | null { - if (!value) { - return null; - } - - try { - const parsed = JSON.parse(value) as ReviewContextPayload; - if (!parsed.reviewId && !parsed.reviewTitle && !parsed.reviewOpinion) { - return null; - } - return parsed; - } catch { - return null; - } -} - -function buildReviewContextVariableValue(variable: ContractVariable, context: ReviewContextPayload | null) { - if (!context) { - return ""; - } - - const matcher = `${variable.name} ${variable.label} ${variable.description}`.toLowerCase(); - const riskSummary = [ - context.overallRisk ? `总体风险:${context.overallRisk}` : null, - context.riskItems?.length ? `重点风险:${context.riskItems.join(";")}` : null, - context.reviewOpinion ? `审查意见:${context.reviewOpinion}` : null, - ].filter(Boolean).join("\n"); - const actionSummary = [ - context.reviewOpinion, - context.suggestions?.length ? context.suggestions.join(";") : null, - ].filter(Boolean).join("\n"); - - if (/risk|review|风险|审查|法务|意见|建议/.test(matcher)) { - return riskSummary || actionSummary; - } - if (/background|context|remark|note|memo|背景|依据|备注|说明|补充/.test(matcher)) { - return actionSummary || riskSummary; - } - if (/contract|title|name|subject|合同|名称|标题|主题/.test(matcher)) { - return context.reviewTitle ?? ""; - } - if (/type|类型/.test(matcher)) { - return context.contractType ?? ""; - } - - return ""; -} - - +const ALL_KEY = "__all__"; export default function ContractsPage() { const [messageApi, contextHolder] = message.useMessage(); - const [form] = Form.useForm(); const router = useRouter(); - const currentUser = useAuthStore((state) => state.currentUser); const searchParams = useSearchParams(); - const restorePayload = useMemo(() => parseRestorePayload(searchParams.get("restore")), [searchParams]); - const reviewContext = useMemo(() => parseReviewContextPayload(searchParams.get("review")), [searchParams]); - const [templateLoading, setTemplateLoading] = useState(true); - const [historyLoading, setHistoryLoading] = useState(true); - const [generating, setGenerating] = useState(false); - const [previewing, setPreviewing] = useState(false); - const [downloadingCurrent, setDownloadingCurrent] = useState(false); - const [historyDownloadingId, setHistoryDownloadingId] = useState(null); + const currentUser = useAuthStore((state) => state.currentUser); + const [form] = Form.useForm(); + + // History state + const [loading, setLoading] = useState(true); + const [items, setItems] = useState([]); + + // Selection state + const [selectedId, setSelectedId] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [expandedKeys, setExpandedKeys] = useState([ALL_KEY]); + + // Detail state + const [lifecycleUpdating, setLifecycleUpdating] = useState(false); + const [downloading, setDownloading] = useState(false); + + // Generation Drawer state + const [genDrawerOpen, setGenDrawerOpen] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [templates, setTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); - const [templateCategoryFilter, setTemplateCategoryFilter] = useState(undefined); - const [industryCategoryFilter, setIndustryCategoryFilter] = useState(undefined); const [previewResult, setPreviewResult] = useState(null); const [generatedContract, setGeneratedContract] = useState(null); - const [historyContracts, setHistoryContracts] = useState([]); - const [historyKeyword, setHistoryKeyword] = useState(""); - const [historyContractType, setHistoryContractType] = useState(undefined); - const [historyLifecycleStatus, setHistoryLifecycleStatus] = useState(undefined); - const [historyPage, setHistoryPage] = useState(1); - const [historyPageSize, setHistoryPageSize] = useState(10); - const [historyTotal, setHistoryTotal] = useState(0); - const [restoredOptions, setRestoredOptions] = useState>({}); - const [appliedRestoreValue, setAppliedRestoreValue] = useState(null); - const [lifecycleUpdating, setLifecycleUpdating] = useState(false); - const [historyCollapsed, setHistoryCollapsed] = useState(true); - const canDownloadContracts = currentUser?.role === "admin"; - const canOpenReviewDetail = (autoReview?: AutoReviewSummary | null) => Boolean(autoReview?.detail_url); - const canOpenReviewChat = (autoReview?: AutoReviewSummary | null) => Boolean(autoReview?.chat_url); - const canDownloadReview = (autoReview?: AutoReviewSummary | null) => Boolean(autoReview?.report_download_url); + const [generating, setGenerating] = useState(false); + const [previewing, setPreviewing] = useState(false); - const clearRestoreQuery = useCallback(() => { - if (!searchParams.get("restore")) { - return; + const selectedContract = useMemo(() => items.find((item) => item.id === selectedId) ?? null, [items, selectedId]); + + const loadContracts = useCallback(async () => { + setLoading(true); + try { + const result = await listGeneratedContracts({ page: 1, page_size: 1000 }); + if (result.code !== 0) { + messageApi.error(result.message || "获取合同历史失败"); + return; + } + setItems(result.data.items); + if (!selectedId && result.data.items.length > 0) { + setSelectedId(result.data.items[0].id); + } + } catch { + messageApi.error("合同历史加载失败,请稍后重试"); + } finally { + setLoading(false); } - - router.replace("/contracts"); - }, [router, searchParams]); - - const applyTemplateSelection = useCallback( - (template: ContractTemplate, payload?: RefillPayload) => { - setSelectedTemplate(template); - setPreviewResult(null); - setGeneratedContract(null); - setCurrentStep(1); - - const initialValues: Record = {}; - template.variables.forEach(variable => { - if (payload?.variable_values?.[variable.name] != null) { - initialValues[variable.name] = payload.variable_values[variable.name]; - return; - } - - const reviewContextValue = buildReviewContextVariableValue(variable, reviewContext); - if (reviewContextValue) { - initialValues[variable.name] = reviewContextValue; - return; - } - - if (variable.default_value) { - initialValues[variable.name] = variable.default_value; - } - }); - - form.setFieldsValue({ - template_id: template.id, - variable_values: initialValues, - export_format: payload?.export_format ?? "docx", - }); - setRestoredOptions(payload?.options ?? {}); - }, - [form, reviewContext] - ); + }, [messageApi, selectedId]); const loadTemplates = useCallback(async () => { - setTemplateLoading(true); try { - const result = await listContractTemplates({ - template_category: templateCategoryFilter, - industry_category: industryCategoryFilter, - }); + const result = await listContractTemplates({}); if (result.code !== 0) { messageApi.error(result.message || "获取模板列表失败"); return; } setTemplates(result.data); } catch { - messageApi.error("模板列表加载失败,请稍后重试"); - } finally { - setTemplateLoading(false); + messageApi.error("模板列表加载失败"); } - }, [industryCategoryFilter, messageApi, templateCategoryFilter]); - - const loadHistory = useCallback( - async ( - page = historyPage, - pageSize = historyPageSize, - contractType = historyContractType, - keyword = historyKeyword, - lifecycleStatus = historyLifecycleStatus, - ) => { - setHistoryLoading(true); - try { - const result = await listGeneratedContracts({ - page, - page_size: pageSize, - contract_type: contractType, - lifecycle_status: lifecycleStatus, - keyword, - }); - if (result.code !== 0) { - messageApi.error(result.message || "历史记录加载失败"); - return; - } - setHistoryContracts(result.data.items); - setHistoryTotal(result.data.total); - setHistoryPage(result.data.page); - setHistoryPageSize(result.data.page_size); - } catch { - messageApi.error("历史记录加载失败,请稍后重试"); - } finally { - setHistoryLoading(false); - } - }, - [historyContractType, historyKeyword, historyLifecycleStatus, historyPage, historyPageSize, messageApi] - ); + }, [messageApi]); useEffect(() => { - const timer = window.setTimeout(() => { - void loadTemplates(); - void loadHistory(1, historyPageSize, historyContractType, historyKeyword, historyLifecycleStatus); - }, 0); + void loadContracts(); + }, [loadContracts]); - return () => window.clearTimeout(timer); - }, [historyContractType, historyKeyword, historyLifecycleStatus, historyPageSize, loadHistory, loadTemplates]); + const filteredItems = useMemo(() => { + if (!searchQuery.trim()) return items; + const q = searchQuery.toLowerCase(); + return items.filter( + (item) => + (item.template_name ?? "").toLowerCase().includes(q) || + (item.file_name ?? "").toLowerCase().includes(q) || + (item.contract_type ?? "").toLowerCase().includes(q) || + (item.metadata?.generated_by_name ?? "").toLowerCase().includes(q), + ); + }, [items, searchQuery]); - useEffect(() => { - const restoreValue = searchParams.get("restore"); - if (!restoreValue || !restorePayload || templates.length === 0 || appliedRestoreValue === restoreValue) { - return; - } + const treeData = useMemo((): DataNode[] => { + const contractTypes = Array.from(new Set(items.map((i) => i.contract_type).filter(Boolean))); + const lifecycleTypes = Array.from(new Set(items.map((i) => i.lifecycle?.status).filter(Boolean))); - const template = templates.find(item => item.id === restorePayload.template_id); - if (!template) { - messageApi.error("未找到需要恢复的合同模板"); - clearRestoreQuery(); - const timer = window.setTimeout(() => setAppliedRestoreValue(restoreValue), 0); - return () => window.clearTimeout(timer); - } + const typeChildren: DataNode[] = contractTypes.map((type) => { + const contractsInType = items.filter((i) => i.contract_type === type); + return { + key: `type:${type}`, + title: ( + + {type} + {contractsInType.length} + + ), + selectable: false, + children: contractsInType.map((c) => ({ + key: c.id, + title: ( +
+ {c.template_name || c.file_name} + {c.lifecycle?.status && ( + + {formatLifecycleStatus(c.lifecycle.status).slice(0, 2)} + + )} +
+ ), + isLeaf: true, + })), + }; + }); - const timer = window.setTimeout(() => { - applyTemplateSelection(template, restorePayload); - setAppliedRestoreValue(restoreValue); - messageApi.success("已恢复历史生成参数,可继续编辑或再次生成"); - }, 0); + const statusChildren: DataNode[] = lifecycleTypes.map((status) => { + const contractsInStatus = items.filter((i) => i.lifecycle?.status === status); + const meta = lifecycleStatusMeta[status] ?? { label: status, color: "default" }; + return { + key: `status:${status}`, + title: ( + + {meta.label} + {contractsInStatus.length} + + ), + selectable: false, + children: contractsInStatus.map((c) => ({ + key: c.id, + title: ( +
+ {c.template_name || c.file_name} + {c.lifecycle?.due_status && ( + + {formatDueStatus(c.lifecycle)} + + )} +
+ ), + isLeaf: true, + })), + }; + }); - return () => window.clearTimeout(timer); - }, [appliedRestoreValue, applyTemplateSelection, clearRestoreQuery, messageApi, restorePayload, searchParams, templates]); + return [ + { + key: ALL_KEY, + title: ( + + 全部合同 + {items.length} + + ), + selectable: false, + children: [ + { key: "group-type", title: 按合同类型, selectable: false, children: typeChildren }, + { key: "group-status", title: 按生命周期, selectable: false, children: statusChildren }, + ], + }, + ]; + }, [items]); - const contractTypeOptions = useMemo(() => { - return Array.from( - new Set([...templates.map(item => item.contract_type), ...historyContracts.map(item => item.contract_type)].filter(Boolean)) - ).map(value => ({ value, label: value })); - }, [historyContracts, templates]); - - const handleTemplateSelect = (templateId: string) => { - const template = templates.find(t => t.id === templateId); - if (!template) { - return; - } - - clearRestoreQuery(); - applyTemplateSelection(template); - }; - - const handlePreview = async () => { - if (!selectedTemplate) { - return; - } - - try { - const values = await form.validateFields(); - const validationMessage = getRequiredVariableValidationMessage(selectedTemplate, values.variable_values); - if (validationMessage) { - messageApi.warning(validationMessage); - return; - } - - setPreviewing(true); - const result = await previewContract({ - template_id: values.template_id, - variable_values: values.variable_values, - }); - - if (result.code !== 0) { - messageApi.error(result.message || "合同预览失败"); - return; - } - - setPreviewResult(result.data); - setCurrentStep(2); - messageApi.success("合同预览生成成功"); - } catch (error: unknown) { - if (Array.isArray((error as { errorFields?: unknown[] }).errorFields)) { - messageApi.warning("请先补齐必填变量,再预览合同"); - return; - } - messageApi.error(getErrorMessage(error, "合同预览失败,请稍后重试")); - } finally { - setPreviewing(false); - } - }; - - const handleGenerate = async () => { - if (!selectedTemplate) { - return; - } - - try { - const values = await form.validateFields(); - const validationMessage = getRequiredVariableValidationMessage(selectedTemplate, values.variable_values); - if (validationMessage) { - messageApi.warning(validationMessage); - return; - } - if (!values.export_format) { - messageApi.warning("请选择导出格式"); - return; - } - - setGenerating(true); - const result = await withTimeout( - generateContract({ - template_id: values.template_id, - variable_values: values.variable_values, - export_format: values.export_format, - options: restoredOptions, - }), - GENERATE_RECOVERY_TIMEOUT_MS, - "合同生成仍在处理中,可稍后在历史结果查看。", - ); - - if (result.code !== 0) { - messageApi.error(result.message || "合同生成失败"); - return; - } - - setGeneratedContract(result.data); - setCurrentStep(3); - setHistoryPage(1); - clearRestoreQuery(); - await loadHistory(1, historyPageSize, historyContractType, historyKeyword, historyLifecycleStatus); - messageApi.success("合同生成成功!"); - } catch (error: unknown) { - if (Array.isArray((error as { errorFields?: unknown[] }).errorFields)) { - messageApi.warning("请先补齐必填变量,再生成合同"); - return; - } - const nextMessage = getErrorMessage(error, "合同生成失败,请稍后重试"); - if (nextMessage.includes("仍在处理中")) { - messageApi.warning(nextMessage); - setHistoryPage(1); - await loadHistory(1, historyPageSize, historyContractType, historyKeyword, historyLifecycleStatus); - return; - } - messageApi.error(nextMessage); - } finally { - setGenerating(false); - } + const handleTreeSelect = (_selectedKeys: React.Key[], info: { node: DataNode }) => { + const key = info.node.key as string; + if (key.startsWith("group-") || key === ALL_KEY) return; + setSelectedId(key); }; const handleDownload = async () => { - if (!generatedContract) { - return; - } - - setDownloadingCurrent(true); + if (!selectedContract) return; + setDownloading(true); try { - await downloadContract(generatedContract.id, generatedContract.file_name); - messageApi.success("文件下载开始"); - } catch (error) { - console.error("下载失败:", error); - messageApi.error("文件下载失败"); + await downloadContract(selectedContract.id, selectedContract.file_name); + messageApi.success("下载开始"); + } catch { + messageApi.error("下载失败"); } finally { - setDownloadingCurrent(false); + setDownloading(false); } }; - const handleHistoryDownload = async (record: GeneratedContractListItem) => { - setHistoryDownloadingId(record.id); + const handleLifecycleTransition = async (newStatus: string) => { + if (!selectedContract || lifecycleUpdating) return; + setLifecycleUpdating(true); try { - await downloadContract(record.id, record.file_name); - messageApi.success("文件下载开始"); - } catch (error) { - console.error("下载失败:", error); - messageApi.error("文件下载失败"); + const result = await updateGeneratedContractLifecycle(selectedContract.id, { lifecycle_status: newStatus }); + if (result.code !== 0) { + messageApi.error(result.message || "状态更新失败"); + return; + } + const updated = result.data; + setItems((prev) => prev.map((item) => item.id === updated.id ? { ...item, lifecycle: updated.lifecycle } : item)); + messageApi.success(`状态已更新为 ${formatLifecycleStatus(newStatus)}`); + } catch { + messageApi.error("状态更新失败"); } finally { - setHistoryDownloadingId(null); + setLifecycleUpdating(false); } }; - const handleViewGeneratedDetail = () => { - if (!generatedContract) { + const handleOpenReviewDetail = (autoReview?: AutoReviewSummary | null) => { + if (!autoReview?.detail_url) { + messageApi.warning("自动审查详情不可用"); return; } - router.push(`/contracts/${generatedContract.id}`); + router.push(autoReview.detail_url); }; - const handleHistorySearch = async (value?: string) => { - const keyword = value ?? historyKeyword; - setHistoryKeyword(keyword); - setHistoryPage(1); - await loadHistory(1, historyPageSize, historyContractType, keyword, historyLifecycleStatus); - }; - - const handleHistoryFilterChange = async (value: string | undefined) => { - setHistoryContractType(value); - setHistoryPage(1); - await loadHistory(1, historyPageSize, value, historyKeyword, historyLifecycleStatus); - }; - - const handleHistoryLifecycleFilterChange = async (value: string | undefined) => { - setHistoryLifecycleStatus(value); - setHistoryPage(1); - await loadHistory(1, historyPageSize, historyContractType, historyKeyword, value); - }; - - const handleHistoryReset = async () => { - setHistoryKeyword(""); - setHistoryContractType(undefined); - setHistoryLifecycleStatus(undefined); - setHistoryPage(1); - await loadHistory(1, historyPageSize, undefined, "", undefined); - }; - - const handleHistoryTableChange = (pagination: TablePaginationConfig) => { - const nextPage = pagination.current ?? 1; - const nextPageSize = pagination.pageSize ?? historyPageSize; - void loadHistory(nextPage, nextPageSize, historyContractType, historyKeyword, historyLifecycleStatus); - }; - - const handleReset = () => { - setSelectedTemplate(null); - setPreviewResult(null); - setGeneratedContract(null); - setCurrentStep(0); - setRestoredOptions({}); - form.resetFields(); - clearRestoreQuery(); - }; - - const handleOpenAutoReviewDetail = (autoReview?: AutoReviewSummary | null) => { - if (!autoReview) { - messageApi.warning("自动审查结果暂不可用"); - return; - } - - const target = autoReview.detail_url; - if (!target) { - messageApi.warning("自动审查详情尚未生成或当前记录未绑定可访问详情"); - return; - } - - router.push(target); - }; - - const handleOpenAutoReviewChat = (autoReview?: AutoReviewSummary | null) => { + const handleOpenReviewChat = (autoReview?: AutoReviewSummary | null) => { if (!autoReview?.chat_url) { - messageApi.warning("自动审查问答上下文尚未生成"); + messageApi.warning("审查问答上下文尚未生成"); return; } - router.push(autoReview.chat_url); }; - const handleDownloadAutoReviewReport = async (autoReview?: AutoReviewSummary | null) => { + const handleDownloadReviewReport = async (autoReview?: AutoReviewSummary | null) => { if (!autoReview?.report_download_url) { - messageApi.warning("当前还没有可下载的审查报告"); + messageApi.warning("暂无可下载的审查报告"); return; } - try { await downloadReviewReport({ file_name: autoReview.report_file_name ?? `review-report-${autoReview.review_id ?? "auto"}.pdf`, @@ -592,631 +330,634 @@ export default function ContractsPage() { }); messageApi.success("审查报告下载开始"); } catch { - messageApi.error("审查报告下载失败,请稍后重试"); + messageApi.error("审查报告下载失败"); } }; - const handleLifecycleTransition = async (record: GeneratedContractListItem, newStatus: string) => { - if (lifecycleUpdating) return; - setLifecycleUpdating(true); + // Generation handlers + const handleTemplateSelect = (template: ContractTemplate) => { + setSelectedTemplate(template); + setPreviewResult(null); + setGeneratedContract(null); + setCurrentStep(1); + form.setFieldsValue({ + template_id: template.id, + export_format: "docx", + }); + }; + + const handlePreview = async () => { + if (!selectedTemplate) return; try { - const result = await updateGeneratedContractLifecycle(record.id, { - lifecycle_status: newStatus, - }); - if (result.code !== 0) { - messageApi.error(result.message || "状态更新失败"); + const values = await form.validateFields(); + const validationMessage = getRequiredVariableValidationMessage(selectedTemplate, values.variable_values); + if (validationMessage) { + messageApi.warning(validationMessage); return; } - const updated = result.data; - setHistoryContracts(prev => prev.map(item => (item.id === updated.id ? { ...item, lifecycle: updated.lifecycle } : item))); - if (generatedContract?.id === updated.id) { - setGeneratedContract(updated); + setPreviewing(true); + const result = await previewContract({ + template_id: values.template_id, + variable_values: values.variable_values, + }); + if (result.code !== 0) { + messageApi.error(result.message || "预览失败"); + return; } - messageApi.success(`生命周期已更新为 ${formatLifecycleStatus(newStatus)}`); + setPreviewResult(result.data); + setCurrentStep(2); + messageApi.success("预览生成成功"); } catch (error: unknown) { - messageApi.error(getErrorMessage(error, "状态更新失败,请稍后重试")); + if (Array.isArray((error as { errorFields?: unknown[] }).errorFields)) { + messageApi.warning("请先补齐必填变量"); + return; + } + messageApi.error(getErrorMessage(error, "预览失败,请稍后重试")); } finally { - setLifecycleUpdating(false); + setPreviewing(false); } }; - const templateColumns: ColumnsType = [ - { - title: "模板名称", - dataIndex: "name", - key: "name", - width: 200, - render: (value: string, record) => ( -
-
{value}
- - {record.contract_type} - - - {formatTemplateCategory(record.template_category)} - {formatIndustryCategory(record.industry_category)} - + const handleGenerate = async () => { + if (!selectedTemplate) return; + try { + const values = await form.validateFields(); + const validationMessage = getRequiredVariableValidationMessage(selectedTemplate, values.variable_values); + if (validationMessage) { + messageApi.warning(validationMessage); + return; + } + setGenerating(true); + const result = await withTimeout( + generateContract({ + template_id: values.template_id, + variable_values: values.variable_values, + export_format: values.export_format, + }), + GENERATE_RECOVERY_TIMEOUT_MS, + "合同仍在处理中,可稍后查看", + ); + if (result.code !== 0) { + messageApi.error(result.message || "生成失败"); + return; + } + setGeneratedContract(result.data); + setCurrentStep(3); + messageApi.success("合同生成成功!"); + await loadContracts(); + } catch (error: unknown) { + if (Array.isArray((error as { errorFields?: unknown[] }).errorFields)) { + messageApi.warning("请先补齐必填变量"); + return; + } + messageApi.error(getErrorMessage(error, "生成失败,请稍后重试")); + } finally { + setGenerating(false); + } + }; + + const handleCloseGeneration = () => { + setGenDrawerOpen(false); + setCurrentStep(0); + setSelectedTemplate(null); + setPreviewResult(null); + setGeneratedContract(null); + form.resetFields(); + }; + + const handleDownloadGenerated = async () => { + if (!generatedContract) return; + setDownloading(true); + try { + await downloadContract(generatedContract.id, generatedContract.file_name); + messageApi.success("下载开始"); + } catch { + messageApi.error("下载失败"); + } finally { + setDownloading(false); + } + }; + + if (currentUser?.role !== "admin") { + return ( +
+ {contextHolder} +
+ 仅管理员可管理合同。
- ), - }, - { - title: "描述", - dataIndex: "description", - key: "description", - width: 300, - render: (value: string) =>
{value || "暂无描述"}
, - }, - { - title: "变量数量", - dataIndex: "variables", - key: "variables", - width: 100, - render: (variables: ContractVariable[]) => {variables.length}个, - }, - { - title: "预览摘要", - dataIndex: "preview_excerpt", - key: "preview_excerpt", - width: 200, - render: (value: string) =>
{value}
, - }, - { - title: "操作", - key: "actions", - width: 120, - render: (_, record) => ( - - ), - }, - ]; - - const historyColumns: ColumnsType = [ - { - title: "模板名称", - dataIndex: "template_name", - key: "template_name", - width: 180, - }, - { - title: "合同类型", - dataIndex: "contract_type", - key: "contract_type", - width: 140, - render: (value: string) => {value || "未分类"}, - }, - { - title: "导出格式", - dataIndex: "export_format", - key: "export_format", - width: 120, - render: (value: string) => value.toUpperCase(), - }, - { - title: "文件名", - dataIndex: "file_name", - key: "file_name", - width: 220, - }, - { - title: "生命周期", - dataIndex: "lifecycle", - key: "lifecycle", - width: 220, - render: (lifecycle: ContractLifecycle | null | undefined) => ( - - {formatLifecycleStatus(lifecycle?.status)} - {formatDueStatus(lifecycle)} - - {lifecycle?.counterparty_name ?? "未设置相对方"} - - - ), - }, - { - title: "资产文件", - dataIndex: "file_id", - key: "file_id", - width: 160, - render: (value: string | null | undefined) => - value && currentUser?.role === "admin" ? ( - - ) : ( - 未关联 - ), - }, - { - title: "生成时间", - dataIndex: "generated_at", - key: "generated_at", - width: 180, - render: (value: string) => new Date(value).toLocaleString("zh-CN"), - }, - { - title: "生成质量", - dataIndex: "generation_quality", - key: "generation_quality", - width: 180, - render: (_, record) => { - const modeMeta = getGenerationModeMeta(record.metadata); - return ( - - {formatGenerationQualityStatus(record.generation_quality?.status)} - 分数 {record.generation_quality?.score ?? "-"} - {modeMeta.label} - {hasRiskReviewSuggestions(record.clause_completion_suggestions) ? 高风险补条款 : null} - {record.clause_completion_suggestions?.length ? 需补条款 {record.clause_completion_suggestions.length} : null} - - ); - }, - }, - { - title: "自动审查", - dataIndex: "auto_review", - key: "auto_review", - width: 210, - render: (autoReview: AutoReviewSummary | null | undefined) => { - if (!autoReview) { - return 未触发; - } - - const highRisk = autoReview.risk_level === "high"; - return ( - - - {formatAutoReviewStatus(autoReview.status)} - {autoReview.risk_level ? {autoReview.risk_level === "high" ? "高风险" : autoReview.risk_level === "medium" ? "中风险" : autoReview.risk_level === "low" ? "低风险" : autoReview.risk_level} : null} - - - {autoReview.current_stage_label ?? autoReview.next_action ?? "-"} - - {highRisk && autoReview.review_opinion ? ( - {autoReview.review_opinion} - ) : null} - - - ); - }, - }, - { - title: "生成人员", - dataIndex: ["metadata", "generated_by_name"], - key: "generated_by_name", - width: 120, - render: (value: string | undefined) => value || "未知", - }, - { - title: "操作", - key: "actions", - width: 230, - fixed: "right", - render: (_, record) => ( - - - - - 生命周期 - - {formatLifecycleStatus(record.lifecycle?.status)} - - - } - content={ - isLifecycleTerminal(record.lifecycle?.status) ? ( -
- 合同已归档,不可再流转 -
- ) : ( - - 流转至: - {getLifecycleNextStatuses(record.lifecycle?.status).map((next) => ( - - ))} - - ) - } - > - - - - - ), - }, - ]; - - const steps = [ - { - title: "选择模板", - content: "从可用模板中选择", - }, - { - title: "填写变量", - content: "填写合同变量值", - }, - { - title: "预览确认", - content: "预览生成结果", - }, - { - title: "生成下载", - content: "生成并下载合同", - }, - ]; - - const restoredOptionEntries = Object.entries(restoredOptions); +
+ ); + } return ( -
+
{contextHolder} - -
-
- 合同生成 - 从模板到下载,一屏完成合同生成。 - 先选模板,再填写变量;预览确认后生成文件,历史结果保留在下方。 +
+ {/* 左侧树形菜单 */} +
- - - - - - {currentStep === 0 && ( - - - setIndustryCategoryFilter(value)} - /> - - {reviewContext ? ( - - ) : null} - {templates.length === 0 && !templateLoading ? ( - - - - +
+ {loading ? ( +
) : ( - }} - pagination={{ pageSize: 6, responsive: true }} - scroll={{ x: 900 }} + expanded ? : } + treeData={treeData} + expandedKeys={expandedKeys} + selectedKeys={selectedId ? [selectedId] : []} + onExpand={setExpandedKeys} + onSelect={handleTreeSelect} + blockNode /> )} - + +
+ +
+ + + {/* 右侧合同详情 */} +
+ {loading ? ( +
+ + 加载中... +
+ ) : selectedContract ? ( +
+ {/* 头部 */} +
+
+
+ {selectedContract.template_name} + {selectedContract.file_name} +
+ + + + + +
+
+ + {selectedContract.contract_type || "未分类"} + {selectedContract.lifecycle?.status && ( + + {formatLifecycleStatus(selectedContract.lifecycle.status)} + + )} + {selectedContract.lifecycle?.due_status && ( + + {formatDueStatus(selectedContract.lifecycle)} + + )} + {selectedContract.export_format.toUpperCase()} + {(selectedContract.file_size / 1024).toFixed(1)} KB + + + 生成时间:{formatDateTime(selectedContract.generated_at)} + 生成人员:{selectedContract.metadata?.generated_by_name || "未知"} + +
+
+ + {/* 自动审查结果 */} + {selectedContract.auto_review && ( +
+
+ + + 自动审查结果 + + {formatAutoReviewStatus(selectedContract.auto_review.status)} + + + {selectedContract.auto_review.risk_level && ( + + {selectedContract.auto_review.risk_level === "high" ? "高风险" : selectedContract.auto_review.risk_level === "medium" ? "中风险" : "低风险"} + + )} +
+
+ {selectedContract.auto_review.review_opinion || selectedContract.auto_review.status_detail || selectedContract.auto_review.current_stage_label || "-"} + + + + + +
+
+ )} + + {/* 生命周期流转 */} +
+
+ + + 生命周期 + +
+ {selectedContract.lifecycle?.status ? ( +
+
+ 当前状态: + + {formatLifecycleStatus(selectedContract.lifecycle.status)} + +
+ {selectedContract.lifecycle.counterparty_name && ( +
+ 相对方: + {selectedContract.lifecycle.counterparty_name} +
+ )} + {!isLifecycleTerminal(selectedContract.lifecycle.status) && ( +
+ 流转至: + + {getLifecycleNextStatuses(selectedContract.lifecycle.status).map((next) => ( + + ))} + +
+ )} +
+ ) : ( + 暂无生命周期信息 + )} +
+ + {/* 生成质量 */} + {selectedContract.generation_quality?.status && ( +
+
+ 生成质量 + + {formatGenerationQualityStatus(selectedContract.generation_quality.status)} + + 分数:{selectedContract.generation_quality.score ?? "-"} +
+ {hasRiskReviewSuggestions(selectedContract.clause_completion_suggestions) && ( + + )} + {selectedContract.clause_completion_suggestions?.length ? ( + 需补条款 {selectedContract.clause_completion_suggestions.length} + ) : null} +
+ )} + + {/* 关联文件 */} + {selectedContract.file_id && ( +
+
+ 关联文件资产 + {selectedContract.file_id} +
+ +
+ )} +
+ ) : ( +
+ +
+ )} +
+ + + {/* 生成新合同 Drawer */} + + + + {currentStep === 0 && ( +
+ 请选择一个合同模板开始生成 + {templates.length === 0 ? ( + + ) : ( +
+ {templates.map((t) => ( +
handleTemplateSelect(t)}> + {t.name} + {t.contract_type} + + {t.variables.length} 个变量 + +
+ ))} +
+ )} +
)} {currentStep >= 1 && selectedTemplate && ( -
- - - {restorePayload && ( - - )} - - {reviewContext && ( - - )} - - {restoredOptionEntries.length > 0 && ( -
-
- 已恢复的导出选项 -
- - {restoredOptionEntries.map(([key, value]) => ( - - {key}: {String(value)} - - ))} - -
- )} - -
- - -
- 当前模板: {selectedTemplate.name} - {selectedTemplate.description} -
- - {selectedTemplate.variables.map(variable => ( - - {variable.label} - {variable.required && 必填} -
- } - name={["variable_values", variable.name]} - rules={[ - { - required: variable.required, - message: `请输入${variable.label}`, - }, - ]} - help={variable.description} - initialValue={variable.default_value} - > -