Files
school2/web/pages/system/file.tsx
T
2026-07-13 15:23:29 +08:00

1079 lines
32 KiB
TypeScript

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<ISysFileGroup[]>([]);
const [fileGroupMap, setFileGroupMap] = useState<Map<React.Key, ISysFileGroup>>(new Map());
const [selectedGroupId, setSelectedGroupId] = useState<React.Key>(0);
const [updateGroupData, setUpdateGroupData] = useState<ISysFileGroup | null>(null);
const groupFormRef = useRef<XinFormRef>(null);
const [groupSearchKeyword, setGroupSearchKeyword] = useState<string>();
const [fileGroupLoading, setFileGroupLoading] = useState<boolean>(true);
// 文件相关状态
const [files, setFiles] = useState<ISysFileInfo[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
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<boolean>(false);
const [targetType, setTargetType] = useState<'copy' | 'move'>();
const [targetOptionValue, setTargetOptionValue] = useState<number | number[]>();
const [targetGroupId, setTargetGroupId] = useState<number>(0);
// 重命名状态
const [renameModalOpen, setRenameModalOpen] = useState(false);
const [currentRenameFile, setCurrentRenameFile] = useState<ISysFileInfo | null>(null);
const [newFileName, setNewFileName] = useState('');
// 批量选择
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// 回收站
const [trashFiles, setTrashFiles] = useState<ISysFileInfo[]>([]);
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<ISysFileInfo | null>(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<React.Key, ISysFileGroup>();
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: <FolderOpenOutlined/>,
children: g.children ? convertToTreeData(g.children) : undefined
}));
if(groupSearchKeyword) {
return convertToTreeData(fileGroups);
}
return [
{
key: 0,
title: t('system.file.root'),
icon: <FolderOpenOutlined/>,
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 <Tag color={options?.color}>{options?.label}</Tag>;
}
/** 下载文件 */
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<ISysFileGroup>[] = [
{
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<ISysFileInfo> = [
{
title: t('system.file.fileName'),
dataIndex: 'file_name',
ellipsis: true,
width: 320,
render: (name: string, r: ISysFileInfo) => <a onClick={() => openFileDetail(r)}>{name}</a>
},
{
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) => <Tag>{disk}</Tag>
},
{
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) => (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<Image
preview={false}
src={url}
width={36}
height={36}
/>
</div>
)
},
{
title: t('system.file.action'), width: 80, key: 'action', align: 'center', fixed: 'right',
render: (_, file) => (
<Dropdown
trigger={['click']}
menu={{
items: [
{
key: 'download',
icon: <DownloadOutlined/>,
label: t('system.file.download'),
onClick: () => handleDownloadFile(file.id!)
},
{
key: 'rename',
icon: <EditOutlined/>,
label: t('system.file.rename'),
onClick: () => openRenameModal(file)
},
{
key: 'move',
icon: <ScissorOutlined/>,
label: t('system.file.move'),
onClick: () => openTargetModal(file.id!, 'move')
},
{
key: 'copy',
icon: <CopyOutlined/>,
label: t('system.file.copy'),
onClick: () => openTargetModal(file.id!, 'copy')
},
{
type: 'divider'
},
{
key: 'delete',
label: <span style={{color: '#ff4d4f'}}>{t('system.file.delete')}</span>,
icon: <DeleteOutlined style={{color: '#ff4d4f'}}/>,
onClick: () => handleDeleteFile(file.id!)
}
]
}}
>
<Button icon={<MoreOutlined/>} size="small"/>
</Dropdown>
)
}
];
/** 回收站文件列 */
const trashColumns: ColumnsType<ISysFileInfo> = [
{
title: t('system.file.fileName'),
dataIndex: 'file_name',
ellipsis: true,
width: 180,
},
{
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) => <Tag>{disk}</Tag>
},
{
title: t('system.file.deletedAt'),
dataIndex: 'deleted_at',
width: 120,
align: 'center',
render: (v) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: t('system.file.action'),
width: 80,
key: 'action',
align: 'center',
fixed: 'right',
render: (_, file) => (
<Space>
<Tooltip title={t('system.file.restore')}>
<Button
size="small"
icon={<UndoOutlined/>}
onClick={() => handleRestoreFile(file.id!)}
/>
</Tooltip>
<Tooltip title={t('system.file.forceDelete')}>
<Button
danger
size="small"
type="primary"
icon={<DeleteOutlined/>}
onClick={() => handleForceDeleteFile(file.id!)}
/>
</Tooltip>
</Space>
)
}
];
/** 文件组标题渲染 **/
const treeTitleRender: TreeProps['titleRender'] = (node) => (
<Dropdown trigger={['click']} menu={{
items: [
{
key: 'add',
label: (
<Space>
<FileAddOutlined/>
{t('system.file.addFolder')}
</Space>
),
onClick: () => handleAddGroup(Number(node.key))
},
{
key: 'edit',
label: (
<Space>
<EditOutlined/>
{t('system.file.editFolder')}
</Space>
),
disabled: Number(node.key) === 0,
onClick: () => fileGroupMap.has(node.key) && handleEditGroup(fileGroupMap.get(node.key)!)},
{ type: 'divider' },
{
key: 'del',
danger: true,
icon: <DeleteOutlined/>,
disabled: Number(node.key) === 0,
onClick: () => handleDeleteGroup(Number(node.key)),
label: t('system.file.deleteFolder')
}
]
}}>
<Space>
<IconFont name="icon-wenjianjia" />
{ isFunction(node.title) ? node.title(node) : node.title }
</Space>
</Dropdown>
)
return (
<>
<div className={'mb-5'}>
<Title level={3}>{t('system.file.page.title')}</Title>
<Text type="secondary">{t('system.file.page.description')}</Text>
</div>
<Row gutter={[16, 16]}>
{/* 左侧文件夹树 */}
<Col xs={24} lg={4}>
<Card
title={
<Space>
<IconFont style={{fontSize: 18}} name='icon-wenjianjia' />
{t('system.file.fileFolder')}
</Space>
}
variant={'borderless'}
styles={{
header: { paddingInline: 16, paddingBlock: 0, minHeight: 48 },
body: { padding: 16, minHeight: 52 },
}}
>
<Input.Search
placeholder={t('system.file.folderSearchPlaceholder')}
style={{marginBottom: 16}}
onSearch={(value) => setGroupSearchKeyword(value)}
/>
<Spin spinning={fileGroupLoading} description={t('system.file.loading')} size="small">
<div style={{minHeight: 200}}>
{ fileGroups.length > 0 && (
<Tree
showLine
defaultExpandAll
onSelect={onTreeSelect}
treeData={buildTreeData}
defaultExpandedKeys={[0]}
selectedKeys={[selectedGroupId]}
titleRender={treeTitleRender}
/>
)}
</div>
</Spin>
</Card>
</Col>
{/* 右侧文件列表 */}
<Col xs={24} lg={20}>
<Card variant={'borderless'} styles={{body: { paddingBlock: 16 }}}>
<Space wrap style={{ marginBottom: 16 }}>
{/* 上传按钮 */}
<Button
type="primary"
icon={<UploadOutlined/>}
children={t('system.file.upload')}
onClick={() => setUploadModalOpen(true)}
/>
{/* 批量操作 */}
{( selectedRowKeys.length > 0 && !trashModelOpen ) && (
<>
<Button
danger
icon={<DeleteOutlined/>}
onClick={handleBatchDelete}
children={t('system.file.batchDelete')}
/>
<Button
icon={<ScissorOutlined/>}
data-type={'move'}
onClick={batchOption}
children={t('system.file.batchMove')}
/>
<Button
icon={<CopyOutlined/>}
data-type={'copy'}
onClick={batchOption}
children={t('system.file.batchCopy')}
/>
</>
)}
{/* 刷新按钮 */}
<Tooltip title={t('system.file.trash')}>
<Button icon={<DeleteOutlined/>} onClick={() => openTrash()} children={t('system.file.trash')}/>
</Tooltip>
<Tooltip title={t('system.file.refresh')}>
<Button icon={<ReloadOutlined/>} onClick={() => loadFiles()}/>
</Tooltip>
</Space>
{/* 文件表格 */}
<Table
columns={columns}
dataSource={files}
rowKey="id"
size={'small'}
loading={loading}
pagination={{
...pagination,
onChange: loadFiles,
showSizeChanger: true,
showTotal: (total) => t('system.file.totalFiles', { total })
}}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
scroll={{x: 1000}}
/>
</Card>
</Col>
{/* 文件分组表单 */}
<XinForm<ISysFileGroup>
formRef={groupFormRef}
layoutType="ModalForm"
columns={groupFormColumns}
onFinish={handleSaveGroup}
modalProps={{
title: updateGroupData ? t('system.file.editFolderTitle') : t('system.file.addFolderTitle'),
onCancel: () => groupFormRef.current?.close()
}}
trigger={<span style={{display: 'none'}} />}
/>
{/* 回收站抽屉 */}
<Drawer
title={t('system.file.trash')}
open={trashModelOpen}
width={860}
onClose={closeTrash}
>
<Space style={{ marginBottom: 20 }}>
<Button
disabled={selectedRowKeys.length === 0}
type={'primary'}
icon={<UndoOutlined/>}
onClick={handleBatchRestore}
children={t('system.file.batchRestore')}
/>
<Button
disabled={selectedRowKeys.length === 0}
icon={<DeleteOutlined/>}
onClick={handleBatchForceDelete}
children={t('system.file.batchForceDelete')}
/>
<Button
danger
type={'primary'}
icon={<DeleteOutlined/>}
onClick={handleEmptyTrash}
children={t('system.file.emptyTrash')}
/>
</Space>
<Table
columns={trashColumns}
dataSource={trashFiles}
rowKey="id"
size={'small'}
loading={trashLoading}
pagination={{
...trashPagination,
onChange: loadTrashFiles,
showSizeChanger: true,
showTotal: (total) => t('system.file.totalFiles', { total })
}}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
scroll={{x: 800}}
/>
</Drawer>
{/* 上传文件表单 */}
<Modal
title={t('system.file.uploadTitle')}
open={uploadModalOpen}
onOk={handleUpload}
onCancel={() => {
setUploadModalOpen(false);
setFileList([]);
setUploadProgress(0);
}}
confirmLoading={uploading}
>
<Form layout="vertical">
<Form.Item label={t('system.file.selectFile')} required>
<Upload.Dragger
fileList={fileList}
onChange={({fileList}) => setFileList(fileList)}
beforeUpload={() => false}
multiple
style={{borderRadius: 8}}
>
<p className="ant-upload-drag-icon">
<CloudUploadOutlined style={{fontSize: 48, color: '#1890ff'}} />
</p>
<p className="ant-upload-text">{t('system.file.uploadDragText')}</p>
<p className="ant-upload-hint">{t('system.file.uploadHint')}</p>
</Upload.Dragger>
</Form.Item>
{uploading && <Form.Item label={t('system.file.uploadProgress')}><Progress percent={uploadProgress} status="active"/></Form.Item>}
</Form>
</Modal>
{/* 重命名弹窗 */}
<Modal
title={t('system.file.renameTitle')}
open={renameModalOpen}
onOk={handleRename}
styles={{ body: { paddingBottom: 16 } }}
onCancel={() => setRenameModalOpen(false)}
>
<div style={{ marginTop: 16, marginBottom: 16 }}>
{t('system.file.renameDescription')}
</div>
<Input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder={t('system.file.newFileNameRequired')}
/>
</Modal>
{/* 复制移动弹窗 */}
<Modal
title={ targetType === 'copy' ? t('system.file.copyTitle') : t('system.file.moveTitle') }
open={targetModalOpen}
onOk={handleTargetOk}
styles={{ body: { paddingBottom: 16 } }}
onCancel={() => setTargetModelOpen(false)}
>
<div style={{ marginTop: 16, marginBottom: 16 }}>
{ targetType === 'copy' ? t('system.file.copyDescription') : t('system.file.moveDescription') }
</div>
<TreeSelect
value={targetGroupId}
onChange={setTargetGroupId}
treeData={buildTreeData}
fieldNames={{label: 'title', value: 'key'}}
style={{width: '100%'}}
/>
</Modal>
{/* 文件详情抽屉 */}
<Drawer
title={t('system.file.fileDetail')}
placement="right"
width={480}
open={detailDrawerOpen}
onClose={() => setDetailDrawerOpen(false)}
>
{currentDetailFile && (
<>
{/* 文件预览 */}
<div style={{display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<Image
src={currentDetailFile.preview_url}
alt={currentDetailFile.file_name}
preview={false}
/>
</div>
<Divider />
{/* 基本信息 */}
<Descriptions
title={t('system.file.basicInfo')}
column={1}
items={[
{
key: 'name',
label: t('system.file.fileName'),
children: currentDetailFile.file_name,
},
{
key: 'size',
label: t('system.file.fileSize'),
children: formatFileSize(currentDetailFile.file_size),
},
{
key: 'type',
label: t('system.file.fileType'),
children: fileTypeRender(currentDetailFile.file_type || 10),
},
{
key: 'ext',
label: t('system.file.fileExt'),
children: currentDetailFile.file_ext,
},
{
key: 'disk',
label: t('system.file.storageMethod'),
children: currentDetailFile.disk,
},
{
key: 'path',
label: t('system.file.filePath'),
children: currentDetailFile.file_path,
},
{
key: 'group',
label: t('system.file.fileGroup'),
children: fileGroupMap.get(currentDetailFile.group_id || 0)?.name || t('system.file.ungrouped'),
},
{
key: 'created_at',
label: t('system.file.createdAt'),
children: currentDetailFile.created_at,
},
{
key: 'updated_at',
label: t('system.file.updatedAt'),
children: currentDetailFile.updated_at,
},
{
key: 'url',
label: t('system.file.accessUrl'),
children: (
<Typography.Link copyable>
{currentDetailFile.file_url}
</Typography.Link>
)
},
]}
/>
</>
)}
</Drawer>
</Row>
</>
);
};
export default FileManagement;