Files
2026-01-25 15:05:03 +08:00

21 KiB

API服务封装文档

1. Axios基础配置

1.1 Axios实例创建

// services/api.js
import axios from 'axios';

// 创建axios实例
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default api;

1.2 环境变量配置

# .env.development
VITE_API_BASE_URL=http://localhost:5000/api/v1
VITE_APP_TITLE=海洋项目管理系统

# .env.production
VITE_API_BASE_URL=https://api.example.com/api/v1
VITE_APP_TITLE=海洋项目管理系统

2. 请求拦截器

2.1 请求前拦截

// services/api.js

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 添加Token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    // 添加时间戳防止缓存
    if (config.method === 'get') {
      config.params = {
        ...config.params,
        _t: Date.now(),
      };
    }

    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

2.2 请求日志(开发环境)

// services/api.js

if (import.meta.env.DEV) {
  api.interceptors.request.use((config) => {
    console.log('Request:', {
      url: config.url,
      method: config.method,
      params: config.params,
      data: config.data,
    });
    return config;
  });
}

3. 响应拦截器

3.1 响应数据处理

// services/api.js
import { message } from 'antd';

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    const { data } = response;

    // 检查业务状态码
    if (data.success === false) {
      message.error(data.message || '请求失败');
      return Promise.reject(new Error(data.message || '请求失败'));
    }

    // 返回数据部分
    return data.data || data;
  },
  (error) => {
    // 错误处理
    if (error.response) {
      handleResponseError(error.response);
    } else if (error.request) {
      handleNetworkError(error);
    } else {
      handleOtherError(error);
    }

    return Promise.reject(error);
  }
);

3.2 错误处理函数

// services/api.js

/**
 * 处理响应错误
 */
const handleResponseError = (response) => {
  const { status, data } = response;

  switch (status) {
    case 400:
      message.error(data.message || '请求参数错误');
      break;
    case 401:
      message.error('登录已过期,请重新登录');
      clearAuth();
      window.location.href = '/login';
      break;
    case 403:
      message.error('权限不足');
      break;
    case 404:
      message.error('请求的资源不存在');
      break;
    case 409:
      message.error(data.message || '资源已存在');
      break;
    case 500:
      message.error('服务器错误,请稍后重试');
      break;
    default:
      message.error(data.message || '请求失败');
  }
};

/**
 * 处理网络错误
 */
const handleNetworkError = (error) => {
  if (error.code === 'ECONNABORTED') {
    message.error('请求超时,请检查网络连接');
  } else {
    message.error('网络错误,请检查网络连接');
  }
};

/**
 * 处理其他错误
 */
const handleOtherError = (error) => {
  message.error(error.message || '发生未知错误');
};

/**
 * 清除认证信息
 */
const clearAuth = () => {
  localStorage.removeItem('token');
  localStorage.removeItem('user');
};

3.3 响应日志(开发环境)

// services/api.js

if (import.meta.env.DEV) {
  api.interceptors.response.use(
    (response) => {
      console.log('Response:', response);
      return response;
    },
    (error) => {
      console.error('Error:', error);
      return Promise.reject(error);
    }
  );
}

4. API模块化设计

4.1 认证API (services/auth.js)

// services/auth.js
import api from './api';

export const authAPI = {
  /**
   * 用户登录
   * @param {string} username - 用户名
   * @param {string} password - 密码
   */
  login: (username, password) => {
    return api.post('/auth/login', { username, password });
  },

  /**
   * 获取当前用户信息
   */
  getCurrentUser: () => {
    return api.get('/auth/me');
  },

  /**
   * 登出
   */
  logout: () => {
    return api.post('/auth/logout');
  },
};

4.2 用户API (services/user.js)

// services/user.js
import api from './api';

export const userAPI = {
  /**
   * 获取用户列表
   * @param {Object} params - 查询参数
   * @param {number} params.page - 页码
   * @param {number} params.page_size - 每页数量
   * @param {string} params.department - 部门筛选
   * @param {string} params.role - 角色筛选
   * @param {string} params.keyword - 关键词搜索
   */
  getList: (params) => {
    return api.get('/users', { params });
  },

  /**
   * 获取用户详情
   * @param {number} id - 用户ID
   */
  getDetail: (id) => {
    return api.get(`/users/${id}`);
  },

  /**
   * 创建用户
   * @param {Object} data - 用户数据
   */
  create: (data) => {
    return api.post('/users', data);
  },

  /**
   * 更新用户
   * @param {number} id - 用户ID
   * @param {Object} data - 更新数据
   */
  update: (id, data) => {
    return api.put(`/users/${id}`, data);
  },

  /**
   * 删除用户
   * @param {number} id - 用户ID
   */
  delete: (id) => {
    return api.delete(`/users/${id}`);
  },

  /**
   * 重置用户密码
   * @param {number} id - 用户ID
   * @param {string} new_password - 新密码
   */
  resetPassword: (id, new_password) => {
    return api.post(`/users/${id}/reset-password`, { new_password });
  },
};

