Files
goalfylearning-admin-web/API_PATTERNS.md

13 KiB
Raw Permalink Blame History

API 调用模式和通用实现指南

核心 API 架构

ApiClient 核心类

位置src/services/api.ts

class ApiClient {
  private instance: any;
  private isRefreshing = false;
  private failedQueue: Array<{
    resolve: (value: any) => void;
    reject: (reason: any) => void;
  }> = [];

  // 特性:
  // 1. 自动附加 Authorization 头
  // 2. 401 错误时自动刷新 token
  // 3. Token 刷新期间的请求入队
  // 4. 自动处理 token 过期和重新登录
}

export const apiClient = new ApiClient();

API 服务层设计模式

模式 1列表 API用户等级配置

文件src/services/userLevelConfigApi.ts

import { apiClient } from './api';
import type { UserLevelConfig, UserLevelConfigListRequest, UserLevelConfigListResponse } from '../types/userLevelConfig';

// 模式:响应数据提取
// - apiClient 返回完整响应对象
// - 最后一层 API 函数负责提取数据或返回完整响应

export const getUserLevelConfigList = async (
  params: UserLevelConfigListRequest
): Promise<UserLevelConfigListResponse> => {
  const response = await apiClient.get('/admin/user-level-configs', { params });
  return response.data;  // 返回响应数据部分
};

export const getAllUserLevelConfigs = async (): Promise<UserLevelConfig[]> => {
  const response = await apiClient.get('/admin/user-level-configs/all');
  return response.data;
};

模式 2列表 API系统配置

文件src/services/systemConfigApi.ts

import { apiClient } from './api';
import type { SystemConfig, SystemConfigListRequest, SystemConfigListResponse } from '../types/systemConfig';

// 注意:不同的 API 可能返回不同的数据结构
// 系统配置返回完整响应对象(包含 code, message, data

export const getSystemConfigList = async (
  params: SystemConfigListRequest
): Promise<SystemConfigListResponse> => {
  const response = await apiClient.get('/admin/system-configs', { params });
  return response;  // 返回完整响应对象
};

模式 3CRUD 操作

// 创建
export const createUserLevelConfig = async (
  data: UserLevelConfigCreateRequest
): Promise<UserLevelConfig> => {
  const response = await apiClient.post('/admin/user-level-configs', data);
  return response.data;
};

// 读取
export const getUserLevelConfigById = async (id: number): Promise<UserLevelConfig> => {
  const response = await apiClient.get(`/admin/user-level-configs/${id}`);
  return response.data;
};

// 更新
export const updateUserLevelConfig = async (
  id: number,
  data: UserLevelConfigUpdateRequest
): Promise<UserLevelConfig> => {
  const response = await apiClient.put(`/admin/user-level-configs/${id}`, data);
  return response.data;
};

// 删除
export const deleteUserLevelConfig = async (id: number): Promise<void> => {
  await apiClient.delete(`/admin/user-level-configs/${id}`);
};

// 特殊操作:状态更新
export const updateUserLevelConfigStatus = async (
  id: number,
  data: UserLevelConfigStatusRequest
): Promise<void> => {
  await apiClient.put(`/admin/user-level-configs/${id}/status`, data);
};

类型定义模式

完整类型定义示例

文件src/types/userLevelConfig.ts

// 1. 主体数据类型
export interface UserLevelConfig {
  id: number;
  level_name: string;
  level_code: string;
  project_limit: number;
  description: string;
  sort_order: number;
  status: number;  // 1=启用, 0=禁用
  created_at: string;
  updated_at: string;
}

// 2. 请求类型
export interface UserLevelConfigListRequest {
  level_name?: string;
  status?: number;
  page?: number;
  size?: number;
}

export interface UserLevelConfigCreateRequest {
  level_name: string;
  level_code: string;
  project_limit: number;
  description?: string;
  sort_order?: number;
}

export interface UserLevelConfigUpdateRequest {
  level_name: string;
  project_limit: number;
  description?: string;
  sort_order?: number;
}

