Files
goalfylearning-admin-web/QUICK_START.md

14 KiB
Raw Blame History

前端开发快速开始指南

项目信息

  • 技术栈: React 18 + TypeScript + Vite + Ant Design 5 + React Router 7
  • 项目路径: /Users/youziba/goalfyagent/goalfymax-admin-web
  • 状态管理: Jotai
  • HTTP客户端: Axios (自动处理 Token 认证和刷新)

快速创建新的配置管理页面

以创建"通用配置"管理页面为例,需要 5 个步骤:

步骤 1创建类型定义文件

文件: src/types/commonConfig.ts

export interface CommonConfig {
  id: number;
  config_key: string;      // 配置键
  config_value: string;    // 配置值
  description: string;     // 描述
  status: number;          // 1=启用, 0=禁用
  created_at: string;
  updated_at: string;
}

export interface CommonConfigListRequest {
  config_key?: string;
  status?: number;
  page?: number;
  size?: number;
}

export interface CommonConfigListResponse {
  data: CommonConfig[];
  total: number;
  page: number;
  size: number;
}

export interface CommonConfigCreateRequest {
  config_key: string;
  config_value: string;
  description?: string;
}

export interface CommonConfigUpdateRequest {
  config_key: string;
  config_value: string;
  description?: string;
}

步骤 2创建 API 服务文件

文件: src/services/commonConfigApi.ts

import { apiClient } from './api';
import type {
  CommonConfig,
  CommonConfigListRequest,
  CommonConfigListResponse,
  CommonConfigCreateRequest,
  CommonConfigUpdateRequest,
} from '../types/commonConfig';

export const getCommonConfigList = async (
  params: CommonConfigListRequest
): Promise<CommonConfigListResponse> => {
  const response = await apiClient.get('/admin/common-configs', { params });
  return response.data;
};

export const getCommonConfigById = async (id: number): Promise<CommonConfig> => {
  const response = await apiClient.get(`/admin/common-configs/${id}`);
  return response.data;
};

export const createCommonConfig = async (
  data: CommonConfigCreateRequest
): Promise<CommonConfig> => {
  const response = await apiClient.post('/admin/common-configs', data);
  return response.data;
};

export const updateCommonConfig = async (
  id: number,
  data: CommonConfigUpdateRequest
): Promise<CommonConfig> => {
  const response = await apiClient.put(`/admin/common-configs/${id}`, data);
  return response.data;
};

export const deleteCommonConfig = async (id: number): Promise<void> => {
  await apiClient.delete(`/admin/common-configs/${id}`);
};

export const updateCommonConfigStatus = async (
  id: number,
  status: number
): Promise<void> => {
  await apiClient.put(`/admin/common-configs/${id}/status`, { status });
};

步骤 3创建页面组件

文件: src/pages/CommonConfigs.tsx

import React, { useEffect, useState } from 'react';
import {
  Table,
  Button,
  Modal,
  Form,
  Input,
  Tag,
  Space,
  Popconfirm,
  message,
  Row,
  Col,
  Card,
} from 'antd';
import {
  PlusOutlined,
  EditOutlined,
  DeleteOutlined,
  CheckCircleOutlined,
  StopOutlined,
} from '@ant-design/icons';
import type { CommonConfig } from '../types/commonConfig';
import {
  getCommonConfigList,
  createCommonConfig,
  updateCommonConfig,
  deleteCommonConfig,
  updateCommonConfigStatus,
} from '../services/commonConfigApi';

