pages_sys.tsx 28 KB

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