export interface UserLevelConfigStatusRequest {
  status: number;
}

// 3. 响应类型
export interface UserLevelConfigListResponse {
  data: UserLevelConfig[];
  total: number;
  page: number;
  size: number;
}

页面组件实现模式

CRUD 页面的标准结构

文件src/pages/UserLevelConfigs.tsx

import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, message } from 'antd';
import type { UserLevelConfig } from '../types/userLevelConfig';
import { getUserLevelConfigList, createUserLevelConfig, updateUserLevelConfig, deleteUserLevelConfig } from '../services/userLevelConfigApi';

export default function UserLevelConfigs() {
  // ==================== 状态管理 ====================
  const [loading, setLoading] = useState(false);
  const [list, setList] = useState<UserLevelConfig[]>([]);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const [size, setSize] = useState(10);
  
  // 编辑弹窗状态
  const [editOpen, setEditOpen] = useState(false);
  const [editing, setEditing] = useState<UserLevelConfig | null>(null);
  const [form] = Form.useForm();
  
  // 创建弹窗状态
  const [createOpen, setCreateOpen] = useState(false);
  const [createForm] = Form.useForm();

  // ==================== 数据加载 ====================
  const fetchList = async () => {
    setLoading(true);
    try {
      const res = await getUserLevelConfigList({ page, size });
      // 处理响应数据:兼容数组和对象两种格式
      if (Array.isArray(res)) {
        setList(res);
        setTotal(res.length);
      } else {
        setList(res?.data ?? []);
        setTotal(res?.total ?? 0);
      }
    } catch (e) {
      console.error('获取列表失败:', e);
      message.error('获取列表失败');
    } finally {
      setLoading(false);
    }
  };

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

  // ==================== 创建操作 ====================
  const openCreate = () => {
    createForm.resetFields();
    setCreateOpen(true);
  };

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

  // ==================== 编辑操作 ====================
  const openEdit = (config: UserLevelConfig) => {
    setEditing(config);
    form.setFieldsValue({
      level_name: config.level_name,
      project_limit: config.project_limit,
      description: config.description,
      sort_order: config.sort_order,
    });
    setEditOpen(true);
  };

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

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

  // ==================== 渲染 ====================
  const columns = [
    // 列定义...
  ];

  return (
    <div>
      {/* 主表格 */}
      <Table dataSource={list} columns={columns} loading={loading} />
      
      {/* 创建弹窗 */}
      <Modal title="新建" open={createOpen} onOk={submitCreate}>
        <Form form={createForm} layout="vertical">
          {/* 表单字段 */}
        </Form>
      </Modal>
      
      {/* 编辑弹窗 */}
      <Modal title="编辑" open={editOpen} onOk={submitEdit}>
        <Form form={form} layout="vertical">
          {/* 表单字段 */}
        </Form>
      </Modal>
    </div>
  );
}

权限和路由集成

添加新页面的完整检查清单

1. 创建类型定义

  • src/types/xxxConfig.ts - 定义数据模型和请求/响应类型

2. 创建 API 服务

  • src/services/xxxConfigApi.ts - 实现 CRUD API 方法

3. 创建页面组件

  • src/pages/XxxConfigs.tsx - 实现 CRUD UI

4. 注册路由

  • src/App.tsx - 添加路由定义
    <Route path="/system/xxx-configs" element={<XxxConfigs />} />
    

5. 添加菜单项

  • src/components/DynamicMenu.tsx - 添加菜单项定义
    {
      key: 'system-xxx-configs',
      label: 'Xxx 配置',
      icon: <SettingOutlined />,
      path: '/xxx-configs',
    }
    

6. 更新 Layout 子导航

  • src/components/Layout.tsx - 添加子导航按钮(如果需要)
    <button 
      className={activeSubTab === 'xxx-configs' ? 'active' : ''}
      onClick={() => navigate('/system/xxx-configs')}
    >
      Xxx 配置
    </button>
    

错误处理最佳实践

API 错误处理

