silver-knowledge-admin-code-guide.md 17 KB

银龄智库管理后台代码实施指南

1. 创建管理页面组件

1.1 SilverKnowledges.tsx 列表页面

文件路径: src/client/admin/pages/SilverKnowledges.tsx

import React, { useState, useRef } from 'react';
import { Card, Button, Space, Tag, Modal, message, Switch, Popconfirm } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { useNavigate } from 'react-router';
import { useAuth } from '../hooks/AuthProvider';
import type { InferResponseType } from 'hono/client';
import { silverUsersClient } from '@/client/api';
import dayjs from 'dayjs';

type SilverKnowledge = InferResponseType<typeof silverUsersClient.knowledges.$get, 200>['data'][0];

const SilverKnowledges: React.FC = () => {
  const navigate = useNavigate();
  const { user } = useAuth();
  const actionRef = useRef<ActionType>();
  const [loading, setLoading] = useState(false);

  const handleAdd = () => {
    navigate('/admin/silver-knowledges/new');
  };

  const handleEdit = (record: SilverKnowledge) => {
    navigate(`/admin/silver-knowledges/${record.id}/edit`);
  };

  const handleDelete = async (id: number) => {
    try {
      await silverUsersClient.knowledges[':id'].$delete({
        param: { id: id.toString() }
      });
      message.success('删除成功');
      actionRef.current?.reload();
    } catch (error) {
      message.error('删除失败');
    }
  };

  const handleToggleStatus = async (record: SilverKnowledge, newStatus: number) => {
    try {
      await silverUsersClient.knowledges[':id'].$put({
        param: { id: record.id.toString() },
        json: { status: newStatus }
      });
      message.success('状态更新成功');
      actionRef.current?.reload();
    } catch (error) {
      message.error('状态更新失败');
    }
  };

  const handleToggleFeatured = async (record: SilverKnowledge, isFeatured: boolean) => {
    try {
      await silverUsersClient.knowledges[':id'].$put({
        param: { id: record.id.toString() },
        json: { isFeatured: isFeatured ? 1 : 0 }
      });
      message.success(isFeatured ? '已设为推荐' : '已取消推荐');
      actionRef.current?.reload();
    } catch (error) {
      message.error('推荐设置失败');
    }
  };

  const columns: ProColumns<SilverKnowledge>[] = [
    {
      title: 'ID',
      dataIndex: 'id',
      width: 80,
      hideInSearch: true,
    },
    {
      title: '标题',
      dataIndex: 'title',
      ellipsis: true,
      width: 200,
    },
    {
      title: '分类',
      dataIndex: 'category',
      render: (_, record) => record.category?.name || '-',
      width: 100,
    },
    {
      title: '类型',
      dataIndex: 'type',
      valueEnum: {
        1: { text: '文章', status: 'Processing' },
        2: { text: '视频', status: 'Success' },
        3: { text: '文档', status: 'Default' },
        4: { text: '课程', status: 'Warning' },
        5: { text: '经验分享', status: 'Success' },
        6: { text: '案例分享', status: 'Processing' },
        7: { text: '研究报告', status: 'Error' },
      },
      width: 100,
    },
    {
      title: '状态',
      dataIndex: 'status',
      render: (_, record) => {
        const statusMap = {
          0: <Tag color="default">草稿</Tag>,
          1: <Tag color="success">已发布</Tag>,
          2: <Tag color="warning">已隐藏</Tag>,
          3: <Tag color="error">已删除</Tag>,
          4: <Tag color="processing">审核中</Tag>,
        };
        return statusMap[record.status] || '-';
      },
      valueEnum: {
        0: { text: '草稿', status: 'Default' },
        1: { text: '已发布', status: 'Success' },
        2: { text: '已隐藏', status: 'Warning' },
        3: { text: '已删除', status: 'Error' },
        4: { text: '审核中', status: 'Processing' },
      },
      width: 100,
    },
    {
      title: '作者',
      dataIndex: ['user', 'nickname'],
      width: 100,
      hideInSearch: true,
    },
    {
      title: '浏览',
      dataIndex: 'viewCount',
      width: 80,
      hideInSearch: true,
    },
    {
      title: '点赞',
      dataIndex: 'likeCount',
      width: 80,
      hideInSearch: true,
    },
    {
      title: '收藏',
      dataIndex: 'favoriteCount',
      width: 80,
      hideInSearch: true,
    },
    {
      title: '推荐',
      dataIndex: 'isFeatured',
      render: (_, record) => (
        <Switch
          checked={record.isFeatured === 1}
          onChange={(checked) => handleToggleFeatured(record, checked)}
          checkedChildren="是"
          unCheckedChildren="否"
        />
      ),
      width: 80,
      hideInSearch: true,
    },
    {
      title: '创建时间',
      dataIndex: 'createdAt',
      render: (_, record) => dayjs(record.createdAt).format('YYYY-MM-DD HH:mm'),
      width: 150,
      hideInSearch: true,
    },
    {
      title: '操作',
      valueType: 'option',
      width: 200,
      render: (_, record) => (
        <Space>
          <Button
            type="link"
            size="small"
            icon={<EyeOutlined />}
            onClick={() => window.open(`/knowledge/${record.id}`, '_blank')}
          >
            查看
          </Button>
          <Button
            type="link"
            size="small"
            icon={<EditOutlined />}
            onClick={() => handleEdit(record)}
          >
            编辑
          </Button>
          <Popconfirm
            title="确定要删除吗?"
            onConfirm={() => handleDelete(record.id)}
            okText="确定"
            cancelText="取消"
          >
            <Button danger type="link" size="small" icon={<DeleteOutlined />}>
              删除
            </Button>
          </Popconfirm>
        </Space>
      ),
    },
  ];

  return (
    <Card>
      <ProTable<SilverKnowledge>
        headerTitle="银龄智库管理"
        actionRef={actionRef}
        rowKey="id"
        search={{
          labelWidth: 120,
        }}
        toolBarRender={() => [
          <Button key="add" type="primary" onClick={handleAdd} icon={<PlusOutlined />}>
            新建知识
          </Button>,
        ]}
        request={async (params) => {
          const response = await silverUsersClient.knowledges.$get({
            query: {
              page: params.current || 1,
              pageSize: params.pageSize || 10,
              keyword: params.title,
              ...params,
            }
          });
          const data = await response.json();
          return {
            data: data.data,
            success: response.ok,
            total: data.pagination.total,
          };
        }}
        columns={columns}
        pagination={{
          showSizeChanger: true,
          showQuickJumper: true,
        }}
      />
    </Card>
  );
};