4.3 项目API (services/project.js)

// services/project.js
import api from './api';

export const projectAPI = {
  /**
   * 获取项目列表
   * @param {Object} params - 查询参数
   */
  getList: (params) => {
    return api.get('/projects', { params });
  },

  /**
   * 获取项目详情
   * @param {number} id - 项目ID
   */
  getDetail: (id) => {
    return api.get(`/projects/${id}`);
  },

  /**
   * 创建项目
   * @param {Object} data - 项目数据
   */
  create: (data) => {
    return api.post('/projects', data);
  },

  /**
   * 更新项目
   * @param {number} id - 项目ID
   * @param {Object} data - 更新数据
   */
  update: (id, data) => {
    return api.put(`/projects/${id}`, data);
  },

  /**
   * 删除项目
   * @param {number} id - 项目ID
   */
  delete: (id) => {
    return api.delete(`/projects/${id}`);
  },

  /**
   * 批量删除项目
   * @param {Array<number>} ids - 项目ID数组
   */
  batchDelete: (ids) => {
    return api.post('/projects/batch-delete', { ids });
  },

  /**
   * 获取基础统计
   * @param {Object} params - 查询参数
   */
  getStatistics: (params) => {
    return api.get('/projects/statistics', { params });
  },

  /**
   * 获取分组统计
   * @param {Object} params - 查询参数
   * @param {string} params.group_by - 分组字段
   */
  getGroupStatistics: (params) => {
    return api.get('/projects/statistics/group', { params });
  },

  /**
   * 获取时间维度统计
   * @param {Object} params - 查询参数
   * @param {string} params.time_field - 时间字段
   */
  getTimelineStatistics: (params) => {
    return api.get('/projects/statistics/timeline', { params });
  },

  /**
   * 导出项目数据
   * @param {Object} params - 查询参数
   */
  export: (params) => {
    return api.get('/projects/export', {
      params,
      responseType: 'blob',
    });
  },
};

4.4 统计API (services/statistics.js)

// services/statistics.js
import api from './api';
import { projectAPI } from './project';

// 重用projectAPI的统计方法
export const statisticsAPI = {
  ...projectAPI.getStatistics,
  ...projectAPI.getGroupStatistics,
  ...projectAPI.getTimelineStatistics,

  /**
   * 获取仪表盘数据
   */
  getDashboard: () => {
    return api.get('/statistics/dashboard');
  },

  /**
   * 获取项目趋势
   * @param {Object} params - 查询参数
   */
  getTrend: (params) => {
    return api.get('/statistics/trend', { params });
  },
};

5. 自定义Hooks封装

5.1 useApi Hook

// hooks/useApi.js
import { useState, useCallback } from 'react';
import { message } from 'antd';

/**
 * API调用Hook
 * @param {Function} apiFunc - API函数
 */
export const useApi = (apiFunc) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const execute = useCallback(
    async (...args) => {
      try {
        setLoading(true);
        setError(null);
        const result = await apiFunc(...args);
        setData(result);
        return result;
      } catch (err) {
        setError(err);
        throw err;
      } finally {
        setLoading(false);
      }
    },
    [apiFunc]
  );

  return { data, loading, error, execute };
};

5.2 useList Hook

// hooks/useList.js
import { useState, useCallback } from 'react';

/**
 * 列表数据Hook
 * @param {Function} apiFunc - API函数
 */
export const useList = (apiFunc) => {
  const [list, setList] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 10,
  });

  const fetchList = useCallback(
    async (params = {}) => {
      try {
        setLoading(true);
        const result = await apiFunc({
          page: pagination.current,
          page_size: pagination.pageSize,
          ...params,
        });

        setList(result.items || []);
        setTotal(result.total || 0);
      } catch (error) {
        console.error('Fetch list error:', error);
      } finally {
        setLoading(false);
      }
    },
    [apiFunc, pagination]
  );

  const handleTableChange = useCallback(
    (newPagination) => {
      setPagination(newPagination);
    },
    []
  );

  const refresh = useCallback(() => {
    fetchList();
  }, [fetchList]);

  return {
    list,
    total,
    loading,
    pagination,
    fetchList,
    handleTableChange,
    refresh,
  };
};

5.3 useCreate Hook

// hooks/useCreate.js
import { useState, useCallback } from 'react';
import { message } from 'antd';

