2
0

pages_sys.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Layout, Menu, Button, Table, Space,
  4. Form, Input, Select, message, Modal,
  5. Card, Spin, Row, Col, Breadcrumb, Avatar,
  6. Dropdown, ConfigProvider, theme, Typography,
  7. Switch, Badge, Image, Upload, Divider, Descriptions,
  8. Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
  9. } from 'antd';
  10. import {
  11. UploadOutlined,
  12. FileImageOutlined,
  13. FileExcelOutlined,
  14. FileWordOutlined,
  15. FilePdfOutlined,
  16. FileOutlined,
  17. } from '@ant-design/icons';
  18. import {
  19. useQuery,
  20. } from '@tanstack/react-query';
  21. import dayjs from 'dayjs';
  22. import weekday from 'dayjs/plugin/weekday';
  23. import localeData from 'dayjs/plugin/localeData';
  24. import { uploadMinIOWithPolicy,uploadOSSWithPolicy } from '@d8d-appcontainer/api';
  25. import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
  26. import 'dayjs/locale/zh-cn';
  27. import type {
  28. FileLibrary, FileCategory
  29. } from '../share/types.ts';
  30. import {
  31. OssType,
  32. } from '../share/types.ts';
  33. import { FileAPI , UserAPI } from './api/index.ts';
  34. // 配置 dayjs 插件
  35. dayjs.extend(weekday);
  36. dayjs.extend(localeData);
  37. // 设置 dayjs 语言
  38. dayjs.locale('zh-cn');
  39. const { Title } = Typography;
  40. // 仪表盘页面
  41. export const DashboardPage = () => {
  42. return (
  43. <div>
  44. <Title level={2}>仪表盘</Title>
  45. <Row gutter={16}>
  46. <Col span={8}>
  47. <Card>
  48. <Statistic
  49. title="活跃用户"
  50. value={112893}
  51. loading={false}
  52. />
  53. </Card>
  54. </Col>
  55. <Col span={8}>
  56. <Card>
  57. <Statistic
  58. title="系统消息"
  59. value={93}
  60. loading={false}
  61. />
  62. </Card>
  63. </Col>
  64. <Col span={8}>
  65. <Card>
  66. <Statistic
  67. title="在线用户"
  68. value={1128}
  69. loading={false}
  70. />
  71. </Card>
  72. </Col>
  73. </Row>
  74. </div>
  75. );
  76. };
  77. // 用户管理页面
  78. export const UsersPage = () => {
  79. const [searchParams, setSearchParams] = useState({
  80. page: 1,
  81. limit: 10,
  82. search: ''
  83. });
  84. const [modalVisible, setModalVisible] = useState(false);
  85. const [modalTitle, setModalTitle] = useState('');
  86. const [editingUser, setEditingUser] = useState<any>(null);
  87. const [form] = Form.useForm();
  88. const { data: usersData, isLoading, refetch } = useQuery({
  89. queryKey: ['users', searchParams],
  90. queryFn: async () => {
  91. return await UserAPI.getUsers(searchParams);
  92. }
  93. });
  94. const users = usersData?.data || [];
  95. const pagination = {
  96. current: searchParams.page,
  97. pageSize: searchParams.limit,
  98. total: usersData?.pagination?.total || 0
  99. };
  100. // 处理搜索
  101. const handleSearch = (values: any) => {
  102. setSearchParams(prev => ({
  103. ...prev,
  104. search: values.search || '',
  105. page: 1
  106. }));
  107. };
  108. // 处理分页变化
  109. const handleTableChange = (newPagination: any) => {
  110. setSearchParams(prev => ({
  111. ...prev,
  112. page: newPagination.current,
  113. limit: newPagination.pageSize
  114. }));
  115. };
  116. // 打开创建用户模态框
  117. const showCreateModal = () => {
  118. setModalTitle('创建用户');
  119. setEditingUser(null);
  120. form.resetFields();
  121. setModalVisible(true);
  122. };
  123. // 打开编辑用户模态框
  124. const showEditModal = (user: any) => {
  125. setModalTitle('编辑用户');
  126. setEditingUser(user);
  127. form.setFieldsValue(user);
  128. setModalVisible(true);
  129. };
  130. // 处理模态框确认
  131. const handleModalOk = async () => {
  132. try {
  133. const values = await form.validateFields();
  134. if (editingUser) {
  135. // 编辑用户
  136. await UserAPI.updateUser(editingUser.id, values);
  137. message.success('用户更新成功');
  138. } else {
  139. // 创建用户
  140. await UserAPI.createUser(values);
  141. message.success('用户创建成功');
  142. }
  143. setModalVisible(false);
  144. form.resetFields();
  145. refetch(); // 刷新用户列表
  146. } catch (error) {
  147. console.error('表单提交失败:', error);
  148. message.error('操作失败,请重试');
  149. }
  150. };
  151. // 处理删除用户
  152. const handleDelete = async (id: number) => {
  153. try {
  154. await UserAPI.deleteUser(id);
  155. message.success('用户删除成功');
  156. refetch(); // 刷新用户列表
  157. } catch (error) {
  158. console.error('删除用户失败:', error);
  159. message.error('删除失败,请重试');
  160. }
  161. };
  162. const columns = [
  163. {
  164. title: '用户名',
  165. dataIndex: 'username',
  166. key: 'username',
  167. },
  168. {
  169. title: '昵称',
  170. dataIndex: 'nickname',
  171. key: 'nickname',
  172. },
  173. {
  174. title: '邮箱',
  175. dataIndex: 'email',
  176. key: 'email',
  177. },
  178. {
  179. title: '角色',
  180. dataIndex: 'role',
  181. key: 'role',
  182. render: (role: string) => (
  183. <Tag color={role === 'admin' ? 'red' : 'blue'}>
  184. {role === 'admin' ? '管理员' : '普通用户'}
  185. </Tag>
  186. ),
  187. },
  188. {
  189. title: '创建时间',
  190. dataIndex: 'created_at',
  191. key: 'created_at',
  192. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
  193. },
  194. {
  195. title: '操作',
  196. key: 'action',
  197. render: (_: any, record: any) => (
  198. <Space size="middle">
  199. <Button type="link" onClick={() => showEditModal(record)}>
  200. 编辑
  201. </Button>
  202. <Popconfirm
  203. title="确定要删除此用户吗?"
  204. onConfirm={() => handleDelete(record.id)}
  205. okText="确定"
  206. cancelText="取消"
  207. >
  208. <Button type="link" danger>
  209. 删除
  210. </Button>
  211. </Popconfirm>
  212. </Space>
  213. ),
  214. },
  215. ];
  216. return (
  217. <div>
  218. <Title level={2}>用户管理</Title>
  219. <Card>
  220. <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
  221. <Form.Item name="search" label="搜索">
  222. <Input placeholder="用户名/昵称/邮箱" allowClear />
  223. </Form.Item>
  224. <Form.Item>
  225. <Space>
  226. <Button type="primary" htmlType="submit">
  227. 搜索
  228. </Button>
  229. <Button type="primary" onClick={showCreateModal}>
  230. 创建用户
  231. </Button>
  232. </Space>
  233. </Form.Item>
  234. </Form>
  235. <Table
  236. columns={columns}
  237. dataSource={users}
  238. loading={isLoading}
  239. rowKey="id"
  240. pagination={{
  241. ...pagination,
  242. showSizeChanger: true,
  243. showQuickJumper: true,
  244. showTotal: (total) => `共 ${total} 条记录`
  245. }}
  246. onChange={handleTableChange}
  247. />
  248. </Card>
  249. {/* 创建/编辑用户模态框 */}
  250. <Modal
  251. title={modalTitle}
  252. open={modalVisible}
  253. onOk={handleModalOk}
  254. onCancel={() => {
  255. setModalVisible(false);
  256. form.resetFields();
  257. }}
  258. width={600}
  259. >
  260. <Form
  261. form={form}
  262. layout="vertical"
  263. >
  264. <Form.Item
  265. name="username"
  266. label="用户名"
  267. rules={[
  268. { required: true, message: '请输入用户名' },
  269. { min: 3, message: '用户名至少3个字符' }
  270. ]}
  271. >
  272. <Input placeholder="请输入用户名" />
  273. </Form.Item>
  274. <Form.Item
  275. name="nickname"
  276. label="昵称"
  277. rules={[{ required: true, message: '请输入昵称' }]}
  278. >
  279. <Input placeholder="请输入昵称" />
  280. </Form.Item>
  281. <Form.Item
  282. name="email"
  283. label="邮箱"
  284. rules={[
  285. { required: true, message: '请输入邮箱' },
  286. { type: 'email', message: '请输入有效的邮箱地址' }
  287. ]}
  288. >
  289. <Input placeholder="请输入邮箱" />
  290. </Form.Item>
  291. {!editingUser && (
  292. <Form.Item
  293. name="password"
  294. label="密码"
  295. rules={[
  296. { required: true, message: '请输入密码' },
  297. { min: 6, message: '密码至少6个字符' }
  298. ]}
  299. >
  300. <Input.Password placeholder="请输入密码" />
  301. </Form.Item>
  302. )}
  303. <Form.Item
  304. name="role"
  305. label="角色"
  306. rules={[{ required: true, message: '请选择角色' }]}
  307. >
  308. <Select placeholder="请选择角色">
  309. <Select.Option value="user">普通用户</Select.Option>
  310. <Select.Option value="admin">管理员</Select.Option>
  311. </Select>
  312. </Form.Item>
  313. </Form>
  314. </Modal>
  315. </div>
  316. );
  317. };
  318. // 文件库管理页面
  319. export const FileLibraryPage = () => {
  320. const [loading, setLoading] = useState(false);
  321. const [fileList, setFileList] = useState<FileLibrary[]>([]);
  322. const [categories, setCategories] = useState<FileCategory[]>([]);
  323. const [pagination, setPagination] = useState({
  324. current: 1,
  325. pageSize: 10,
  326. total: 0
  327. });
  328. const [searchParams, setSearchParams] = useState({
  329. fileType: '',
  330. keyword: ''
  331. });
  332. const [uploadModalVisible, setUploadModalVisible] = useState(false);
  333. const [fileDetailModalVisible, setFileDetailModalVisible] = useState(false);
  334. const [currentFile, setCurrentFile] = useState<FileLibrary | null>(null);
  335. const [uploadLoading, setUploadLoading] = useState(false);
  336. const [form] = Form.useForm();
  337. const [categoryForm] = Form.useForm();
  338. const [categoryModalVisible, setCategoryModalVisible] = useState(false);
  339. const [currentCategory, setCurrentCategory] = useState<FileCategory | null>(null);
  340. // 获取文件图标
  341. const getFileIcon = (fileType: string) => {
  342. if (fileType.includes('image')) {
  343. return <FileImageOutlined style={{ fontSize: '24px', color: '#1890ff' }} />;
  344. } else if (fileType.includes('pdf')) {
  345. return <FilePdfOutlined style={{ fontSize: '24px', color: '#ff4d4f' }} />;
  346. } else if (fileType.includes('excel') || fileType.includes('sheet')) {
  347. return <FileExcelOutlined style={{ fontSize: '24px', color: '#52c41a' }} />;
  348. } else if (fileType.includes('word') || fileType.includes('document')) {
  349. return <FileWordOutlined style={{ fontSize: '24px', color: '#2f54eb' }} />;
  350. } else {
  351. return <FileOutlined style={{ fontSize: '24px', color: '#faad14' }} />;
  352. }
  353. };
  354. // 加载文件列表
  355. const fetchFileList = async () => {
  356. setLoading(true);
  357. try {
  358. const response = await FileAPI.getFileList({
  359. page: pagination.current,
  360. pageSize: pagination.pageSize,
  361. ...searchParams
  362. });
  363. if (response && response.data) {
  364. setFileList(response.data.list);
  365. setPagination({
  366. ...pagination,
  367. total: response.data.pagination.total
  368. });
  369. }
  370. } catch (error) {
  371. console.error('获取文件列表失败:', error);
  372. message.error('获取文件列表失败');
  373. } finally {
  374. setLoading(false);
  375. }
  376. };
  377. // 加载文件分类
  378. const fetchCategories = async () => {
  379. try {
  380. const response = await FileAPI.getCategories();
  381. if (response && response.data) {
  382. setCategories(response.data);
  383. }
  384. } catch (error) {
  385. console.error('获取文件分类失败:', error);
  386. message.error('获取文件分类失败');
  387. }
  388. };
  389. // 组件挂载时加载数据
  390. useEffect(() => {
  391. fetchFileList();
  392. fetchCategories();
  393. }, [pagination.current, pagination.pageSize, searchParams]);
  394. // 上传文件
  395. const handleUpload = async (file: File) => {
  396. try {
  397. setUploadLoading(true);
  398. // 1. 获取上传策略
  399. const policyResponse = await FileAPI.getUploadPolicy(file.name);
  400. if (!policyResponse || !policyResponse.data) {
  401. throw new Error('获取上传策略失败');
  402. }
  403. const policy = policyResponse.data;
  404. // 2. 上传文件至 MinIO
  405. const uploadProgress = {
  406. progress: 0,
  407. completed: false,
  408. error: null as Error | null
  409. };
  410. const callbacks = {
  411. onProgress: (event: { progress: number }) => {
  412. uploadProgress.progress = event.progress;
  413. },
  414. onComplete: () => {
  415. uploadProgress.completed = true;
  416. },
  417. onError: (err: Error) => {
  418. uploadProgress.error = err;
  419. }
  420. };
  421. const uploadUrl = window.CONFIG?.OSS_TYPE === OssType.MINIO ? await uploadMinIOWithPolicy(
  422. policy as MinioUploadPolicy,
  423. file,
  424. file.name,
  425. callbacks
  426. ) : await uploadOSSWithPolicy(
  427. policy as OSSUploadPolicy,
  428. file,
  429. file.name,
  430. callbacks
  431. );
  432. if (!uploadUrl || uploadProgress.error) {
  433. throw uploadProgress.error || new Error('上传文件失败');
  434. }
  435. // 3. 保存文件信息到文件库
  436. const fileValues = form.getFieldsValue();
  437. const fileData = {
  438. file_name: file.name,
  439. file_path: uploadUrl,
  440. file_type: file.type,
  441. file_size: file.size,
  442. category_id: fileValues.category_id ? Number(fileValues.category_id) : undefined,
  443. tags: fileValues.tags,
  444. description: fileValues.description
  445. };
  446. const saveResponse = await FileAPI.saveFileInfo(fileData);
  447. if (saveResponse && saveResponse.data) {
  448. message.success('文件上传成功');
  449. setUploadModalVisible(false);
  450. form.resetFields();
  451. fetchFileList();
  452. }
  453. } catch (error) {
  454. console.error('上传文件失败:', error);
  455. message.error('上传文件失败: ' + (error instanceof Error ? error.message : '未知错误'));
  456. } finally {
  457. setUploadLoading(false);
  458. }
  459. };
  460. // 处理文件上传
  461. const uploadProps = {
  462. name: 'file',
  463. multiple: false,
  464. showUploadList: false,
  465. beforeUpload: (file: File) => {
  466. const isLt10M = file.size / 1024 / 1024 < 10;
  467. if (!isLt10M) {
  468. message.error('文件大小不能超过10MB!');
  469. return false;
  470. }
  471. handleUpload(file);
  472. return false;
  473. }
  474. };
  475. // 查看文件详情
  476. const viewFileDetail = async (id: number) => {
  477. try {
  478. const response = await FileAPI.getFileInfo(id);
  479. if (response && response.data) {
  480. setCurrentFile(response.data);
  481. setFileDetailModalVisible(true);
  482. }
  483. } catch (error) {
  484. console.error('获取文件详情失败:', error);
  485. message.error('获取文件详情失败');
  486. }
  487. };
  488. // 下载文件
  489. const downloadFile = async (file: FileLibrary) => {
  490. try {
  491. // 更新下载计数
  492. await FileAPI.updateDownloadCount(file.id);
  493. // 创建一个暂时的a标签用于下载
  494. const link = document.createElement('a');
  495. link.href = file.file_path;
  496. link.target = '_blank';
  497. link.download = file.file_name;
  498. document.body.appendChild(link);
  499. link.click();
  500. document.body.removeChild(link);
  501. message.success('下载已开始');
  502. } catch (error) {
  503. console.error('下载文件失败:', error);
  504. message.error('下载文件失败');
  505. }
  506. };
  507. // 删除文件
  508. const handleDeleteFile = async (id: number) => {
  509. try {
  510. await FileAPI.deleteFile(id);
  511. message.success('文件删除成功');
  512. fetchFileList();
  513. } catch (error) {
  514. console.error('删除文件失败:', error);
  515. message.error('删除文件失败');
  516. }
  517. };
  518. // 处理搜索
  519. const handleSearch = (values: any) => {
  520. setSearchParams(values);
  521. setPagination({
  522. ...pagination,
  523. current: 1
  524. });
  525. };
  526. // 处理表格分页变化
  527. const handleTableChange = (newPagination: any) => {
  528. setPagination({
  529. ...pagination,
  530. current: newPagination.current,
  531. pageSize: newPagination.pageSize
  532. });
  533. };
  534. // 添加或更新分类
  535. const handleCategorySave = async () => {
  536. try {
  537. const values = await categoryForm.validateFields();
  538. if (currentCategory) {
  539. // 更新分类
  540. await FileAPI.updateCategory(currentCategory.id, values);
  541. message.success('分类更新成功');
  542. } else {
  543. // 创建分类
  544. await FileAPI.createCategory(values);
  545. message.success('分类创建成功');
  546. }
  547. setCategoryModalVisible(false);
  548. categoryForm.resetFields();
  549. setCurrentCategory(null);
  550. fetchCategories();
  551. } catch (error) {
  552. console.error('保存分类失败:', error);
  553. message.error('保存分类失败');
  554. }
  555. };
  556. // 编辑分类
  557. const handleEditCategory = (category: FileCategory) => {
  558. setCurrentCategory(category);
  559. categoryForm.setFieldsValue(category);
  560. setCategoryModalVisible(true);
  561. };
  562. // 删除分类
  563. const handleDeleteCategory = async (id: number) => {
  564. try {
  565. await FileAPI.deleteCategory(id);
  566. message.success('分类删除成功');
  567. fetchCategories();
  568. } catch (error) {
  569. console.error('删除分类失败:', error);
  570. message.error('删除分类失败');
  571. }
  572. };
  573. // 文件表格列配置
  574. const columns = [
  575. {
  576. title: '文件名',
  577. key: 'file_name',
  578. render: (text: string, record: FileLibrary) => (
  579. <Space>
  580. {getFileIcon(record.file_type)}
  581. <a onClick={() => viewFileDetail(record.id)}>
  582. {record.original_filename || record.file_name}
  583. </a>
  584. </Space>
  585. )
  586. },
  587. {
  588. title: '文件类型',
  589. dataIndex: 'file_type',
  590. key: 'file_type',
  591. width: 120,
  592. render: (text: string) => text.split('/').pop()
  593. },
  594. {
  595. title: '大小',
  596. dataIndex: 'file_size',
  597. key: 'file_size',
  598. width: 100,
  599. render: (size: number) => {
  600. if (size < 1024) {
  601. return `${size} B`;
  602. } else if (size < 1024 * 1024) {
  603. return `${(size / 1024).toFixed(2)} KB`;
  604. } else {
  605. return `${(size / 1024 / 1024).toFixed(2)} MB`;
  606. }
  607. }
  608. },
  609. {
  610. title: '分类',
  611. dataIndex: 'category_id',
  612. key: 'category_id',
  613. width: 120
  614. },
  615. {
  616. title: '上传者',
  617. dataIndex: 'uploader_name',
  618. key: 'uploader_name',
  619. width: 120
  620. },
  621. {
  622. title: '下载次数',
  623. dataIndex: 'download_count',
  624. key: 'download_count',
  625. width: 120
  626. },
  627. {
  628. title: '上传时间',
  629. dataIndex: 'created_at',
  630. key: 'created_at',
  631. width: 180,
  632. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  633. },
  634. {
  635. title: '操作',
  636. key: 'action',
  637. width: 180,
  638. render: (_: any, record: FileLibrary) => (
  639. <Space size="middle">
  640. <Button type="link" onClick={() => downloadFile(record)}>
  641. 下载
  642. </Button>
  643. <Popconfirm
  644. title="确定要删除这个文件吗?"
  645. onConfirm={() => handleDeleteFile(record.id)}
  646. okText="确定"
  647. cancelText="取消"
  648. >
  649. <Button type="link" danger>删除</Button>
  650. </Popconfirm>
  651. </Space>
  652. )
  653. }
  654. ];
  655. // 分类表格列配置
  656. const categoryColumns = [
  657. {
  658. title: '分类名称',
  659. dataIndex: 'name',
  660. key: 'name'
  661. },
  662. {
  663. title: '分类编码',
  664. dataIndex: 'code',
  665. key: 'code'
  666. },
  667. {
  668. title: '描述',
  669. dataIndex: 'description',
  670. key: 'description'
  671. },
  672. {
  673. title: '操作',
  674. key: 'action',
  675. render: (_: any, record: FileCategory) => (
  676. <Space size="middle">
  677. <Button type="link" onClick={() => handleEditCategory(record)}>
  678. 编辑
  679. </Button>
  680. <Popconfirm
  681. title="确定要删除这个分类吗?"
  682. onConfirm={() => handleDeleteCategory(record.id)}
  683. okText="确定"
  684. cancelText="取消"
  685. >
  686. <Button type="link" danger>删除</Button>
  687. </Popconfirm>
  688. </Space>
  689. )
  690. }
  691. ];
  692. return (
  693. <div>
  694. <Title level={2}>文件库管理</Title>
  695. <Card>
  696. <Tabs defaultActiveKey="files">
  697. <Tabs.TabPane tab="文件管理" key="files">
  698. {/* 搜索表单 */}
  699. <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
  700. <Form.Item name="keyword" label="关键词">
  701. <Input placeholder="文件名/描述/标签" allowClear />
  702. </Form.Item>
  703. <Form.Item name="category_id" label="分类">
  704. <Select placeholder="选择分类" allowClear style={{ width: 160 }}>
  705. {categories.map(category => (
  706. <Select.Option key={category.id} value={category.id}>
  707. {category.name}
  708. </Select.Option>
  709. ))}
  710. </Select>
  711. </Form.Item>
  712. <Form.Item name="fileType" label="文件类型">
  713. <Select placeholder="选择文件类型" allowClear style={{ width: 160 }}>
  714. <Select.Option value="image">图片</Select.Option>
  715. <Select.Option value="document">文档</Select.Option>
  716. <Select.Option value="application">应用</Select.Option>
  717. <Select.Option value="audio">音频</Select.Option>
  718. <Select.Option value="video">视频</Select.Option>
  719. </Select>
  720. </Form.Item>
  721. <Form.Item>
  722. <Button type="primary" htmlType="submit">
  723. 搜索
  724. </Button>
  725. </Form.Item>
  726. <Button
  727. type="primary"
  728. onClick={() => setUploadModalVisible(true)}
  729. icon={<UploadOutlined />}
  730. style={{ marginLeft: 16 }}
  731. >
  732. 上传文件
  733. </Button>
  734. </Form>
  735. {/* 文件列表 */}
  736. <Table
  737. columns={columns}
  738. dataSource={fileList}
  739. rowKey="id"
  740. loading={loading}
  741. pagination={{
  742. current: pagination.current,
  743. pageSize: pagination.pageSize,
  744. total: pagination.total,
  745. showSizeChanger: true,
  746. showQuickJumper: true,
  747. showTotal: (total) => `共 ${total} 条记录`
  748. }}
  749. onChange={handleTableChange}
  750. />
  751. </Tabs.TabPane>
  752. <Tabs.TabPane tab="分类管理" key="categories">
  753. <div style={{ marginBottom: 16 }}>
  754. <Button
  755. type="primary"
  756. onClick={() => {
  757. setCurrentCategory(null);
  758. categoryForm.resetFields();
  759. setCategoryModalVisible(true);
  760. }}
  761. >
  762. 添加分类
  763. </Button>
  764. </div>
  765. <Table
  766. columns={categoryColumns}
  767. dataSource={categories}
  768. rowKey="id"
  769. pagination={{ pageSize: 10 }}
  770. />
  771. </Tabs.TabPane>
  772. </Tabs>
  773. </Card>
  774. {/* 上传文件弹窗 */}
  775. <Modal
  776. title="上传文件"
  777. open={uploadModalVisible}
  778. onCancel={() => setUploadModalVisible(false)}
  779. footer={null}
  780. >
  781. <Form form={form} layout="vertical">
  782. <Form.Item
  783. name="file"
  784. label="文件"
  785. rules={[{ required: true, message: '请选择要上传的文件' }]}
  786. >
  787. <Upload {...uploadProps}>
  788. <Button icon={<UploadOutlined />} loading={uploadLoading}>
  789. 选择文件
  790. </Button>
  791. <div style={{ marginTop: 8 }}>
  792. 支持任意类型文件,单个文件不超过10MB
  793. </div>
  794. </Upload>
  795. </Form.Item>
  796. <Form.Item
  797. name="category_id"
  798. label="分类"
  799. >
  800. <Select placeholder="选择分类" allowClear>
  801. {categories.map(category => (
  802. <Select.Option key={category.id} value={category.id}>
  803. {category.name}
  804. </Select.Option>
  805. ))}
  806. </Select>
  807. </Form.Item>
  808. <Form.Item
  809. name="tags"
  810. label="标签"
  811. >
  812. <Input placeholder="多个标签用逗号分隔" />
  813. </Form.Item>
  814. <Form.Item
  815. name="description"
  816. label="描述"
  817. >
  818. <Input.TextArea rows={4} placeholder="文件描述..." />
  819. </Form.Item>
  820. </Form>
  821. </Modal>
  822. {/* 文件详情弹窗 */}
  823. <Modal
  824. title="文件详情"
  825. open={fileDetailModalVisible}
  826. onCancel={() => setFileDetailModalVisible(false)}
  827. footer={[
  828. <Button key="close" onClick={() => setFileDetailModalVisible(false)}>
  829. 关闭
  830. </Button>,
  831. <Button
  832. key="download"
  833. type="primary"
  834. onClick={() => currentFile && downloadFile(currentFile)}
  835. >
  836. 下载
  837. </Button>
  838. ]}
  839. width={700}
  840. >
  841. {currentFile && (
  842. <Descriptions bordered column={2}>
  843. <Descriptions.Item label="系统文件名" span={2}>
  844. {currentFile.file_name}
  845. </Descriptions.Item>
  846. {currentFile.original_filename && (
  847. <Descriptions.Item label="原始文件名" span={2}>
  848. {currentFile.original_filename}
  849. </Descriptions.Item>
  850. )}
  851. <Descriptions.Item label="文件类型">
  852. {currentFile.file_type}
  853. </Descriptions.Item>
  854. <Descriptions.Item label="文件大小">
  855. {currentFile.file_size < 1024 * 1024
  856. ? `${(currentFile.file_size / 1024).toFixed(2)} KB`
  857. : `${(currentFile.file_size / 1024 / 1024).toFixed(2)} MB`}
  858. </Descriptions.Item>
  859. <Descriptions.Item label="上传者">
  860. {currentFile.uploader_name}
  861. </Descriptions.Item>
  862. <Descriptions.Item label="上传时间">
  863. {dayjs(currentFile.created_at).format('YYYY-MM-DD HH:mm:ss')}
  864. </Descriptions.Item>
  865. <Descriptions.Item label="分类">
  866. {currentFile.category_id}
  867. </Descriptions.Item>
  868. <Descriptions.Item label="下载次数">
  869. {currentFile.download_count}
  870. </Descriptions.Item>
  871. <Descriptions.Item label="标签" span={2}>
  872. {currentFile.tags?.split(',').map(tag => (
  873. <Tag key={tag}>{tag}</Tag>
  874. ))}
  875. </Descriptions.Item>
  876. <Descriptions.Item label="描述" span={2}>
  877. {currentFile.description}
  878. </Descriptions.Item>
  879. {currentFile.file_type.startsWith('image/') && (
  880. <Descriptions.Item label="预览" span={2}>
  881. <Image src={currentFile.file_path} style={{ maxWidth: '100%' }} />
  882. </Descriptions.Item>
  883. )}
  884. </Descriptions>
  885. )}
  886. </Modal>
  887. {/* 分类管理弹窗 */}
  888. <Modal
  889. title={currentCategory ? "编辑分类" : "添加分类"}
  890. open={categoryModalVisible}
  891. onOk={handleCategorySave}
  892. onCancel={() => {
  893. setCategoryModalVisible(false);
  894. categoryForm.resetFields();
  895. setCurrentCategory(null);
  896. }}
  897. >
  898. <Form form={categoryForm} layout="vertical">
  899. <Form.Item
  900. name="name"
  901. label="分类名称"
  902. rules={[{ required: true, message: '请输入分类名称' }]}
  903. >
  904. <Input placeholder="请输入分类名称" />
  905. </Form.Item>
  906. <Form.Item
  907. name="code"
  908. label="分类编码"
  909. rules={[{ required: true, message: '请输入分类编码' }]}
  910. >
  911. <Input placeholder="请输入分类编码" />
  912. </Form.Item>
  913. <Form.Item
  914. name="description"
  915. label="分类描述"
  916. >
  917. <Input.TextArea rows={4} placeholder="分类描述..." />
  918. </Form.Item>
  919. </Form>
  920. </Modal>
  921. </div>
  922. );
  923. };