export default SilverKnowledges;

1.2 SilverKnowledgeForm.tsx 表单页面

文件路径: src/client/admin/pages/SilverKnowledgeForm.tsx

import React, { useEffect, useState } from 'react';
import { Card, Form, Input, Select, Button, Space, Upload, message, Row, Col } from 'antd';
import { InboxOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { useAuth } from '../hooks/AuthProvider';
import { silverUsersClient } from '@/client/api';
import type { InferResponseType, InferRequestType } from 'hono/client';
import dayjs from 'dayjs';

const { Option } = Select;
const { TextArea } = Input;
const { Dragger } = Upload;

type SilverKnowledge = InferResponseType<typeof silverUsersClient.knowledges[':id'].$get, 200>;
type CreateKnowledge = InferRequestType<typeof silverUsersClient.knowledges.$post>['json'];
type UpdateKnowledge = InferRequestType<typeof silverUsersClient.knowledges[':id'].$put>['json'];

const knowledgeTypes = [
  { value: 1, label: '文章' },
  { value: 2, label: '视频' },
  { value: 3, label: '文档' },
  { value: 4, label: '课程' },
  { value: 5, label: '经验分享' },
  { value: 6, label: '案例分享' },
  { value: 7, label: '研究报告' },
];

const knowledgeStatus = [
  { value: 0, label: '草稿' },
  { value: 1, label: '已发布' },
  { value: 2, label: '已隐藏' },
  { value: 4, label: '审核中' },
];

const SilverKnowledgeForm: React.FC = () => {
  const navigate = useNavigate();
  const { id } = useParams();
  const { user } = useAuth();
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);
  const [categories, setCategories] = useState<any[]>([]);

  useEffect(() => {
    fetchCategories();
    if (id) {
      fetchKnowledge();
    }
  }, [id]);

  const fetchCategories = async () => {
    try {
      const response = await silverUsersClient['knowledge-categories'].$get();
      const data = await response.json();
      setCategories(data.data || []);
    } catch (error) {
      console.error('获取分类失败:', error);
    }
  };

  const fetchKnowledge = async () => {
    if (!id) return;
    try {
      const response = await silverUsersClient.knowledges[':id'].$get({
        param: { id }
      });
      const data = await response.json();
      form.setFieldsValue({
        ...data,
        tags: data.tags ? JSON.parse(data.tags) : [],
        attachments: data.attachments ? JSON.parse(data.attachments) : [],
      });
    } catch (error) {
      message.error('获取知识详情失败');
    }
  };

  const handleSubmit = async (values: any) => {
    setLoading(true);
    try {
      const submitData = {
        ...values,
        userId: user?.id,
        tags: values.tags ? JSON.stringify(values.tags) : undefined,
        attachments: values.attachments ? JSON.stringify(values.attachments) : undefined,
      };

      if (id) {
        await silverUsersClient.knowledges[':id'].$put({
          param: { id },
          json: submitData
        });
        message.success('更新成功');
      } else {
        await silverUsersClient.knowledges.$post({
          json: submitData
        });
        message.success('创建成功');
      }
      navigate('/admin/silver-knowledges');
    } catch (error) {
      message.error(id ? '更新失败' : '创建失败');
    } finally {
      setLoading(false);
    }
  };

  const handleBack = () => {
    navigate('/admin/silver-knowledges');
  };

  const uploadProps = {
    name: 'file',
    multiple: true,
    action: '/api/v1/files/upload-policy',
    onChange(info: any) {
      const { status } = info.file;
      if (status === 'done') {
        message.success(`${info.file.name} 上传成功`);
      } else if (status === 'error') {
        message.error(`${info.file.name} 上传失败`);
      }
    },
  };

  return (
    <Card
      title={id ? '编辑知识' : '新建知识'}
      extra={
        <Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
          返回列表
        </Button>
      }
    >
      <Form
        form={form}
        layout="vertical"
        onFinish={handleSubmit}
        initialValues={{
          type: 1,
          status: 0,
          isFeatured: 0,
          sortOrder: 0,
        }}
      >
        <Row gutter={24}>
          <Col span={16}>
            <Form.Item
              name="title"
              label="标题"
              rules={[{ required: true, message: '请输入标题' }]}
            >
              <Input placeholder="请输入知识标题" maxLength={255} />
            </Form.Item>

            <Form.Item
              name="content"
              label="内容"
              rules={[{ required: true, message: '请输入内容' }]}
            >
              <ReactQuill
                theme="snow"
                style={{ height: 300 }}
                placeholder="请输入知识内容"
              />
            </Form.Item>

            <Form.Item
              name="summary"
              label="摘要"
            >
              <TextArea
                rows={4}
                placeholder="请输入知识摘要(选填)"
                maxLength={500}
                showCount
              />
            </Form.Item>
          </Col>

          <Col span={8}>
            <Form.Item
              name="categoryId"
              label="分类"
            >
              <Select placeholder="请选择分类" allowClear>
                {categories.map(category => (
                  <Option key={category.id} value={category.id}>
                    {category.name}
                  </Option>
                ))}
              </Select>
            </Form.Item>

            <Form.Item
              name="type"
              label="类型"
              rules={[{ required: true, message: '请选择类型' }]}
            >
              <Select placeholder="请选择类型">
                {knowledgeTypes.map(type => (
                  <Option key={type.value} value={type.value}>
                    {type.label}
                  </Option>
                ))}
              </Select>
            </Form.Item>

            <Form.Item
              name="status"
              label="状态"
              rules={[{ required: true, message: '请选择状态' }]}
            >
              <Select placeholder="请选择状态">
                {knowledgeStatus.map(status => (
                  <Option key={status.value} value={status.value}>
                    {status.label}
                  </Option>
                ))}
              </Select>
            </Form.Item>

            <Form.Item
              name="tags"
              label="标签"
            >
              <Select
                mode="tags"
                placeholder="请输入标签"
                tokenSeparators={[',']}
              />
            </Form.Item>

            <Form.Item
              name="keywords"
              label="关键词"
            >
              <TextArea
                rows={3}
                placeholder="请输入搜索关键词,用逗号分隔"
              />
            </Form.Item>

            <Form.Item
              name="author"
              label="原作者"
            >
              <Input placeholder="请输入原作者" maxLength={100} />
            </Form.Item>

            <Form.Item
              name="source"
              label="知识来源"
            >
              <Input placeholder="请输入知识来源" maxLength={255} />
            </Form.Item>

            <Form.Item
              name="coverImage"
              label="封面图片"
            >
              <Input placeholder="请输入封面图片URL" />
            </Form.Item>

            <Form.Item
              name="attachments"
              label="附件"
            >
              <Dragger {...uploadProps}>
                <p className="ant-upload-drag-icon">
                  <InboxOutlined />
                </p>
                <p className="ant-upload-text">点击或拖拽文件到此处上传</p>
                <p className="ant-upload-hint">支持单个或批量上传</p>
              </Dragger>
            </Form.Item>
          </Col>
        </Row>

        <Form.Item>
          <Space>
            <Button type="primary" htmlType="submit" loading={loading}>
              {id ? '更新' : '创建'}
            </Button>
            <Button onClick={handleBack}>取消</Button>
          </Space>
        </Form.Item>
      </Form>
    </Card>
  );
};

