Files
leaudit-platform-frontend/app/components/dify-dataset-manager/document-list.tsx
T

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>
)}
</>
);
}