Procházet zdrojové kódy

✨ feat(admin): 添加知识分类管理功能

- 在菜单中添加知识分类管理选项
- 创建知识分类管理页面,包含列表、新增、编辑、删除功能
- 实现分类的树形结构展示和搜索功能
- 添加分类状态切换和排序功能
- 配置知识分类管理路由
yourname před 7 měsíci
rodič
revize
56c155b32a

+ 8 - 0
src/client/admin/menu.tsx

@@ -13,6 +13,7 @@ import {
   AuditOutlined,
   PictureOutlined,
   BookOutlined,
+  FolderOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -133,6 +134,13 @@ export const useMenu = () => {
       path: '/admin/silver-knowledges',
       permission: 'silver-knowledge:manage'
     },
+    {
+      key: 'knowledge-categories',
+      label: '知识分类管理',
+      icon: <FolderOutlined />,
+      path: '/admin/knowledge-categories',
+      permission: 'silver-knowledge:manage'
+    },
   ];
 
   // 用户菜单项

+ 358 - 0
src/client/admin/pages/KnowledgeCategories.tsx

@@ -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;

+ 6 - 0
src/client/admin/routes.tsx

@@ -13,6 +13,7 @@ import { SilverJobsPage } from './pages/SilverJobs';
 import CompanyCertificationPage from './pages/CompanyCertification';
 import HomeIconsPage from './pages/HomeIcons';
 import SilverKnowledgesPage from './pages/SilverKnowledges';
+import KnowledgeCategories from './pages/KnowledgeCategories';
 
 export const router = createBrowserRouter([
   {
@@ -75,6 +76,11 @@ export const router = createBrowserRouter([
         element: <SilverKnowledgesPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'knowledge-categories',
+        element: <KnowledgeCategories />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,