From 34fa184b699e60577a429cac075495acc878f14e Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 21 Jan 2026 11:48:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E6=B7=BB=E5=8A=A0=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现知识集的创建、编辑、删除功能 - 实现知识条目的创建、编辑、删除功能 - 添加知识集详情页面展示功能 - 实现知识条目导入数据集文件功能 - 添加知识管理主页列表展示功能 - 实现知识集和知识条目的状态管理 - 集成标签管理和搜索过滤功能 - 添加知识条目的批量操作支持 --- .../Detail/KnowledgeSetDetail.tsx | 327 ++++++++++++++++++ .../Home/KnowledgeManagementPage.tsx | 249 +++++++++++++ .../components/CreateKnowledgeSet.tsx | 201 +++++++++++ .../components/ImportKnowledgeItemsDialog.tsx | 97 ++++++ .../components/KnowledgeItemEditor.tsx | 198 +++++++++++ .../knowledge-management.api.ts | 56 +++ .../knowledge-management.const.tsx | 160 +++++++++ .../knowledge-management.model.ts | 68 ++++ 8 files changed, 1356 insertions(+) create mode 100644 frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/Home/KnowledgeManagementPage.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/components/CreateKnowledgeSet.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/components/ImportKnowledgeItemsDialog.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/knowledge-management.api.ts create mode 100644 frontend/src/pages/KnowledgeManagement/knowledge-management.const.tsx create mode 100644 frontend/src/pages/KnowledgeManagement/knowledge-management.model.ts diff --git a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx new file mode 100644 index 0000000..3ca5f2f --- /dev/null +++ b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx @@ -0,0 +1,327 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + App, + Breadcrumb, + Button, + Card, + Descriptions, + Empty, + Table, + Tag, +} from "antd"; +import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons"; +import { useNavigate, useParams } from "react-router"; +import DetailHeader from "@/components/DetailHeader"; +import { SearchControls } from "@/components/SearchControls"; +import useFetchData from "@/hooks/useFetchData"; +import { + deleteKnowledgeItemByIdUsingDelete, + deleteKnowledgeSetByIdUsingDelete, + queryKnowledgeItemsUsingGet, + queryKnowledgeSetByIdUsingGet, +} from "../knowledge-management.api"; +import { + knowledgeContentTypeOptions, + knowledgeStatusMap, + mapKnowledgeItem, + KnowledgeItemView, +} from "../knowledge-management.const"; +import { + KnowledgeItem, + KnowledgeSet, + KnowledgeStatusType, +} from "../knowledge-management.model"; +import CreateKnowledgeSet from "../components/CreateKnowledgeSet"; +import KnowledgeItemEditor from "../components/KnowledgeItemEditor"; +import ImportKnowledgeItemsDialog from "../components/ImportKnowledgeItemsDialog"; +import { formatDate } from "@/utils/unit"; + +const KnowledgeSetDetail = () => { + const navigate = useNavigate(); + const { message } = App.useApp(); + const { id } = useParams<{ id: string }>(); + const [knowledgeSet, setKnowledgeSet] = useState(null); + const [showEdit, setShowEdit] = useState(false); + const [itemEditorOpen, setItemEditorOpen] = useState(false); + const [currentItem, setCurrentItem] = useState(null); + + const fetchKnowledgeSet = useCallback(async () => { + if (!id) return; + const { data } = await queryKnowledgeSetByIdUsingGet(id); + setKnowledgeSet(data); + }, [id]); + + useEffect(() => { + fetchKnowledgeSet(); + }, [fetchKnowledgeSet]); + + const { + loading, + tableData: items, + searchParams, + pagination, + fetchData, + setSearchParams, + handleFiltersChange, + handleKeywordChange, + } = useFetchData( + (params) => (id ? queryKnowledgeItemsUsingGet(id, params) : Promise.resolve({ data: [] })), + mapKnowledgeItem, + 30000, + false, + [], + 0 + ); + + const isReadOnly = + knowledgeSet?.status === KnowledgeStatusType.ARCHIVED || + knowledgeSet?.status === KnowledgeStatusType.DEPRECATED; + + const handleDeleteSet = async () => { + if (!knowledgeSet) return; + await deleteKnowledgeSetByIdUsingDelete(knowledgeSet.id); + message.success("知识集已删除"); + navigate("/data/knowledge-management"); + }; + + const handleDeleteItem = async (item: KnowledgeItemView) => { + if (!id) return; + await deleteKnowledgeItemByIdUsingDelete(id, item.id); + message.success("知识条目已删除"); + fetchData(); + }; + + const statusMeta = knowledgeSet?.status + ? knowledgeStatusMap[knowledgeSet.status] + : undefined; + + const statistics = useMemo( + () => [ + { + key: "items", + icon: , + label: "条目数", + value: pagination.total || 0, + }, + { + key: "updated", + icon: , + label: "更新时间", + value: knowledgeSet?.updatedAt ? formatDate(knowledgeSet.updatedAt) : "-", + }, + ], + [pagination.total, knowledgeSet?.updatedAt] + ); + + const itemColumns = [ + { + title: "标题", + dataIndex: "title", + key: "title", + fixed: "left" as const, + width: 220, + ellipsis: true, + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 120, + render: (status: KnowledgeItemView["status"]) => ( + {status?.label} + ), + }, + { + title: "类型", + dataIndex: "contentType", + key: "contentType", + width: 120, + render: (contentType: string) => + knowledgeContentTypeOptions.find((opt) => opt.value === contentType)?.label || + contentType, + }, + { + title: "负责人", + dataIndex: "owner", + key: "owner", + width: 120, + ellipsis: true, + }, + { + title: "来源", + dataIndex: "sourceType", + key: "sourceType", + width: 140, + ellipsis: true, + }, + { + title: "更新时间", + dataIndex: "updatedAt", + key: "updatedAt", + width: 180, + ellipsis: true, + }, + { + title: "操作", + key: "actions", + width: 140, + render: (_: unknown, record: KnowledgeItemView) => ( +
+
+ ), + }, + ]; + + return ( +
+
+ + + navigate("/data/knowledge-management")}>知识管理 + + {knowledgeSet?.name || "-"} + +
+ + , + onClick: () => setShowEdit(true), + danger: false, + }, + { + key: "delete", + label: "删除", + icon: , + danger: true, + confirm: { + title: "确认删除该知识集吗?", + description: "删除后将无法恢复,请谨慎操作。", + cancelText: "取消", + okText: "删除", + okType: "danger", + onConfirm: () => handleDeleteSet(), + }, + }, + ]} + /> + + { + setShowEdit(false); + fetchKnowledgeSet(); + }} + onClose={() => setShowEdit(false)} + /> + + + + {knowledgeSet?.domain || "-"} + {knowledgeSet?.businessLine || "-"} + {knowledgeSet?.owner || "-"} + {knowledgeSet?.sensitivity || "-"} + + {knowledgeSet?.validFrom || "-"} ~ {knowledgeSet?.validTo || "-"} + + {knowledgeSet?.sourceType || "-"} + + + +
+
+ setSearchParams({ ...searchParams, filter: { type: [], status: [], tags: [] } })} + showViewToggle={false} + showReload={false} + /> +
+ + +
+
+ + {items.length === 0 ? ( + + ) : ( + + )} + + + { + setItemEditorOpen(false); + setCurrentItem(null); + }} + onSuccess={() => { + setItemEditorOpen(false); + setCurrentItem(null); + fetchData(); + }} + /> + + ); +}; + +export default KnowledgeSetDetail; diff --git a/frontend/src/pages/KnowledgeManagement/Home/KnowledgeManagementPage.tsx b/frontend/src/pages/KnowledgeManagement/Home/KnowledgeManagementPage.tsx new file mode 100644 index 0000000..39ec726 --- /dev/null +++ b/frontend/src/pages/KnowledgeManagement/Home/KnowledgeManagementPage.tsx @@ -0,0 +1,249 @@ +import { useEffect, useMemo, useState } from "react"; +import { Card, Button, Table, Tooltip, Tag, App } from "antd"; +import { DeleteOutlined, EditOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router"; +import CardView from "@/components/CardView"; +import { SearchControls } from "@/components/SearchControls"; +import TagManager from "@/components/business/TagManagement"; +import useFetchData from "@/hooks/useFetchData"; +import { + deleteKnowledgeSetByIdUsingDelete, + queryKnowledgeSetsUsingGet, +} from "../knowledge-management.api"; +import { + knowledgeStatusOptions, + mapKnowledgeSet, + KnowledgeSetView, +} from "../knowledge-management.const"; +import { KnowledgeSet } from "../knowledge-management.model"; +import CreateKnowledgeSet from "../components/CreateKnowledgeSet"; +import { + createDatasetTagUsingPost, + deleteDatasetTagUsingDelete, + queryDatasetTagsUsingGet, + updateDatasetTagUsingPut, +} from "@/pages/DataManagement/dataset.api"; + +export default function KnowledgeManagementPage() { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [viewMode, setViewMode] = useState<"card" | "list">("card"); + const [isEdit, setIsEdit] = useState(false); + const [currentSet, setCurrentSet] = useState(null); + const [tags, setTags] = useState([]); + + useEffect(() => { + const fetchTags = async () => { + const { data } = await queryDatasetTagsUsingGet(); + setTags(Array.isArray(data) ? data.map((tag) => tag.name) : []); + }; + fetchTags(); + }, []); + + const filterOptions = useMemo( + () => [ + { + key: "status", + label: "状态", + options: knowledgeStatusOptions, + }, + { + key: "tags", + label: "标签", + mode: "multiple", + options: tags.map((tag) => ({ label: tag, value: tag })), + }, + ], + [tags] + ); + + const { + loading, + tableData, + searchParams, + pagination, + fetchData, + setSearchParams, + handleFiltersChange, + handleKeywordChange, + } = useFetchData( + queryKnowledgeSetsUsingGet, + mapKnowledgeSet, + 30000, + false, + [], + 0 + ); + + const handleDeleteSet = async (setId: string) => { + await deleteKnowledgeSetByIdUsingDelete(setId); + message.success("知识集删除成功"); + fetchData({ pageOffset: 0 }); + }; + + const operations = [ + { + key: "edit", + label: "编辑", + icon: , + onClick: (item: KnowledgeSetView) => { + setIsEdit(true); + setCurrentSet({ + ...(item as unknown as KnowledgeSet), + status: item.rawStatus, + }); + }, + }, + { + key: "delete", + label: "删除", + danger: true, + icon: , + confirm: { + title: "确认删除", + description: "此操作不可撤销,是否继续?", + okText: "删除", + okType: "danger", + cancelText: "取消", + }, + onClick: (item: KnowledgeSetView) => handleDeleteSet(item.id), + }, + ]; + + const columns = [ + { + title: "知识集", + dataIndex: "name", + key: "name", + fixed: "left" as const, + width: 200, + ellipsis: true, + render: (_: unknown, record: KnowledgeSetView) => ( + + ), + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 120, + render: (status: KnowledgeSetView["status"]) => ( + {status?.label} + ), + }, + { + title: "领域", + dataIndex: "domain", + key: "domain", + width: 120, + ellipsis: true, + }, + { + title: "业务线", + dataIndex: "businessLine", + key: "businessLine", + width: 120, + ellipsis: true, + }, + { + title: "负责人", + dataIndex: "owner", + key: "owner", + width: 120, + ellipsis: true, + }, + { + title: "更新时间", + dataIndex: "updatedAt", + key: "updatedAt", + width: 180, + ellipsis: true, + }, + { + title: "操作", + key: "actions", + fixed: "right" as const, + width: 140, + render: (_: unknown, record: KnowledgeSetView) => ( +
+ {operations.map((op) => ( + +
+ ), + }, + ]; + + return ( +
+
+

知识管理

+
+ deleteDatasetTagUsingDelete({ ids })} + onUpdate={updateDatasetTagUsingPut} + onFetch={queryDatasetTagsUsingGet} + /> + { + fetchData(); + }} + onClose={() => { + setIsEdit(false); + setCurrentSet(null); + }} + /> +
+
+ + setSearchParams({ ...searchParams, filter: {} })} + viewMode={viewMode} + onViewModeChange={setViewMode} + showViewToggle + onReload={fetchData} + /> + + {viewMode === "card" ? ( + navigate(`/data/knowledge-management/detail/${item.id}`)} + /> + ) : ( + +
+ + )} + + ); +} diff --git a/frontend/src/pages/KnowledgeManagement/components/CreateKnowledgeSet.tsx b/frontend/src/pages/KnowledgeManagement/components/CreateKnowledgeSet.tsx new file mode 100644 index 0000000..925589b --- /dev/null +++ b/frontend/src/pages/KnowledgeManagement/components/CreateKnowledgeSet.tsx @@ -0,0 +1,201 @@ +import { useEffect, useState } from "react"; +import { Button, DatePicker, Form, Input, message, Modal, Select } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import dayjs from "dayjs"; +import { + createKnowledgeSetUsingPost, + updateKnowledgeSetByIdUsingPut, +} from "../knowledge-management.api"; +import { + knowledgeSourceTypeOptions, + knowledgeStatusOptions, +} from "../knowledge-management.const"; +import { + KnowledgeSet, + KnowledgeStatusType, +} from "../knowledge-management.model"; +import { queryDatasetTagsUsingGet } from "@/pages/DataManagement/dataset.api"; + +export default function CreateKnowledgeSet({ + isEdit, + data, + showBtn = true, + onUpdate, + onClose, +}: { + isEdit?: boolean; + showBtn?: boolean; + data?: Partial | null; + onUpdate: () => void; + onClose: () => void; +}) { + const [open, setOpen] = useState(false); + const [form] = Form.useForm(); + const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]); + + const fetchTags = async () => { + try { + const { data: tagData } = await queryDatasetTagsUsingGet(); + const options = Array.isArray(tagData) + ? tagData.map((tag) => ({ label: tag.name, value: tag.name })) + : []; + setTagOptions(options); + } catch (error) { + console.error("获取标签失败", error); + } + }; + + useEffect(() => { + if (open) { + fetchTags(); + } + }, [open]); + + useEffect(() => { + if (isEdit && data) { + setOpen(true); + form.setFieldsValue({ + name: data.name, + description: data.description, + status: data.status ?? KnowledgeStatusType.DRAFT, + domain: data.domain, + businessLine: data.businessLine, + owner: data.owner, + validFrom: data.validFrom ? dayjs(data.validFrom) : null, + validTo: data.validTo ? dayjs(data.validTo) : null, + sourceType: data.sourceType, + sensitivity: data.sensitivity, + tags: data.tags?.map((tag) => tag.name) || [], + metadata: data.metadata, + }); + } + }, [isEdit, data, form]); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const validFrom = values.validFrom ? values.validFrom.format("YYYY-MM-DD") : undefined; + const validTo = values.validTo ? values.validTo.format("YYYY-MM-DD") : undefined; + + if (validFrom && validTo && dayjs(validFrom).isAfter(dayjs(validTo))) { + message.warning("有效期开始不能晚于结束时间"); + return; + } + + const payload = { + ...values, + validFrom, + validTo, + tags: values.tags || [], + }; + + if (isEdit && data?.id) { + await updateKnowledgeSetByIdUsingPut(data.id, payload); + message.success("知识集更新成功"); + } else { + await createKnowledgeSetUsingPost(payload); + message.success("知识集创建成功"); + } + + setOpen(false); + form.resetFields(); + onUpdate(); + } catch { + message.error("操作失败,请重试"); + } + }; + + const handleClose = () => { + setOpen(false); + form.resetFields(); + onClose?.(); + }; + + const isReadOnly = + data?.status === KnowledgeStatusType.ARCHIVED || + data?.status === KnowledgeStatusType.DEPRECATED; + + return ( + <> + {showBtn && ( + + )} + +
+
+ + + + + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+ + + + + + + +
+ + ); +} diff --git a/frontend/src/pages/KnowledgeManagement/components/ImportKnowledgeItemsDialog.tsx b/frontend/src/pages/KnowledgeManagement/components/ImportKnowledgeItemsDialog.tsx new file mode 100644 index 0000000..7e4c537 --- /dev/null +++ b/frontend/src/pages/KnowledgeManagement/components/ImportKnowledgeItemsDialog.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { Button, Form, Modal, message, Select } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import DatasetFileTransfer from "@/components/business/DatasetFileTransfer"; +import { Dataset, DatasetFile } from "@/pages/DataManagement/dataset.model"; +import { + importKnowledgeItemsUsingPost, +} from "../knowledge-management.api"; +import { knowledgeStatusOptions } from "../knowledge-management.const"; +import { KnowledgeStatusType } from "../knowledge-management.model"; + +export default function ImportKnowledgeItemsDialog({ + setId, + onImported, + disabled, +}: { + setId: string; + onImported: () => void; + disabled?: boolean; +}) { + const [open, setOpen] = useState(false); + const [form] = Form.useForm(); + const [selectedFilesMap, setSelectedFilesMap] = useState>({}); + const [selectedDataset, setSelectedDataset] = useState(null); + + const handleSubmit = async () => { + if (!selectedDataset) { + message.warning("请先选择数据集"); + return; + } + const fileIds = Object.keys(selectedFilesMap); + if (fileIds.length === 0) { + message.warning("请至少选择一个文件"); + return; + } + + const values = await form.validateFields(); + + try { + await importKnowledgeItemsUsingPost(setId, { + datasetId: selectedDataset.id, + fileIds, + status: values.status || undefined, + }); + message.success("导入成功"); + setOpen(false); + setSelectedFilesMap({}); + setSelectedDataset(null); + form.resetFields(); + onImported(); + } catch { + message.error("导入失败,请重试"); + } + }; + + return ( + <> + + { + setOpen(false); + setSelectedFilesMap({}); + setSelectedDataset(null); + }} + onOk={handleSubmit} + okText="确定" + cancelText="取消" + width={1000} + maskClosable={false} + > +
+ + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + +