|
@@ -0,0 +1,440 @@
|
|
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
|
|
+import {
|
|
|
|
|
+ Layout, Menu, Button, Table, Space,
|
|
|
|
|
+ Form, Input, Select, message, Modal,
|
|
|
|
|
+ Card, Spin, Row, Col, Breadcrumb, Avatar,
|
|
|
|
|
+ Dropdown, ConfigProvider, theme, Typography,
|
|
|
|
|
+ Switch, Badge, Image, Upload, Divider, Descriptions,
|
|
|
|
|
+ Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
|
|
|
|
|
+} from 'antd';
|
|
|
|
|
+import {
|
|
|
|
|
+ UploadOutlined,
|
|
|
|
|
+ FileImageOutlined,
|
|
|
|
|
+ FileExcelOutlined,
|
|
|
|
|
+ FileWordOutlined,
|
|
|
|
|
+ FilePdfOutlined,
|
|
|
|
|
+ FileOutlined,
|
|
|
|
|
+} from '@ant-design/icons';
|
|
|
|
|
+import {
|
|
|
|
|
+ useQuery,
|
|
|
|
|
+} from '@tanstack/react-query';
|
|
|
|
|
+import dayjs from 'dayjs';
|
|
|
|
|
+import weekday from 'dayjs/plugin/weekday';
|
|
|
|
|
+import localeData from 'dayjs/plugin/localeData';
|
|
|
|
|
+import 'dayjs/locale/zh-cn';
|
|
|
|
|
+import type {
|
|
|
|
|
+ FileLibrary, FileCategory, KnowInfo
|
|
|
|
|
+} from '../share/types.ts';
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ AuditStatus,AuditStatusNameMap,
|
|
|
|
|
+ OssType,
|
|
|
|
|
+} from '../share/types.ts';
|
|
|
|
|
+
|
|
|
|
|
+import { getEnumOptions } from './utils.ts';
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ FileAPI,
|
|
|
|
|
+ UserAPI,
|
|
|
|
|
+} from './api.ts';
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 配置 dayjs 插件
|
|
|
|
|
+dayjs.extend(weekday);
|
|
|
|
|
+dayjs.extend(localeData);
|
|
|
|
|
+
|
|
|
|
|
+// 设置 dayjs 语言
|
|
|
|
|
+dayjs.locale('zh-cn');
|
|
|
|
|
+
|
|
|
|
|
+const { Title } = Typography;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 知识库管理页面组件
|
|
|
|
|
+export const KnowInfoPage = () => {
|
|
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
+ const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
|
|
|
|
|
+ const [editingId, setEditingId] = useState<number | null>(null);
|
|
|
|
|
+ const [form] = Form.useForm();
|
|
|
|
|
+ const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
+ const [searchParams, setSearchParams] = useState({
|
|
|
|
|
+ title: '',
|
|
|
|
|
+ category: '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 10,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 使用React Query获取知识库文章列表
|
|
|
|
|
+ const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
|
|
|
|
|
+ queryKey: ['articles', searchParams],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const { title, category, page, limit } = searchParams;
|
|
|
|
|
+ const params = new URLSearchParams();
|
|
|
|
|
+
|
|
|
|
|
+ if (title) params.append('title', title);
|
|
|
|
|
+ if (category) params.append('category', category);
|
|
|
|
|
+ params.append('page', String(page));
|
|
|
|
|
+ params.append('limit', String(limit));
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(`/api/know-info?${params.toString()}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('获取知识库文章列表失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return await response.json();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const articles = articlesData?.data || [];
|
|
|
|
|
+ const pagination = articlesData?.pagination || { current: 1, pageSize: 10, total: 0 };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取单个知识库文章
|
|
|
|
|
+ const fetchArticle = async (id: number) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/know-info/${id}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('获取知识库文章详情失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return await response.json();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取知识库文章详情失败');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理表单提交
|
|
|
|
|
+ const handleSubmit = async (values: Partial<KnowInfo>) => {
|
|
|
|
|
+ setIsLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = formMode === 'create'
|
|
|
|
|
+ ? '/api/know-info'
|
|
|
|
|
+ : `/api/know-info/${editingId}`;
|
|
|
|
|
+
|
|
|
|
|
+ const method = formMode === 'create' ? 'POST' : 'PUT';
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ method,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify(values),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(formMode === 'create' ? '创建知识库文章失败' : '更新知识库文章失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
|
|
|
|
|
+ setModalVisible(false);
|
|
|
|
|
+ form.resetFields();
|
|
|
|
|
+ refetch();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error((error as Error).message);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理编辑
|
|
|
|
|
+ const handleEdit = async (id: number) => {
|
|
|
|
|
+ const article = await fetchArticle(id);
|
|
|
|
|
+
|
|
|
|
|
+ if (article) {
|
|
|
|
|
+ setFormMode('edit');
|
|
|
|
|
+ setEditingId(id);
|
|
|
|
|
+ form.setFieldsValue(article);
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理删除
|
|
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/know-info/${id}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error('删除知识库文章失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message.success('删除知识库文章成功');
|
|
|
|
|
+ refetch();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error((error as Error).message);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理搜索
|
|
|
|
|
+ const handleSearch = (values: any) => {
|
|
|
|
|
+ setSearchParams(prev => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ title: values.title || '',
|
|
|
|
|
+ category: values.category || '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ }));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理分页
|
|
|
|
|
+ const handlePageChange = (page: number, pageSize?: number) => {
|
|
|
|
|
+ setSearchParams(prev => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit: pageSize || prev.limit,
|
|
|
|
|
+ }));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理添加
|
|
|
|
|
+ const handleAdd = () => {
|
|
|
|
|
+ setFormMode('create');
|
|
|
|
|
+ setEditingId(null);
|
|
|
|
|
+ form.resetFields();
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 审核状态映射
|
|
|
|
|
+ const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
|
|
|
|
|
+
|
|
|
|
|
+ // 表格列定义
|
|
|
|
|
+ const columns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'ID',
|
|
|
|
|
+ dataIndex: 'id',
|
|
|
|
|
+ key: 'id',
|
|
|
|
|
+ width: 80,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '标题',
|
|
|
|
|
+ dataIndex: 'title',
|
|
|
|
|
+ key: 'title',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '分类',
|
|
|
|
|
+ dataIndex: 'category',
|
|
|
|
|
+ key: 'category',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '标签',
|
|
|
|
|
+ dataIndex: 'tags',
|
|
|
|
|
+ key: 'tags',
|
|
|
|
|
+ render: (tags: string) => tags ? tags.split(',').map(tag => (
|
|
|
|
|
+ <Tag key={tag}>{tag}</Tag>
|
|
|
|
|
+ )) : null,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '作者',
|
|
|
|
|
+ dataIndex: 'author',
|
|
|
|
|
+ key: 'author',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '审核状态',
|
|
|
|
|
+ dataIndex: 'audit_status',
|
|
|
|
|
+ key: 'audit_status',
|
|
|
|
|
+ render: (status: AuditStatus) => {
|
|
|
|
|
+ let color = '';
|
|
|
|
|
+ let text = '';
|
|
|
|
|
+
|
|
|
|
|
+ switch(status) {
|
|
|
|
|
+ case AuditStatus.PENDING:
|
|
|
|
|
+ color = 'orange';
|
|
|
|
|
+ text = '待审核';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case AuditStatus.APPROVED:
|
|
|
|
|
+ color = 'green';
|
|
|
|
|
+ text = '已通过';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case AuditStatus.REJECTED:
|
|
|
|
|
+ color = 'red';
|
|
|
|
|
+ text = '已拒绝';
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ color = 'default';
|
|
|
|
|
+ text = '未知';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <Tag color={color}>{text}</Tag>;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建时间',
|
|
|
|
|
+ dataIndex: 'created_at',
|
|
|
|
|
+ key: 'created_at',
|
|
|
|
|
+ render: (date: string) => new Date(date).toLocaleString(),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ key: 'action',
|
|
|
|
|
+ render: (_: any, record: KnowInfo) => (
|
|
|
|
|
+ <Space size="middle">
|
|
|
|
|
+ <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
|
|
|
|
|
+ <Popconfirm
|
|
|
|
|
+ title="确定要删除这篇文章吗?"
|
|
|
|
|
+ onConfirm={() => handleDelete(record.id)}
|
|
|
|
|
+ okText="确定"
|
|
|
|
|
+ cancelText="取消"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button type="link" danger>删除</Button>
|
|
|
|
|
+ </Popconfirm>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Card title="知识库管理" className="mb-4">
|
|
|
|
|
+ <Form
|
|
|
|
|
+ layout="inline"
|
|
|
|
|
+ onFinish={handleSearch}
|
|
|
|
|
+ style={{ marginBottom: '16px' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form.Item name="title" label="标题">
|
|
|
|
|
+ <Input placeholder="请输入文章标题" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item name="category" label="分类">
|
|
|
|
|
+ <Input placeholder="请输入文章分类" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button type="primary" htmlType="submit">
|
|
|
|
|
+ 搜索
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button onClick={() => {
|
|
|
|
|
+ setSearchParams({
|
|
|
|
|
+ title: '',
|
|
|
|
|
+ category: '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 10,
|
|
|
|
|
+ });
|
|
|
|
|
+ }}>
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button type="primary" onClick={handleAdd}>
|
|
|
|
|
+ 添加文章
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ dataSource={articles}
|
|
|
|
|
+ rowKey="id"
|
|
|
|
|
+ loading={isListLoading}
|
|
|
|
|
+ pagination={{
|
|
|
|
|
+ current: pagination.current,
|
|
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
|
|
+ total: pagination.total,
|
|
|
|
|
+ onChange: handlePageChange,
|
|
|
|
|
+ showSizeChanger: true,
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
|
|
|
|
|
+ open={modalVisible}
|
|
|
|
|
+ onOk={() => form.submit()}
|
|
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
|
|
+ width={800}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form
|
|
|
|
|
+ form={form}
|
|
|
|
|
+ layout="vertical"
|
|
|
|
|
+ onFinish={handleSubmit}
|
|
|
|
|
+ initialValues={{
|
|
|
|
|
+ audit_status: AuditStatus.PENDING,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Row gutter={16}>
|
|
|
|
|
+ <Col span={12}>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="title"
|
|
|
|
|
+ label="文章标题"
|
|
|
|
|
+ rules={[{ required: true, message: '请输入文章标题' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入文章标题" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ <Col span={12}>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="category"
|
|
|
|
|
+ label="文章分类"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入文章分类" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ </Row>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="tags"
|
|
|
|
|
+ label="文章标签"
|
|
|
|
|
+ help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="content"
|
|
|
|
|
+ label="文章内容"
|
|
|
|
|
+ rules={[{ required: true, message: '请输入文章内容' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input.TextArea rows={15} placeholder="请输入文章内容,支持Markdown格式" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Row gutter={16}>
|
|
|
|
|
+ <Col span={12}>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="author"
|
|
|
|
|
+ label="文章作者"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入文章作者" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ <Col span={12}>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="cover_url"
|
|
|
|
|
+ label="封面图片URL"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入封面图片URL" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ </Row>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="audit_status"
|
|
|
|
|
+ label="审核状态"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Select options={auditStatusOptions} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button type="primary" htmlType="submit" loading={isLoading}>
|
|
|
|
|
+ {formMode === 'create' ? '创建' : '保存'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button onClick={() => setModalVisible(false)}>取消</Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|