/**
 * 创建数据Hook
 * @param {Function} apiFunc - API函数
 * @param {Function} onSuccess - 成功回调
 */
export const useCreate = (apiFunc, onSuccess) => {
  const [loading, setLoading] = useState(false);

  const create = useCallback(
    async (data) => {
      try {
        setLoading(true);
        await apiFunc(data);
        message.success('创建成功');
        if (onSuccess) {
          onSuccess();
        }
      } catch (error) {
        console.error('Create error:', error);
      } finally {
        setLoading(false);
      }
    },
    [apiFunc, onSuccess]
  );

  return { create, loading };
};

5.4 useUpdate Hook

// hooks/useUpdate.js
import { useState, useCallback } from 'react';
import { message } from 'antd';

/**
 * 更新数据Hook
 * @param {Function} apiFunc - API函数
 * @param {Function} onSuccess - 成功回调
 */
export const useUpdate = (apiFunc, onSuccess) => {
  const [loading, setLoading] = useState(false);

  const update = useCallback(
    async (id, data) => {
      try {
        setLoading(true);
        await apiFunc(id, data);
        message.success('更新成功');
        if (onSuccess) {
          onSuccess();
        }
      } catch (error) {
        console.error('Update error:', error);
      } finally {
        setLoading(false);
      }
    },
    [apiFunc, onSuccess]
  );

  return { update, loading };
};

5.5 useDelete Hook

// hooks/useDelete.js
import { useState, useCallback } from 'react';
import { Modal, message } from 'antd';

/**
 * 删除数据Hook
 * @param {Function} apiFunc - API函数
 * @param {Function} onSuccess - 成功回调
 */
export const useDelete = (apiFunc, onSuccess) => {
  const [loading, setLoading] = useState(false);

  const handleDelete = useCallback(
    async (id, options = {}) => {
      const { title = '确认删除', content = '确定要删除吗?' } = options;

      Modal.confirm({
        title,
        content,
        onOk: async () => {
          try {
            setLoading(true);
            await apiFunc(id);
            message.success('删除成功');
            if (onSuccess) {
              onSuccess();
            }
          } catch (error) {
            console.error('Delete error:', error);
          } finally {
            setLoading(false);
          }
        },
      });
    },
    [apiFunc, onSuccess]
  );

  return { handleDelete, loading };
};

6. 请求工具函数

6.1 防抖函数

// utils/debounce.js

/**
 * 防抖函数
 * @param {Function} func - 需要防抖的函数
 * @param {number} delay - 延迟时间
 */
export const debounce = (func, delay = 300) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
};

6.2 节流函数

// utils/throttle.js

/**
 * 节流函数
 * @param {Function} func - 需要节流的函数
 * @param {number} delay - 延迟时间
 */
export const throttle = (func, delay = 300) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      func.apply(this, args);
      lastCall = now;
    }
  };
};

6.3 取消重复请求

// utils/request.js

const pendingRequests = new Map();

/**
 * 生成请求key
 */
const generateRequestKey = (config) => {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
};

/**
 * 取消重复请求
 */
export const cancelDuplicateRequest = (config) => {
  const requestKey = generateRequestKey(config);

  if (pendingRequests.has(requestKey)) {
    const cancel = pendingRequests.get(requestKey);
    cancel('取消重复请求');
    pendingRequests.delete(requestKey);
  }
};

/**
 * 添加待处理请求
 */
export const addPendingRequest = (config, cancel) => {
  const requestKey = generateRequestKey(config);
  pendingRequests.set(requestKey, cancel);
};

/**
 * 移除待处理请求
 */
export const removePendingRequest = (config) => {
  const requestKey = generateRequestKey(config);
  pendingRequests.delete(requestKey);
};

7. 文件上传和下载

7.1 文件上传

// services/upload.js
import api from './api';

/**
 * 上传文件
 * @param {File} file - 文件对象
 * @param {Function} onProgress - 进度回调
 */
export const uploadFile = (file, onProgress) => {
  const formData = new FormData();
  formData.append('file', file);

  return api.post('/upload', formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    onUploadProgress: (progressEvent) => {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      if (onProgress) {
        onProgress(percentCompleted);
      }
    },
  });
};

/**
 * 批量上传文件
 * @param {File[]} files - 文件数组
 */
export const uploadFiles = async (files) => {
  const uploadPromises = files.map((file) => uploadFile(file));
  return Promise.all(uploadPromises);
};

7.2 文件下载

// services/download.js
import api from './api';

/**
 * 下载文件
 * @param {string} url - 下载地址
 * @param {string} filename - 文件名
 */
export const downloadFile = async (url, filename) => {
  try {
    const response = await api.get(url, {
      responseType: 'blob',
    });

    const blob = new Blob([response]);
    const downloadUrl = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(downloadUrl);
  } catch (error) {
    console.error('Download error:', error);
  }
};

