| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390 |
- import React, { useState, useEffect } from 'react';
- import {
- Layout, Menu, Button, Table, Space,
- Form, Input, Select, message, Modal,
- Card, Spin, Row, Col, Breadcrumb, Avatar,
- Dropdown, ConfigProvider, theme, Typography,
- Switch, Badge, Image, Upload, Divider, Descriptions,
- Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
- } from 'antd';
- import {
- UploadOutlined,
- FileImageOutlined,
- FileExcelOutlined,
- FileWordOutlined,
- FilePdfOutlined,
- FileOutlined,
- } from '@ant-design/icons';
- import {
- useQuery,
- } from '@tanstack/react-query';
- import dayjs from 'dayjs';
- import weekday from 'dayjs/plugin/weekday';
- import localeData from 'dayjs/plugin/localeData';
- import { uploadMinIOWithPolicy,uploadOSSWithPolicy } from '@d8d-appcontainer/api';
- import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
- import 'dayjs/locale/zh-cn';
- import type {
- FileLibrary, FileCategory, KnowInfo
- } from '../share/types.ts';
- import {
- AuditStatus,AuditStatusNameMap,
- OssType,
- } from '../share/types.ts';
- import { getEnumOptions } from './utils.ts';
- import {
- FileAPI,
- UserAPI,
- } from './api.ts';
- // 配置 dayjs 插件
- dayjs.extend(weekday);
- dayjs.extend(localeData);
- // 设置 dayjs 语言
- dayjs.locale('zh-cn');
- const { Title } = Typography;
- // 仪表盘页面
- export const DashboardPage = () => {
- return (
- <div>
- <Title level={2}>仪表盘</Title>
- <Row gutter={16}>
- <Col span={8}>
- <Card>
- <Statistic
- title="活跃用户"
- value={112893}
- loading={false}
- />
- </Card>
- </Col>
- <Col span={8}>
- <Card>
- <Statistic
- title="系统消息"
- value={93}
- loading={false}
- />
- </Card>
- </Col>
- <Col span={8}>
- <Card>
- <Statistic
- title="在线用户"
- value={1128}
- loading={false}
- />
- </Card>
- </Col>
- </Row>
- </div>
- );
- };
- // 用户管理页面
- export const UsersPage = () => {
- const [searchParams, setSearchParams] = useState({
- page: 1,
- limit: 10,
- search: ''
- });
- const [modalVisible, setModalVisible] = useState(false);
- const [modalTitle, setModalTitle] = useState('');
- const [editingUser, setEditingUser] = useState<any>(null);
- const [form] = Form.useForm();
- const { data: usersData, isLoading, refetch } = useQuery({
- queryKey: ['users', searchParams],
- queryFn: async () => {
- return await UserAPI.getUsers(searchParams);
- }
- });
- const users = usersData?.data || [];
- const pagination = {
- current: searchParams.page,
- pageSize: searchParams.limit,
- total: usersData?.pagination?.total || 0
- };
- // 处理搜索
- const handleSearch = (values: any) => {
- setSearchParams(prev => ({
- ...prev,
- search: values.search || '',
- page: 1
- }));
- };
- // 处理分页变化
- const handleTableChange = (newPagination: any) => {
- setSearchParams(prev => ({
- ...prev,
- page: newPagination.current,
- limit: newPagination.pageSize
- }));
- };
- // 打开创建用户模态框
- const showCreateModal = () => {
- setModalTitle('创建用户');
- setEditingUser(null);
- form.resetFields();
- setModalVisible(true);
- };
- // 打开编辑用户模态框
- const showEditModal = (user: any) => {
- setModalTitle('编辑用户');
- setEditingUser(user);
- form.setFieldsValue(user);
- setModalVisible(true);
- };
- // 处理模态框确认
- const handleModalOk = async () => {
- try {
- const values = await form.validateFields();
-
- if (editingUser) {
- // 编辑用户
- await UserAPI.updateUser(editingUser.id, values);
- message.success('用户更新成功');
- } else {
- // 创建用户
- await UserAPI.createUser(values);
- message.success('用户创建成功');
- }
-
- setModalVisible(false);
- form.resetFields();
- refetch(); // 刷新用户列表
- } catch (error) {
- console.error('表单提交失败:', error);
- message.error('操作失败,请重试');
- }
- };
- // 处理删除用户
- const handleDelete = async (id: number) => {
- try {
- await UserAPI.deleteUser(id);
- message.success('用户删除成功');
- refetch(); // 刷新用户列表
- } catch (error) {
- console.error('删除用户失败:', error);
- message.error('删除失败,请重试');
- }
- };
-
- const columns = [
- {
- title: '用户名',
- dataIndex: 'username',
- key: 'username',
- },
- {
- title: '昵称',
- dataIndex: 'nickname',
- key: 'nickname',
- },
- {
- title: '邮箱',
- dataIndex: 'email',
- key: 'email',
- },
- {
- title: '角色',
- dataIndex: 'role',
- key: 'role',
- render: (role: string) => (
- <Tag color={role === 'admin' ? 'red' : 'blue'}>
- {role === 'admin' ? '管理员' : '普通用户'}
- </Tag>
- ),
- },
- {
- title: '创建时间',
- dataIndex: 'created_at',
- key: 'created_at',
- render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
- },
- {
- title: '操作',
- key: 'action',
- render: (_: any, record: any) => (
- <Space size="middle">
- <Button type="link" onClick={() => showEditModal(record)}>
- 编辑
- </Button>
- <Popconfirm
- title="确定要删除此用户吗?"
- onConfirm={() => handleDelete(record.id)}
- okText="确定"
- cancelText="取消"
- >
- <Button type="link" danger>
- 删除
- </Button>
- </Popconfirm>
- </Space>
- ),
- },
- ];
-
- return (
- <div>
- <Title level={2}>用户管理</Title>
- <Card>
- <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
- <Form.Item name="search" label="搜索">
- <Input placeholder="用户名/昵称/邮箱" allowClear />
- </Form.Item>
- <Form.Item>
- <Space>
- <Button type="primary" htmlType="submit">
- 搜索
- </Button>
- <Button type="primary" onClick={showCreateModal}>
- 创建用户
- </Button>
- </Space>
- </Form.Item>
- </Form>
- <Table
- columns={columns}
- dataSource={users}
- loading={isLoading}
- rowKey="id"
- pagination={{
- ...pagination,
- showSizeChanger: true,
- showQuickJumper: true,
- showTotal: (total) => `共 ${total} 条记录`
- }}
- onChange={handleTableChange}
- />
- </Card>
- {/* 创建/编辑用户模态框 */}
- <Modal
- title={modalTitle}
- open={modalVisible}
- onOk={handleModalOk}
- onCancel={() => {
- setModalVisible(false);
- form.resetFields();
- }}
- width={600}
- >
- <Form
- form={form}
- layout="vertical"
- >
- <Form.Item
- name="username"
- label="用户名"
- rules={[
- { required: true, message: '请输入用户名' },
- { min: 3, message: '用户名至少3个字符' }
- ]}
- >
- <Input placeholder="请输入用户名" />
- </Form.Item>
- <Form.Item
- name="nickname"
- label="昵称"
- rules={[{ required: true, message: '请输入昵称' }]}
- >
- <Input placeholder="请输入昵称" />
- </Form.Item>
- <Form.Item
- name="email"
- label="邮箱"
- rules={[
- { required: true, message: '请输入邮箱' },
- { type: 'email', message: '请输入有效的邮箱地址' }
- ]}
- >
- <Input placeholder="请输入邮箱" />
- </Form.Item>
- {!editingUser && (
- <Form.Item
- name="password"
- label="密码"
- rules={[
- { required: true, message: '请输入密码' },
- { min: 6, message: '密码至少6个字符' }
- ]}
- >
- <Input.Password placeholder="请输入密码" />
- </Form.Item>
- )}
- <Form.Item
- name="role"
- label="角色"
- rules={[{ required: true, message: '请选择角色' }]}
- >
- <Select placeholder="请选择角色">
- <Select.Option value="user">普通用户</Select.Option>
- <Select.Option value="admin">管理员</Select.Option>
- </Select>
- </Form.Item>
- </Form>
- </Modal>
- </div>
- );
- };
- // 知识库管理页面组件
- export const KnowInfoPage = () => {
- const [modalVisible, setModalVisible] = useState(false);
- const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
- const [editingId, setEditingId] = useState<number | null>(null);
- const [form] = Form.useForm();
- const [isLoading, setIsLoading] = useState(false);
- const [searchParams, setSearchParams] = useState({
- title: '',
- category: '',
- page: 1,
- limit: 10,
- });
-
- // 使用React Query获取知识库文章列表
- const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
- queryKey: ['articles', searchParams],
- queryFn: async () => {
- const { title, category, page, limit } = searchParams;
- const params = new URLSearchParams();
-
- if (title) params.append('title', title);
- if (category) params.append('category', category);
- params.append('page', String(page));
- params.append('limit', String(limit));
-
- const response = await fetch(`/api/know-info?${params.toString()}`, {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- });
-
- if (!response.ok) {
- throw new Error('获取知识库文章列表失败');
- }
-
- return await response.json();
- }
- });
-
- const articles = articlesData?.data || [];
- const pagination = articlesData?.pagination || { current: 1, pageSize: 10, total: 0 };
-
- // 获取单个知识库文章
- const fetchArticle = async (id: number) => {
- try {
- const response = await fetch(`/api/know-info/${id}`, {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- });
-
- if (!response.ok) {
- throw new Error('获取知识库文章详情失败');
- }
-
- return await response.json();
- } catch (error) {
- message.error('获取知识库文章详情失败');
- return null;
- }
- };
-
- // 处理表单提交
- const handleSubmit = async (values: Partial<KnowInfo>) => {
- setIsLoading(true);
-
- try {
- const url = formMode === 'create'
- ? '/api/know-info'
- : `/api/know-info/${editingId}`;
-
- const method = formMode === 'create' ? 'POST' : 'PUT';
-
- const response = await fetch(url, {
- method,
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- body: JSON.stringify(values),
- });
-
- if (!response.ok) {
- throw new Error(formMode === 'create' ? '创建知识库文章失败' : '更新知识库文章失败');
- }
-
- message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
- setModalVisible(false);
- form.resetFields();
- refetch();
- } catch (error) {
- message.error((error as Error).message);
- } finally {
- setIsLoading(false);
- }
- };
-
- // 处理编辑
- const handleEdit = async (id: number) => {
- const article = await fetchArticle(id);
-
- if (article) {
- setFormMode('edit');
- setEditingId(id);
- form.setFieldsValue(article);
- setModalVisible(true);
- }
- };
-
- // 处理删除
- const handleDelete = async (id: number) => {
- try {
- const response = await fetch(`/api/know-info/${id}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- });
-
- if (!response.ok) {
- throw new Error('删除知识库文章失败');
- }
-
- message.success('删除知识库文章成功');
- refetch();
- } catch (error) {
- message.error((error as Error).message);
- }
- };
-
- // 处理搜索
- const handleSearch = (values: any) => {
- setSearchParams(prev => ({
- ...prev,
- title: values.title || '',
- category: values.category || '',
- page: 1,
- }));
- };
-
- // 处理分页
- const handlePageChange = (page: number, pageSize?: number) => {
- setSearchParams(prev => ({
- ...prev,
- page,
- limit: pageSize || prev.limit,
- }));
- };
-
- // 处理添加
- const handleAdd = () => {
- setFormMode('create');
- setEditingId(null);
- form.resetFields();
- setModalVisible(true);
- };
-
- // 审核状态映射
- const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
-
- // 表格列定义
- const columns = [
- {
- title: 'ID',
- dataIndex: 'id',
- key: 'id',
- width: 80,
- },
- {
- title: '标题',
- dataIndex: 'title',
- key: 'title',
- },
- {
- title: '分类',
- dataIndex: 'category',
- key: 'category',
- },
- {
- title: '标签',
- dataIndex: 'tags',
- key: 'tags',
- render: (tags: string) => tags ? tags.split(',').map(tag => (
- <Tag key={tag}>{tag}</Tag>
- )) : null,
- },
- {
- title: '作者',
- dataIndex: 'author',
- key: 'author',
- },
- {
- title: '审核状态',
- dataIndex: 'audit_status',
- key: 'audit_status',
- render: (status: AuditStatus) => {
- let color = '';
- let text = '';
-
- switch(status) {
- case AuditStatus.PENDING:
- color = 'orange';
- text = '待审核';
- break;
- case AuditStatus.APPROVED:
- color = 'green';
- text = '已通过';
- break;
- case AuditStatus.REJECTED:
- color = 'red';
- text = '已拒绝';
- break;
- default:
- color = 'default';
- text = '未知';
- }
-
- return <Tag color={color}>{text}</Tag>;
- },
- },
- {
- title: '创建时间',
- dataIndex: 'created_at',
- key: 'created_at',
- render: (date: string) => new Date(date).toLocaleString(),
- },
- {
- title: '操作',
- key: 'action',
- render: (_: any, record: KnowInfo) => (
- <Space size="middle">
- <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
- <Popconfirm
- title="确定要删除这篇文章吗?"
- onConfirm={() => handleDelete(record.id)}
- okText="确定"
- cancelText="取消"
- >
- <Button type="link" danger>删除</Button>
- </Popconfirm>
- </Space>
- ),
- },
- ];
-
- return (
- <div>
- <Card title="知识库管理" className="mb-4">
- <Form
- layout="inline"
- onFinish={handleSearch}
- style={{ marginBottom: '16px' }}
- >
- <Form.Item name="title" label="标题">
- <Input placeholder="请输入文章标题" />
- </Form.Item>
-
- <Form.Item name="category" label="分类">
- <Input placeholder="请输入文章分类" />
- </Form.Item>
-
- <Form.Item>
- <Space>
- <Button type="primary" htmlType="submit">
- 搜索
- </Button>
- <Button onClick={() => {
- setSearchParams({
- title: '',
- category: '',
- page: 1,
- limit: 10,
- });
- }}>
- 重置
- </Button>
- <Button type="primary" onClick={handleAdd}>
- 添加文章
- </Button>
- </Space>
- </Form.Item>
- </Form>
-
- <Table
- columns={columns}
- dataSource={articles}
- rowKey="id"
- loading={isListLoading}
- pagination={{
- current: pagination.current,
- pageSize: pagination.pageSize,
- total: pagination.total,
- onChange: handlePageChange,
- showSizeChanger: true,
- }}
- />
- </Card>
-
- <Modal
- title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
- open={modalVisible}
- onOk={() => form.submit()}
- onCancel={() => setModalVisible(false)}
- width={800}
- >
- <Form
- form={form}
- layout="vertical"
- onFinish={handleSubmit}
- initialValues={{
- audit_status: AuditStatus.PENDING,
- }}
- >
- <Row gutter={16}>
- <Col span={12}>
- <Form.Item
- name="title"
- label="文章标题"
- rules={[{ required: true, message: '请输入文章标题' }]}
- >
- <Input placeholder="请输入文章标题" />
- </Form.Item>
- </Col>
- <Col span={12}>
- <Form.Item
- name="category"
- label="文章分类"
- >
- <Input placeholder="请输入文章分类" />
- </Form.Item>
- </Col>
- </Row>
-
- <Form.Item
- name="tags"
- label="文章标签"
- help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
- >
- <Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
- </Form.Item>
-
- <Form.Item
- name="content"
- label="文章内容"
- rules={[{ required: true, message: '请输入文章内容' }]}
- >
- <Input.TextArea rows={15} placeholder="请输入文章内容,支持Markdown格式" />
- </Form.Item>
-
- <Row gutter={16}>
- <Col span={12}>
- <Form.Item
- name="author"
- label="文章作者"
- >
- <Input placeholder="请输入文章作者" />
- </Form.Item>
- </Col>
- <Col span={12}>
- <Form.Item
- name="cover_url"
- label="封面图片URL"
- >
- <Input placeholder="请输入封面图片URL" />
- </Form.Item>
- </Col>
- </Row>
-
- <Form.Item
- name="audit_status"
- label="审核状态"
- >
- <Select options={auditStatusOptions} />
- </Form.Item>
-
- <Form.Item>
- <Space>
- <Button type="primary" htmlType="submit" loading={isLoading}>
- {formMode === 'create' ? '创建' : '保存'}
- </Button>
- <Button onClick={() => setModalVisible(false)}>取消</Button>
- </Space>
- </Form.Item>
- </Form>
- </Modal>
- </div>
- );
- };
- // 文件库管理页面
- 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>
- );
- };
|