Files
tps-dashboard/frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx
T

238 lines
7.0 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag } from 'antd'
import { InboxOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
import useAuthStore from '../../stores/authStore'
import knowledgeService from '../../services/knowledgeService'
const { Dragger } = Upload
// source_type 枚举值(固定五类,不写死但供 Select 用)
const SOURCE_TYPE_OPTIONS = [
{ label: '全部类型', value: '' },
{ label: '杂志文章', value: 'military_report' },
{ label: '节目文稿', value: 'manuscript' },
{ label: '报题单', value: 'baoti' },
]
// source_type 中文标签
const SOURCE_TYPE_LABEL = {
military_report: '杂志文章',
manuscript: '节目文稿',
baoti: '报题单',
manual: '其他',
}
export default function KnowledgeBase() {
const { user } = useAuthStore()
const [items, setItems] = useState([])
const [sources, setSources] = useState([])
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
const [sourceDetailFilter, setSourceDetailFilter] = useState('')
const fetchItems = async () => {
setLoading(true)
try {
const data = await knowledgeService.listItems(sourceTypeFilter || null)
setItems(data)
} catch {
message.error('加载知识库失败')
} finally {
setLoading(false)
}
}
const fetchSources = async () => {
try {
const data = await knowledgeService.listSources()
setSources(data.map(s => ({ label: s.source, value: s.source })))
} catch {
// ignore
}
}
useEffect(() => {
fetchItems()
fetchSources()
}, [sourceTypeFilter])
// 上传
const handleUpload = async (file) => {
if (!file.name.endsWith('.md')) {
message.warning('仅支持 .md 文件')
return false
}
setUploading(true)
try {
const result = await knowledgeService.uploadFiles([file])
const { uploaded, errors } = result
if (uploaded.length > 0) {
message.success(`成功入库 ${uploaded.length} 篇`)
fetchItems()
fetchSources()
}
if (errors.length > 0) {
errors.forEach(e => message.error(`${e.file}: ${e.error}`))
}
} catch {
message.error('上传失败')
} finally {
setUploading(false)
}
return false // 阻止默认上传行为
}
// 删除
const handleDelete = async (id) => {
try {
await knowledgeService.deleteItem(id)
message.success('删除成功')
fetchItems()
fetchSources()
} catch {
message.error('删除失败')
}
}
// 列表筛选后过滤 source_detail
const displayedItems = sourceDetailFilter
? items.filter(i => i.source_detail === sourceDetailFilter)
: items
const columns = [
{
title: '标题',
dataIndex: 'title',
key: 'title',
render: (text, record) => (
<span style={{ fontWeight: 500, color: '#3b4a3b' }}>{text}</span>
),
},
{
title: '作者',
dataIndex: 'author',
key: 'author',
render: (val) => val || null, // 无则不显示
},
{
title: '播出/发表时间',
dataIndex: 'publish_date',
key: 'publish_date',
render: (val) => val || null, // 无则不显示
},
{
title: '出处',
dataIndex: 'source_detail',
key: 'source_detail',
render: (val) => val || '-',
},
{
title: '类型',
dataIndex: 'source_type',
key: 'source_type',
render: (val) => (
<Tag color="default" style={{ borderRadius: 8 }}>
{SOURCE_TYPE_LABEL[val] || val}
</Tag>
),
},
{
title: '入库时间',
dataIndex: 'created_at',
key: 'created_at',
render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Popconfirm
title="确认删除"
description="删除后不可恢复,该篇及其向量将一并清除"
onConfirm={() => handleDelete(record.id)}
okText="确认删除"
cancelText="取消"
placement="left"
>
<Button size="small" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
),
},
]
return (
<div style={{ maxWidth: 1100, 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>
</div>
{/* 上传区 */}
<Card style={{ borderRadius: 14, marginBottom: 12 }} bodyStyle={{ padding: 0 }}>
<Dragger
accept=".md"
multiple
showUploadList={false}
beforeUpload={handleUpload}
disabled={uploading}
style={{ padding: '28px 20px', borderRadius: 14 }}
>
<p className="ant-upload-drag-icon" style={{ marginBottom: 8 }}>
<InboxOutlined style={{ fontSize: 40, color: '#6b8e6b' }} />
</p>
<p className="ant-upload-text" style={{ fontSize: 15, fontWeight: 500, color: '#3b4a3b' }}>
{uploading ? '正在上传并生成向量…' : '点击或拖拽 .md 文件上传'}
</p>
<p className="ant-upload-hint" style={{ fontSize: 12, color: '#888' }}>
支持批量上传系统自动解析 yaml frontmatter 并生成语义向量
</p>
</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
/>
<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 style={{ borderRadius: 14 }}>
<Table
columns={columns}
dataSource={displayedItems}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10, size: 'small' }}
locale={{ emptyText: '暂无知识库条目,上传 md 文件开始入库' }}
/>
</Card>
</div>
)
}