// 页面中的错误处理
const fetchList = async () => {
  try {
    const res = await getUserLevelConfigList({ page, size });
    setList(res?.data ?? []);
  } catch (error: any) {
    // 1. 优先使用 API 返回的错误信息
    const errorMsg = error?.response?.data?.message;
    
    // 2. 其次使用通用错误信息
    message.error(errorMsg || '获取列表失败');
    
    // 3. 记录详细错误用于调试
    console.error('获取列表失败:', error);
  }
};

// 创建/更新时的错误处理
const submitCreate = async () => {
  try {
    const values = await createForm.validateFields();
    await createUserLevelConfig(values);
    message.success('创建成功');
  } catch (error: any) {
    // 区分表单验证错误和 API 错误
    if (error?.response?.data?.message) {
      message.error(error.response.data.message);
    } else {
      message.error('创建失败');
    }
  }
};

通用 CRUD 表格列定义

标准列定义模式

const columns = [
  // 基本字段列
  {
    title: '名称',
    dataIndex: 'level_name',
    key: 'level_name',
  },
  {
    title: '代码',
    dataIndex: 'level_code',
    key: 'level_code',
  },
  // 数值列(带格式化)
  {
    title: '数值',
    dataIndex: 'value',
    key: 'value',
    render: (value: number) => (value === 0 ? '不限' : value),
  },
  // 状态列(带标签)
  {
    title: '状态',
    dataIndex: 'status',
    key: 'status',
    render: (value: number) =>
      value === 1 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
  },
  // 操作列(最后)
  {
    title: '操作',
    key: 'action',
    render: (_: any, record: T) => (
      <Space>
        <Button type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}>
          编辑
        </Button>
        <Button type="link" icon={<DeleteOutlined />} danger onClick={() => handleDelete(record)}>
          删除
        </Button>
      </Space>
    ),
  },
];

表单字段最佳实践

创建表单示例

<Form form={createForm} layout="vertical">
  {/* 必填字段 */}
  <Form.Item
    name="level_name"
    label="等级名称"
    rules={[{ required: true, message: '请输入等级名称' }]}
  >
    <Input placeholder="请输入等级名称" />
  </Form.Item>

  {/* 代码字段(创建时必填,编辑时只读) */}
  <Form.Item
    name="level_code"
    label="等级代码"
    rules={[{ required: true, message: '请输入等级代码' }]}
  >
    <Input placeholder="请输入等级代码" />
  </Form.Item>

  {/* 数值字段 */}
  <Form.Item
    name="project_limit"
    label="项目数限制0表示不限"
    rules={[{ required: true, message: '请输入项目数限制' }]}
    initialValue={0}
  >
    <InputNumber min={0} style={{ width: '100%' }} />
  </Form.Item>

  {/* 可选文本字段 */}
  <Form.Item name="description" label="描述">
    <Input.TextArea rows={3} placeholder="请输入描述" />
  </Form.Item>

  {/* 排序字段 */}
  <Form.Item name="sort_order" label="排序顺序" initialValue={0}>
    <InputNumber min={0} style={{ width: '100%' }} />
  </Form.Item>
</Form>

分页实现

标准分页逻辑

// 状态
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [total, setTotal] = useState(0);

// 依赖项变化时重新加载
useEffect(() => {
  fetchList();
}, [page, size]);

// 表格分页配置
<Table
  dataSource={list}
  columns={columns}
  pagination={{
    current: page,
    pageSize: size,
    total: total,
    onChange: (p, s) => {
      setPage(p);
      setSize(s);
    },
  }}
/>

总结

API 调用流程

用户操作
  ↓
页面组件(使用 API 函数)
  ↓
API 服务函数(处理参数和响应)
  ↓
ApiClient自动处理 token 和拦截器)
  ↓
AxiosHTTP 请求)
  ↓
后端 API

关键要点

  1. 分层设计:每一层职责清晰
  2. 类型安全:完整的 TypeScript 类型支持
  3. 错误处理:统一的错误处理机制
  4. 权限集成:无缝的权限管理
  5. 状态管理:简化的本地状态管理
  6. 自动化:自动 Token 管理和刷新