feat: 知识库树形视图(按来源分组),左侧树导航+右侧列表联动

This commit is contained in:
simonkoson
2026-05-27 09:35:36 +08:00
parent 7999fd14c8
commit 74a0508755
7 changed files with 477 additions and 55 deletions
@@ -0,0 +1,44 @@
.kt-container {
display: flex;
flex-direction: column;
height: 100%;
}
.kt-header {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.kt-tree-wrapper {
flex: 1;
overflow-y: auto;
padding: 8px 4px;
}
.kt-empty {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
color: #999;
font-size: 13px;
}
/* 节点文字 */
.kt-node-title {
font-size: 13px;
color: #3b4a3b;
}
/* 全部根节点高亮 */
.kt-node-root {
font-weight: 600;
color: #3b4a3b;
}
/* Tree 选中态 */
.kt-container .ant-tree-node-selected .kt-node-title {
color: #6b8e6b;
font-weight: 600;
}
@@ -0,0 +1,130 @@
/**
* 知识库树形导航组件(交互层)
*
* 职责:
* - 树的展开/收起/记忆状态
* - 节点高亮 + 联动回调
* - 「全部展开 / 全部收起」操作按钮
*
* 数据分组逻辑完全委托给 useKnowledgeGrouping(数据分组层),
* 本组件只管交互,不感知分组的业务含义。
* 将来切换分组维度时,只改 useKnowledgeGrouping,本组件一行不动。
*/
import { useState, useCallback } from 'react'
import { Tree, Button, Space } from 'antd'
import { CaretDownFilled, NodeIndexOutlined } from '@ant-design/icons'
import './KnowledgeTree.css'
const { TreeNode } = Tree
export default function KnowledgeTree({
treeData, // 来自 getGroupedItems() API 的原始树数据(含全部节点)
onNodeSelect, // 选中节点时回调,参数: { key, type, detail } | null(全部)
selectedKey, // 当前选中的 key(用于高亮)
}) {
const [expandedKeys, setExpandedKeys] = useState(() => {
// 默认全部展开
if (!treeData || treeData.length === 0) return []
const root = treeData[0]
return [root.key, ...(root.children || []).map(c => c.key)]
})
// 全部展开
const handleExpandAll = useCallback(() => {
if (!treeData || treeData.length === 0) return
const root = treeData[0]
const allKeys = [root.key]
for (const child of root.children || []) {
allKeys.push(child.key)
if (child.children) {
for (const grandchild of child.children) {
allKeys.push(grandchild.key)
}
}
}
setExpandedKeys(allKeys)
}, [treeData])
// 全部收起
const handleCollapseAll = useCallback(() => {
setExpandedKeys([])
}, [])
// 节点选择
const handleSelect = useCallback((selectedKeys, info) => {
if (selectedKeys.length === 0) {
onNodeSelect(null)
return
}
const key = selectedKeys[0]
// 解析 key 格式:
// "all" → 全部根节点
// "manuscript" → 大类节点
// "manuscript|具体出处" → 二级节点
if (key === 'all') {
onNodeSelect(null)
return
}
const parts = key.split('|')
const type = parts[0]
const detail = parts[1] || null
onNodeSelect({ key, type, detail })
}, [onNodeSelect])
// 渲染节点
const renderNode = (node, isRoot = false) => {
return (
<TreeNode
key={node.key}
title={
<span className={`kt-node-title ${isRoot ? 'kt-node-root' : ''}`}>
{node.label}
</span>
}
icon={isRoot ? <NodeIndexOutlined /> : undefined}
>
{(node.children || []).map(child => renderNode(child, false))}
</TreeNode>
)
}
if (!treeData || treeData.length === 0) {
return (
<div className="kt-empty">
暂无知识库条目
</div>
)
}
return (
<div className="kt-container">
<div className="kt-header">
<Space size="small">
<Button size="small" onClick={handleExpandAll}>
全部展开
</Button>
<Button size="small" onClick={handleCollapseAll}>
全部收起
</Button>
</Space>
</div>
<div className="kt-tree-wrapper">
<Tree
showIcon
showLine={{ showLeafIcon: false }}
expandedKeys={expandedKeys}
selectedKeys={selectedKey ? [selectedKey] : []}
onExpand={(keys) => setExpandedKeys(keys)}
onSelect={handleSelect}
blockNode
switcherIcon={<CaretDownFilled />}
>
{treeData.map(node => renderNode(node, true))}
</Tree>
</div>
</div>
)
}