pages_sys.tsx 28 KB

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