/**
 * 导出Excel
 * @param {Object} params - 查询参数
 */
export const exportExcel = async (params) => {
  try {
    const response = await api.get('/export', {
      params,
      responseType: 'blob',
    });

    const blob = new Blob([response], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });
    const downloadUrl = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = `export_${Date.now()}.xlsx`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(downloadUrl);
  } catch (error) {
    console.error('Export error:', error);
  }
};

8. 错误处理和重试

8.1 请求重试

// services/api.js

/**
 * 请求重试配置
 */
const retryConfig = {
  retries: 3,
  retryDelay: 1000,
  retryCondition: (error) => {
    // 网络错误或5xx错误时重试
    return !error.response || error.response.status >= 500;
  },
};

api.interceptors.response.use(undefined, async (error) => {
  const { config } = error;

  if (!config || !config.retryCount) {
    return Promise.reject(error);
  }

  const { retries, retryDelay, retryCondition } = retryConfig;

  // 检查是否满足重试条件
  if (!retryCondition(error)) {
    return Promise.reject(error);
  }

  // 检查重试次数
  config.retryCount = config.retryCount || 0;
  if (config.retryCount >= retries) {
    return Promise.reject(error);
  }

  // 增加重试计数
  config.retryCount++;

  // 延迟重试
  await new Promise((resolve) => setTimeout(resolve, retryDelay));

  // 重新请求
  return api(config);
});

9. 缓存策略

9.1 内存缓存

// utils/cache.js

const cache = new Map();

/**
 * 设置缓存
 */
export const setCache = (key, value, ttl = 60000) => {
  const expiry = Date.now() + ttl;
  cache.set(key, { value, expiry });
};

/**
 * 获取缓存
 */
export const getCache = (key) => {
  const item = cache.get(key);

  if (!item) {
    return null;
  }

  if (Date.now() > item.expiry) {
    cache.delete(key);
    return null;
  }

  return item.value;
};

/**
 * 清除缓存
 */
export const clearCache = (key) => {
  if (key) {
    cache.delete(key);
  } else {
    cache.clear();
  }
};

9.2 带缓存的请求

// hooks/useCachedApi.js
import { useState, useEffect } from 'react';
import { getCache, setCache } from '../utils/cache';

/**
 * 带缓存的API调用Hook
 */
export const useCachedApi = (apiFunc, cacheKey, ttl = 60000) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      // 检查缓存
      const cached = getCache(cacheKey);
      if (cached) {
        setData(cached);
        return;
      }

      // 发起请求
      try {
        setLoading(true);
        const result = await apiFunc();
        setData(result);
        setCache(cacheKey, result, ttl);
      } catch (error) {
        console.error('API error:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [apiFunc, cacheKey, ttl]);

  return { data, loading };
};

10. API使用示例

10.1 在组件中使用

import React, { useEffect } from 'react';
import { useList } from '../hooks/useList';
import { projectAPI } from '../services/project';
import Table from '../components/Common/Table';

const ProjectList = () => {
  const { list, total, loading, pagination, handleTableChange } = useList(
    projectAPI.getList
  );

  const columns = [
    {
      title: '项目编号',
      dataIndex: 'project_no',
      key: 'project_no',
    },
    {
      title: '项目名称',
      dataIndex: 'name',
      key: 'name',
    },
    // ...其他列
  ];

  return (
    <Table
      columns={columns}
      dataSource={list}
      loading={loading}
      pagination={{
        current: pagination.current,
        pageSize: pagination.pageSize,
        total,
      }}
      onChange={handleTableChange}
    />
  );
};

export default ProjectList;

10.2 表单提交

import React from 'react';
import { Form, Input, Button } from 'antd';
import { useCreate } from '../hooks/useCreate';
import { projectAPI } from '../services/project';

const ProjectForm = ({ onSuccess }) => {
  const [form] = Form.useForm();
  const { create, loading } = useCreate(projectAPI.create, onSuccess);

  const handleSubmit = async (values) => {
    await create(values);
  };

  return (
    <Form form={form} onFinish={handleSubmit} layout="vertical">
      <Form.Item
        name="name"
        label="项目名称"
        rules={[{ required: true, message: '请输入项目名称' }]}
      >
        <Input placeholder="请输入项目名称" />
      </Form.Item>
      {/* ...其他表单项 */}
      <Form.Item>
        <Button type="primary" htmlType="submit" loading={loading}>
          提交
        </Button>
      </Form.Item>
    </Form>
  );
};

export default ProjectForm;

文档维护: 前端程序员 文档类型: API服务封装文档 最后更新: 2026-01-25