02d89e6385
Add knowledge document GET/PUT and admin edit UI; auto-create support tickets on client 400 validation errors across user/shop/partner apps; batch create tasks and publish from support tickets; restore mini-user store env single-column layout. Co-authored-by: Cursor <cursoragent@cursor.com>
477 lines
14 KiB
TypeScript
477 lines
14 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Button,
|
|
Drawer,
|
|
Form,
|
|
Input,
|
|
Modal,
|
|
Popconfirm,
|
|
Space,
|
|
Switch,
|
|
Table,
|
|
Tag,
|
|
Typography,
|
|
Upload,
|
|
message,
|
|
} from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { UploadOutlined } from '@ant-design/icons';
|
|
import type {
|
|
KnowledgeBaseDto,
|
|
KnowledgeDocumentDetailDto,
|
|
KnowledgeDocumentDto,
|
|
} from '@dukang/shared-types';
|
|
import { request } from '../lib/api';
|
|
import { fmtTime } from '../lib/constants';
|
|
import { useAdminList } from '../lib/useAdminList';
|
|
import { uploadFileToOss } from '../lib/upload';
|
|
|
|
type FormValues = {
|
|
name: string;
|
|
description?: string;
|
|
enabled: boolean;
|
|
};
|
|
|
|
type DocForm = {
|
|
title: string;
|
|
contentText?: string;
|
|
};
|
|
|
|
export default function KnowledgeBasesPage() {
|
|
const [filterForm] = Form.useForm();
|
|
const [form] = Form.useForm<FormValues>();
|
|
const [docForm] = Form.useForm<DocForm>();
|
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editing, setEditing] = useState<KnowledgeBaseDto | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [drawerKb, setDrawerKb] = useState<KnowledgeBaseDto | null>(null);
|
|
const [docs, setDocs] = useState<KnowledgeDocumentDto[]>([]);
|
|
const [docsLoading, setDocsLoading] = useState(false);
|
|
const [docModalOpen, setDocModalOpen] = useState(false);
|
|
const [docSaving, setDocSaving] = useState(false);
|
|
const [editingDoc, setEditingDoc] = useState<KnowledgeDocumentDto | null>(null);
|
|
const [editDocModalOpen, setEditDocModalOpen] = useState(false);
|
|
const [editDocSaving, setEditDocSaving] = useState(false);
|
|
const [editDocForm] = Form.useForm<DocForm>();
|
|
|
|
const { data, loading, page, pageSize, setPage, setPageSize, reload } =
|
|
useAdminList<KnowledgeBaseDto>(
|
|
'/admin/knowledge-bases',
|
|
() => {
|
|
const qs = new URLSearchParams();
|
|
if (filters.name) qs.set('name', filters.name);
|
|
if (filters.enabled) qs.set('enabled', filters.enabled);
|
|
return qs;
|
|
},
|
|
[filters],
|
|
);
|
|
|
|
const loadDocs = async (kb: KnowledgeBaseDto) => {
|
|
setDrawerKb(kb);
|
|
setDocsLoading(true);
|
|
try {
|
|
const res = await request<{ items: KnowledgeDocumentDto[] }>(
|
|
`/admin/knowledge-bases/${kb.id}/documents`,
|
|
);
|
|
setDocs(res.items);
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setDocsLoading(false);
|
|
}
|
|
};
|
|
|
|
const openCreate = () => {
|
|
setEditing(null);
|
|
form.setFieldsValue({ name: '', description: '', enabled: true });
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const openEdit = (row: KnowledgeBaseDto) => {
|
|
setEditing(row);
|
|
form.setFieldsValue({
|
|
name: row.name,
|
|
description: row.description || '',
|
|
enabled: row.enabled,
|
|
});
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const onSave = async () => {
|
|
const values = await form.validateFields();
|
|
setSaving(true);
|
|
try {
|
|
if (editing) {
|
|
await request(`/admin/knowledge-bases/${editing.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(values),
|
|
});
|
|
message.success('已保存');
|
|
} else {
|
|
await request('/admin/knowledge-bases', {
|
|
method: 'POST',
|
|
body: JSON.stringify(values),
|
|
});
|
|
message.success('已创建');
|
|
}
|
|
setModalOpen(false);
|
|
reload();
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const columns: ColumnsType<KnowledgeBaseDto> = [
|
|
{ title: '名称', dataIndex: 'name' },
|
|
{ title: '说明', dataIndex: 'description', ellipsis: true },
|
|
{ title: '文档数', dataIndex: 'documentCount', width: 90 },
|
|
{
|
|
title: '启用',
|
|
dataIndex: 'enabled',
|
|
width: 90,
|
|
render: (v: boolean) => (v ? <Tag color="green">开</Tag> : <Tag>关</Tag>),
|
|
},
|
|
{
|
|
title: '创建人',
|
|
dataIndex: 'createdByName',
|
|
width: 100,
|
|
render: (v: string | null, row) => v || row.createdByHqAccountId,
|
|
},
|
|
{
|
|
title: '更新时间',
|
|
dataIndex: 'updatedAt',
|
|
width: 170,
|
|
render: (t: string) => fmtTime(t),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 240,
|
|
render: (_, row) => (
|
|
<Space>
|
|
<Button type="link" size="small" onClick={() => void loadDocs(row)}>
|
|
文档
|
|
</Button>
|
|
{row.canEditFull ? (
|
|
<Button type="link" size="small" onClick={() => openEdit(row)}>
|
|
编辑
|
|
</Button>
|
|
) : null}
|
|
{row.canEditFull ? (
|
|
<Popconfirm
|
|
title="确认删除知识库及全部文档?"
|
|
onConfirm={async () => {
|
|
try {
|
|
await request(`/admin/knowledge-bases/${row.id}`, { method: 'DELETE' });
|
|
message.success('已删除');
|
|
reload();
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}}
|
|
>
|
|
<Button type="link" size="small" danger>
|
|
删除
|
|
</Button>
|
|
</Popconfirm>
|
|
) : null}
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
const docColumns: ColumnsType<KnowledgeDocumentDto> = [
|
|
{ title: '标题', dataIndex: 'title' },
|
|
{ title: '文件', dataIndex: 'fileName', ellipsis: true },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 90,
|
|
render: (s: string) =>
|
|
s === 'READY' ? <Tag color="green">可检索</Tag> : s === 'FAILED' ? <Tag color="red">失败</Tag> : <Tag>无正文</Tag>,
|
|
},
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'createdAt',
|
|
width: 170,
|
|
render: (t: string) => fmtTime(t),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 140,
|
|
render: (_, row) =>
|
|
drawerKb?.canEditFull ? (
|
|
<Space>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
onClick={async () => {
|
|
if (!drawerKb) return;
|
|
try {
|
|
const detail = await request<KnowledgeDocumentDetailDto>(
|
|
`/admin/knowledge-bases/${drawerKb.id}/documents/${row.id}`,
|
|
);
|
|
setEditingDoc(row);
|
|
editDocForm.setFieldsValue({
|
|
title: detail.title,
|
|
contentText: detail.contentText || '',
|
|
});
|
|
setEditDocModalOpen(true);
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title="删除文档?"
|
|
onConfirm={async () => {
|
|
try {
|
|
await request(`/admin/knowledge-bases/${drawerKb.id}/documents/${row.id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
message.success('已删除');
|
|
void loadDocs(drawerKb);
|
|
reload();
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}}
|
|
>
|
|
<Button type="link" size="small" danger>
|
|
删除
|
|
</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
) : null,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Space style={{ marginBottom: 16 }} wrap>
|
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
|
知识库
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
支持粘贴文本或上传 .txt/.md;可绑定到企微机器人供 AI 检索
|
|
</Typography.Text>
|
|
</Space>
|
|
|
|
<Form
|
|
form={filterForm}
|
|
layout="inline"
|
|
style={{ marginBottom: 16 }}
|
|
onFinish={(v) =>
|
|
setFilters({
|
|
name: v.name || '',
|
|
enabled: v.enabled === undefined || v.enabled === null ? '' : String(v.enabled),
|
|
})
|
|
}
|
|
>
|
|
<Form.Item name="name" label="名称">
|
|
<Input allowClear />
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Button type="primary" htmlType="submit">
|
|
查询
|
|
</Button>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Button type="primary" onClick={openCreate}>
|
|
新建知识库
|
|
</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
|
|
<Table
|
|
rowKey="id"
|
|
loading={loading}
|
|
columns={columns}
|
|
dataSource={data?.items}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total: data?.total,
|
|
showSizeChanger: true,
|
|
onChange: (p, ps) => {
|
|
setPage(p);
|
|
setPageSize(ps);
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title={editing ? '编辑知识库' : '新建知识库'}
|
|
open={modalOpen}
|
|
onCancel={() => setModalOpen(false)}
|
|
onOk={() => void onSave()}
|
|
confirmLoading={saving}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="说明">
|
|
<Input.TextArea rows={2} />
|
|
</Form.Item>
|
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Drawer
|
|
title={drawerKb ? `文档 · ${drawerKb.name}` : '文档'}
|
|
open={!!drawerKb}
|
|
width={720}
|
|
onClose={() => setDrawerKb(null)}
|
|
extra={
|
|
drawerKb?.canEditFull ? (
|
|
<Button type="primary" onClick={() => { docForm.resetFields(); setDocModalOpen(true); }}>
|
|
上传/粘贴
|
|
</Button>
|
|
) : null
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="id"
|
|
size="small"
|
|
loading={docsLoading}
|
|
columns={docColumns}
|
|
dataSource={docs}
|
|
pagination={false}
|
|
/>
|
|
</Drawer>
|
|
|
|
<Modal
|
|
title="添加知识文档"
|
|
open={docModalOpen}
|
|
onCancel={() => setDocModalOpen(false)}
|
|
confirmLoading={docSaving}
|
|
onOk={async () => {
|
|
if (!drawerKb) return;
|
|
const values = await docForm.validateFields();
|
|
setDocSaving(true);
|
|
try {
|
|
await request(`/admin/knowledge-bases/${drawerKb.id}/documents`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
title: values.title,
|
|
contentText: values.contentText || null,
|
|
}),
|
|
});
|
|
message.success('已添加');
|
|
setDocModalOpen(false);
|
|
void loadDocs(drawerKb);
|
|
reload();
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setDocSaving(false);
|
|
}
|
|
}}
|
|
destroyOnClose
|
|
width={640}
|
|
>
|
|
<Form form={docForm} layout="vertical">
|
|
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="contentText" label="正文(可粘贴)">
|
|
<Input.TextArea rows={8} placeholder="支持 Markdown / 纯文本" />
|
|
</Form.Item>
|
|
<Form.Item label="或上传文本文件">
|
|
<Upload
|
|
accept=".txt,.md,.markdown,.csv,.json,.log,text/*"
|
|
maxCount={1}
|
|
customRequest={async ({ file, onSuccess, onError }) => {
|
|
if (!drawerKb) return;
|
|
try {
|
|
const raw = file as File;
|
|
const uploaded = await uploadFileToOss(raw, {
|
|
bizType: 'KB_DOCUMENT',
|
|
mediaType: 'FILE',
|
|
});
|
|
const title =
|
|
docForm.getFieldValue('title') || raw.name.replace(/\.[^.]+$/, '');
|
|
await request(`/admin/knowledge-bases/${drawerKb.id}/documents`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
title,
|
|
fileName: raw.name,
|
|
fileUrl: uploaded.url,
|
|
mimeType: raw.type || null,
|
|
sizeBytes: raw.size,
|
|
}),
|
|
});
|
|
message.success('上传成功');
|
|
setDocModalOpen(false);
|
|
void loadDocs(drawerKb);
|
|
reload();
|
|
onSuccess?.(uploaded);
|
|
} catch (e) {
|
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
message.error(err.message);
|
|
onError?.(err);
|
|
}
|
|
}}
|
|
showUploadList={false}
|
|
>
|
|
<Button icon={<UploadOutlined />}>上传到 OSS 并入库</Button>
|
|
</Upload>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="编辑知识文档"
|
|
open={editDocModalOpen}
|
|
onCancel={() => {
|
|
setEditDocModalOpen(false);
|
|
setEditingDoc(null);
|
|
}}
|
|
confirmLoading={editDocSaving}
|
|
onOk={async () => {
|
|
if (!drawerKb || !editingDoc) return;
|
|
const values = await editDocForm.validateFields();
|
|
setEditDocSaving(true);
|
|
try {
|
|
await request(`/admin/knowledge-bases/${drawerKb.id}/documents/${editingDoc.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
title: values.title,
|
|
contentText: values.contentText || null,
|
|
}),
|
|
});
|
|
message.success('已保存');
|
|
setEditDocModalOpen(false);
|
|
setEditingDoc(null);
|
|
void loadDocs(drawerKb);
|
|
reload();
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setEditDocSaving(false);
|
|
}
|
|
}}
|
|
destroyOnClose
|
|
width={640}
|
|
>
|
|
<Form form={editDocForm} layout="vertical">
|
|
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="contentText" label="正文(可粘贴)">
|
|
<Input.TextArea rows={8} placeholder="支持 Markdown / 纯文本" />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|