feat: M2-02/03 admin KB page — source list + reference tracking drawer
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ce00b58c4a
commit
0b0612760c
@ -1,51 +1,114 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Table, Tag, Typography, Button, App } from 'antd'
|
import { Table, Tag, Typography, Button, App, Drawer, Space, Descriptions } from 'antd'
|
||||||
import { ReloadOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { ReloadOutlined, DeleteOutlined, EyeOutlined, FileTextOutlined, LinkOutlined } from '@ant-design/icons'
|
||||||
import { getKnowledgeBases, deleteKnowledgeBase, type KnowledgeBase } from '@/services/knowledge-api'
|
import { api } from '@/services/http-client'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const { Title } = Typography
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
function KBPage() {
|
function KBPage() {
|
||||||
const { modal, message } = App.useApp()
|
const { modal, message } = App.useApp()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
|
const [selectedKb, setSelectedKb] = useState<any>(null)
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['knowledge-bases', page, pageSize],
|
queryKey: ['knowledge-bases', page, pageSize],
|
||||||
queryFn: () => getKnowledgeBases(page, pageSize),
|
queryFn: (): Promise<any> => api.get(`/admin-api/knowledge-bases?page=${page}&limit=${pageSize}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = (kb: KnowledgeBase) => modal.confirm({
|
const { data: sources } = useQuery({
|
||||||
title: '删除知识库', content: `确定删除「${kb.title}」?`, okType: 'danger',
|
queryKey: ['kb-sources', selectedKb?.id],
|
||||||
onOk: async () => { await deleteKnowledgeBase(kb.id); qc.invalidateQueries({ queryKey: ['knowledge-bases'] }); message.success('已删除') },
|
queryFn: (): Promise<any> => api.get(`/admin-api/knowledge-bases/${selectedKb?.id}/sources`),
|
||||||
|
enabled: !!selectedKb?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [refSourceId, setRefSourceId] = useState<string | null>(null)
|
||||||
|
const { data: references } = useQuery({
|
||||||
|
queryKey: ['source-refs', refSourceId],
|
||||||
|
queryFn: (): Promise<any> => api.get(`/admin-api/knowledge-bases/sources/${refSourceId}/references`),
|
||||||
|
enabled: !!refSourceId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDelete = (kb: any) => modal.confirm({
|
||||||
|
title: '删除知识库', content: `确定删除「${kb.title}」?`, okType: 'danger',
|
||||||
|
onOk: async () => { await api.delete(`/admin-api/knowledge-bases/${kb.id}`); qc.invalidateQueries({ queryKey: ['knowledge-bases'] }); message.success('已删除') },
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceCols = [
|
||||||
|
{ title: '标题', dataIndex: 'title', width: 180, ellipsis: true },
|
||||||
|
{ title: '文件名', dataIndex: 'originalFilename', width: 180, ellipsis: true },
|
||||||
|
{ title: '类型', dataIndex: 'type', width: 70 },
|
||||||
|
{ title: '解析', dataIndex: 'parseStatus', width: 70, render: (s: string) => <Tag color={s==='done'?'green':s==='processing'?'blue':s==='failed'?'red':'default'}>{s}</Tag> },
|
||||||
|
{ title: '版本', dataIndex: 'version', width: 60, align: 'center' as const },
|
||||||
|
{ title: '文本长度', dataIndex: 'textLength', width: 80, render: (v: number) => v?.toLocaleString() || '-' },
|
||||||
|
{ title: '引用', width: 70, render: (_: any, r: any) => (
|
||||||
|
<Button type="link" size="small" icon={<LinkOutlined />} onClick={() => setRefSourceId(r.id)}>查看</Button>
|
||||||
|
)},
|
||||||
|
]
|
||||||
|
|
||||||
|
const refCols = [
|
||||||
|
{ title: '工件类型', dataIndex: 'artifactType', width: 120 },
|
||||||
|
{ title: '工件ID', dataIndex: 'artifactId', width: 150, ellipsis: true },
|
||||||
|
{ title: '页码', dataIndex: 'pageNumber', width: 60, align: 'center' as const },
|
||||||
|
{ title: '章节', dataIndex: 'sectionTitle', width: 150, ellipsis: true },
|
||||||
|
{ title: '摘录', dataIndex: 'excerptText', ellipsis: true, render: (v: string) => v ? <Text style={{ fontSize: 12 }}>{v.slice(0, 100)}</Text> : '-' },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
<Title level={5} style={{ margin: 0 }}>知识库列表</Title>
|
<Title level={5} style={{ margin: 0 }}>知识库列表</Title>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['knowledge-bases'] })}>刷新</Button>
|
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['knowledge-bases'] })}>刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table<KnowledgeBase>
|
<Table
|
||||||
dataSource={data?.items || []} loading={isLoading} rowKey="id"
|
dataSource={data?.items || []} loading={isLoading} rowKey="id"
|
||||||
pagination={{ current: page, pageSize, total: data?.total || 0, showSizeChanger: true, showTotal: t => `共 ${t} 条`, onChange: (p, ps) => { setPage(p); setPageSize(ps) } }}
|
pagination={{ current: page, pageSize, total: data?.total || 0, showSizeChanger: true, showTotal: t => `共 ${t} 条`, onChange: (p, ps) => { setPage(p); setPageSize(ps) } }}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: '名称', dataIndex: 'title', width: 200, ellipsis: true },
|
{ title: '名称', dataIndex: 'title', width: 200, ellipsis: true },
|
||||||
{ title: '用户', width: 120, render: (_, r) => r.user?.nickname || r.user?.email || '-' },
|
{ title: '用户', width: 120, render: (_: any, r: any) => r.user?.nickname || r.user?.email || '-' },
|
||||||
{ title: '知识点', dataIndex: 'itemCount', width: 80, align: 'center' },
|
{ title: '知识点', dataIndex: 'itemCount', width: 80, align: 'center' },
|
||||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag> },
|
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag> },
|
||||||
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (d: string) => dayjs(d).format('YYYY-MM-DD HH:mm') },
|
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (d: string) => dayjs(d).format('YYYY-MM-DD HH:mm') },
|
||||||
{
|
{
|
||||||
title: '操作', width: 80, align: 'center',
|
title: '操作', width: 120, align: 'center',
|
||||||
render: (_, r) => (
|
render: (_, r) => (
|
||||||
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r)} />
|
<Space size="small">
|
||||||
|
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedKb(r); setDrawerOpen(true); setRefSourceId(null) }}>详情</Button>
|
||||||
|
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r)} />
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Drawer title={selectedKb?.title || '知识库详情'} open={drawerOpen} onClose={() => { setDrawerOpen(false); setRefSourceId(null) }} width={800}>
|
||||||
|
{selectedKb && (
|
||||||
|
<>
|
||||||
|
<Descriptions column={2} size="small" bordered style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="ID">{selectedKb.id}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户">{selectedKb.user?.nickname || selectedKb.user?.email || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="描述">{selectedKb.description || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="知识点数">{selectedKb._count?.items || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">{selectedKb.status}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间">{dayjs(selectedKb.createdAt).format('YYYY-MM-DD HH:mm')}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
<Title level={5} style={{ fontSize: 14 }}><FileTextOutlined /> 资料来源 ({sources?.length || 0})</Title>
|
||||||
|
<Table dataSource={sources || []} columns={sourceCols} rowKey="id" pagination={false} size="small" scroll={{ x: 900 }} style={{ marginBottom: 16 }} />
|
||||||
|
|
||||||
|
{refSourceId && (
|
||||||
|
<>
|
||||||
|
<Title level={5} style={{ fontSize: 14 }}><LinkOutlined /> 引用追踪 ({references?.length || 0})</Title>
|
||||||
|
<Table dataSource={references || []} columns={refCols} rowKey="id" pagination={false} size="small" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user