export default function CommonConfigs() {
  const [loading, setLoading] = useState(false);
  const [list, setList] = useState<CommonConfig[]>([]);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const [size, setSize] = useState(10);
  const [editOpen, setEditOpen] = useState(false);
  const [editing, setEditing] = useState<CommonConfig | null>(null);
  const [createOpen, setCreateOpen] = useState(false);
  const [form] = Form.useForm();
  const [createForm] = Form.useForm();

  // 获取列表
  const fetchList = async () => {
    setLoading(true);
    try {
      const res = await getCommonConfigList({ page, size });
      setList(res?.data ?? []);
      setTotal(res?.total ?? 0);
    } catch (e) {
      message.error('获取列表失败');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchList();
  }, [page, size]);

  const openEdit = (config: CommonConfig) => {
    setEditing(config);
    form.setFieldsValue({
      config_key: config.config_key,
      config_value: config.config_value,
      description: config.description,
    });
    setEditOpen(true);
  };

  const submitEdit = async () => {
    try {
      const values = await form.validateFields();
      if (!editing) return;
      await updateCommonConfig(editing.id, values);
      message.success('更新成功');
      setEditOpen(false);
      fetchList();
    } catch (error) {
      message.error('更新失败');
    }
  };

  const openCreate = () => {
    createForm.resetFields();
    setCreateOpen(true);
  };

  const submitCreate = async () => {
    try {
      const values = await createForm.validateFields();
      await createCommonConfig(values);
      message.success('创建成功');
      setCreateOpen(false);
      fetchList();
    } catch (error: any) {
      message.error(error?.response?.data?.message || '创建失败');
    }
  };

  const handleDelete = async (config: CommonConfig) => {
    try {
      await deleteCommonConfig(config.id);
      message.success('删除成功');
      fetchList();
    } catch (error) {
      message.error('删除失败');
    }
  };

  const handleToggleStatus = async (config: CommonConfig) => {
    try {
      const newStatus = config.status === 1 ? 0 : 1;
      await updateCommonConfigStatus(config.id, newStatus);
      message.success(newStatus === 1 ? '已启用' : '已禁用');
      fetchList();
    } catch (error) {
      message.error('状态更新失败');
    }
  };

  const columns = [
    {
      title: '配置键',
      dataIndex: 'config_key',
      key: 'config_key',
    },
    {
      title: '配置值',
      dataIndex: 'config_value',
      key: 'config_value',
    },
    {
      title: '描述',
      dataIndex: 'description',
      key: 'description',
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (value: number) =>
        value === 1 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, config: CommonConfig) => (
        <Space>
          <Button type="link" icon={<EditOutlined />} onClick={() => openEdit(config)}>
            编辑
          </Button>
          <Button
            type="link"
            icon={config.status === 1 ? <StopOutlined /> : <CheckCircleOutlined />}
            onClick={() => handleToggleStatus(config)}
          >
            {config.status === 1 ? '禁用' : '启用'}
          </Button>
          <Popconfirm title="确定删除?" onConfirm={() => handleDelete(config)}>
            <Button type="link" danger icon={<DeleteOutlined />}>
              删除
            </Button>
          </Popconfirm>
        </Space>
      ),
    },
  ];

  return (
    <div>
      <Row gutter={16} style={{ marginBottom: 16 }}>
        <Col span={24}>
          <Card>
            <div style={{ marginBottom: 16 }}>
              <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
                新建配置
              </Button>
            </div>
            <Table
              dataSource={list}
              columns={columns}
              rowKey="id"
              loading={loading}
              pagination={{
                current: page,
                pageSize: size,
                total: total,
                onChange: (p, s) => {
                  setPage(p);
                  setSize(s);
                },
              }}
            />
          </Card>
        </Col>
      </Row>

      {/* 编辑弹窗 */}
      <Modal
        title="编辑配置"
        open={editOpen}
        onOk={submitEdit}
        onCancel={() => setEditOpen(false)}
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="config_key"
            label="配置键"
            rules={[{ required: true, message: '请输入配置键' }]}
          >
            <Input placeholder="请输入配置键" />
          </Form.Item>
          <Form.Item
            name="config_value"
            label="配置值"
            rules={[{ required: true, message: '请输入配置值' }]}
          >
            <Input placeholder="请输入配置值" />
          </Form.Item>
          <Form.Item name="description" label="描述">
            <Input.TextArea rows={3} />
          </Form.Item>
        </Form>
      </Modal>

      {/* 创建弹窗 */}
      <Modal
        title="新建配置"
        open={createOpen}
        onOk={submitCreate}
        onCancel={() => setCreateOpen(false)}
      >
        <Form form={createForm} layout="vertical">
          <Form.Item
            name="config_key"
            label="配置键"
            rules={[{ required: true, message: '请输入配置键' }]}
          >
            <Input placeholder="请输入配置键app_name" />
          </Form.Item>
          <Form.Item
            name="config_value"
            label="配置值"
            rules={[{ required: true, message: '请输入配置值' }]}
          >
            <Input placeholder="请输入配置值" />
          </Form.Item>
          <Form.Item name="description" label="描述">
            <Input.TextArea rows={3} placeholder="请输入描述" />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
}

步骤 4在 App.tsx 中添加路由

src/App.tsx 中找到系统管理路由部分,添加:

import CommonConfigs from './pages/CommonConfigs';

