diff --git a/frontend/src/app/documents/new/page.tsx b/frontend/src/app/documents/new/page.tsx index cd0fb8d8..2a4335d1 100644 --- a/frontend/src/app/documents/new/page.tsx +++ b/frontend/src/app/documents/new/page.tsx @@ -1,13 +1,24 @@ "use client"; import { - AppstoreOutlined, - BankOutlined, + CaretDownOutlined, + CaretRightOutlined, FileAddOutlined, - FilterOutlined, SearchOutlined, } from "@ant-design/icons"; -import { Alert, Button, Empty, Input, Select, Space, Spin, Tag, Typography, message } from "antd"; +import { + Button, + Empty, + Input, + List, + Space, + Spin, + Tag, + Tree, + Typography, + message, +} from "antd"; +import type { DataNode } from "antd/es/tree"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; @@ -17,7 +28,7 @@ import { type LegalDocumentTypeItem, } from "@/services/legal-document"; -const { Title, Text, Paragraph } = Typography; +const { Title, Text } = Typography; const documentNameMap: Record = { procurement_goods: "货物买卖合同", @@ -45,13 +56,6 @@ const categoryLabelMap: Record = { "notices-letters": "通知函件", }; -const industryLabelMap: Record = { - "general-enterprise": "通用企业", - technology: "科技互联网", - "professional-services": "专业服务", - "real-estate": "房地产", -}; - const scenarioLabelMap: Record = { "procurement-supply": "采购供应", "technical-service": "技术服务", @@ -88,9 +92,7 @@ const riskLabelMap: Record = { }; function formatLabel(map: Record, value?: string | null) { - if (!value) { - return "-"; - } + if (!value) return "-"; return map[value] ?? value; } @@ -98,50 +100,7 @@ function formatDocumentName(item: LegalDocumentTypeItem) { return documentNameMap[item.code] ?? item.name; } -function buildOptions(values: string[], map: Record) { - return values.map((value) => ({ - value, - label: formatLabel(map, value), - })); -} - -function DocumentTypeCard({ - item, - onOpen, -}: { - item: LegalDocumentTypeItem; - onOpen: (code: string) => void; -}) { - return ( -
-
-
- -
-
- - {formatLabel(categoryLabelMap, item.category)} - {formatLabel(industryLabelMap, item.industry)} - {riskLabelMap[item.risk_level] ?? item.risk_level} - - {formatDocumentName(item)} - - {item.description || "按结构化要素生成法律文书,生成后可继续进入审查、问答和修改流程。"} - -
-
-
-
- 场景 - {formatLabel(scenarioLabelMap, item.scenario)} -
- -
-
- ); -} +const ALL_KEY = "__all__"; export default function NewDocumentPage() { const [messageApi, contextHolder] = message.useMessage(); @@ -153,20 +112,14 @@ export default function NewDocumentPage() { industries: [], scenarios: [], }); - const [query, setQuery] = useState(""); - const [category, setCategory] = useState(); - const [industry, setIndustry] = useState(); - const [scenario, setScenario] = useState(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState(ALL_KEY); + const [expandedKeys, setExpandedKeys] = useState([ALL_KEY]); const loadDocumentTypes = useCallback(async () => { setLoading(true); try { - const result = await listLegalDocumentTypes({ - query, - category, - industry, - scenario, - }); + const result = await listLegalDocumentTypes({ active_only: true }); if (result.code !== 0) { messageApi.error(result.message || "文书类型加载失败"); return; @@ -178,141 +131,164 @@ export default function NewDocumentPage() { } finally { setLoading(false); } - }, [category, industry, messageApi, query, scenario]); + }, [messageApi]); useEffect(() => { - const timer = window.setTimeout(() => { - void loadDocumentTypes(); - }, 0); + const timer = window.setTimeout(() => void loadDocumentTypes(), 0); return () => window.clearTimeout(timer); }, [loadDocumentTypes]); - const categoryOptions = useMemo(() => buildOptions(facets.categories, categoryLabelMap), [facets.categories]); - const industryOptions = useMemo(() => buildOptions(facets.industries, industryLabelMap), [facets.industries]); - const scenarioOptions = useMemo(() => buildOptions(facets.scenarios, scenarioLabelMap), [facets.scenarios]); + const filteredItems = useMemo(() => { + let result = items; + if (selectedCategory !== ALL_KEY) { + result = result.filter((item) => item.category === selectedCategory); + } + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + result = result.filter( + (item) => + formatDocumentName(item).toLowerCase().includes(q) || + item.code.toLowerCase().includes(q) || + (item.description ?? "").toLowerCase().includes(q), + ); + } + return result; + }, [items, selectedCategory, searchQuery]); - const handleReset = () => { - setQuery(""); - setCategory(undefined); - setIndustry(undefined); - setScenario(undefined); + const treeData = useMemo((): DataNode[] => { + const categoryCounts: Record = {}; + for (const item of items) { + categoryCounts[item.category] = (categoryCounts[item.category] || 0) + 1; + } + + const children: DataNode[] = Object.entries(categoryLabelMap) + .map(([key, label]) => ({ + key, + title: ( + + {label} + {categoryCounts[key] ?? 0} + + ), + })) + .sort((a, b) => (categoryCounts[a.key as string] ?? 0) - (categoryCounts[b.key as string] ?? 0)); + + return [ + { + key: ALL_KEY, + title: ( + + 全部文书 + {items.length} + + ), + children, + }, + ]; + }, [items]); + + const handleTreeSelect = (_selectedKeys: React.Key[], info: { node: DataNode }) => { + const key = info.node.key as string; + setSelectedCategory(key); + setExpandedKeys((prev) => (prev.includes(key) ? prev : [...prev, key])); }; return (
{contextHolder} - -
-
- 法律文书 - 按业务场景起草法律文书 - - 从文书类型、行业和使用场景进入,再填写动态要素。系统会保存草稿,并把生成来源、待补字段、AI 提醒和相关法条带到结果页。 - -
-
- 文书入口 - 文书类型入口 -
-
- {items.length} - 当前可用类型 -
-
- {facets.categories.length} - 分类入口 -
-
- {facets.scenarios.length} - 业务场景 -
-
-
-
- -
+
+ {/* 左侧树形菜单 */} +