import React, {useEffect, useMemo, useRef, useState} from 'react'; import { Button, Card, Col, Divider, Drawer, Dropdown, Form, Image, Input, message, Modal, Typography, Progress, Row, Space, Spin, Table, Tag, Tooltip, Tree, TreeSelect, Upload, type UploadFile, type ButtonProps, Descriptions, type TagProps } from 'antd'; const { Title, Text } = Typography; import {useTranslation} from 'react-i18next'; import type {TreeProps} from 'antd/es/tree'; import { CloudUploadOutlined, CopyOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, FileAddOutlined, FolderOpenOutlined, MoreOutlined, ReloadOutlined, ScissorOutlined, UndoOutlined, UploadOutlined } from '@ant-design/icons'; import type {ISysFileGroup} from '@/domain/iSysFileGroup'; import type {ISysFileInfo} from '@/domain/iSysFile'; import { createFileGroup, deleteFileGroup, queryFileGroupList, updateFileGroup } from '@/api/system/sysFileGroup'; import { deleteFile, getFileList, uploadFile, forceDeleteFile, batchDeleteFiles, batchForceDeleteFiles, restoreFile, copyFile, moveFile, renameFile, cleanTrashed, getTrashedFileList, batchRestoreFiles } from '@/api/system/sysFile'; import XinForm from '@/components/XinForm'; import type { XinFormRef } from '@/components/XinForm/typings'; import type { FormColumn } from '@/components/XinFormField/FieldRender/typings'; import type {ColumnsType} from 'antd/es/table'; import dayjs from 'dayjs'; import IconFont from '@/components/IconFont'; import {isArray, isFunction} from "lodash"; interface FileOptions { value: number; color: TagProps['color']; label: React.ReactNode } const FileManagement: React.FC = () => { const {t} = useTranslation(); // 文件组相关状态 const [fileGroups, setFileGroups] = useState([]); const [fileGroupMap, setFileGroupMap] = useState>(new Map()); const [selectedGroupId, setSelectedGroupId] = useState(0); const [updateGroupData, setUpdateGroupData] = useState(null); const groupFormRef = useRef(null); const [groupSearchKeyword, setGroupSearchKeyword] = useState(); const [fileGroupLoading, setFileGroupLoading] = useState(true); // 文件相关状态 const [files, setFiles] = useState([]); const [fileList, setFileList] = useState([]); const [loading, setLoading] = useState(false); const [uploadModalOpen, setUploadModalOpen] = useState(false); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [pagination, setPagination] = useState({current: 1, pageSize: 10, total: 0}); // 复制移动状态 const [targetModalOpen, setTargetModelOpen] = useState(false); const [targetType, setTargetType] = useState<'copy' | 'move'>(); const [targetOptionValue, setTargetOptionValue] = useState(); const [targetGroupId, setTargetGroupId] = useState(0); // 重命名状态 const [renameModalOpen, setRenameModalOpen] = useState(false); const [currentRenameFile, setCurrentRenameFile] = useState(null); const [newFileName, setNewFileName] = useState(''); // 批量选择 const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 回收站 const [trashFiles, setTrashFiles] = useState([]); const [trashLoading, setTrashLoading] = useState(false); const [trashModelOpen, setTrashModelOpen] = useState(false); const [trashPagination, setTrashPagination] = useState({current: 1, pageSize: 10, total: 0}); // 文件详情 const [detailDrawerOpen, setDetailDrawerOpen] = useState(false); const [currentDetailFile, setCurrentDetailFile] = useState(null); /** 加载文件夹列表 */ const loadFileGroups = async () => { try { setFileGroupLoading(true); const {data} = await queryFileGroupList(groupSearchKeyword ? { keywordSearch: groupSearchKeyword } : undefined); const groups = data.data || []; setFileGroups(groups); const groupMap = new Map(); const buildMap = (items: ISysFileGroup[]) => { items.forEach(item => { groupMap.set(item.id, item); if (item.children) buildMap(item.children); }); }; buildMap(groups); setFileGroupMap(groupMap); } finally { setFileGroupLoading(false); } }; /** 加载文件列表 */ const loadFiles = async (page = 1, pageSize = 20) => { try { setLoading(true); setSelectedRowKeys([]); const {data} = await getFileList({ group_id: (Number(selectedGroupId) || 0), page, pageSize }); setFiles(data.data?.data || []); setPagination({ current: data.data?.current_page || page, pageSize: data.data?.per_page || pageSize, total: data.data?.total || 0 }); } finally { setLoading(false); } }; /** 加载回收站文件列表 */ const loadTrashFiles = async (page = 1, pageSize = 10) => { try { setTrashLoading(true); setSelectedRowKeys([]); const {data} = await getTrashedFileList({ page, pageSize }); setTrashFiles(data.data?.data || []); setTrashPagination({ current: data.data?.current_page || page, pageSize: data.data?.per_page || pageSize, total: data.data?.total || 0 }); } finally { setTrashLoading(false); } }; useEffect(() => { loadFileGroups(); }, [groupSearchKeyword]); useEffect(() => { loadFiles(); }, [selectedGroupId]); /** 树形数据根节点 */ const buildTreeData = useMemo(() => { const convertToTreeData = (groups: ISysFileGroup[]): any[] => groups.map(g => ({ key: g.id, title: g.name, icon: , children: g.children ? convertToTreeData(g.children) : undefined })); if(groupSearchKeyword) { return convertToTreeData(fileGroups); } return [ { key: 0, title: t('system.file.root'), icon: , children: convertToTreeData(fileGroups) } ] }, [fileGroups]); /** 树形选择 */ const onTreeSelect: TreeProps['onSelect'] = (keys) => { if(keys.length > 0) { setSelectedGroupId(keys[0]) } }; /** 添加文件夹 */ const handleAddGroup = (parentId: number) => { setUpdateGroupData(null); groupFormRef.current?.resetFields(); groupFormRef.current?.setFieldsValue({parent_id: parentId}); groupFormRef.current?.open(); }; /** 编辑文件夹 */ const handleEditGroup = (group: ISysFileGroup) => { setUpdateGroupData(group); groupFormRef.current?.setFieldsValue(group); groupFormRef.current?.open(); }; /** 保存文件夹 */ const handleSaveGroup = async (values: ISysFileGroup) => { const isEdit = !!updateGroupData; await (isEdit ? updateFileGroup({...values, id: updateGroupData.id}) : createFileGroup(values)); message.success(isEdit ? t('system.file.saveFolderSuccess', {action: t('system.file.actionEdit')}) : t('system.file.saveFolderSuccess', {action: t('system.file.actionAdd')})); groupFormRef.current?.close(); await loadFileGroups(); return true; }; /** 删除文件夹 */ const handleDeleteGroup = async (id: number) => { window.$modal?.confirm({ title: t('system.file.confirmDeleteFolder'), content: t('system.file.deleteFolderHint'), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await deleteFileGroup(id); message.success(t('system.file.deleteFolderSuccess')); if (selectedGroupId === id) setSelectedGroupId(0); await loadFileGroups(); } }); }; /** 上传文件 */ const handleUpload = async () => { if (!fileList.length) return message.warning(t('system.file.selectFileWarning')); try { setUploading(true); for (const file of fileList) { await uploadFile(file.originFileObj as File, Number(selectedGroupId), setUploadProgress); } message.success(t('system.file.uploadSuccess')); setUploadModalOpen(false); setFileList([]); setUploadProgress(0); await loadFiles(); } finally { setUploading(false); } }; /** 删除文件 */ const handleDeleteFile = async (id: number) => { window.$modal?.confirm({ title: t('system.file.confirmDelete'), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await deleteFile(id); message.success(t('system.file.deleteSuccess')); await loadFiles(); } }) }; /** 批量删除 */ const handleBatchDelete = async () => { if (!selectedRowKeys.length) return message.warning(t('system.file.noSelected')); window.$modal?.confirm({ title: t('system.file.confirmBatchDelete', { count: selectedRowKeys.length }), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await batchDeleteFiles(selectedRowKeys as number[]); message.success(t('system.file.batchDeleteSuccess')); await loadFiles(); } }); }; /** 提交(移动、复制) */ const handleTargetOk = async () => { if (!targetOptionValue) return message.warning(t('system.file.noSelected')); const isBatch = isArray(targetOptionValue); if (isBatch && targetOptionValue.length < 1) return message.warning(t('system.file.noSelected')); if(targetType === 'move') { await moveFile(targetOptionValue, targetGroupId); message.success(t('system.file.moveSuccess')); } else { await copyFile(targetOptionValue, targetGroupId); message.success(t('system.file.moveSuccess')); } setTargetModelOpen(false); await loadFiles(); }; /** 打开(移动、复制)弹窗 */ const openTargetModal = (ids: number | number[], type: 'move' | 'copy') => { setTargetType(type); setTargetOptionValue(ids); setTargetModelOpen(true); }; /** 批量(移动、复制)操作 */ const batchOption: ButtonProps['onClick'] = (e) => { const type = e.currentTarget.dataset.type as ('copy' | 'move'); console.log(type, e.currentTarget.dataset); const keys = selectedRowKeys.map(key => Number(key)); openTargetModal(keys, type); } /** 打开重命名弹窗 */ const openRenameModal = (file: ISysFileInfo) => { setCurrentRenameFile(file); setNewFileName(file.file_name || ''); setRenameModalOpen(true); }; /** 确认重命名 */ const handleRename = async () => { if (!currentRenameFile?.id || !newFileName.trim()) return; await renameFile(currentRenameFile.id, newFileName.trim()); message.success(t('system.file.renameSuccess')); setRenameModalOpen(false); await loadFiles(); }; /** 打开回收站 */ const openTrash = async () => { setTrashModelOpen(true); setSelectedRowKeys([]); await loadTrashFiles(); } /** 关闭回收站 */ const closeTrash = async () => { setTrashModelOpen(false); setSelectedRowKeys([]); await loadFiles(); } /** 彻底删除文件 */ const handleForceDeleteFile = async (id: number) => { window.$modal?.confirm({ title: t('system.file.confirmForceDelete'), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await forceDeleteFile(id); message.success(t('system.file.forceDeleteSuccess')); await loadTrashFiles(); } }) }; /** 恢复文件 */ const handleRestoreFile = async (id: number) => { window.$modal?.confirm({ title: t('system.file.confirmRestore'), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await restoreFile(id); message.success(t('system.file.restoreSuccess')); await loadTrashFiles(); } }) }; /** 批量彻底删除 */ const handleBatchForceDelete = async () => { if (!selectedRowKeys.length) return message.warning(t('system.file.noSelected')); window.$modal?.confirm({ title: t('system.file.confirmBatchForceDelete', {count: selectedRowKeys.length}), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await batchForceDeleteFiles(selectedRowKeys as number[]); message.success(t('system.file.batchForceDeleteSuccess')); await loadTrashFiles(); } }) }; /** 批量恢复 */ const handleBatchRestore = async () => { if (!selectedRowKeys.length) return message.warning(t('system.file.noSelected')); window.$modal?.confirm({ title: t('system.file.confirmBatchRestore', {count: selectedRowKeys.length}), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { await batchRestoreFiles(selectedRowKeys as number[]); message.success(t('system.file.batchRestoreSuccess')); await loadTrashFiles(); } }) }; /** 清空回收站 */ const handleEmptyTrash = async () => { window.$modal?.confirm({ title: t('system.file.confirmEmptyTrash'), okText: t('system.file.ok'), cancelText: t('system.file.cancel'), onOk: async () => { const {data} = await cleanTrashed(); message.success(t('system.file.emptyTrashSuccess', {count: data.data?.count || 0})); await loadTrashFiles(); } }) }; /** 打开文件详情 */ const openFileDetail = (file: ISysFileInfo) => { setCurrentDetailFile(file); setDetailDrawerOpen(true); }; /** 格式化文件大小 */ const formatFileSize = (size = 0) => { const units = ['B', 'KB', 'MB', 'GB']; let i = 0, s = size; while (s >= 1024 && i < 3) { s /= 1024; i++; } return `${s.toFixed(2)} ${units[i]}`; }; /** 文件类型 */ const fileOptions: FileOptions[] = [ { value: 10, color: 'purple', label: t('system.file.type.image') }, { value: 20, color: 'blue', label: t('system.file.type.audio') }, { value: 30, color: 'magenta', label: t('system.file.type.video') }, { value: 40, color: 'orange', label: t('system.file.type.archive') }, { value: 50, color: 'success', label: t('system.file.type.document') }, { value: 99, color: 'error', label: t('system.file.type.other') } ]; /** 文件类型渲染 */ const fileTypeRender = (type: number) => { const options = fileOptions.find(opt => opt.value === type); return {options?.label}; } /** 下载文件 */ const handleDownloadFile = async (id: number) => { const link = document.createElement('a'); link.href = import.meta.env.VITE_BASE_URL + `/system/file/list/download/${id}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; /** 文件组表单列 */ const groupFormColumns: FormColumn[] = [ { title: t('system.file.folderName'), dataIndex: 'name', valueType: 'text', rules: [{required: true, message: t('system.file.folderNameRequired')}] }, { title: t('system.file.parentFolder'), dataIndex: 'parent_id', valueType: 'treeSelect', fieldProps: { treeData: buildTreeData, fieldNames: {label: 'title', value: 'key'}, disabled: true } }, { title: t('system.file.sort'), dataIndex: 'sort', valueType: 'digit', rules: [{required: true, message: t('system.file.sortRequired')}], initialValue: 0 }, { title: t('system.file.describe'), dataIndex: 'describe', valueType: 'textarea' } ] /** 文件列 */ const columns: ColumnsType = [ { title: t('system.file.fileName'), dataIndex: 'file_name', ellipsis: true, width: 320, render: (name: string, r: ISysFileInfo) => openFileDetail(r)}>{name} }, { title: t('system.file.fileSize'), dataIndex: 'file_size', width: 80, align: 'center', render: formatFileSize, sorter: true }, { title: t('system.file.fileType'), dataIndex: 'file_type', width: 80, align: 'center', render: fileTypeRender }, { title: t('system.file.disk'), dataIndex: 'disk', width: 80, align: 'center', render: (disk: string) => {disk} }, { title: t('system.file.createdAt'), dataIndex: 'created_at', width: 180, align: 'center', render: (v) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-' }, { title: t('system.file.preview'), dataIndex: 'preview_url', width: 80, align: 'center', render: (url: string) => (
) }, { title: t('system.file.action'), width: 80, key: 'action', align: 'center', fixed: 'right', render: (_, file) => ( , label: t('system.file.download'), onClick: () => handleDownloadFile(file.id!) }, { key: 'rename', icon: , label: t('system.file.rename'), onClick: () => openRenameModal(file) }, { key: 'move', icon: , label: t('system.file.move'), onClick: () => openTargetModal(file.id!, 'move') }, { key: 'copy', icon: , label: t('system.file.copy'), onClick: () => openTargetModal(file.id!, 'copy') }, { type: 'divider' }, { key: 'delete', label: {t('system.file.delete')}, icon: , onClick: () => handleDeleteFile(file.id!) } ] }} >