Files
d8d-admin-mobile-starter-pu…/client/admin/pages_file_library.tsx
yourname d0d88ab950 创建了3个新文件:
pages_dashboard.tsx (系统仪表盘功能)
pages_users.tsx (用户管理功能)
pages_file_library.tsx (文件库管理功能)
更新了所有引用:

修改了web_app.tsx中的导入语句
确保路由配置正确指向新文件
原pages_sys.tsx文件已不再使用,可以安全删除
2025-05-13 09:17:50 +00:00

673 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Button, Table, Space, Form, Input, Select,
message, Modal, Card, Typography, Tag, Popconfirm,
Tabs, Image, Upload, Descriptions
} from 'antd';
import {
UploadOutlined,
FileImageOutlined,
FileExcelOutlined,
FileWordOutlined,
FilePdfOutlined,
FileOutlined,
} from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
import { uploadMinIOWithPolicy, uploadOSSWithPolicy } from '@d8d-appcontainer/api';
import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
import { FileAPI } from './api/index.ts';
import type { FileLibrary, FileCategory } from '../share/types.ts';
import { OssType } from '../share/types.ts';
const { Title } = Typography;
// 文件库管理页面
export const FileLibraryPage = () => {
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState<FileLibrary[]>([]);
const [categories, setCategories] = useState<FileCategory[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0
});
const [searchParams, setSearchParams] = useState({
fileType: '',
keyword: ''
});
const [uploadModalVisible, setUploadModalVisible] = useState(false);
const [fileDetailModalVisible, setFileDetailModalVisible] = useState(false);
const [currentFile, setCurrentFile] = useState<FileLibrary | null>(null);
const [uploadLoading, setUploadLoading] = useState(false);
const [form] = Form.useForm();
const [categoryForm] = Form.useForm();
const [categoryModalVisible, setCategoryModalVisible] = useState(false);
const [currentCategory, setCurrentCategory] = useState<FileCategory | null>(null);
// 获取文件图标
const getFileIcon = (fileType: string) => {
if (fileType.includes('image')) {
return <FileImageOutlined style={{ fontSize: '24px', color: '#1890ff' }} />;
} else if (fileType.includes('pdf')) {
return <FilePdfOutlined style={{ fontSize: '24px', color: '#ff4d4f' }} />;
} else if (fileType.includes('excel') || fileType.includes('sheet')) {
return <FileExcelOutlined style={{ fontSize: '24px', color: '#52c41a' }} />;
} else if (fileType.includes('word') || fileType.includes('document')) {
return <FileWordOutlined style={{ fontSize: '24px', color: '#2f54eb' }} />;
} else {
return <FileOutlined style={{ fontSize: '24px', color: '#faad14' }} />;
}
};
// 加载文件列表
const fetchFileList = async () => {
setLoading(true);
try {
const response = await FileAPI.getFileList({
page: pagination.current,
pageSize: pagination.pageSize,
...searchParams
});
if (response && response.data) {
setFileList(response.data.list);
setPagination({
...pagination,
total: response.data.pagination.total
});
}
} catch (error) {
console.error('获取文件列表失败:', error);
message.error('获取文件列表失败');
} finally {
setLoading(false);
}
};
// 加载文件分类
const fetchCategories = async () => {
try {
const response = await FileAPI.getCategories();
if (response && response.data) {
setCategories(response.data);
}
} catch (error) {
console.error('获取文件分类失败:', error);
message.error('获取文件分类失败');
}
};
// 组件挂载时加载数据
useEffect(() => {
fetchFileList();
fetchCategories();
}, [pagination.current, pagination.pageSize, searchParams]);
// 上传文件
const handleUpload = async (file: File) => {
try {
setUploadLoading(true);
// 1. 获取上传策略
const policyResponse = await FileAPI.getUploadPolicy(file.name);
if (!policyResponse || !policyResponse.data) {
throw new Error('获取上传策略失败');
}
const policy = policyResponse.data;
// 2. 上传文件至 MinIO
const uploadProgress = {
progress: 0,
completed: false,
error: null as Error | null
};
const callbacks = {
onProgress: (event: { progress: number }) => {
uploadProgress.progress = event.progress;
},
onComplete: () => {
uploadProgress.completed = true;
},
onError: (err: Error) => {
uploadProgress.error = err;
}
};
const uploadUrl = window.CONFIG?.OSS_TYPE === OssType.MINIO ? await uploadMinIOWithPolicy(
policy as MinioUploadPolicy,
file,
file.name,
callbacks
) : await uploadOSSWithPolicy(
policy as OSSUploadPolicy,
file,
file.name,
callbacks
);
if (!uploadUrl || uploadProgress.error) {
throw uploadProgress.error || new Error('上传文件失败');
}
// 3. 保存文件信息到文件库
const fileValues = form.getFieldsValue();
const fileData = {
file_name: file.name,
file_path: uploadUrl,
file_type: file.type,
file_size: file.size,
category_id: fileValues.category_id ? Number(fileValues.category_id) : undefined,
tags: fileValues.tags,
description: fileValues.description
};
const saveResponse = await FileAPI.saveFileInfo(fileData);
if (saveResponse && saveResponse.data) {
message.success('文件上传成功');
setUploadModalVisible(false);
form.resetFields();
fetchFileList();
}
} catch (error) {
console.error('上传文件失败:', error);
message.error('上传文件失败: ' + (error instanceof Error ? error.message : '未知错误'));
} finally {
setUploadLoading(false);
}
};
// 处理文件上传
const uploadProps = {
name: 'file',
multiple: false,
showUploadList: false,
beforeUpload: (file: File) => {
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('文件大小不能超过10MB');
return false;
}
handleUpload(file);
return false;
}
};
// 查看文件详情
const viewFileDetail = async (id: number) => {
try {
const response = await FileAPI.getFileInfo(id);
if (response && response.data) {
setCurrentFile(response.data);
setFileDetailModalVisible(true);
}
} catch (error) {
console.error('获取文件详情失败:', error);
message.error('获取文件详情失败');
}
};
// 下载文件
const downloadFile = async (file: FileLibrary) => {
try {
// 更新下载计数
await FileAPI.updateDownloadCount(file.id);
// 创建一个暂时的a标签用于下载
const link = document.createElement('a');
link.href = file.file_path;
link.target = '_blank';
link.download = file.file_name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('下载已开始');
} catch (error) {
console.error('下载文件失败:', error);
message.error('下载文件失败');
}
};
// 删除文件
const handleDeleteFile = async (id: number) => {
try {
await FileAPI.deleteFile(id);
message.success('文件删除成功');
fetchFileList();
} catch (error) {
console.error('删除文件失败:', error);
message.error('删除文件失败');
}
};
// 处理搜索
const handleSearch = (values: any) => {
setSearchParams(values);
setPagination({
...pagination,
current: 1
});
};
// 处理表格分页变化
const handleTableChange = (newPagination: any) => {
setPagination({
...pagination,
current: newPagination.current,
pageSize: newPagination.pageSize
});
};
// 添加或更新分类
const handleCategorySave = async () => {
try {
const values = await categoryForm.validateFields();
if (currentCategory) {
// 更新分类
await FileAPI.updateCategory(currentCategory.id, values);
message.success('分类更新成功');
} else {
// 创建分类
await FileAPI.createCategory(values);
message.success('分类创建成功');
}
setCategoryModalVisible(false);
categoryForm.resetFields();
setCurrentCategory(null);
fetchCategories();
} catch (error) {
console.error('保存分类失败:', error);
message.error('保存分类失败');
}
};
// 编辑分类
const handleEditCategory = (category: FileCategory) => {
setCurrentCategory(category);
categoryForm.setFieldsValue(category);
setCategoryModalVisible(true);
};
// 删除分类
const handleDeleteCategory = async (id: number) => {
try {
await FileAPI.deleteCategory(id);
message.success('分类删除成功');
fetchCategories();
} catch (error) {
console.error('删除分类失败:', error);
message.error('删除分类失败');
}
};
// 文件表格列配置
const columns = [
{
title: '文件名',
key: 'file_name',
render: (text: string, record: FileLibrary) => (
<Space>
{getFileIcon(record.file_type)}
<a onClick={() => viewFileDetail(record.id)}>
{record.original_filename || record.file_name}
</a>
</Space>
)
},
{
title: '文件类型',
dataIndex: 'file_type',
key: 'file_type',
width: 120,
render: (text: string) => text.split('/').pop()
},
{
title: '大小',
dataIndex: 'file_size',
key: 'file_size',
width: 100,
render: (size: number) => {
if (size < 1024) {
return `${size} B`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)} KB`;
} else {
return `${(size / 1024 / 1024).toFixed(2)} MB`;
}
}
},
{
title: '分类',
dataIndex: 'category_id',
key: 'category_id',
width: 120
},
{
title: '上传者',
dataIndex: 'uploader_name',
key: 'uploader_name',
width: 120
},
{
title: '下载次数',
dataIndex: 'download_count',
key: 'download_count',
width: 120
},
{
title: '上传时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
render: (_: any, record: FileLibrary) => (
<Space size="middle">
<Button type="link" onClick={() => downloadFile(record)}>
</Button>
<Popconfirm
title="确定要删除这个文件吗?"
onConfirm={() => handleDeleteFile(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger></Button>
</Popconfirm>
</Space>
)
}
];
// 分类表格列配置
const categoryColumns = [
{
title: '分类名称',
dataIndex: 'name',
key: 'name'
},
{
title: '分类编码',
dataIndex: 'code',
key: 'code'
},
{
title: '描述',
dataIndex: 'description',
key: 'description'
},
{
title: '操作',
key: 'action',
render: (_: any, record: FileCategory) => (
<Space size="middle">
<Button type="link" onClick={() => handleEditCategory(record)}>
</Button>
<Popconfirm
title="确定要删除这个分类吗?"
onConfirm={() => handleDeleteCategory(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger></Button>
</Popconfirm>
</Space>
)
}
];
return (
<div>
<Title level={2}></Title>
<Card>
<Tabs defaultActiveKey="files">
<Tabs.TabPane tab="文件管理" key="files">
{/* 搜索表单 */}
<Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
<Form.Item name="keyword" label="关键词">
<Input placeholder="文件名/描述/标签" allowClear />
</Form.Item>
<Form.Item name="category_id" label="分类">
<Select placeholder="选择分类" allowClear style={{ width: 160 }}>
{categories.map(category => (
<Select.Option key={category.id} value={category.id}>
{category.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="fileType" label="文件类型">
<Select placeholder="选择文件类型" allowClear style={{ width: 160 }}>
<Select.Option value="image"></Select.Option>
<Select.Option value="document"></Select.Option>
<Select.Option value="application"></Select.Option>
<Select.Option value="audio"></Select.Option>
<Select.Option value="video"></Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
<Button
type="primary"
onClick={() => setUploadModalVisible(true)}
icon={<UploadOutlined />}
style={{ marginLeft: 16 }}
>
</Button>
</Form>
{/* 文件列表 */}
<Table
columns={columns}
dataSource={fileList}
rowKey="id"
loading={loading}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
}}
onChange={handleTableChange}
/>
</Tabs.TabPane>
<Tabs.TabPane tab="分类管理" key="categories">
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
onClick={() => {
setCurrentCategory(null);
categoryForm.resetFields();
setCategoryModalVisible(true);
}}
>
</Button>
</div>
<Table
columns={categoryColumns}
dataSource={categories}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Tabs.TabPane>
</Tabs>
</Card>
{/* 上传文件弹窗 */}
<Modal
title="上传文件"
open={uploadModalVisible}
onCancel={() => setUploadModalVisible(false)}
footer={null}
>
<Form form={form} layout="vertical">
<Form.Item
name="file"
label="文件"
rules={[{ required: true, message: '请选择要上传的文件' }]}
>
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />} loading={uploadLoading}>
</Button>
<div style={{ marginTop: 8 }}>
10MB
</div>
</Upload>
</Form.Item>
<Form.Item
name="category_id"
label="分类"
>
<Select placeholder="选择分类" allowClear>
{categories.map(category => (
<Select.Option key={category.id} value={category.id}>
{category.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="tags"
label="标签"
>
<Input placeholder="多个标签用逗号分隔" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} placeholder="文件描述..." />
</Form.Item>
</Form>
</Modal>
{/* 文件详情弹窗 */}
<Modal
title="文件详情"
open={fileDetailModalVisible}
onCancel={() => setFileDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setFileDetailModalVisible(false)}>
</Button>,
<Button
key="download"
type="primary"
onClick={() => currentFile && downloadFile(currentFile)}
>
</Button>
]}
width={700}
>
{currentFile && (
<Descriptions bordered column={2}>
<Descriptions.Item label="系统文件名" span={2}>
{currentFile.file_name}
</Descriptions.Item>
{currentFile.original_filename && (
<Descriptions.Item label="原始文件名" span={2}>
{currentFile.original_filename}
</Descriptions.Item>
)}
<Descriptions.Item label="文件类型">
{currentFile.file_type}
</Descriptions.Item>
<Descriptions.Item label="文件大小">
{currentFile.file_size < 1024 * 1024
? `${(currentFile.file_size / 1024).toFixed(2)} KB`
: `${(currentFile.file_size / 1024 / 1024).toFixed(2)} MB`}
</Descriptions.Item>
<Descriptions.Item label="上传者">
{currentFile.uploader_name}
</Descriptions.Item>
<Descriptions.Item label="上传时间">
{dayjs(currentFile.created_at).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="分类">
{currentFile.category_id}
</Descriptions.Item>
<Descriptions.Item label="下载次数">
{currentFile.download_count}
</Descriptions.Item>
<Descriptions.Item label="标签" span={2}>
{currentFile.tags?.split(',').map(tag => (
<Tag key={tag}>{tag}</Tag>
))}
</Descriptions.Item>
<Descriptions.Item label="描述" span={2}>
{currentFile.description}
</Descriptions.Item>
{currentFile.file_type.startsWith('image/') && (
<Descriptions.Item label="预览" span={2}>
<Image src={currentFile.file_path} style={{ maxWidth: '100%' }} />
</Descriptions.Item>
)}
</Descriptions>
)}
</Modal>
{/* 分类管理弹窗 */}
<Modal
title={currentCategory ? "编辑分类" : "添加分类"}
open={categoryModalVisible}
onOk={handleCategorySave}
onCancel={() => {
setCategoryModalVisible(false);
categoryForm.resetFields();
setCurrentCategory(null);
}}
>
<Form form={categoryForm} layout="vertical">
<Form.Item
name="name"
label="分类名称"
rules={[{ required: true, message: '请输入分类名称' }]}
>
<Input placeholder="请输入分类名称" />
</Form.Item>
<Form.Item
name="code"
label="分类编码"
rules={[{ required: true, message: '请输入分类编码' }]}
>
<Input placeholder="请输入分类编码" />
</Form.Item>
<Form.Item
name="description"
label="分类描述"
>
<Input.TextArea rows={4} placeholder="分类描述..." />
</Form.Item>
</Form>
</Modal>
</div>
);
};