| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- 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<typeof fileClient.$get, 200>['data'][0];
- type FileListResponse = InferResponseType<typeof fileClient.$get, 200>;
- type ClientItem = InferResponseType<typeof clientClient.$get, 200>['data'][0];
- type UpdateFileRequest = InferRequestType<typeof fileClient[':id']['$put']>['json'];
- const Files: React.FC = () => {
- const { message } = App.useApp();
- const [form] = Form.useForm();
- const [modalVisible, setModalVisible] = useState(false);
- const [editingKey, setEditingKey] = useState<number | null>(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<FileListResponse> => {
- 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) => (
- <span className="inline-block px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded-full">
- {type}
- </span>
- ),
- },
- {
- title: '文件大小',
- dataIndex: 'size',
- key: 'size',
- width: 120,
- render: (size: number) => (
- <span className="text-sm">
- {size ? `${(size / 1024).toFixed(2)} KB` : '-'}
- </span>
- ),
- },
- {
- title: '上传时间',
- dataIndex: 'uploadTime',
- key: 'uploadTime',
- width: 180,
- render: (time: string) => (
- <span className="text-sm text-gray-600">
- {time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'}
- </span>
- ),
- },
- {
- title: '上传用户',
- dataIndex: 'uploadUser',
- key: 'uploadUser',
- width: 120,
- render: (uploadUser?: { username: string; nickname?: string }) => (
- <span className="text-sm">
- {uploadUser ? (uploadUser.nickname || uploadUser.username) : '-'}
- </span>
- ),
- },
- {
- title: '操作',
- key: 'action',
- width: 120,
- fixed: 'right' as const,
- render: (_: any, record: FileItem) => (
- <Space size="small">
- <Button
- type="text"
- icon={<EditOutlined />}
- onClick={() => showModal(record)}
- className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
- >
- 编辑
- </Button>
- <Popconfirm
- title="确认删除"
- description={`确定要删除文件"${record.name}"吗?此操作不可恢复。`}
- onConfirm={() => deleteFile.mutate(record.id)}
- okText="确认"
- cancelText="取消"
- okButtonProps={{ danger: true }}
- >
- <Button
- type="text"
- danger
- icon={<DeleteOutlined />}
- className="hover:bg-red-50"
- >
- 删除
- </Button>
- </Popconfirm>
- </Space>
- ),
- },
- ];
-
- return (
- <div className="p-6">
- <div className="mb-6 flex justify-between items-center">
- <h2 className="text-2xl font-bold text-gray-900">文件管理</h2>
- <Button
- type="primary"
- icon={<UploadOutlined />}
- onClick={handleDirectUpload}
- className="h-10 flex items-center"
- >
- 上传文件
- </Button>
- </div>
-
- <div className="mb-6">
- <div className="flex items-center gap-4">
- <Input
- placeholder="搜索文件名称或类型"
- prefix={<SearchOutlined />}
- value={searchText}
- onChange={(e) => setSearchText(e.target.value)}
- onPressEnter={handleSearch}
- className="w-80 h-10"
- allowClear
- />
- <Button
- type="default"
- onClick={handleSearch}
- className="h-10"
- >
- 搜索
- </Button>
- </div>
- </div>
-
- <div className="bg-white rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
- <Table
- columns={columns}
- dataSource={data?.data || []}
- rowKey="id"
- loading={loading}
- pagination={{
- ...tablePagination,
- showSizeChanger: true,
- showQuickJumper: true,
- showTotal: (total, range) =>
- `显示 ${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'}
- />
- </div>
-
- <Modal
- title="编辑文件信息"
- open={modalVisible}
- onCancel={handleCancel}
- footer={[
- <Button key="cancel" onClick={handleCancel}>
- 取消
- </Button>,
- <Button
- key="submit"
- type="primary"
- onClick={handleSubmit}
- loading={updateFile.isPending}
- >
- 确定
- </Button>,
- ]}
- width={600}
- centered
- destroyOnClose
- maskClosable={false}
- >
- <Form form={form} layout="vertical">
- <Form.Item name="name" label="文件名称">
- <Input className="h-10" />
- </Form.Item>
-
- <Form.Item name="description" label="文件描述">
- <Input.TextArea
- rows={4}
- placeholder="请输入文件描述"
- className="rounded-md"
- />
- </Form.Item>
-
- <Form.Item name="type" label="文件类型" hidden>
- <Input />
- </Form.Item>
-
- <Form.Item name="size" label="文件大小" hidden>
- <Input />
- </Form.Item>
- </Form>
- </Modal>
- </div>
- );
- };
- export default Files;
|