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' import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree' 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 [treeData, setTreeData] = useState([]) const [loading, setLoading] = useState(false) const [uploading, setUploading] = useState(false) const [sources, setSources] = useState([]) // 出处下拉选项 const [sourceTypeFilter, setSourceTypeFilter] = useState('') const [sourceDetailFilter, setSourceDetailFilter] = useState('') const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail } const fetchItems = async () => { setLoading(true) try { const data = await knowledgeService.listItems(sourceTypeFilter || null) setItems(data) } catch { message.error('加载知识库失败') } finally { setLoading(false) } } const fetchTree = async () => { try { const data = await knowledgeService.getGroupedItems() setTreeData(data || []) } catch { // ignore } } const fetchSources = async () => { try { const data = await knowledgeService.listSources() // 接口返回 [{source: "航空知识 2026年第1期"}, ...],提取 source 字段 setSources(data.map(s => s.source).filter(Boolean)) } catch { // ignore } } useEffect(() => { fetchItems() fetchTree() 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() fetchTree() } 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() fetchTree() } catch { message.error('删除失败') } } // 树节点选中 → 联动过滤 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 = [ { title: '标题', dataIndex: 'title', key: 'title', width: 240, render: (text) => ( {text} ), }, { title: '作者', dataIndex: 'author', key: 'author', width: 80, render: (val) => val || '-', }, { title: '播出/发表时间', dataIndex: 'publish_date', key: 'publish_date', width: 156, render: (val) => val || '-', }, { title: '出处', dataIndex: 'source_detail', key: 'source_detail', width: 170, render: (val) => val || '-', }, { title: '类型', dataIndex: 'source_type', key: 'source_type', width: 88, render: (val) => ( {SOURCE_TYPE_LABEL[val] || val} ), }, { title: '入库时间', dataIndex: 'created_at', key: 'created_at', width: 140, render: (val) => val ? new Date(val).toLocaleString('zh-CN', { hour12: false }).replace(' ', ' ') : '-', }, { title: '操作', key: 'action', width: 86, render: (_, record) => ( handleDelete(record.id)} okText="确认删除" cancelText="取消" placement="left" > ), }, ] return (

知识库

上传 md 笔记 · 语义检索 · 报题参考

{/* 上传区 */}

{uploading ? '正在上传并生成向量…' : '点击或拖拽 .md 文件上传'}

支持批量上传,系统自动解析 yaml frontmatter 并生成语义向量

{/* 主体:左侧树 + 右侧列表 */}
{/* 左侧树 */} {/* 右侧列表 + 筛选栏 */}
{/* 筛选栏 */} 筛选: ({ label: s, value: s }))]} value={sourceDetailFilter} onChange={val => { setSourceDetailFilter(val); setSelectedTreeNode(null) }} style={{ width: 200 }} allowClear /> {/* 列表 */} ) }