fix: 知识库key格式统一、过滤逻辑修复、布局宽度稳定
This commit is contained in:
@@ -27,6 +27,30 @@ class KnowledgeService:
|
||||
"报题单": "baoti",
|
||||
}
|
||||
|
||||
# 来源大类固定显示顺序(制片人 Obsidian 习惯)
|
||||
SOURCE_TYPE_ORDER = [
|
||||
"manuscript", # 节目文稿
|
||||
"military_report", # 杂志文章
|
||||
"baoti", # 报题单
|
||||
"manual", # 其他
|
||||
]
|
||||
|
||||
# 二级分组维度映射(与前端 useKnowledgeGrouping.js 的 SECONDARY_GROUP_FIELD 保持一致)
|
||||
# key = source_type, value = 用来做二级分组的字段名,None = 不建二级节点
|
||||
SECONDARY_GROUP_FIELD = {
|
||||
"manuscript": "author", # 节目文稿 → 按作者(编导)
|
||||
"military_report": "source_detail", # 杂志文章 → 按出处
|
||||
"baoti": None, # 报题单 → 不分组
|
||||
"manual": None, # 其他 → 不分组
|
||||
}
|
||||
|
||||
SOURCE_TYPE_LABEL = {
|
||||
"military_report": "杂志文章",
|
||||
"manuscript": "节目文稿",
|
||||
"baoti": "报题单",
|
||||
"manual": "其他",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.embedder = EmbeddingService()
|
||||
|
||||
@@ -243,46 +267,22 @@ class KnowledgeService:
|
||||
|
||||
def get_grouped_items(self) -> list[dict]:
|
||||
"""
|
||||
按 source_type → source_detail 两层聚合,返回树形结构数据。
|
||||
按 source_type → 二级字段(author / source_detail)两层聚合,返回树形结构数据。
|
||||
按 SOURCE_TYPE_ORDER 固定顺序排列。
|
||||
|
||||
返回格式:
|
||||
[
|
||||
{
|
||||
"key": "全部",
|
||||
"label": "全部(N条)",
|
||||
"count": 总条目数,
|
||||
"children": [
|
||||
{
|
||||
"key": "manuscript",
|
||||
"label": "节目文稿(n条)",
|
||||
"count": n,
|
||||
"children": [ # source_detail 为 null 时直接为空数组,不造空节点
|
||||
{"key": "...", "label": "具体出处", "count": n}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
]
|
||||
二级节点 key 格式:`{source_type}|{二级字段名}|{字段值}`
|
||||
例:manuscript|author|左鑫
|
||||
military_report|source_detail|航空知识 2026年第1期
|
||||
|
||||
注意:source_detail 为 null 的条目(如节目文稿、报题单),
|
||||
归入对应 source_type 大类下,但不单独成子节点(大类的 children 直接为其条目列表,
|
||||
但前端 Tree 组件渲染时,若 children 为空则不展示二级节点,大类直接可点击)。
|
||||
二级字段值为 null / 空字串 → 归入对应大类,不造空节点。
|
||||
"""
|
||||
SOURCE_TYPE_LABEL = {
|
||||
"military_report": "杂志文章",
|
||||
"manuscript": "节目文稿",
|
||||
"baoti": "报题单",
|
||||
"manual": "其他",
|
||||
}
|
||||
|
||||
with Session(engine) as session:
|
||||
items = session.exec(select(KnowledgeItem)).all()
|
||||
|
||||
total_count = len(items)
|
||||
|
||||
# 按 source_type 分组
|
||||
type_groups: dict = {}
|
||||
# 按 source_type 分组,初始化所有已知类别(保证空类别也按顺序出现)
|
||||
type_groups: dict = {st: [] for st in self.SOURCE_TYPE_ORDER}
|
||||
for item in items:
|
||||
st = item.source_type or "manual"
|
||||
if st not in type_groups:
|
||||
@@ -290,30 +290,48 @@ class KnowledgeService:
|
||||
type_groups[st].append(item)
|
||||
|
||||
children = []
|
||||
for st, st_items in type_groups.items():
|
||||
# 按 source_detail 细分
|
||||
detail_groups: dict = {}
|
||||
for item in st_items:
|
||||
tags = item.tags or {}
|
||||
sd = tags.get("source_detail") if isinstance(tags, dict) else None
|
||||
if sd not in detail_groups:
|
||||
detail_groups[sd] = []
|
||||
detail_groups[sd].append(item)
|
||||
for st in self.SOURCE_TYPE_ORDER:
|
||||
st_items = type_groups.get(st, [])
|
||||
if not st_items:
|
||||
# 空类别也按顺序出现(0条)
|
||||
children.append({
|
||||
"key": st,
|
||||
"label": f"{self.SOURCE_TYPE_LABEL.get(st, st)}(0条)",
|
||||
"count": 0,
|
||||
"children": [],
|
||||
})
|
||||
continue
|
||||
|
||||
# 构建二级节点:仅 source_detail 非 null 的才建子节点
|
||||
secondary_field = self.SECONDARY_GROUP_FIELD.get(st)
|
||||
grandchildren = []
|
||||
for sd, sd_items in detail_groups.items():
|
||||
if sd is not None:
|
||||
grandchildren.append({
|
||||
"key": f"{st}|{sd}",
|
||||
"label": f"{sd}({len(sd_items)}条)",
|
||||
"count": len(sd_items),
|
||||
})
|
||||
|
||||
label = SOURCE_TYPE_LABEL.get(st, st)
|
||||
if secondary_field is not None:
|
||||
# 按二级字段分组
|
||||
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
|
||||
field_val = sd
|
||||
else:
|
||||
field_val = (getattr(item, secondary_field, None) or "").strip() or None
|
||||
|
||||
if field_val not in detail_groups:
|
||||
detail_groups[field_val] = []
|
||||
detail_groups[field_val].append(item)
|
||||
|
||||
for sd, sd_items in detail_groups.items():
|
||||
if sd is not None:
|
||||
grandchildren.append({
|
||||
"key": f"{st}|{secondary_field}|{sd}",
|
||||
"label": f"{sd}({len(sd_items)}条)",
|
||||
"count": len(sd_items),
|
||||
})
|
||||
|
||||
children.append({
|
||||
"key": st,
|
||||
"label": f"{label}({len(st_items)}条)",
|
||||
"label": f"{self.SOURCE_TYPE_LABEL.get(st, st)}({len(st_items)}条)",
|
||||
"count": len(st_items),
|
||||
"children": grandchildren,
|
||||
})
|
||||
@@ -323,4 +341,4 @@ class KnowledgeService:
|
||||
"label": f"全部({total_count}条)",
|
||||
"count": total_count,
|
||||
"children": children,
|
||||
}]
|
||||
}]
|
||||
@@ -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) // { type, secondaryField, secondaryValue }
|
||||
const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { key, type, secondaryField, secondaryValue }
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true)
|
||||
@@ -110,13 +110,15 @@ export default function KnowledgeBase() {
|
||||
}
|
||||
|
||||
// 树节点选中 → 联动过滤
|
||||
// 新格式回调:{ key, type, secondaryField, secondaryValue }
|
||||
// 回调参数:{ key, type, secondaryField, secondaryValue } | null
|
||||
const handleNodeSelect = (node) => {
|
||||
setSelectedTreeNode(node)
|
||||
}
|
||||
|
||||
// 根据树节点过滤列表
|
||||
// 支持新旧两种 key 格式兼容(secondaryField 为 null 时走旧逻辑)
|
||||
// 二级节点 key 格式:type|fieldName|fieldValue
|
||||
// secondaryField === 'source_detail' → 用 item.source_detail 过滤
|
||||
// secondaryField === 'author' → 用 item.author 过滤
|
||||
const displayedItems = (() => {
|
||||
let result = items
|
||||
|
||||
@@ -124,16 +126,19 @@ export default function KnowledgeBase() {
|
||||
const { type, secondaryField, secondaryValue } = selectedTreeNode
|
||||
result = result.filter(i => {
|
||||
if (i.source_type !== type) return false
|
||||
// 二级过滤:新格式(type|field|value)
|
||||
if (secondaryField !== null && secondaryValue !== null) {
|
||||
// 按指定二级字段过滤
|
||||
const itemFieldVal = (i[secondaryField] || '').trim() || null
|
||||
if (itemFieldVal !== secondaryValue) return false
|
||||
if (secondaryField === 'source_detail') {
|
||||
if (i.source_detail !== secondaryValue) return false
|
||||
} else {
|
||||
// 其他二级字段(如 author)直接用字段名匹配
|
||||
const itemFieldVal = (i[secondaryField] || '').trim() || null
|
||||
if (itemFieldVal !== secondaryValue) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if (sourceDetailFilter) {
|
||||
// 保留原有的 source_detail 筛选逻辑
|
||||
// 保留原有的 source_detail 筛选逻辑(来自右侧 Select 下拉)
|
||||
result = result.filter(i => i.source_detail === sourceDetailFilter)
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ export default function KnowledgeBase() {
|
||||
})()
|
||||
|
||||
// 构建 selectedKey 供 KnowledgeTree 高亮
|
||||
// 格式:type|field|value(与 useKnowledgeGrouping 输出的 key 格式一致)
|
||||
// 格式:type|field|value(三段,与后端 get_grouped_items 输出的 key 一致)
|
||||
const buildSelectedKey = () => {
|
||||
if (!selectedTreeNode) return 'all'
|
||||
const { type, secondaryField, secondaryValue } = selectedTreeNode
|
||||
@@ -239,12 +244,13 @@ export default function KnowledgeBase() {
|
||||
</div>
|
||||
|
||||
{/* 双列对齐布局:左侧树 | 右侧筛选+列表 */}
|
||||
<Row gutter={12} align="stretch">
|
||||
{/* 左列:树 */}
|
||||
<Col span={5} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* 左列固定 280px,右列撑满,ant-row gutter=12 保持两列间距 */}
|
||||
<Row gutter={12}>
|
||||
{/* 左列:树(固定宽度,min-width 防止收缩) */}
|
||||
<Col span={5} style={{ minWidth: 280 }}>
|
||||
<Card
|
||||
style={{ borderRadius: 14, flex: 1 }}
|
||||
bodyStyle={{ padding: 0, flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
style={{ borderRadius: 14 }}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<KnowledgeTree
|
||||
treeData={treeData}
|
||||
@@ -254,10 +260,10 @@ export default function KnowledgeBase() {
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右列:筛选栏 + 列表 */}
|
||||
<Col span={19} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* 筛选栏 */}
|
||||
<Card style={{ borderRadius: 14, marginBottom: 12 }}>
|
||||
{/* 右列:筛选栏 + 列表(两栏独立等高,顶部对齐) */}
|
||||
<Col span={19} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* 筛选栏(顶部对齐,与左列卡片顶线平齐) */}
|
||||
<Card style={{ borderRadius: 14 }}>
|
||||
<Space size="middle" wrap>
|
||||
<span style={{ fontSize: 14, color: '#666' }}>筛选:</span>
|
||||
<Select
|
||||
|
||||
Reference in New Issue
Block a user