export default SilverKnowledgeForm;

2. 更新菜单配置

2.1 修改 menu.tsx

添加导入:

import { BookOutlined } from '@ant-design/icons';

添加菜单项:

{
  key: 'silver-knowledges',
  label: '银龄智库发布',
  icon: <BookOutlined />,
  path: '/admin/silver-knowledges',
  permission: 'silver-knowledge:manage'
}

3. 更新路由配置

3.1 修改 routes.tsx

添加导入:

import { SilverKnowledgesPage } from './pages/SilverKnowledges';
import { SilverKnowledgeFormPage } from './pages/SilverKnowledgeForm';

添加路由:

{
  path: 'silver-knowledges',
  element: <SilverKnowledgesPage />,
  errorElement: <ErrorPage />
},
{
  path: 'silver-knowledges/new',
  element: <SilverKnowledgeFormPage />,
  errorElement: <ErrorPage />
},
{
  path: 'silver-knowledges/:id/edit',
  element: <SilverKnowledgeFormPage />,
  errorElement: <ErrorPage />
}

4. 客户端API配置

4.1 修改 api.ts

添加类型定义:

import type { SilverKnowledgeRoutes } from '@/server/api/silver-users/knowledges';

export const silverUsersClient = hc<SilverKnowledgeRoutes>('/api/v1', {
  fetch: axiosFetch,
}).api.v1['silver-users'];

5. 依赖安装

# 安装富文本编辑器
npm install react-quill @types/react-quill

# 安装日期处理
npm install dayjs

6. 使用说明

6.1 访问路径

  • 列表页面: /admin/silver-knowledges
  • 新建页面: /admin/silver-knowledges/new
  • 编辑页面: /admin/silver-knowledges/:id/edit

6.2 功能特点

  • 完整的CRUD操作
  • 富文本编辑器支持
  • 图片和附件上传
  • 状态管理
  • 推荐设置
  • 搜索和筛选
  • 批量操作

6.3 权限控制

需要用户具有 silver-knowledge:manage 权限才能访问