From 347a06e48b948519eeef12a9a28462bbe9b0b88b Mon Sep 17 00:00:00 2001 From: simonkoson <28867558@qq.com> Date: Wed, 27 May 2026 14:35:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80Grid=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E6=81=A2=E5=A4=8Dflex=E5=B9=B6=E5=88=97=EF=BC=8C=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E4=BA=8C=E7=BA=A7=E5=88=86=E7=BB=84=E6=98=A0=E5=B0=84?= =?UTF-8?q?=EF=BC=8C=E7=A9=BA=E5=A4=A7=E7=B1=BB=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/knowledge_service.py | 24 +++--- .../KnowledgeTree/KnowledgeTree.jsx | 38 ++++----- .../src/pages/KnowledgeBase/KnowledgeBase.jsx | 81 +++++++------------ 3 files changed, 57 insertions(+), 86 deletions(-) diff --git a/backend/app/services/knowledge_service.py b/backend/app/services/knowledge_service.py index fffd5c1..b9051e0 100644 --- a/backend/app/services/knowledge_service.py +++ b/backend/app/services/knowledge_service.py @@ -35,13 +35,13 @@ class KnowledgeService: "manual", # 其他 ] - # 二级分组维度映射(与前端 useKnowledgeGrouping.js 的 SECONDARY_GROUP_FIELD 保持一致) + # 二级分组维度映射(与前端 useKnowledgeGrouping.js 的 SECONDARY_GROUP_FIELD 一致) # key = source_type, value = 用来做二级分组的字段名,None = 不建二级节点 SECONDARY_GROUP_FIELD = { - "manuscript": "author", # 节目文稿 → 按作者(编导) - "military_report": "source_detail", # 杂志文章 → 按出处 - "baoti": None, # 报题单 → 不分组 - "manual": None, # 其他 → 不分组 + "manuscript": "author", # 节目文稿 → 按作者(编导) + "military_report": "source_detail", # 杂志文章 → 按出处 + "baoti": None, # 报题单 → 不分组 + "manual": None, # 其他 → 不分组 } SOURCE_TYPE_LABEL = { @@ -268,20 +268,21 @@ class KnowledgeService: def get_grouped_items(self) -> list[dict]: """ 按 source_type → 二级字段(author / source_detail)两层聚合,返回树形结构数据。 - 按 SOURCE_TYPE_ORDER 固定顺序排列。 + 按 SOURCE_TYPE_ORDER 固定顺序排列,仅显示有数据的大类(count > 0)。 二级节点 key 格式:`{source_type}|{二级字段名}|{字段值}` 例:manuscript|author|左鑫 military_report|source_detail|航空知识 2026年第1期 二级字段值为 null / 空字串 → 归入对应大类,不造空节点。 + 空大类(0条)不渲染。 """ with Session(engine) as session: items = session.exec(select(KnowledgeItem)).all() total_count = len(items) - # 按 source_type 分组,初始化所有已知类别(保证空类别也按顺序出现) + # 按 source_type 分组,初始化所有已知类别 type_groups: dict = {st: [] for st in self.SOURCE_TYPE_ORDER} for item in items: st = item.source_type or "manual" @@ -292,14 +293,8 @@ class KnowledgeService: children = [] for st in self.SOURCE_TYPE_ORDER: st_items = type_groups.get(st, []) + # 空类别(0条)不渲染 if not st_items: - # 空类别也按顺序出现(0条) - children.append({ - "key": st, - "label": f"{self.SOURCE_TYPE_LABEL.get(st, st)}(0条)", - "count": 0, - "children": [], - }) continue secondary_field = self.SECONDARY_GROUP_FIELD.get(st) @@ -309,7 +304,6 @@ class KnowledgeService: # 按二级字段分组 detail_groups: dict = {} for item in st_items: - # 取二级字段值:author 直接取,source_detail 从 tags(JSONB) 取 if secondary_field == "source_detail": tags = item.tags or {} sd = tags.get("source_detail") if isinstance(tags, dict) else None diff --git a/frontend/src/components/KnowledgeTree/KnowledgeTree.jsx b/frontend/src/components/KnowledgeTree/KnowledgeTree.jsx index 42dd926..bda7281 100644 --- a/frontend/src/components/KnowledgeTree/KnowledgeTree.jsx +++ b/frontend/src/components/KnowledgeTree/KnowledgeTree.jsx @@ -6,15 +6,20 @@ * - 节点高亮 + 联动回调 * - 「全部展开 / 全部收起」操作按钮 * - * 数据分组逻辑完全委托给 useKnowledgeGrouping(数据分组层), + * 数据分组逻辑完全委托给后端 get_grouped_items(数据分组层), * 本组件只管交互,不感知分组的业务含义。 * - * 二级节点 key 格式(由 useKnowledgeGrouping 控制): - * - 大类节点: "manuscript" - * - 二级节点(新格式): "manuscript|author|左鑫" - * type|二级字段名|字段值 + * 二级节点 key 格式(由后端控制): + * - 大类节点: "manuscript" + * - 二级节点(新格式): "manuscript|author|左鑫" + * type|二级字段名|字段值 + * - 二级节点(出处,杂志文章): "military_report|source_detail|航空知识..." * - * 将来切换分组维度时,只改 useKnowledgeGrouping,本组件只调整 key 解析逻辑(解包字段数)。 + * 回调给 KnowledgeBase:统一返回 { type, detail } + * - 大类节点 → { type: "manuscript", detail: null } + * - 作者二级节点 → { type: "manuscript", detail: "左鑫" } + * - 出处二级节点 → { type: "military_report", detail: "航空知识..." } + * (detail=author名 或 source_detail原文,与 displayedItems 过滤逻辑对齐) */ import { useState, useCallback } from 'react' @@ -26,7 +31,7 @@ const { TreeNode } = Tree export default function KnowledgeTree({ treeData, // 来自 getGroupedItems() API 的原始树数据(含全部节点) - onNodeSelect, // 选中节点时回调,参数: { key, type, secondaryField, secondaryValue } | null(全部) + onNodeSelect, // 选中节点时回调,参数: { key, type, detail } | null(全部) selectedKey, // 当前选中的 key(用于高亮) }) { const [expandedKeys, setExpandedKeys] = useState(() => { @@ -66,9 +71,10 @@ export default function KnowledgeTree({ const key = selectedKeys[0] // 解析 key 格式: - // "all" → 全部根节点 - // "manuscript" → 大类节点 - // "manuscript|author|左鑫" → 二级节点(type|二级字段名|字段值) + // "all" → 全部根节点 + // "manuscript" → 大类节点 + // "manuscript|author|左鑫" → 二级节点(新格式:type|fieldName|fieldValue) + // "military_report|source_detail|..." → 二级节点(出处) if (key === 'all') { onNodeSelect(null) return @@ -77,15 +83,11 @@ export default function KnowledgeTree({ const parts = key.split('|') if (parts.length === 1) { // 大类节点 - onNodeSelect({ key, type: parts[0], secondaryField: null, secondaryValue: null }) + onNodeSelect({ key, type: parts[0], detail: null }) } else { - // 二级节点:新格式 type|fieldName|fieldValue - onNodeSelect({ - key, - type: parts[0], - secondaryField: parts[1], - secondaryValue: parts[2], - }) + // 二级节点:parts[0]=type, parts[1]=字段名(废弃不用), parts[2]=字段值 + // 统一以 parts[2] 作为 detail(作者名 或 出处名),与 displayedItems 过滤逻辑对齐 + onNodeSelect({ key, type: parts[0], detail: parts[2] }) } }, [onNodeSelect]) diff --git a/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx b/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx index 48a9f6d..bf5c0ad 100644 --- a/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag, Row, Col } from 'antd' -import { InboxOutlined, DeleteOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons' +import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag } from 'antd' +import { DeleteOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons' import useAuthStore from '../../stores/authStore' import knowledgeService from '../../services/knowledgeService' import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree' @@ -32,7 +32,7 @@ export default function KnowledgeBase() { const [sources, setSources] = useState([]) // 出处下拉选项 const [sourceTypeFilter, setSourceTypeFilter] = useState('') const [sourceDetailFilter, setSourceDetailFilter] = useState('') - const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { key, type, secondaryField, secondaryValue } + const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail } const fetchItems = async () => { setLoading(true) @@ -110,52 +110,30 @@ export default function KnowledgeBase() { } // 树节点选中 → 联动过滤 - // 回调参数:{ key, type, secondaryField, secondaryValue } | null const handleNodeSelect = (node) => { setSelectedTreeNode(node) } // 根据树节点过滤列表 - // 二级节点 key 格式:type|fieldName|fieldValue - // secondaryField === 'source_detail' → 用 item.source_detail 过滤 - // secondaryField === 'author' → 用 item.author 过滤 const displayedItems = (() => { let result = items + // 树节点过滤优先(覆盖原有 Select 筛选) if (selectedTreeNode) { - const { type, secondaryField, secondaryValue } = selectedTreeNode + const { type, detail } = selectedTreeNode result = result.filter(i => { if (i.source_type !== type) return false - if (secondaryField !== null && secondaryValue !== null) { - if (secondaryField === 'source_detail') { - if (i.source_detail !== secondaryValue) return false - } else { - // 其他二级字段(如 author)直接用字段名匹配 - const itemFieldVal = (i[secondaryField] || '').trim() || null - if (itemFieldVal !== secondaryValue) return false - } - } + if (detail !== null && i.source_detail !== detail) return false return true }) } else if (sourceDetailFilter) { - // 保留原有的 source_detail 筛选逻辑(来自右侧 Select 下拉) + // 保留原有的 source_detail 筛选逻辑 result = result.filter(i => i.source_detail === sourceDetailFilter) } return result })() - // 构建 selectedKey 供 KnowledgeTree 高亮 - // 格式:type|field|value(三段,与后端 get_grouped_items 输出的 key 一致) - const buildSelectedKey = () => { - if (!selectedTreeNode) return 'all' - const { type, secondaryField, secondaryValue } = selectedTreeNode - if (secondaryField === null || secondaryValue === null) { - return type - } - return `${type}|${secondaryField}|${secondaryValue}` - } - const columns = [ { title: '标题', @@ -220,14 +198,14 @@ export default function KnowledgeBase() { ] return ( -
上传 md 笔记 · 语义检索 · 报题参考