feat(frontend): Day 21 knowledge base management frontend completed
This commit is contained in:
115
frontend/src/components/knowledge/CreateKBDialog.tsx
Normal file
115
frontend/src/components/knowledge/CreateKBDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Form, Input, message } from 'antd';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface CreateKBDialogProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onCreate: (name: string, description?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const CreateKBDialog: React.FC<CreateKBDialogProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onCreate,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
await onCreate(values.name, values.description);
|
||||
|
||||
message.success('知识库创建成功!');
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证错误
|
||||
return;
|
||||
}
|
||||
message.error(error.message || '创建失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建知识库"
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="知识库名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入知识库名称' },
|
||||
{ max: 50, message: '名称不能超过50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="例如:文献综述资料"
|
||||
maxLength={50}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述(选填)"
|
||||
rules={[
|
||||
{ max: 200, message: '描述不能超过200个字符' },
|
||||
]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="简要描述该知识库的用途..."
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: '#f0f5ff',
|
||||
borderRadius: 4,
|
||||
fontSize: 13,
|
||||
color: '#595959'
|
||||
}}>
|
||||
<p style={{ margin: 0, marginBottom: 8 }}>
|
||||
📌 <strong>提示</strong>
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
<li>每个用户最多创建 3 个知识库</li>
|
||||
<li>每个知识库最多上传 50 个文档</li>
|
||||
<li>支持的文件格式:PDF、DOC、DOCX、TXT、MD</li>
|
||||
<li>单个文件最大10MB</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKBDialog;
|
||||
|
||||
211
frontend/src/components/knowledge/DocumentList.tsx
Normal file
211
frontend/src/components/knowledge/DocumentList.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React from 'react';
|
||||
import { Table, Tag, Button, Popconfirm, Space, Typography, Progress, Tooltip } from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { Document } from '../../api/knowledgeBaseApi';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface DocumentListProps {
|
||||
documents: Document[];
|
||||
loading: boolean;
|
||||
onDelete: (doc: Document) => void;
|
||||
onReprocess: (doc: Document) => void;
|
||||
}
|
||||
|
||||
const DocumentList: React.FC<DocumentListProps> = ({
|
||||
documents,
|
||||
loading,
|
||||
onDelete,
|
||||
onReprocess,
|
||||
}) => {
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const getStatusTag = (status: Document['status']) => {
|
||||
const statusConfig = {
|
||||
uploading: {
|
||||
color: 'blue',
|
||||
icon: <LoadingOutlined />,
|
||||
text: '上传中'
|
||||
},
|
||||
parsing: {
|
||||
color: 'processing',
|
||||
icon: <ClockCircleOutlined />,
|
||||
text: '解析中'
|
||||
},
|
||||
indexing: {
|
||||
color: 'processing',
|
||||
icon: <ClockCircleOutlined />,
|
||||
text: '索引中'
|
||||
},
|
||||
completed: {
|
||||
color: 'success',
|
||||
icon: <CheckCircleOutlined />,
|
||||
text: '已就绪'
|
||||
},
|
||||
error: {
|
||||
color: 'error',
|
||||
icon: <CloseCircleOutlined />,
|
||||
text: '失败'
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[status] || statusConfig.uploading;
|
||||
|
||||
return (
|
||||
<Tag icon={config.icon} color={config.color}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Document> = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'filename',
|
||||
key: 'filename',
|
||||
ellipsis: true,
|
||||
render: (filename: string) => (
|
||||
<Space>
|
||||
<FileTextOutlined style={{ color: '#1890ff' }} />
|
||||
<Text ellipsis>{filename}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'fileSizeBytes',
|
||||
key: 'fileSizeBytes',
|
||||
width: 120,
|
||||
render: (size: number) => formatFileSize(size),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status: Document['status'], record: Document) => {
|
||||
if (status === 'error' && record.errorMessage) {
|
||||
return (
|
||||
<Tooltip title={record.errorMessage}>
|
||||
{getStatusTag(status)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return getStatusTag(status);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 150,
|
||||
render: (progress: number, record: Document) => {
|
||||
if (record.status === 'completed') {
|
||||
return (
|
||||
<Space size={4}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.segmentsCount || 0} 段
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>|</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.tokensCount || 0} tokens
|
||||
</Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.status === 'error') {
|
||||
return <Text type="danger">处理失败</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Progress
|
||||
percent={progress}
|
||||
size="small"
|
||||
status={progress === 100 ? 'success' : 'active'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'uploadedAt',
|
||||
key: 'uploadedAt',
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
render: (_: any, record: Document) => (
|
||||
<Space size="small">
|
||||
{record.status === 'error' && (
|
||||
<Tooltip title="重新处理">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => onReprocess(record)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
description={`删除文档 "${record.filename}"?`}
|
||||
onConfirm={() => onDelete(record)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={documents}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 个文档`,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: '暂无文档,请上传文件',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentList;
|
||||
|
||||
144
frontend/src/components/knowledge/DocumentUpload.tsx
Normal file
144
frontend/src/components/knowledge/DocumentUpload.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, message, Progress, Card } from 'antd';
|
||||
import { InboxOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { useKnowledgeBaseStore } from '../../stores/useKnowledgeBaseStore';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
|
||||
interface DocumentUploadProps {
|
||||
kbId: string;
|
||||
onUploadSuccess: () => void;
|
||||
disabled?: boolean;
|
||||
maxDocuments?: number;
|
||||
currentDocumentCount?: number;
|
||||
}
|
||||
|
||||
const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
kbId,
|
||||
onUploadSuccess,
|
||||
disabled = false,
|
||||
maxDocuments = 50,
|
||||
currentDocumentCount = 0,
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
];
|
||||
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.md'];
|
||||
|
||||
const isAtLimit = currentDocumentCount >= maxDocuments;
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
// 检查文档数量限制
|
||||
if (isAtLimit) {
|
||||
message.error(`已达到文档数量上限(${maxDocuments}个)`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.some(ext => file.name.endsWith(ext))) {
|
||||
message.error(`不支持的文件类型。支持:PDF、DOC、DOCX、TXT、MD`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
// 检查文件大小 (10MB)
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
message.error('文件大小不能超过 10MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
return false; // 阻止自动上传,我们手动处理
|
||||
};
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
// 使用 knowledgeBaseStore 的 uploadDocument 方法
|
||||
await useKnowledgeBaseStore.getState().uploadDocument(
|
||||
kbId,
|
||||
file as File,
|
||||
(progress) => {
|
||||
setUploadProgress(progress);
|
||||
}
|
||||
);
|
||||
|
||||
message.success(`${(file as File).name} 上传成功`);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess('ok');
|
||||
}
|
||||
|
||||
// 通知父组件刷新文档列表
|
||||
onUploadSuccess();
|
||||
} catch (error: any) {
|
||||
console.error('Upload error:', error);
|
||||
message.error(error.message || '上传失败');
|
||||
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Dragger
|
||||
name="file"
|
||||
multiple={false}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={customRequest}
|
||||
disabled={disabled || isAtLimit || uploading}
|
||||
showUploadList={false}
|
||||
style={{ marginBottom: uploading ? 16 : 0 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
{isAtLimit ? `已达到文档数量上限(${maxDocuments}个)` : '点击或拖拽文件到此区域上传'}
|
||||
</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持格式:PDF、DOC、DOCX、TXT、MD | 单个文件最大10MB
|
||||
</p>
|
||||
<p className="ant-upload-hint" style={{ marginTop: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||||
当前已上传: {currentDocumentCount}/{maxDocuments} 个文档
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
{uploading && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Progress
|
||||
percent={uploadProgress}
|
||||
status={uploadProgress === 100 ? 'success' : 'active'}
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
}}
|
||||
/>
|
||||
<p style={{ marginTop: 8, textAlign: 'center', color: '#595959', fontSize: 13 }}>
|
||||
{uploadProgress < 100 ? '正在上传...' : '上传完成,正在处理...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentUpload;
|
||||
|
||||
111
frontend/src/components/knowledge/EditKBDialog.tsx
Normal file
111
frontend/src/components/knowledge/EditKBDialog.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, message } from 'antd';
|
||||
import type { KnowledgeBase } from '../../api/knowledgeBaseApi';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface EditKBDialogProps {
|
||||
open: boolean;
|
||||
knowledgeBase: KnowledgeBase | null;
|
||||
onCancel: () => void;
|
||||
onUpdate: (id: string, name: string, description?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const EditKBDialog: React.FC<EditKBDialogProps> = ({
|
||||
open,
|
||||
knowledgeBase,
|
||||
onCancel,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 当知识库数据变化时,更新表单
|
||||
useEffect(() => {
|
||||
if (open && knowledgeBase) {
|
||||
form.setFieldsValue({
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description || '',
|
||||
});
|
||||
}
|
||||
}, [open, knowledgeBase, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!knowledgeBase) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
await onUpdate(knowledgeBase.id, values.name, values.description);
|
||||
|
||||
message.success('知识库更新成功!');
|
||||
onCancel();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证错误
|
||||
return;
|
||||
}
|
||||
message.error(error.message || '更新失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑知识库"
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="知识库名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入知识库名称' },
|
||||
{ max: 50, message: '名称不能超过50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="例如:文献综述资料"
|
||||
maxLength={50}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述(选填)"
|
||||
rules={[
|
||||
{ max: 200, message: '描述不能超过200个字符' },
|
||||
]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="简要描述该知识库的用途..."
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditKBDialog;
|
||||
|
||||
203
frontend/src/components/knowledge/KnowledgeBaseList.tsx
Normal file
203
frontend/src/components/knowledge/KnowledgeBaseList.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React from 'react';
|
||||
import { Card, Button, Empty, Tag, Popconfirm, Space, Typography } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
FolderOutlined,
|
||||
FileTextOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { KnowledgeBase } from '../../api/knowledgeBaseApi';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
interface KnowledgeBaseListProps {
|
||||
knowledgeBases: KnowledgeBase[];
|
||||
loading: boolean;
|
||||
onCreateClick: () => void;
|
||||
onEditClick: (kb: KnowledgeBase) => void;
|
||||
onDeleteClick: (kb: KnowledgeBase) => void;
|
||||
onSelectClick: (kb: KnowledgeBase) => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseList: React.FC<KnowledgeBaseListProps> = ({
|
||||
knowledgeBases,
|
||||
loading,
|
||||
onCreateClick,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
onSelectClick,
|
||||
}) => {
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const canCreateMore = knowledgeBases.length < 3;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 标题和创建按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>我的知识库</Title>
|
||||
<Text type="secondary">
|
||||
已使用 {knowledgeBases.length}/3 个知识库
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onCreateClick}
|
||||
disabled={!canCreateMore || loading}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 配额提示 */}
|
||||
{!canCreateMore && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Tag color="warning">
|
||||
已达到知识库数量上限(3个)
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 知识库列表 */}
|
||||
{knowledgeBases.length === 0 ? (
|
||||
<Card>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="还没有知识库,创建第一个吧!"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onCreateClick}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
</Empty>
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||||
gap: 16
|
||||
}}>
|
||||
{knowledgeBases.map((kb) => (
|
||||
<Card
|
||||
key={kb.id}
|
||||
hoverable
|
||||
onClick={() => onSelectClick(kb)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditClick(kb);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确认删除?"
|
||||
description={`删除知识库 "${kb.name}" 将同时删除其中的所有文档,此操作不可恢复。`}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDeleteClick(kb);
|
||||
}}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: '#1890ff20',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<FolderOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Text strong ellipsis style={{ flex: 1 }}>
|
||||
{kb.name}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2 }}
|
||||
type="secondary"
|
||||
style={{ marginBottom: 12, minHeight: 44 }}
|
||||
>
|
||||
{kb.description || '暂无描述'}
|
||||
</Paragraph>
|
||||
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<FileTextOutlined style={{ marginRight: 8, color: '#8c8c8c' }} />
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{kb._count?.documents || kb.fileCount || 0} 个文档
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
总大小: {formatFileSize(kb.totalSizeBytes)}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
创建于 {new Date(kb.createdAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseList;
|
||||
|
||||
Reference in New Issue
Block a user