pages_sys.tsx 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  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 KnowInfoPage = () => {
  325. const [modalVisible, setModalVisible] = useState(false);
  326. const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
  327. const [editingId, setEditingId] = useState<number | null>(null);
  328. const [form] = Form.useForm();
  329. const [isLoading, setIsLoading] = useState(false);
  330. const [searchParams, setSearchParams] = useState({
  331. title: '',
  332. category: '',
  333. page: 1,
  334. limit: 10,
  335. });
  336. // 使用React Query获取知识库文章列表
  337. const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
  338. queryKey: ['articles', searchParams],
  339. queryFn: async () => {
  340. const { title, category, page, limit } = searchParams;
  341. const params = new URLSearchParams();
  342. if (title) params.append('title', title);
  343. if (category) params.append('category', category);
  344. params.append('page', String(page));
  345. params.append('limit', String(limit));
  346. const response = await fetch(`/api/know-info?${params.toString()}`, {
  347. headers: {
  348. 'Authorization': `Bearer ${localStorage.getItem('token')}`,
  349. },
  350. });
  351. if (!response.ok) {
  352. throw new Error('获取知识库文章列表失败');
  353. }
  354. return await response.json();
  355. }
  356. });
  357. const articles = articlesData?.data || [];
  358. const pagination = articlesData?.pagination || { current: 1, pageSize: 10, total: 0 };
  359. // 获取单个知识库文章
  360. const fetchArticle = async (id: number) => {
  361. try {
  362. const response = await fetch(`/api/know-info/${id}`, {
  363. headers: {
  364. 'Authorization': `Bearer ${localStorage.getItem('token')}`,
  365. },
  366. });
  367. if (!response.ok) {
  368. throw new Error('获取知识库文章详情失败');
  369. }
  370. return await response.json();
  371. } catch (error) {
  372. message.error('获取知识库文章详情失败');
  373. return null;
  374. }
  375. };
  376. // 处理表单提交
  377. const handleSubmit = async (values: Partial<KnowInfo>) => {
  378. setIsLoading(true);
  379. try {
  380. const url = formMode === 'create'
  381. ? '/api/know-info'
  382. : `/api/know-info/${editingId}`;
  383. const method = formMode === 'create' ? 'POST' : 'PUT';
  384. const response = await fetch(url, {
  385. method,
  386. headers: {
  387. 'Content-Type': 'application/json',
  388. 'Authorization': `Bearer ${localStorage.getItem('token')}`,
  389. },
  390. body: JSON.stringify(values),
  391. });
  392. if (!response.ok) {
  393. throw new Error(formMode === 'create' ? '创建知识库文章失败' : '更新知识库文章失败');
  394. }
  395. message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
  396. setModalVisible(false);
  397. form.resetFields();
  398. refetch();
  399. } catch (error) {
  400. message.error((error as Error).message);
  401. } finally {
  402. setIsLoading(false);
  403. }
  404. };
  405. // 处理编辑
  406. const handleEdit = async (id: number) => {
  407. const article = await fetchArticle(id);
  408. if (article) {
  409. setFormMode('edit');
  410. setEditingId(id);
  411. form.setFieldsValue(article);
  412. setModalVisible(true);
  413. }
  414. };
  415. // 处理删除
  416. const handleDelete = async (id: number) => {
  417. try {
  418. const response = await fetch(`/api/know-info/${id}`, {
  419. method: 'DELETE',
  420. headers: {
  421. 'Authorization': `Bearer ${localStorage.getItem('token')}`,
  422. },
  423. });
  424. if (!response.ok) {
  425. throw new Error('删除知识库文章失败');
  426. }
  427. message.success('删除知识库文章成功');
  428. refetch();
  429. } catch (error) {
  430. message.error((error as Error).message);
  431. }
  432. };
  433. // 处理搜索
  434. const handleSearch = (values: any) => {
  435. setSearchParams(prev => ({
  436. ...prev,
  437. title: values.title || '',
  438. category: values.category || '',
  439. page: 1,
  440. }));
  441. };
  442. // 处理分页
  443. const handlePageChange = (page: number, pageSize?: number) => {
  444. setSearchParams(prev => ({
  445. ...prev,
  446. page,
  447. limit: pageSize || prev.limit,
  448. }));
  449. };
  450. // 处理添加
  451. const handleAdd = () => {
  452. setFormMode('create');
  453. setEditingId(null);
  454. form.resetFields();
  455. setModalVisible(true);
  456. };
  457. // 审核状态映射
  458. const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
  459. // 表格列定义
  460. const columns = [
  461. {
  462. title: 'ID',
  463. dataIndex: 'id',
  464. key: 'id',
  465. width: 80,
  466. },
  467. {
  468. title: '标题',
  469. dataIndex: 'title',
  470. key: 'title',
  471. },
  472. {
  473. title: '分类',
  474. dataIndex: 'category',
  475. key: 'category',
  476. },
  477. {
  478. title: '标签',
  479. dataIndex: 'tags',
  480. key: 'tags',
  481. render: (tags: string) => tags ? tags.split(',').map(tag => (
  482. <Tag key={tag}>{tag}</Tag>
  483. )) : null,
  484. },
  485. {
  486. title: '作者',
  487. dataIndex: 'author',
  488. key: 'author',
  489. },
  490. {
  491. title: '审核状态',
  492. dataIndex: 'audit_status',
  493. key: 'audit_status',
  494. render: (status: AuditStatus) => {
  495. let color = '';
  496. let text = '';
  497. switch(status) {
  498. case AuditStatus.PENDING:
  499. color = 'orange';
  500. text = '待审核';
  501. break;
  502. case AuditStatus.APPROVED:
  503. color = 'green';
  504. text = '已通过';
  505. break;
  506. case AuditStatus.REJECTED:
  507. color = 'red';
  508. text = '已拒绝';
  509. break;
  510. default:
  511. color = 'default';
  512. text = '未知';
  513. }
  514. return <Tag color={color}>{text}</Tag>;
  515. },
  516. },
  517. {
  518. title: '创建时间',
  519. dataIndex: 'created_at',
  520. key: 'created_at',
  521. render: (date: string) => new Date(date).toLocaleString(),
  522. },
  523. {
  524. title: '操作',
  525. key: 'action',
  526. render: (_: any, record: KnowInfo) => (
  527. <Space size="middle">
  528. <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
  529. <Popconfirm
  530. title="确定要删除这篇文章吗?"
  531. onConfirm={() => handleDelete(record.id)}
  532. okText="确定"
  533. cancelText="取消"
  534. >
  535. <Button type="link" danger>删除</Button>
  536. </Popconfirm>
  537. </Space>
  538. ),
  539. },
  540. ];
  541. return (
  542. <div>
  543. <Card title="知识库管理" className="mb-4">
  544. <Form
  545. layout="inline"
  546. onFinish={handleSearch}
  547. style={{ marginBottom: '16px' }}
  548. >
  549. <Form.Item name="title" label="标题">
  550. <Input placeholder="请输入文章标题" />
  551. </Form.Item>
  552. <Form.Item name="category" label="分类">
  553. <Input placeholder="请输入文章分类" />
  554. </Form.Item>
  555. <Form.Item>
  556. <Space>
  557. <Button type="primary" htmlType="submit">
  558. 搜索
  559. </Button>
  560. <Button onClick={() => {
  561. setSearchParams({
  562. title: '',
  563. category: '',
  564. page: 1,
  565. limit: 10,
  566. });
  567. }}>
  568. 重置
  569. </Button>
  570. <Button type="primary" onClick={handleAdd}>
  571. 添加文章
  572. </Button>
  573. </Space>
  574. </Form.Item>
  575. </Form>
  576. <Table
  577. columns={columns}
  578. dataSource={articles}
  579. rowKey="id"
  580. loading={isListLoading}
  581. pagination={{
  582. current: pagination.current,
  583. pageSize: pagination.pageSize,
  584. total: pagination.total,
  585. onChange: handlePageChange,
  586. showSizeChanger: true,
  587. }}
  588. />
  589. </Card>
  590. <Modal
  591. title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
  592. open={modalVisible}
  593. onOk={() => form.submit()}
  594. onCancel={() => setModalVisible(false)}
  595. width={800}
  596. >
  597. <Form
  598. form={form}
  599. layout="vertical"
  600. onFinish={handleSubmit}
  601. initialValues={{
  602. audit_status: AuditStatus.PENDING,
  603. }}
  604. >
  605. <Row gutter={16}>
  606. <Col span={12}>
  607. <Form.Item
  608. name="title"
  609. label="文章标题"
  610. rules={[{ required: true, message: '请输入文章标题' }]}
  611. >
  612. <Input placeholder="请输入文章标题" />
  613. </Form.Item>
  614. </Col>
  615. <Col span={12}>
  616. <Form.Item
  617. name="category"
  618. label="文章分类"
  619. >
  620. <Input placeholder="请输入文章分类" />
  621. </Form.Item>
  622. </Col>
  623. </Row>
  624. <Form.Item
  625. name="tags"
  626. label="文章标签"
  627. help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
  628. >
  629. <Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
  630. </Form.Item>
  631. <Form.Item
  632. name="content"
  633. label="文章内容"
  634. rules={[{ required: true, message: '请输入文章内容' }]}
  635. >
  636. <Input.TextArea rows={15} placeholder="请输入文章内容,支持Markdown格式" />
  637. </Form.Item>
  638. <Row gutter={16}>
  639. <Col span={12}>
  640. <Form.Item
  641. name="author"
  642. label="文章作者"
  643. >
  644. <Input placeholder="请输入文章作者" />
  645. </Form.Item>
  646. </Col>
  647. <Col span={12}>
  648. <Form.Item
  649. name="cover_url"
  650. label="封面图片URL"
  651. >
  652. <Input placeholder="请输入封面图片URL" />
  653. </Form.Item>
  654. </Col>
  655. </Row>
  656. <Form.Item
  657. name="audit_status"
  658. label="审核状态"
  659. >
  660. <Select options={auditStatusOptions} />
  661. </Form.Item>
  662. <Form.Item>
  663. <Space>
  664. <Button type="primary" htmlType="submit" loading={isLoading}>
  665. {formMode === 'create' ? '创建' : '保存'}
  666. </Button>
  667. <Button onClick={() => setModalVisible(false)}>取消</Button>
  668. </Space>
  669. </Form.Item>
  670. </Form>
  671. </Modal>
  672. </div>
  673. );
  674. };
  675. // 文件库管理页面
  676. export const FileLibraryPage = () => {
  677. const [loading, setLoading] = useState(false);
  678. const [fileList, setFileList] = useState<FileLibrary[]>([]);
  679. const [categories, setCategories] = useState<FileCategory[]>([]);
  680. const [pagination, setPagination] = useState({
  681. current: 1,
  682. pageSize: 10,
  683. total: 0
  684. });
  685. const [searchParams, setSearchParams] = useState({
  686. fileType: '',
  687. keyword: ''
  688. });
  689. const [uploadModalVisible, setUploadModalVisible] = useState(false);
  690. const [fileDetailModalVisible, setFileDetailModalVisible] = useState(false);
  691. const [currentFile, setCurrentFile] = useState<FileLibrary | null>(null);
  692. const [uploadLoading, setUploadLoading] = useState(false);
  693. const [form] = Form.useForm();
  694. const [categoryForm] = Form.useForm();
  695. const [categoryModalVisible, setCategoryModalVisible] = useState(false);
  696. const [currentCategory, setCurrentCategory] = useState<FileCategory | null>(null);
  697. // 获取文件图标
  698. const getFileIcon = (fileType: string) => {
  699. if (fileType.includes('image')) {
  700. return <FileImageOutlined style={{ fontSize: '24px', color: '#1890ff' }} />;
  701. } else if (fileType.includes('pdf')) {
  702. return <FilePdfOutlined style={{ fontSize: '24px', color: '#ff4d4f' }} />;
  703. } else if (fileType.includes('excel') || fileType.includes('sheet')) {
  704. return <FileExcelOutlined style={{ fontSize: '24px', color: '#52c41a' }} />;
  705. } else if (fileType.includes('word') || fileType.includes('document')) {
  706. return <FileWordOutlined style={{ fontSize: '24px', color: '#2f54eb' }} />;
  707. } else {
  708. return <FileOutlined style={{ fontSize: '24px', color: '#faad14' }} />;
  709. }
  710. };
  711. // 加载文件列表
  712. const fetchFileList = async () => {
  713. setLoading(true);
  714. try {
  715. const response = await FileAPI.getFileList({
  716. page: pagination.current,
  717. pageSize: pagination.pageSize,
  718. ...searchParams
  719. });
  720. if (response && response.data) {
  721. setFileList(response.data.list);
  722. setPagination({
  723. ...pagination,
  724. total: response.data.pagination.total
  725. });
  726. }
  727. } catch (error) {
  728. console.error('获取文件列表失败:', error);
  729. message.error('获取文件列表失败');
  730. } finally {
  731. setLoading(false);
  732. }
  733. };
  734. // 加载文件分类
  735. const fetchCategories = async () => {
  736. try {
  737. const response = await FileAPI.getCategories();
  738. if (response && response.data) {
  739. setCategories(response.data);
  740. }
  741. } catch (error) {
  742. console.error('获取文件分类失败:', error);
  743. message.error('获取文件分类失败');
  744. }
  745. };
  746. // 组件挂载时加载数据
  747. useEffect(() => {
  748. fetchFileList();
  749. fetchCategories();
  750. }, [pagination.current, pagination.pageSize, searchParams]);
  751. // 上传文件
  752. const handleUpload = async (file: File) => {
  753. try {
  754. setUploadLoading(true);
  755. // 1. 获取上传策略
  756. const policyResponse = await FileAPI.getUploadPolicy(file.name);
  757. if (!policyResponse || !policyResponse.data) {
  758. throw new Error('获取上传策略失败');
  759. }
  760. const policy = policyResponse.data;
  761. // 2. 上传文件至 MinIO
  762. const uploadProgress = {
  763. progress: 0,
  764. completed: false,
  765. error: null as Error | null
  766. };
  767. const callbacks = {
  768. onProgress: (event: { progress: number }) => {
  769. uploadProgress.progress = event.progress;
  770. },
  771. onComplete: () => {
  772. uploadProgress.completed = true;
  773. },
  774. onError: (err: Error) => {
  775. uploadProgress.error = err;
  776. }
  777. };
  778. const uploadUrl = window.CONFIG?.OSS_TYPE === OssType.MINIO ? await uploadMinIOWithPolicy(
  779. policy as MinioUploadPolicy,
  780. file,
  781. file.name,
  782. callbacks
  783. ) : await uploadOSSWithPolicy(
  784. policy as OSSUploadPolicy,
  785. file,
  786. file.name,
  787. callbacks
  788. );
  789. if (!uploadUrl || uploadProgress.error) {
  790. throw uploadProgress.error || new Error('上传文件失败');
  791. }
  792. // 3. 保存文件信息到文件库
  793. const fileValues = form.getFieldsValue();
  794. const fileData = {
  795. file_name: file.name,
  796. file_path: uploadUrl,
  797. file_type: file.type,
  798. file_size: file.size,
  799. category_id: fileValues.category_id ? Number(fileValues.category_id) : undefined,
  800. tags: fileValues.tags,
  801. description: fileValues.description
  802. };
  803. const saveResponse = await FileAPI.saveFileInfo(fileData);
  804. if (saveResponse && saveResponse.data) {
  805. message.success('文件上传成功');
  806. setUploadModalVisible(false);
  807. form.resetFields();
  808. fetchFileList();
  809. }
  810. } catch (error) {
  811. console.error('上传文件失败:', error);
  812. message.error('上传文件失败: ' + (error instanceof Error ? error.message : '未知错误'));
  813. } finally {
  814. setUploadLoading(false);
  815. }
  816. };
  817. // 处理文件上传
  818. const uploadProps = {
  819. name: 'file',
  820. multiple: false,
  821. showUploadList: false,
  822. beforeUpload: (file: File) => {
  823. const isLt10M = file.size / 1024 / 1024 < 10;
  824. if (!isLt10M) {
  825. message.error('文件大小不能超过10MB!');
  826. return false;
  827. }
  828. handleUpload(file);
  829. return false;
  830. }
  831. };
  832. // 查看文件详情
  833. const viewFileDetail = async (id: number) => {
  834. try {
  835. const response = await FileAPI.getFileInfo(id);
  836. if (response && response.data) {
  837. setCurrentFile(response.data);
  838. setFileDetailModalVisible(true);
  839. }
  840. } catch (error) {
  841. console.error('获取文件详情失败:', error);
  842. message.error('获取文件详情失败');
  843. }
  844. };
  845. // 下载文件
  846. const downloadFile = async (file: FileLibrary) => {
  847. try {
  848. // 更新下载计数
  849. await FileAPI.updateDownloadCount(file.id);
  850. // 创建一个暂时的a标签用于下载
  851. const link = document.createElement('a');
  852. link.href = file.file_path;
  853. link.target = '_blank';
  854. link.download = file.file_name;
  855. document.body.appendChild(link);
  856. link.click();
  857. document.body.removeChild(link);
  858. message.success('下载已开始');
  859. } catch (error) {
  860. console.error('下载文件失败:', error);
  861. message.error('下载文件失败');
  862. }
  863. };
  864. // 删除文件
  865. const handleDeleteFile = async (id: number) => {
  866. try {
  867. await FileAPI.deleteFile(id);
  868. message.success('文件删除成功');
  869. fetchFileList();
  870. } catch (error) {
  871. console.error('删除文件失败:', error);
  872. message.error('删除文件失败');
  873. }
  874. };
  875. // 处理搜索
  876. const handleSearch = (values: any) => {
  877. setSearchParams(values);
  878. setPagination({
  879. ...pagination,
  880. current: 1
  881. });
  882. };
  883. // 处理表格分页变化
  884. const handleTableChange = (newPagination: any) => {
  885. setPagination({
  886. ...pagination,
  887. current: newPagination.current,
  888. pageSize: newPagination.pageSize
  889. });
  890. };
  891. // 添加或更新分类
  892. const handleCategorySave = async () => {
  893. try {
  894. const values = await categoryForm.validateFields();
  895. if (currentCategory) {
  896. // 更新分类
  897. await FileAPI.updateCategory(currentCategory.id, values);
  898. message.success('分类更新成功');
  899. } else {
  900. // 创建分类
  901. await FileAPI.createCategory(values);
  902. message.success('分类创建成功');
  903. }
  904. setCategoryModalVisible(false);
  905. categoryForm.resetFields();
  906. setCurrentCategory(null);
  907. fetchCategories();
  908. } catch (error) {
  909. console.error('保存分类失败:', error);
  910. message.error('保存分类失败');
  911. }
  912. };
  913. // 编辑分类
  914. const handleEditCategory = (category: FileCategory) => {
  915. setCurrentCategory(category);
  916. categoryForm.setFieldsValue(category);
  917. setCategoryModalVisible(true);
  918. };
  919. // 删除分类
  920. const handleDeleteCategory = async (id: number) => {
  921. try {
  922. await FileAPI.deleteCategory(id);
  923. message.success('分类删除成功');
  924. fetchCategories();
  925. } catch (error) {
  926. console.error('删除分类失败:', error);
  927. message.error('删除分类失败');
  928. }
  929. };
  930. // 文件表格列配置
  931. const columns = [
  932. {
  933. title: '文件名',
  934. key: 'file_name',
  935. render: (text: string, record: FileLibrary) => (
  936. <Space>
  937. {getFileIcon(record.file_type)}
  938. <a onClick={() => viewFileDetail(record.id)}>
  939. {record.original_filename || record.file_name}
  940. </a>
  941. </Space>
  942. )
  943. },
  944. {
  945. title: '文件类型',
  946. dataIndex: 'file_type',
  947. key: 'file_type',
  948. width: 120,
  949. render: (text: string) => text.split('/').pop()
  950. },
  951. {
  952. title: '大小',
  953. dataIndex: 'file_size',
  954. key: 'file_size',
  955. width: 100,
  956. render: (size: number) => {
  957. if (size < 1024) {
  958. return `${size} B`;
  959. } else if (size < 1024 * 1024) {
  960. return `${(size / 1024).toFixed(2)} KB`;
  961. } else {
  962. return `${(size / 1024 / 1024).toFixed(2)} MB`;
  963. }
  964. }
  965. },
  966. {
  967. title: '分类',
  968. dataIndex: 'category_id',
  969. key: 'category_id',
  970. width: 120
  971. },
  972. {
  973. title: '上传者',
  974. dataIndex: 'uploader_name',
  975. key: 'uploader_name',
  976. width: 120
  977. },
  978. {
  979. title: '下载次数',
  980. dataIndex: 'download_count',
  981. key: 'download_count',
  982. width: 120
  983. },
  984. {
  985. title: '上传时间',
  986. dataIndex: 'created_at',
  987. key: 'created_at',
  988. width: 180,
  989. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  990. },
  991. {
  992. title: '操作',
  993. key: 'action',
  994. width: 180,
  995. render: (_: any, record: FileLibrary) => (
  996. <Space size="middle">
  997. <Button type="link" onClick={() => downloadFile(record)}>
  998. 下载
  999. </Button>
  1000. <Popconfirm
  1001. title="确定要删除这个文件吗?"
  1002. onConfirm={() => handleDeleteFile(record.id)}
  1003. okText="确定"
  1004. cancelText="取消"
  1005. >
  1006. <Button type="link" danger>删除</Button>
  1007. </Popconfirm>
  1008. </Space>
  1009. )
  1010. }
  1011. ];
  1012. // 分类表格列配置
  1013. const categoryColumns = [
  1014. {
  1015. title: '分类名称',
  1016. dataIndex: 'name',
  1017. key: 'name'
  1018. },
  1019. {
  1020. title: '分类编码',
  1021. dataIndex: 'code',
  1022. key: 'code'
  1023. },
  1024. {
  1025. title: '描述',
  1026. dataIndex: 'description',
  1027. key: 'description'
  1028. },
  1029. {
  1030. title: '操作',
  1031. key: 'action',
  1032. render: (_: any, record: FileCategory) => (
  1033. <Space size="middle">
  1034. <Button type="link" onClick={() => handleEditCategory(record)}>
  1035. 编辑
  1036. </Button>
  1037. <Popconfirm
  1038. title="确定要删除这个分类吗?"
  1039. onConfirm={() => handleDeleteCategory(record.id)}
  1040. okText="确定"
  1041. cancelText="取消"
  1042. >
  1043. <Button type="link" danger>删除</Button>
  1044. </Popconfirm>
  1045. </Space>
  1046. )
  1047. }
  1048. ];
  1049. return (
  1050. <div>
  1051. <Title level={2}>文件库管理</Title>
  1052. <Card>
  1053. <Tabs defaultActiveKey="files">
  1054. <Tabs.TabPane tab="文件管理" key="files">
  1055. {/* 搜索表单 */}
  1056. <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
  1057. <Form.Item name="keyword" label="关键词">
  1058. <Input placeholder="文件名/描述/标签" allowClear />
  1059. </Form.Item>
  1060. <Form.Item name="category_id" label="分类">
  1061. <Select placeholder="选择分类" allowClear style={{ width: 160 }}>
  1062. {categories.map(category => (
  1063. <Select.Option key={category.id} value={category.id}>
  1064. {category.name}
  1065. </Select.Option>
  1066. ))}
  1067. </Select>
  1068. </Form.Item>
  1069. <Form.Item name="fileType" label="文件类型">
  1070. <Select placeholder="选择文件类型" allowClear style={{ width: 160 }}>
  1071. <Select.Option value="image">图片</Select.Option>
  1072. <Select.Option value="document">文档</Select.Option>
  1073. <Select.Option value="application">应用</Select.Option>
  1074. <Select.Option value="audio">音频</Select.Option>
  1075. <Select.Option value="video">视频</Select.Option>
  1076. </Select>
  1077. </Form.Item>
  1078. <Form.Item>
  1079. <Button type="primary" htmlType="submit">
  1080. 搜索
  1081. </Button>
  1082. </Form.Item>
  1083. <Button
  1084. type="primary"
  1085. onClick={() => setUploadModalVisible(true)}
  1086. icon={<UploadOutlined />}
  1087. style={{ marginLeft: 16 }}
  1088. >
  1089. 上传文件
  1090. </Button>
  1091. </Form>
  1092. {/* 文件列表 */}
  1093. <Table
  1094. columns={columns}
  1095. dataSource={fileList}
  1096. rowKey="id"
  1097. loading={loading}
  1098. pagination={{
  1099. current: pagination.current,
  1100. pageSize: pagination.pageSize,
  1101. total: pagination.total,
  1102. showSizeChanger: true,
  1103. showQuickJumper: true,
  1104. showTotal: (total) => `共 ${total} 条记录`
  1105. }}
  1106. onChange={handleTableChange}
  1107. />
  1108. </Tabs.TabPane>
  1109. <Tabs.TabPane tab="分类管理" key="categories">
  1110. <div style={{ marginBottom: 16 }}>
  1111. <Button
  1112. type="primary"
  1113. onClick={() => {
  1114. setCurrentCategory(null);
  1115. categoryForm.resetFields();
  1116. setCategoryModalVisible(true);
  1117. }}
  1118. >
  1119. 添加分类
  1120. </Button>
  1121. </div>
  1122. <Table
  1123. columns={categoryColumns}
  1124. dataSource={categories}
  1125. rowKey="id"
  1126. pagination={{ pageSize: 10 }}
  1127. />
  1128. </Tabs.TabPane>
  1129. </Tabs>
  1130. </Card>
  1131. {/* 上传文件弹窗 */}
  1132. <Modal
  1133. title="上传文件"
  1134. open={uploadModalVisible}
  1135. onCancel={() => setUploadModalVisible(false)}
  1136. footer={null}
  1137. >
  1138. <Form form={form} layout="vertical">
  1139. <Form.Item
  1140. name="file"
  1141. label="文件"
  1142. rules={[{ required: true, message: '请选择要上传的文件' }]}
  1143. >
  1144. <Upload {...uploadProps}>
  1145. <Button icon={<UploadOutlined />} loading={uploadLoading}>
  1146. 选择文件
  1147. </Button>
  1148. <div style={{ marginTop: 8 }}>
  1149. 支持任意类型文件,单个文件不超过10MB
  1150. </div>
  1151. </Upload>
  1152. </Form.Item>
  1153. <Form.Item
  1154. name="category_id"
  1155. label="分类"
  1156. >
  1157. <Select placeholder="选择分类" allowClear>
  1158. {categories.map(category => (
  1159. <Select.Option key={category.id} value={category.id}>
  1160. {category.name}
  1161. </Select.Option>
  1162. ))}
  1163. </Select>
  1164. </Form.Item>
  1165. <Form.Item
  1166. name="tags"
  1167. label="标签"
  1168. >
  1169. <Input placeholder="多个标签用逗号分隔" />
  1170. </Form.Item>
  1171. <Form.Item
  1172. name="description"
  1173. label="描述"
  1174. >
  1175. <Input.TextArea rows={4} placeholder="文件描述..." />
  1176. </Form.Item>
  1177. </Form>
  1178. </Modal>
  1179. {/* 文件详情弹窗 */}
  1180. <Modal
  1181. title="文件详情"
  1182. open={fileDetailModalVisible}
  1183. onCancel={() => setFileDetailModalVisible(false)}
  1184. footer={[
  1185. <Button key="close" onClick={() => setFileDetailModalVisible(false)}>
  1186. 关闭
  1187. </Button>,
  1188. <Button
  1189. key="download"
  1190. type="primary"
  1191. onClick={() => currentFile && downloadFile(currentFile)}
  1192. >
  1193. 下载
  1194. </Button>
  1195. ]}
  1196. width={700}
  1197. >
  1198. {currentFile && (
  1199. <Descriptions bordered column={2}>
  1200. <Descriptions.Item label="系统文件名" span={2}>
  1201. {currentFile.file_name}
  1202. </Descriptions.Item>
  1203. {currentFile.original_filename && (
  1204. <Descriptions.Item label="原始文件名" span={2}>
  1205. {currentFile.original_filename}
  1206. </Descriptions.Item>
  1207. )}
  1208. <Descriptions.Item label="文件类型">
  1209. {currentFile.file_type}
  1210. </Descriptions.Item>
  1211. <Descriptions.Item label="文件大小">
  1212. {currentFile.file_size < 1024 * 1024
  1213. ? `${(currentFile.file_size / 1024).toFixed(2)} KB`
  1214. : `${(currentFile.file_size / 1024 / 1024).toFixed(2)} MB`}
  1215. </Descriptions.Item>
  1216. <Descriptions.Item label="上传者">
  1217. {currentFile.uploader_name}
  1218. </Descriptions.Item>
  1219. <Descriptions.Item label="上传时间">
  1220. {dayjs(currentFile.created_at).format('YYYY-MM-DD HH:mm:ss')}
  1221. </Descriptions.Item>
  1222. <Descriptions.Item label="分类">
  1223. {currentFile.category_id}
  1224. </Descriptions.Item>
  1225. <Descriptions.Item label="下载次数">
  1226. {currentFile.download_count}
  1227. </Descriptions.Item>
  1228. <Descriptions.Item label="标签" span={2}>
  1229. {currentFile.tags?.split(',').map(tag => (
  1230. <Tag key={tag}>{tag}</Tag>
  1231. ))}
  1232. </Descriptions.Item>
  1233. <Descriptions.Item label="描述" span={2}>
  1234. {currentFile.description}
  1235. </Descriptions.Item>
  1236. {currentFile.file_type.startsWith('image/') && (
  1237. <Descriptions.Item label="预览" span={2}>
  1238. <Image src={currentFile.file_path} style={{ maxWidth: '100%' }} />
  1239. </Descriptions.Item>
  1240. )}
  1241. </Descriptions>
  1242. )}
  1243. </Modal>
  1244. {/* 分类管理弹窗 */}
  1245. <Modal
  1246. title={currentCategory ? "编辑分类" : "添加分类"}
  1247. open={categoryModalVisible}
  1248. onOk={handleCategorySave}
  1249. onCancel={() => {
  1250. setCategoryModalVisible(false);
  1251. categoryForm.resetFields();
  1252. setCurrentCategory(null);
  1253. }}
  1254. >
  1255. <Form form={categoryForm} layout="vertical">
  1256. <Form.Item
  1257. name="name"
  1258. label="分类名称"
  1259. rules={[{ required: true, message: '请输入分类名称' }]}
  1260. >
  1261. <Input placeholder="请输入分类名称" />
  1262. </Form.Item>
  1263. <Form.Item
  1264. name="code"
  1265. label="分类编码"
  1266. rules={[{ required: true, message: '请输入分类编码' }]}
  1267. >
  1268. <Input placeholder="请输入分类编码" />
  1269. </Form.Item>
  1270. <Form.Item
  1271. name="description"
  1272. label="分类描述"
  1273. >
  1274. <Input.TextArea rows={4} placeholder="分类描述..." />
  1275. </Form.Item>
  1276. </Form>
  1277. </Modal>
  1278. </div>
  1279. );
  1280. };