// 在 Routes 中的系统管理部分添加:
<Route path="/system/common-configs" element={<CommonConfigs />} />

步骤 5在 Layout.tsx 中添加菜单项

src/components/Layout.tsx 中找到系统管理子菜单部分(data-tabs="admin"),添加:

<button 
  className={activeSubTab === 'common-configs' ? 'active' : ''}
  onClick={() => navigate('/system/common-configs')}
>
  通用配置
</button>

核心文件速查表

功能 文件位置 说明
全局路由 src/App.tsx 所有页面路由定义
菜单导航 src/components/Layout.tsx 菜单项和导航配置
API 客户端 src/services/api.ts Axios 包装,自动处理 Token
权限检查 src/hooks/usePagePermissions.ts 获取用户权限信息
认证管理 src/hooks/useAuth.ts SSO 认证相关逻辑
全局状态 src/atoms/auth.ts Jotai 状态原子定义

常用开发模式

1. 获取列表并分页

const [list, setList] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [total, setTotal] = useState(0);

const fetchList = async () => {
  const res = await getXxxList({ page, size });
  setList(res?.data ?? []);
  setTotal(res?.total ?? 0);
};

useEffect(() => {
  fetchList();
}, [page, size]);

// 在 Table 中使用
<Table
  dataSource={list}
  pagination={{
    current: page,
    pageSize: size,
    total: total,
    onChange: (p, s) => {
      setPage(p);
      setSize(s);
    },
  }}
/>

2. 打开编辑弹窗并回显数据

const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<T | null>(null);
const [form] = Form.useForm();

const openEdit = (record: T) => {
  setEditing(record);
  form.setFieldsValue({
    field1: record.field1,
    field2: record.field2,
  });
  setEditOpen(true);
};

const submitEdit = async () => {
  const values = await form.validateFields();
  await updateXxx(editing!.id, values);
  message.success('更新成功');
  setEditOpen(false);
  fetchList();
};

3. 权限检查

import { usePagePermissions } from '../hooks/usePagePermissions';

function MyComponent() {
  const { getAccessiblePages } = usePagePermissions();
  const accessiblePages = getAccessiblePages();
  
  if (!accessiblePages.includes('/system')) {
    return <div>无权访问</div>;
  }
  
  return <div>内容</div>;
}

4. 调用 API

import { apiClient } from '../services/api';

// GET 请求
const data = await apiClient.get('/admin/xxx');

// POST 请求
const result = await apiClient.post('/admin/xxx', { key: 'value' });

// PUT 请求
const updated = await apiClient.put('/admin/xxx/123', { key: 'new-value' });

// DELETE 请求
await apiClient.delete('/admin/xxx/123');

Ant Design 常用组件

组件 用途 导入
Button 按钮 from 'antd'
Table 数据表格 from 'antd'
Form 表单 from 'antd'
Input 文本输入 from 'antd'
InputNumber 数字输入 from 'antd'
Select 下拉选择 from 'antd'
Modal 弹窗对话框 from 'antd'
Message 消息提示 from 'antd'
Tag 标签 from 'antd'
Card 卡片容器 from 'antd'
Space 间距布局 from 'antd'
Popconfirm 确认弹窗 from 'antd'

开发工作流

  1. 创建类型定义src/types/xxx.ts
  2. 创建 API 服务src/services/xxxApi.ts
  3. 创建页面组件src/pages/Xxx.tsx
  4. 添加路由src/App.tsx
  5. 添加菜单src/components/Layout.tsx
  6. 测试npm run dev

API 命名规范

  • 列表: GET /admin/xxx
  • 详情: GET /admin/xxx/:id
  • 创建: POST /admin/xxx
  • 更新: PUT /admin/xxx/:id
  • 删除: DELETE /admin/xxx/:id
  • 状态: PUT /admin/xxx/:id/status

注意事项

  1. Token 自动管理: API 请求会自动附带 Token401 错误会自动刷新 Token
  2. 权限检查: 菜单和路由需要通过 usePagePermissions 检查用户权限
  3. 类型安全: 始终使用 TypeScript 定义接口,避免使用 any
  4. 表单验证: 使用 Form.useForm()validateFields() 进行表单验证
  5. 消息提示: 使用 message.success() / message.error() 等方法提示用户
  6. 异步处理: 使用 async/await 简化异步逻辑

参考文档

完整的开发指南请查看: /Users/youziba/goalfyagent/goalfymax-admin-web/FRONTEND_GUIDE.md