import React, { useState, useEffect } from 'react'; import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, Upload, Popconfirm } from 'antd'; import { App } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined } from '@ant-design/icons'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { fileClient, clientClient } from '@/client/api'; import type { InferResponseType, InferRequestType } from 'hono/client'; import dayjs from 'dayjs'; import { uploadMinIOWithPolicy } from '@/client/utils/minio'; // 定义类型 type FileItem = InferResponseType['data'][0]; type FileListResponse = InferResponseType; type ClientItem = InferResponseType['data'][0]; type UpdateFileRequest = InferRequestType['json']; const Files: React.FC = () => { const { message } = App.useApp(); const [form] = Form.useForm(); const [modalVisible, setModalVisible] = useState(false); const [editingKey, setEditingKey] = useState(null); const [searchText, setSearchText] = useState(''); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, }); const queryClient = useQueryClient(); // 获取文件列表数据 const fetchFiles = async ({ page, pageSize }: { page: number; pageSize: number }): Promise => { const response = await fileClient.$get({ query: { page, pageSize, keyword: searchText } }); if (!response.ok) throw new Error('Failed to fetch files'); return await response.json() as FileListResponse; }; const { data, isLoading: loading, error: filesError } = useQuery({ queryKey: ['files', pagination.current, pagination.pageSize, searchText], queryFn: () => fetchFiles({ page: pagination.current, pageSize: pagination.pageSize }), }); // 错误处理 if (filesError) { message.error(`获取文件列表失败: ${filesError instanceof Error ? filesError.message : '未知错误'}`); } // 从API响应获取分页数据 const tablePagination = data?.pagination || pagination; // 搜索 const handleSearch = () => { setPagination({ ...pagination, current: 1 }); }; // 分页变化 const handleTableChange = (newPagination: any) => { setPagination(newPagination); }; // 显示编辑弹窗 const showModal = (record: FileItem) => { setModalVisible(true); setEditingKey(record.id); form.setFieldsValue({ name: record.name, description: record.description, type: record.type, size: record.size, }); }; // 关闭弹窗 const handleCancel = () => { setModalVisible(false); form.resetFields(); }; // 更新文件记录 const updateFile = useMutation({ mutationFn: ({ id, data }: { id: number; data: UpdateFileRequest }) => fileClient[':id'].$put({ param: { id }, json: data }), onSuccess: () => { message.success('文件记录更新成功'); queryClient.invalidateQueries({ queryKey: ['files'] }); setModalVisible(false); }, onError: (error: Error) => { message.error(`操作失败: ${error instanceof Error ? error.message : '未知错误'}`); } }); // 删除文件记录 const deleteFile = useMutation({ mutationFn: (id: number) => fileClient[':id'].$delete({ param: { id } }), onSuccess: () => { message.success('文件记录删除成功'); queryClient.invalidateQueries({ queryKey: ['files'] }); }, onError: (error: Error) => { message.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`); } }); // 直接上传文件 const handleDirectUpload = async () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = false; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { message.loading('正在上传文件...'); await uploadMinIOWithPolicy('/files', file, file.name); message.success('文件上传成功'); queryClient.invalidateQueries({ queryKey: ['files'] }); } catch (error) { message.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; input.click(); }; // 提交表单(仅用于编辑已上传文件) const handleSubmit = async () => { try { const values = await form.validateFields(); const payload = { name: values.name, description: values.description, }; if (editingKey) { await updateFile.mutateAsync({ id: editingKey, data: payload }); } } catch (error) { message.error('表单验证失败,请检查输入'); } }; // 表格列定义 const columns = [ { title: '文件ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' as const, }, { title: '文件名称', dataIndex: 'name', key: 'name', width: 300, ellipsis: true, }, { title: '文件类型', dataIndex: 'type', key: 'type', width: 120, render: (type: string) => ( {type} ), }, { title: '文件大小', dataIndex: 'size', key: 'size', width: 120, render: (size: number) => ( {size ? `${(size / 1024).toFixed(2)} KB` : '-'} ), }, { title: '上传时间', dataIndex: 'uploadTime', key: 'uploadTime', width: 180, render: (time: string) => ( {time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'} ), }, { title: '上传用户', dataIndex: 'uploadUser', key: 'uploadUser', width: 120, render: (uploadUser?: { username: string; nickname?: string }) => ( {uploadUser ? (uploadUser.nickname || uploadUser.username) : '-'} ), }, { title: '操作', key: 'action', width: 120, fixed: 'right' as const, render: (_: any, record: FileItem) => ( deleteFile.mutate(record.id)} okText="确认" cancelText="取消" okButtonProps={{ danger: true }} > ), }, ]; return (

文件管理

} value={searchText} onChange={(e) => setSearchText(e.target.value)} onPressEnter={handleSearch} className="w-80 h-10" allowClear />
`显示 ${range[0]}-${range[1]} 条,共 ${total} 条`, }} onChange={handleTableChange} bordered={false} scroll={{ x: 'max-content' }} className="[&_.ant-table]:!rounded-lg [&_.ant-table-thead>tr>th]:!bg-gray-50 [&_.ant-table-thead>tr>th]:!font-semibold [&_.ant-table-thead>tr>th]:!text-gray-700 [&_.ant-table-thead>tr>th]:!border-b-2 [&_.ant-table-thead>tr>th]:!border-gray-200" rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} /> 取消 , , ]} width={600} centered destroyOnClose maskClosable={false} >
); }; export default Files;