feat: 知识库树形视图(按来源分组),左侧树导航+右侧列表联动
This commit is contained in:
@@ -90,3 +90,17 @@ def list_distinct_sources(
|
|||||||
svc = KnowledgeService()
|
svc = KnowledgeService()
|
||||||
sources = svc.get_distinct_sources()
|
sources = svc.get_distinct_sources()
|
||||||
return [{"source": s} for s in sources]
|
return [{"source": s} for s in sources]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/grouped")
|
||||||
|
def get_grouped_knowledge_items(
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
返回按 source_type → source_detail 两层聚合的树形结构,
|
||||||
|
含「全部」根节点,供知识库树形导航使用。
|
||||||
|
三角色均可读。
|
||||||
|
"""
|
||||||
|
svc = KnowledgeService()
|
||||||
|
return svc.get_grouped_items()
|
||||||
|
|||||||
@@ -240,3 +240,87 @@ class KnowledgeService:
|
|||||||
def get_embedding_count(self) -> int:
|
def get_embedding_count(self) -> int:
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
return len(session.exec(select(KnowledgeEmbedding)).all())
|
return len(session.exec(select(KnowledgeEmbedding)).all())
|
||||||
|
|
||||||
|
def get_grouped_items(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
按 source_type → source_detail 两层聚合,返回树形结构数据。
|
||||||
|
|
||||||
|
返回格式:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "全部",
|
||||||
|
"label": "全部(N条)",
|
||||||
|
"count": 总条目数,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "manuscript",
|
||||||
|
"label": "节目文稿(n条)",
|
||||||
|
"count": n,
|
||||||
|
"children": [ # source_detail 为 null 时直接为空数组,不造空节点
|
||||||
|
{"key": "...", "label": "具体出处", "count": n}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
注意:source_detail 为 null 的条目(如节目文稿、报题单),
|
||||||
|
归入对应 source_type 大类下,但不单独成子节点(大类的 children 直接为其条目列表,
|
||||||
|
但前端 Tree 组件渲染时,若 children 为空则不展示二级节点,大类直接可点击)。
|
||||||
|
"""
|
||||||
|
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 = {}
|
||||||
|
for item in items:
|
||||||
|
st = item.source_type or "manual"
|
||||||
|
if st not in type_groups:
|
||||||
|
type_groups[st] = []
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 构建二级节点:仅 source_detail 非 null 的才建子节点
|
||||||
|
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)
|
||||||
|
children.append({
|
||||||
|
"key": st,
|
||||||
|
"label": f"{label}({len(st_items)}条)",
|
||||||
|
"count": len(st_items),
|
||||||
|
"children": grandchildren,
|
||||||
|
})
|
||||||
|
|
||||||
|
return [{
|
||||||
|
"key": "all",
|
||||||
|
"label": f"全部({total_count}条)",
|
||||||
|
"count": total_count,
|
||||||
|
"children": children,
|
||||||
|
}]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 知识库数据分组层(交互层与分组维度完全解耦)
|
||||||
|
*
|
||||||
|
* 本模块职责:
|
||||||
|
* - 接收 items 数组,按指定维度(当前为 source_type → source_detail)生成分组树数据
|
||||||
|
* - 完全不涉及 UI(无 React 组件、无 Ant Design 引用)
|
||||||
|
*
|
||||||
|
* 将来切换为「按主题/装备分组」时:
|
||||||
|
* - 只改本文件中的 groupItemsBySource 函数
|
||||||
|
* - 前端 KnowledgeTree 组件一行都不用动
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按来源(source_type → source_detail)分组
|
||||||
|
* @param {Array} items - 知识库条目数组,含 source_type, source_detail 字段
|
||||||
|
* @returns {Array} 树形结构数据,供 KnowledgeTree 组件渲染
|
||||||
|
*
|
||||||
|
* 返回格式:
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* key: "all",
|
||||||
|
* label: "全部(N条)",
|
||||||
|
* count: 总条目数,
|
||||||
|
* children: [
|
||||||
|
* {
|
||||||
|
* key: "manuscript",
|
||||||
|
* label: "节目文稿(n条)",
|
||||||
|
* count: n,
|
||||||
|
* children: [ // 无 source_detail 则为空数组,不造空节点
|
||||||
|
* { key: "manuscript|具体出处", label: "具体出处(n条)", count: n }
|
||||||
|
* ]
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* 注意:source_detail 为 null 的条目归入对应 source_type 大类,
|
||||||
|
* 但不单独成子节点(大类的 children 为空数组时,大类直接可点击)。
|
||||||
|
*/
|
||||||
|
export function groupItemsBySource(items) {
|
||||||
|
const SOURCE_TYPE_LABEL = {
|
||||||
|
military_report: '杂志文章',
|
||||||
|
manuscript: '节目文稿',
|
||||||
|
baoti: '报题单',
|
||||||
|
manual: '其他',
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = items.length
|
||||||
|
|
||||||
|
// 按 source_type 分组
|
||||||
|
const typeGroups = {}
|
||||||
|
for (const item of items) {
|
||||||
|
const st = item.source_type || 'manual'
|
||||||
|
if (!typeGroups[st]) {
|
||||||
|
typeGroups[st] = []
|
||||||
|
}
|
||||||
|
typeGroups[st].push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = []
|
||||||
|
for (const [st, stItems] of Object.entries(typeGroups)) {
|
||||||
|
// 按 source_detail 细分
|
||||||
|
const detailGroups = {}
|
||||||
|
for (const item of stItems) {
|
||||||
|
const sd = item.source_detail || null
|
||||||
|
if (!detailGroups[sd]) {
|
||||||
|
detailGroups[sd] = []
|
||||||
|
}
|
||||||
|
detailGroups[sd].push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建二级节点:仅 source_detail 非 null 的才建子节点
|
||||||
|
const grandchildren = []
|
||||||
|
for (const [sd, sdItems] of Object.entries(detailGroups)) {
|
||||||
|
if (sd !== null) {
|
||||||
|
grandchildren.push({
|
||||||
|
key: `${st}|${sd}`,
|
||||||
|
label: `${sd}(${sdItems.length}条)`,
|
||||||
|
count: sdItems.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = SOURCE_TYPE_LABEL[st] || st
|
||||||
|
children.push({
|
||||||
|
key: st,
|
||||||
|
label: `${label}(${stItems.length}条)`,
|
||||||
|
count: stItems.length,
|
||||||
|
children: grandchildren,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: `全部(${totalCount}条)`,
|
||||||
|
count: totalCount,
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag }
|
|||||||
import { InboxOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { InboxOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import useAuthStore from '../../stores/authStore'
|
import useAuthStore from '../../stores/authStore'
|
||||||
import knowledgeService from '../../services/knowledgeService'
|
import knowledgeService from '../../services/knowledgeService'
|
||||||
|
import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree'
|
||||||
|
|
||||||
const { Dragger } = Upload
|
const { Dragger } = Upload
|
||||||
|
|
||||||
@@ -25,11 +26,12 @@ const SOURCE_TYPE_LABEL = {
|
|||||||
export default function KnowledgeBase() {
|
export default function KnowledgeBase() {
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
const [items, setItems] = useState([])
|
const [items, setItems] = useState([])
|
||||||
const [sources, setSources] = useState([])
|
const [treeData, setTreeData] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
|
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
|
||||||
const [sourceDetailFilter, setSourceDetailFilter] = useState('')
|
const [sourceDetailFilter, setSourceDetailFilter] = useState('')
|
||||||
|
const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail }
|
||||||
|
|
||||||
const fetchItems = async () => {
|
const fetchItems = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -43,10 +45,10 @@ export default function KnowledgeBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSources = async () => {
|
const fetchTree = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await knowledgeService.listSources()
|
const data = await knowledgeService.getGroupedItems()
|
||||||
setSources(data.map(s => ({ label: s.source, value: s.source })))
|
setTreeData(data || [])
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItems()
|
fetchItems()
|
||||||
fetchSources()
|
fetchTree()
|
||||||
}, [sourceTypeFilter])
|
}, [sourceTypeFilter])
|
||||||
|
|
||||||
// 上传
|
// 上传
|
||||||
@@ -70,7 +72,7 @@ export default function KnowledgeBase() {
|
|||||||
if (uploaded.length > 0) {
|
if (uploaded.length > 0) {
|
||||||
message.success(`成功入库 ${uploaded.length} 篇`)
|
message.success(`成功入库 ${uploaded.length} 篇`)
|
||||||
fetchItems()
|
fetchItems()
|
||||||
fetchSources()
|
fetchTree()
|
||||||
}
|
}
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
errors.forEach(e => message.error(`${e.file}: ${e.error}`))
|
errors.forEach(e => message.error(`${e.file}: ${e.error}`))
|
||||||
@@ -89,16 +91,36 @@ export default function KnowledgeBase() {
|
|||||||
await knowledgeService.deleteItem(id)
|
await knowledgeService.deleteItem(id)
|
||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
fetchItems()
|
fetchItems()
|
||||||
fetchSources()
|
fetchTree()
|
||||||
} catch {
|
} catch {
|
||||||
message.error('删除失败')
|
message.error('删除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表筛选后过滤 source_detail
|
// 树节点选中 → 联动过滤
|
||||||
const displayedItems = sourceDetailFilter
|
const handleNodeSelect = (node) => {
|
||||||
? items.filter(i => i.source_detail === sourceDetailFilter)
|
setSelectedTreeNode(node)
|
||||||
: items
|
}
|
||||||
|
|
||||||
|
// 根据树节点过滤列表
|
||||||
|
const displayedItems = (() => {
|
||||||
|
let result = items
|
||||||
|
|
||||||
|
// 树节点过滤优先(覆盖原有 Select 筛选)
|
||||||
|
if (selectedTreeNode) {
|
||||||
|
const { type, detail } = selectedTreeNode
|
||||||
|
result = result.filter(i => {
|
||||||
|
if (i.source_type !== type) return false
|
||||||
|
if (detail !== null && i.source_detail !== detail) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else if (sourceDetailFilter) {
|
||||||
|
// 保留原有的 source_detail 筛选逻辑
|
||||||
|
result = result.filter(i => i.source_detail === sourceDetailFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})()
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -113,13 +135,13 @@ export default function KnowledgeBase() {
|
|||||||
title: '作者',
|
title: '作者',
|
||||||
dataIndex: 'author',
|
dataIndex: 'author',
|
||||||
key: 'author',
|
key: 'author',
|
||||||
render: (val) => val || null, // 无则不显示
|
render: (val) => val || null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '播出/发表时间',
|
title: '播出/发表时间',
|
||||||
dataIndex: 'publish_date',
|
dataIndex: 'publish_date',
|
||||||
key: 'publish_date',
|
key: 'publish_date',
|
||||||
render: (val) => val || null, // 无则不显示
|
render: (val) => val || null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '出处',
|
title: '出处',
|
||||||
@@ -164,7 +186,7 @@ export default function KnowledgeBase() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 1100, padding: '12px' }}>
|
<div style={{ maxWidth: 1200, padding: '12px' }}>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: '#3b4a3b' }}>知识库</h2>
|
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: '#3b4a3b' }}>知识库</h2>
|
||||||
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#888' }}>上传 md 笔记 · 语义检索 · 报题参考</p>
|
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#888' }}>上传 md 笔记 · 语义检索 · 报题参考</p>
|
||||||
@@ -192,6 +214,22 @@ export default function KnowledgeBase() {
|
|||||||
</Dragger>
|
</Dragger>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 主体:左侧树 + 右侧列表 */}
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||||
|
{/* 左侧树 */}
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 14, width: 280, flexShrink: 0 }}
|
||||||
|
bodyStyle={{ padding: 0, height: '100%' }}
|
||||||
|
>
|
||||||
|
<KnowledgeTree
|
||||||
|
treeData={treeData}
|
||||||
|
onNodeSelect={handleNodeSelect}
|
||||||
|
selectedKey={selectedTreeNode ? `${selectedTreeNode.type}${selectedTreeNode.detail ? '|' + selectedTreeNode.detail : ''}` : 'all'}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 右侧列表 + 筛选栏 */}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{/* 筛选栏 */}
|
{/* 筛选栏 */}
|
||||||
<Card style={{ borderRadius: 14, marginBottom: 12 }}>
|
<Card style={{ borderRadius: 14, marginBottom: 12 }}>
|
||||||
<Space size="middle" wrap>
|
<Space size="middle" wrap>
|
||||||
@@ -200,21 +238,21 @@ export default function KnowledgeBase() {
|
|||||||
placeholder="按类型"
|
placeholder="按类型"
|
||||||
options={SOURCE_TYPE_OPTIONS}
|
options={SOURCE_TYPE_OPTIONS}
|
||||||
value={sourceTypeFilter}
|
value={sourceTypeFilter}
|
||||||
onChange={val => { setSourceTypeFilter(val); setSourceDetailFilter('') }}
|
onChange={val => { setSourceTypeFilter(val); setSourceDetailFilter(''); setSelectedTreeNode(null) }}
|
||||||
style={{ width: 140 }}
|
style={{ width: 140 }}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
placeholder="按出处"
|
placeholder="按出处"
|
||||||
options={[{ label: '全部出处', value: '' }, ...sources]}
|
options={[{ label: '全部出处', value: '' }, ...(items.length ? [] : [])]}
|
||||||
value={sourceDetailFilter}
|
value={sourceDetailFilter}
|
||||||
onChange={setSourceDetailFilter}
|
onChange={val => { setSourceDetailFilter(val); setSelectedTreeNode(null) }}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={() => { fetchItems(); fetchSources() }}
|
onClick={() => { fetchItems(); fetchTree() }}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
刷新
|
刷新
|
||||||
@@ -234,5 +272,7 @@ export default function KnowledgeBase() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,14 @@ const knowledgeService = {
|
|||||||
const resp = await http.get('/knowledge/sources')
|
const resp = await http.get('/knowledge/sources')
|
||||||
return resp.data
|
return resp.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按来源分组的树形结构(含「全部」根节点)
|
||||||
|
*/
|
||||||
|
async getGroupedItems() {
|
||||||
|
const resp = await http.get('/knowledge/grouped')
|
||||||
|
return resp.data
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default knowledgeService
|
export default knowledgeService
|
||||||
Reference in New Issue
Block a user