387 lines
13 KiB
TypeScript
387 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Button,
|
|
Input,
|
|
Table,
|
|
Tag,
|
|
Space,
|
|
Tooltip,
|
|
Popconfirm,
|
|
Switch,
|
|
message,
|
|
Empty,
|
|
Spin,
|
|
} from 'antd';
|
|
import {
|
|
SearchOutlined,
|
|
ReloadOutlined,
|
|
DeleteOutlined,
|
|
FileTextOutlined,
|
|
CloudUploadOutlined,
|
|
EyeOutlined,
|
|
ClockCircleOutlined,
|
|
CheckCircleOutlined,
|
|
SyncOutlined,
|
|
ExclamationCircleOutlined,
|
|
PauseCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
|
|
import { deleteDocument, toggleDocumentStatus } from '~/api/dify-dataset/api/documentApi';
|
|
import DocumentUpload from './document-upload';
|
|
import '../../styles/components/dify-dataset-manager/index.css';
|
|
|
|
interface DocumentListProps {
|
|
datasetId: string;
|
|
datasetName: string;
|
|
documents: Document[];
|
|
loading: boolean;
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
onPageChange: (page: number) => void;
|
|
onDocumentDeleted: (documentId: string) => void;
|
|
onDocumentStatusChanged: (documentId: string, enabled: boolean) => void;
|
|
onRefresh: () => void;
|
|
onViewDocument?: (document: Document) => void;
|
|
}
|
|
|
|
/**
|
|
* 文档列表组件
|
|
*/
|
|
export default function DocumentList({
|
|
datasetId,
|
|
documents,
|
|
loading,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
onPageChange,
|
|
onDocumentDeleted,
|
|
onDocumentStatusChanged,
|
|
onRefresh,
|
|
onViewDocument,
|
|
}: DocumentListProps) {
|
|
const [searchValue, setSearchValue] = useState('');
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
// 显示上传页面的状态
|
|
const [showUploadPage, setShowUploadPage] = useState(false);
|
|
|
|
/**
|
|
* 获取状态标签配置
|
|
*/
|
|
const getStatusConfig = (status: IndexingStatus) => {
|
|
const configs: Record<IndexingStatus, { color: string; icon: React.ReactNode; text: string }> = {
|
|
completed: { color: 'success', icon: <CheckCircleOutlined />, text: '已完成' },
|
|
indexing: { color: 'processing', icon: <SyncOutlined spin />, text: '索引中' },
|
|
waiting: { color: 'warning', icon: <ClockCircleOutlined />, text: '等待中' },
|
|
parsing: { color: 'processing', icon: <SyncOutlined spin />, text: '解析中' },
|
|
cleaning: { color: 'processing', icon: <SyncOutlined spin />, text: '清洗中' },
|
|
splitting: { color: 'processing', icon: <SyncOutlined spin />, text: '分段中' },
|
|
paused: { color: 'default', icon: <PauseCircleOutlined />, text: '已暂停' },
|
|
error: { color: 'error', icon: <ExclamationCircleOutlined />, text: '错误' },
|
|
};
|
|
return configs[status] || { color: 'default', icon: null, text: status };
|
|
};
|
|
|
|
/**
|
|
* 格式化日期
|
|
*/
|
|
const formatDate = (timestamp: number) => {
|
|
return new Date(timestamp * 1000).toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 格式化数字
|
|
*/
|
|
const formatNumber = (num: number) => {
|
|
if (num >= 10000) {
|
|
return (num / 10000).toFixed(1) + 'w';
|
|
}
|
|
if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'k';
|
|
}
|
|
return num.toString();
|
|
};
|
|
|
|
/**
|
|
* 处理删除文档
|
|
*/
|
|
const handleDelete = async (documentId: string) => {
|
|
setDeletingId(documentId);
|
|
try {
|
|
await deleteDocument(datasetId, documentId);
|
|
message.success('删除成功');
|
|
onDocumentDeleted(documentId);
|
|
} catch (err: any) {
|
|
console.error('删除文档失败:', err);
|
|
message.error(err.message || '删除失败');
|
|
} finally {
|
|
setDeletingId(null);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 处理启用/禁用文档
|
|
*/
|
|
const handleToggleStatus = async (documentId: string, enabled: boolean) => {
|
|
try {
|
|
await toggleDocumentStatus(datasetId, documentId, enabled);
|
|
message.success(enabled ? '已启用' : '已禁用');
|
|
onDocumentStatusChanged(documentId, enabled);
|
|
} catch (err: any) {
|
|
console.error('切换文档状态失败:', err);
|
|
message.error(err.message || '操作失败');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 点击上传按钮,显示上传页面
|
|
*/
|
|
const handleUploadClick = () => {
|
|
if (!datasetId) {
|
|
message.error('请先选择知识库');
|
|
return;
|
|
}
|
|
setShowUploadPage(true);
|
|
};
|
|
|
|
/**
|
|
* 关闭上传页面
|
|
*/
|
|
const handleUploadClose = () => {
|
|
setShowUploadPage(false);
|
|
};
|
|
|
|
/**
|
|
* 上传成功回调
|
|
*/
|
|
const handleUploadSuccess = () => {
|
|
setShowUploadPage(false);
|
|
onRefresh();
|
|
};
|
|
|
|
// 过滤文档
|
|
const filteredDocuments = documents.filter((doc) =>
|
|
doc.name.toLowerCase().includes(searchValue.toLowerCase())
|
|
);
|
|
|
|
// 表格列定义
|
|
const columns: ColumnsType<Document> = [
|
|
{
|
|
title: '文档名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
ellipsis: true,
|
|
render: (name: string) => (
|
|
<div className="flex items-center gap-2">
|
|
<FileTextOutlined className="text-gray-400" />
|
|
<span className="font-medium">{name}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'indexing_status',
|
|
key: 'indexing_status',
|
|
width: 120,
|
|
render: (status: IndexingStatus) => {
|
|
const config = getStatusConfig(status);
|
|
return (
|
|
<Tag color={config.color} icon={config.icon}>
|
|
{config.text}
|
|
</Tag>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '字数',
|
|
dataIndex: 'word_count',
|
|
key: 'word_count',
|
|
width: 100,
|
|
render: (count: number) => formatNumber(count),
|
|
},
|
|
{
|
|
title: '命中次数',
|
|
dataIndex: 'hit_count',
|
|
key: 'hit_count',
|
|
width: 100,
|
|
render: (count: number) => formatNumber(count),
|
|
},
|
|
{
|
|
title: '启用',
|
|
dataIndex: 'enabled',
|
|
key: 'enabled',
|
|
width: 80,
|
|
render: (enabled: boolean, record) => (
|
|
<Switch
|
|
size="small"
|
|
checked={enabled}
|
|
onChange={(checked) => handleToggleStatus(record.id, checked)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 160,
|
|
render: (timestamp: number) => formatDate(timestamp),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 120,
|
|
render: (_, record) => (
|
|
<Space size="small">
|
|
<Tooltip title="查看分段">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => onViewDocument?.(record)}
|
|
/>
|
|
</Tooltip>
|
|
<Popconfirm
|
|
title="确定要删除这个文档吗?"
|
|
description="删除后无法恢复"
|
|
onConfirm={() => handleDelete(record.id)}
|
|
okText="确定"
|
|
cancelText="取消"
|
|
okButtonProps={{ danger: true }}
|
|
>
|
|
<Tooltip title="删除">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
loading={deletingId === record.id}
|
|
/>
|
|
</Tooltip>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* 上传页面 */}
|
|
{showUploadPage ? (
|
|
<DocumentUpload
|
|
datasetId={datasetId}
|
|
onClose={handleUploadClose}
|
|
onSuccess={handleUploadSuccess}
|
|
/>
|
|
) : (
|
|
<div className="document-list-page">
|
|
{/* 页面头部 */}
|
|
<div className="page-header">
|
|
<div className="header-left">
|
|
<h1>文档</h1>
|
|
</div>
|
|
<div className="header-actions">
|
|
<Tooltip title="刷新">
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
onClick={onRefresh}
|
|
loading={loading}
|
|
/>
|
|
</Tooltip>
|
|
<Button
|
|
type="primary"
|
|
icon={<CloudUploadOutlined />}
|
|
onClick={handleUploadClick}
|
|
disabled={!datasetId}
|
|
>
|
|
添加文件
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索栏 */}
|
|
<div className="document-search-bar">
|
|
<Input
|
|
placeholder="搜索文档..."
|
|
prefix={<SearchOutlined />}
|
|
value={searchValue}
|
|
onChange={(e) => setSearchValue(e.target.value)}
|
|
allowClear
|
|
style={{ width: 280 }}
|
|
/>
|
|
</div>
|
|
|
|
{/* 文档表格 */}
|
|
<div className="document-table-wrapper">
|
|
{loading && documents.length === 0 ? (
|
|
<div className="loading-state">
|
|
<Spin size="large" />
|
|
<div className="loading-text">加载中...</div>
|
|
</div>
|
|
) : filteredDocuments.length === 0 ? (
|
|
<div className="empty-state">
|
|
<Empty description={searchValue ? '未找到匹配的文档' : '暂无文档'}>
|
|
{!searchValue && (
|
|
<Button
|
|
type="primary"
|
|
icon={<CloudUploadOutlined />}
|
|
onClick={handleUploadClick}
|
|
>
|
|
上传第一个文档
|
|
</Button>
|
|
)}
|
|
</Empty>
|
|
</div>
|
|
) : (
|
|
<Table
|
|
className="document-table"
|
|
columns={columns}
|
|
dataSource={filteredDocuments}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={false}
|
|
size="small"
|
|
scroll={{ x: 'max-content' }}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* 底部分页器 */}
|
|
{filteredDocuments.length > 0 && (
|
|
<div className="document-pagination">
|
|
<span className="pagination-total">共 {total} 条</span>
|
|
<div className="pagination-controls">
|
|
<Button
|
|
size="small"
|
|
disabled={page <= 1}
|
|
onClick={() => onPageChange(page - 1)}
|
|
>
|
|
上一页
|
|
</Button>
|
|
<span className="pagination-info">
|
|
第 {page} 页 / 共 {Math.ceil(total / pageSize)} 页
|
|
</span>
|
|
<Button
|
|
size="small"
|
|
disabled={page >= Math.ceil(total / pageSize)}
|
|
onClick={() => onPageChange(page + 1)}
|
|
>
|
|
下一页
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|