feat: 知识库树形视图(按来源分组),左侧树导航+右侧列表联动
This commit is contained in:
@@ -3,6 +3,7 @@ import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag }
|
||||
import { InboxOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import useAuthStore from '../../stores/authStore'
|
||||
import knowledgeService from '../../services/knowledgeService'
|
||||
import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree'
|
||||
|
||||
const { Dragger } = Upload
|
||||
|
||||
@@ -25,11 +26,12 @@ const SOURCE_TYPE_LABEL = {
|
||||
export default function KnowledgeBase() {
|
||||
const { user } = useAuthStore()
|
||||
const [items, setItems] = useState([])
|
||||
const [sources, setSources] = useState([])
|
||||
const [treeData, setTreeData] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
|
||||
const [sourceDetailFilter, setSourceDetailFilter] = useState('')
|
||||
const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail }
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true)
|
||||
@@ -43,10 +45,10 @@ export default function KnowledgeBase() {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSources = async () => {
|
||||
const fetchTree = async () => {
|
||||
try {
|
||||
const data = await knowledgeService.listSources()
|
||||
setSources(data.map(s => ({ label: s.source, value: s.source })))
|
||||
const data = await knowledgeService.getGroupedItems()
|
||||
setTreeData(data || [])
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -54,7 +56,7 @@ export default function KnowledgeBase() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems()
|
||||
fetchSources()
|
||||
fetchTree()
|
||||
}, [sourceTypeFilter])
|
||||
|
||||
// 上传
|
||||
@@ -70,7 +72,7 @@ export default function KnowledgeBase() {
|
||||
if (uploaded.length > 0) {
|
||||
message.success(`成功入库 ${uploaded.length} 篇`)
|
||||
fetchItems()
|
||||
fetchSources()
|
||||
fetchTree()
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
errors.forEach(e => message.error(`${e.file}: ${e.error}`))
|
||||
@@ -89,16 +91,36 @@ export default function KnowledgeBase() {
|
||||
await knowledgeService.deleteItem(id)
|
||||
message.success('删除成功')
|
||||
fetchItems()
|
||||
fetchSources()
|
||||
fetchTree()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 列表筛选后过滤 source_detail
|
||||
const displayedItems = sourceDetailFilter
|
||||
? items.filter(i => i.source_detail === sourceDetailFilter)
|
||||
: items
|
||||
// 树节点选中 → 联动过滤
|
||||
const handleNodeSelect = (node) => {
|
||||
setSelectedTreeNode(node)
|
||||
}
|
||||
|
||||
// 根据树节点过滤列表
|
||||
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 = [
|
||||
{
|
||||
@@ -113,13 +135,13 @@ export default function KnowledgeBase() {
|
||||
title: '作者',
|
||||
dataIndex: 'author',
|
||||
key: 'author',
|
||||
render: (val) => val || null, // 无则不显示
|
||||
render: (val) => val || null,
|
||||
},
|
||||
{
|
||||
title: '播出/发表时间',
|
||||
dataIndex: 'publish_date',
|
||||
key: 'publish_date',
|
||||
render: (val) => val || null, // 无则不显示
|
||||
render: (val) => val || null,
|
||||
},
|
||||
{
|
||||
title: '出处',
|
||||
@@ -164,7 +186,7 @@ export default function KnowledgeBase() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1100, padding: '12px' }}>
|
||||
<div style={{ maxWidth: 1200, padding: '12px' }}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: '#3b4a3b' }}>知识库</h2>
|
||||
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#888' }}>上传 md 笔记 · 语义检索 · 报题参考</p>
|
||||
@@ -192,47 +214,65 @@ export default function KnowledgeBase() {
|
||||
</Dragger>
|
||||
</Card>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<Card style={{ borderRadius: 14, marginBottom: 12 }}>
|
||||
<Space size="middle" wrap>
|
||||
<span style={{ fontSize: 13, color: '#666' }}>筛选:</span>
|
||||
<Select
|
||||
placeholder="按类型"
|
||||
options={SOURCE_TYPE_OPTIONS}
|
||||
value={sourceTypeFilter}
|
||||
onChange={val => { setSourceTypeFilter(val); setSourceDetailFilter('') }}
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
{/* 主体:左侧树 + 右侧列表 */}
|
||||
<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'}
|
||||
/>
|
||||
<Select
|
||||
placeholder="按出处"
|
||||
options={[{ label: '全部出处', value: '' }, ...sources]}
|
||||
value={sourceDetailFilter}
|
||||
onChange={setSourceDetailFilter}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { fetchItems(); fetchSources() }}
|
||||
size="small"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
{/* 列表 */}
|
||||
<Card style={{ borderRadius: 14 }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={displayedItems}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
locale={{ emptyText: '暂无知识库条目,上传 md 文件开始入库' }}
|
||||
/>
|
||||
</Card>
|
||||
{/* 右侧列表 + 筛选栏 */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* 筛选栏 */}
|
||||
<Card style={{ borderRadius: 14, marginBottom: 12 }}>
|
||||
<Space size="middle" wrap>
|
||||
<span style={{ fontSize: 13, color: '#666' }}>筛选:</span>
|
||||
<Select
|
||||
placeholder="按类型"
|
||||
options={SOURCE_TYPE_OPTIONS}
|
||||
value={sourceTypeFilter}
|
||||
onChange={val => { setSourceTypeFilter(val); setSourceDetailFilter(''); setSelectedTreeNode(null) }}
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="按出处"
|
||||
options={[{ label: '全部出处', value: '' }, ...(items.length ? [] : [])]}
|
||||
value={sourceDetailFilter}
|
||||
onChange={val => { setSourceDetailFilter(val); setSelectedTreeNode(null) }}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => { fetchItems(); fetchTree() }}
|
||||
size="small"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 列表 */}
|
||||
<Card style={{ borderRadius: 14 }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={displayedItems}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
locale={{ emptyText: '暂无知识库条目,上传 md 文件开始入库' }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user