|
|
@@ -0,0 +1,358 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Card, Table, Button, Modal, Form, Input, Select, message, Space, Switch, Popconfirm, TreeSelect } from 'antd';
|
|
|
+import { PlusOutlined, EditOutlined, DeleteOutlined, FolderOutlined } from '@ant-design/icons';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
+import { silverUsersClient } from '@/client/api';
|
|
|
+
|
|
|
+const { TextArea } = Input;
|
|
|
+
|
|
|
+type KnowledgeCategory = InferResponseType<typeof silverUsersClient['knowledge-categories']['$get'], 200>['data'][0];
|
|
|
+type CreateCategoryRequest = InferRequestType<typeof silverUsersClient['knowledge-categories']['$post']>['json'];
|
|
|
+type UpdateCategoryRequest = InferRequestType<typeof silverUsersClient['knowledge-categories'][':id']['$put']>['json'];
|
|
|
+
|
|
|
+const KnowledgeCategories: React.FC = () => {
|
|
|
+ const [data, setData] = useState<KnowledgeCategory[]>([]);
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
+ const [modalType, setModalType] = useState<'create' | 'edit'>('create');
|
|
|
+ const [currentRecord, setCurrentRecord] = useState<KnowledgeCategory | null>(null);
|
|
|
+ const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
|
|
+ const [form] = Form.useForm();
|
|
|
+ const [searchText, setSearchText] = useState('');
|
|
|
+ const [categories, setCategories] = useState<KnowledgeCategory[]>([]);
|
|
|
+
|
|
|
+ const fetchData = async (page = 1, pageSize = 10) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await silverUsersClient['knowledge-categories'].$get({
|
|
|
+ query: { page, pageSize, keyword: searchText || undefined }
|
|
|
+ });
|
|
|
+ const result = await response.json();
|
|
|
+ setData(result.data);
|
|
|
+ setPagination({
|
|
|
+ current: page,
|
|
|
+ pageSize,
|
|
|
+ total: result.pagination.total
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ message.error('获取数据失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchData();
|
|
|
+ loadCategories();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const loadCategories = async () => {
|
|
|
+ try {
|
|
|
+ const response = await silverUsersClient['knowledge-categories'].$get({
|
|
|
+ query: { page: 1, pageSize: 100 }
|
|
|
+ });
|
|
|
+ const result = await response.json();
|
|
|
+ setCategories(result.data);
|
|
|
+ } catch (error) {
|
|
|
+ message.error('加载分类失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleCreate = () => {
|
|
|
+ setModalType('create');
|
|
|
+ setCurrentRecord(null);
|
|
|
+ form.resetFields();
|
|
|
+ setModalVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEdit = (record: KnowledgeCategory) => {
|
|
|
+ setModalType('edit');
|
|
|
+ setCurrentRecord(record);
|
|
|
+ form.setFieldsValue({
|
|
|
+ ...record,
|
|
|
+ parentId: record.parentId || undefined
|
|
|
+ });
|
|
|
+ setModalVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
+ try {
|
|
|
+ await silverUsersClient['knowledge-categories'][':id']['$delete']({
|
|
|
+ param: { id: id.toString() }
|
|
|
+ });
|
|
|
+ message.success('删除成功');
|
|
|
+ fetchData(pagination.current, pagination.pageSize);
|
|
|
+ } catch (error) {
|
|
|
+ message.error('删除失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ const values = await form.validateFields();
|
|
|
+
|
|
|
+ if (modalType === 'create') {
|
|
|
+ await silverUsersClient['knowledge-categories'].$post({
|
|
|
+ json: values as CreateCategoryRequest
|
|
|
+ });
|
|
|
+ message.success('创建成功');
|
|
|
+ } else {
|
|
|
+ await silverUsersClient['knowledge-categories'][':id']['$put']({
|
|
|
+ param: { id: currentRecord!.id.toString() },
|
|
|
+ json: values as UpdateCategoryRequest
|
|
|
+ });
|
|
|
+ message.success('更新成功');
|
|
|
+ }
|
|
|
+
|
|
|
+ setModalVisible(false);
|
|
|
+ fetchData(pagination.current, pagination.pageSize);
|
|
|
+ loadCategories(); // 重新加载分类树
|
|
|
+ } catch (error) {
|
|
|
+ message.error(modalType === 'create' ? '创建失败' : '更新失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTableChange = (pagination: any) => {
|
|
|
+ fetchData(pagination.current, pagination.pageSize);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSearch = () => {
|
|
|
+ fetchData(1, pagination.pageSize);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleStatusChange = async (id: number, isActive: boolean) => {
|
|
|
+ try {
|
|
|
+ await silverUsersClient['knowledge-categories'][':id']['$put']({
|
|
|
+ param: { id: id.toString() },
|
|
|
+ json: { isActive: isActive ? 1 : 0 } as UpdateCategoryRequest
|
|
|
+ });
|
|
|
+ message.success('状态更新成功');
|
|
|
+ fetchData(pagination.current, pagination.pageSize);
|
|
|
+ } catch (error) {
|
|
|
+ message.error('状态更新失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 构建分类树结构
|
|
|
+ const buildCategoryTree = (categories: KnowledgeCategory[], parentId: number | null = null): any[] => {
|
|
|
+ return categories
|
|
|
+ .filter(cat => cat.parentId === parentId)
|
|
|
+ .map(cat => ({
|
|
|
+ title: cat.name,
|
|
|
+ value: cat.id,
|
|
|
+ children: buildCategoryTree(categories, cat.id)
|
|
|
+ }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const columns: ColumnsType<KnowledgeCategory> = [
|
|
|
+ {
|
|
|
+ title: 'ID',
|
|
|
+ dataIndex: 'id',
|
|
|
+ key: 'id',
|
|
|
+ width: 80,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '分类名称',
|
|
|
+ dataIndex: 'name',
|
|
|
+ key: 'name',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (name: string, record) => (
|
|
|
+ <Space>
|
|
|
+ <FolderOutlined />
|
|
|
+ <span>{name}</span>
|
|
|
+ </Space>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '描述',
|
|
|
+ dataIndex: 'description',
|
|
|
+ key: 'description',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (description: string | null) => description || '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '知识数量',
|
|
|
+ dataIndex: 'knowledgeCount',
|
|
|
+ key: 'knowledgeCount',
|
|
|
+ width: 100,
|
|
|
+ align: 'center'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '排序',
|
|
|
+ dataIndex: 'sortOrder',
|
|
|
+ key: 'sortOrder',
|
|
|
+ width: 80,
|
|
|
+ align: 'center'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'isActive',
|
|
|
+ key: 'isActive',
|
|
|
+ width: 100,
|
|
|
+ align: 'center',
|
|
|
+ render: (isActive: number, record) => (
|
|
|
+ <Switch
|
|
|
+ checked={isActive === 1}
|
|
|
+ onChange={(checked) => handleStatusChange(record.id, checked)}
|
|
|
+ checkedChildren="启用"
|
|
|
+ unCheckedChildren="禁用"
|
|
|
+ />
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '创建时间',
|
|
|
+ dataIndex: 'createdAt',
|
|
|
+ key: 'createdAt',
|
|
|
+ width: 180,
|
|
|
+ render: (text: string) => new Date(text).toLocaleString()
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ key: 'action',
|
|
|
+ width: 150,
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_, record) => (
|
|
|
+ <Space size="middle">
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => handleEdit(record)}
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </Button>
|
|
|
+ <Popconfirm
|
|
|
+ title="确定要删除这个分类吗?"
|
|
|
+ description="删除后该分类下的知识将变为未分类状态"
|
|
|
+ onConfirm={() => handleDelete(record.id)}
|
|
|
+ okText="确定"
|
|
|
+ cancelText="取消"
|
|
|
+ >
|
|
|
+ <Button type="link" danger icon={<DeleteOutlined />}>
|
|
|
+ 删除
|
|
|
+ </Button>
|
|
|
+ </Popconfirm>
|
|
|
+ </Space>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="p-6">
|
|
|
+ <Card
|
|
|
+ title={
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <span>知识分类管理</span>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={handleCreate}
|
|
|
+ >
|
|
|
+ 新增分类
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ className="shadow-sm"
|
|
|
+ >
|
|
|
+ <div className="mb-4 flex gap-4">
|
|
|
+ <Input.Search
|
|
|
+ placeholder="搜索分类名称或描述"
|
|
|
+ value={searchText}
|
|
|
+ onChange={(e) => setSearchText(e.target.value)}
|
|
|
+ onSearch={handleSearch}
|
|
|
+ style={{ width: 300 }}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={data}
|
|
|
+ rowKey="id"
|
|
|
+ loading={loading}
|
|
|
+ pagination={{
|
|
|
+ ...pagination,
|
|
|
+ showSizeChanger: true,
|
|
|
+ showQuickJumper: true,
|
|
|
+ showTotal: (total) => `共 ${total} 条记录`,
|
|
|
+ }}
|
|
|
+ onChange={handleTableChange}
|
|
|
+ scroll={{ x: 1000 }}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ title={modalType === 'create' ? '新增知识分类' : '编辑知识分类'}
|
|
|
+ open={modalVisible}
|
|
|
+ onOk={handleSubmit}
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
+ width={600}
|
|
|
+ destroyOnClose
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="vertical"
|
|
|
+ initialValues={{ isActive: 1, sortOrder: 0 }}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ label="分类名称"
|
|
|
+ name="name"
|
|
|
+ rules={[{ required: true, message: '请输入分类名称' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入分类名称" maxLength={100} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="父分类"
|
|
|
+ name="parentId"
|
|
|
+ >
|
|
|
+ <TreeSelect
|
|
|
+ placeholder="请选择父分类(不选则为顶级分类)"
|
|
|
+ treeData={buildCategoryTree(categories)}
|
|
|
+ allowClear
|
|
|
+ treeDefaultExpandAll
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="分类描述"
|
|
|
+ name="description"
|
|
|
+ >
|
|
|
+ <TextArea
|
|
|
+ rows={3}
|
|
|
+ placeholder="请输入分类描述"
|
|
|
+ maxLength={500}
|
|
|
+ showCount
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="分类图标"
|
|
|
+ name="icon"
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入分类图标标识" maxLength={100} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="排序"
|
|
|
+ name="sortOrder"
|
|
|
+ rules={[{ required: true, message: '请输入排序值' }]}
|
|
|
+ >
|
|
|
+ <Input type="number" placeholder="请输入排序值,数值越大越靠前" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="状态"
|
|
|
+ name="isActive"
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={true}
|
|
|
+ >
|
|
|
+ <Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default KnowledgeCategories;
|