---
name: Vue3接口联调
description: 用于规范 Vue 3 项目的接口联调流程,提供标准的 API 请求模式、数据转换、状态管理、错误处理和调试方案。当用户需要进行前后端接口联调时调用。
---
你是一个资深的前端架构师,请按照以下规范为用户提供 Vue 3 接口联调方案。
## 1. 核心规范
### 1.1 技术栈
- **框架**: Vue 3.4+
- **HTTP库**: Axios 1.6+
- **状态管理**: Pinia 2.x
- **语言**: TypeScript 5.0+
- **构建**: Vite 5.x
- **包管理**: pnpm
### 1.2 命名规范
- **API 模块文件**: 使用 kebab-case,例如 `user-api.ts`、`order-api.ts`
- **API 函数名**: 使用 camelCase,前缀为动词(get/list/create/update/delete),例如 `getUserList`、`createUser`
- **API Hook 名**: 使用 `use` 前缀,例如 `useUserList`、`useUserDetail`
- **状态管理 Store**: 使用 camelCase,例如 `useUserStore`、`useAuthStore`
- **Mock 数据文件**: 使用 kebab-case,例如 `user-mock.ts`、`order-mock.ts`
### 1.3 文件结构
```
src/
├── api/ # API 层
│ ├── index.ts # 主入口
│ ├── request.ts # 请求实例封装
│ ├── modules/ # API 模块
│ │ ├── user.ts
│ │ ├── order.ts
│ │ └── common.ts
│ └── mock/ # Mock 数据
│ ├── index.ts
│ ├── user.ts
│ └── order.ts
├── stores/ # 状态管理
│ ├── index.ts
│ ├── modules/
│ │ ├── user.ts
│ │ ├── auth.ts
│ │ └── app.ts
│ └── types.ts
├── composables/ # 组合式函数
│ ├── api/
│ │ ├── useUserList.ts
│ │ ├── useUserDetail.ts
│ │ └── useCreateUser.ts
│ ├── useLoading.ts
│ ├── usePagination.ts
│ └── useRequest.ts
├── types/ # 类型定义
│ ├── index.ts
│ ├── api.ts
│ ├── user.ts
│ └── order.ts
├── utils/ # 工具函数
│ ├── data-transform.ts # 数据转换
│ ├── validator.ts # 数据验证
│ └── format.ts # 数据格式化
└── views/ # 页面组件
├── User/
│ ├── List.vue
│ └── Detail.vue
└── Order/
└── List.vue
```
## 2. 代码结构模板
### 2.1 API 请求封装模板
```typescript
// src/api/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, CancelTokenSource } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { authStore } from '@/stores/modules/auth';
/**
* 基础配置
*/
const BASE_CONFIG = {
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
},
withCredentials: true
};
/**
* 状态码
*/
const STATUS_CODE = {
SUCCESS: 200,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500
};
/**
* 创建 Axios 实例
*/
const createAxiosInstance = (): AxiosInstance => {
const instance = axios.create(BASE_CONFIG);
return instance;
};
/**
* 请求实例
*/
const request = createAxiosInstance();
/**
* 请求取消管理
*/
const pendingRequests = new Map<string, CancelTokenSource>();
/**
* 生成请求唯一标识
*/
const generateRequestKey = (config: AxiosRequestConfig): string => {
const { method, url, params, data } = config;
return [
method?.toUpperCase(),
url,
JSON.stringify(params || {}),
JSON.stringify(data || {})
].join('&');
};
/**
* 添加待取消请求
*/
const addPendingRequest = (config: AxiosRequestConfig): void => {
const key = generateRequestKey(config);
const source = axios.CancelToken.source();
config.cancelToken = source.token;
if (pendingRequests.has(key)) {
pendingRequests.get(key)?.cancel('Duplicate request');
}
pendingRequests.set(key, source);
};
/**
* 移除待取消请求
*/
const removePendingRequest = (config: AxiosRequestConfig): void => {
const key = generateRequestKey(config);
pendingRequests.delete(key);
};
/**
* 取消所有请求
*/
export const cancelAllRequests = (): void => {
pendingRequests.forEach((source) => {
source.cancel('Operation canceled by the user.');
});
pendingRequests.clear();
};
/**
* 请求拦截器
*/
request.interceptors.request.use(
(config) => {
const auth = authStore();
// 添加 Token
if (auth.token) {
config.headers.Authorization = `Bearer ${auth.token}`;
}
// 管理待取消请求
addPendingRequest(config);
// 开发环境打印请求日志
if (import.meta.env.DEV) {
console.log('[Request]', {
url: config.url,
method: config.method,
params: config.params,
data: config.data
});
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
/**
* 响应拦截器
*/
request.interceptors.response.use(
(response: AxiosResponse) => {
removePendingRequest(response.config);
const { data, config } = response;
// 开发环境打印响应日志
if (import.meta.env.DEV) {
console.log('[Response]', {
url: config.url,
status: response.status,
data
});
}
// 检查业务状态码
if (data.code !== STATUS_CODE.SUCCESS) {
handleBusinessError(data);
return Promise.reject(data);
}
return data;
},
(error: AxiosError) => {
if (error.config) {
removePendingRequest(error.config);
}
handleHttpError(error);
return Promise.reject(error);
}
);
/**
* 处理业务错误
*/
const handleBusinessError = (data: any): void => {
switch (data.code) {
case STATUS_CODE.UNAUTHORIZED:
ElMessageBox.confirm('登录状态已过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const auth = authStore();
auth.logout();
window.location.href = '/login';
});
break;
case STATUS_CODE.FORBIDDEN:
ElMessage.error('没有权限访问该资源');
break;
case STATUS_CODE.NOT_FOUND:
ElMessage.error('请求的资源不存在');
break;
default:
ElMessage.error(data.message || '请求失败');
break;
}
};
/**
* 处理 HTTP 错误
*/
const handleHttpError = (error: AxiosError): void => {
if (axios.isCancel(error)) {
console.log('Request canceled:', error.message);
return;
}
let message = '网络错误';
if (error.response) {
const { status } = error.response;
switch (status) {
case 400:
message = '请求参数错误';
break;
case 401:
message = '未授权,请登录';
break;
case 403:
message = '拒绝访问';
break;
case 404:
message = '请求地址不存在';
break;
case 500:
message = '服务器错误';
break;
default:
message = `请求失败,状态码:${status}`;
break;
}
} else if (error.request) {
message = '网络连接失败,请检查网络';
}
ElMessage.error(message);
};
export default request;
```
### 2.2 API 模块模板
```typescript
// src/api/modules/user.ts
import request from '../request';
import type { IUser, IUserListParams, ICreateUserParams, IUpdateUserParams, IPageResponse, IResponse } from '@/types';
/**
* 用户 API
*/
export const userApi = {
/**
* 获取用户列表
*/
getList: (params: IUserListParams): Promise<IResponse<IPageResponse<IUser>>> => {
return request.get('/api/user/list', { params });
},
/**
* 获取用户详情
*/
getDetail: (id: number): Promise<IResponse<IUser>> => {
return request.get(`/api/user/${id}`);
},
/**
* 创建用户
*/
create: (data: ICreateUserParams): Promise<IResponse<IUser>> => {
return request.post('/api/user', data);
},
/**
* 更新用户
*/
update: (id: number, data: IUpdateUserParams): Promise<IResponse<IUser>> => {
return request.put(`/api/user/${id}`, data);
},
/**
* 删除用户
*/
delete: (id: number): Promise<IResponse<void>> => {
return request.delete(`/api/user/${id}`);
},
/**
* 批量删除用户
*/
batchDelete: (ids: number[]): Promise<IResponse<void>> => {
return request.post('/api/user/batch-delete', { ids });
},
/**
* 导出用户列表
*/
export: (params: IUserListParams): Promise<void> => {
return request.get('/api/user/export', {
params,
responseType: 'blob'
}).then((response) => {
const blob = new Blob([response.data]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `user-list-${Date.now()}.xlsx`;
link.click();
URL.revokeObjectURL(link.href);
});
},
/**
* 导入用户
*/
import: (file: File): Promise<IResponse<void>> => {
const formData = new FormData();
formData.append('file', file);
return request.post('/api/user/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
};
```
### 2.3 数据转换工具模板
```typescript
// src/utils/data-transform.ts
/**
* 数据转换器
*/
/**
* 前端数据转后端数据
*/
export const toBackend = <T = any>(data: any, mapping?: Record<string, string>): T => {
const result: any = {};
for (const [key, value] of Object.entries(data)) {
if (value === undefined || value === null) continue;
let newKey = key;
if (mapping && mapping[key]) {
newKey = mapping[key];
}
if (Array.isArray(value)) {
result[newKey] = value.map((item) => {
if (typeof item === 'object' && item !== null) {
return toBackend(item, mapping);
}
return item;
});
} else if (typeof value === 'object' && value !== null) {
result[newKey] = toBackend(value, mapping);
} else {
result[newKey] = value;
}
}
return result;
};
/**
* 后端数据转前端数据
*/
export const toFrontend = <T = any>(data: any, mapping?: Record<string, string>): T => {
const result: any = {};
for (const [key, value] of Object.entries(data)) {
let newKey = key;
if (mapping && mapping[key]) {
newKey = mapping[key];
}
if (Array.isArray(value)) {
result[newKey] = value.map((item) => {
if (typeof item === 'object' && item !== null) {
return toFrontend(item, mapping);
}
return item;
});
} else if (typeof value === 'object' && value !== null) {
result[newKey] = toFrontend(value, mapping);
} else {
result[newKey] = value;
}
}
return result;
};
/**
* 字段映射示例
*/
export const FIELD_MAPPING = {
userName: 'user_name',
createTime: 'create_time',
updateTime: 'update_time'
};
/**
* 批量转换数据
*/
export const batchTransform = <T = any>(list: any[], transformer: (data: any) => T): T[] => {
return list.map((item) => transformer(item));
};
```
### 2.4 API Hook 模板
```typescript
// src/composables/api/useUserList.ts
import { ref, reactive, onMounted, watch } from 'vue';
import { userApi } from '@/api/modules/user';
import type { IUser, IUserListParams, IPageResponse } from '@/types';
/**
* 用户列表 Hook
*/
export const useUserList = (defaultParams?: Partial<IUserListParams>) => {
// 列表数据
const list = ref<IUser[]>([]);
// 加载状态
const loading = ref(false);
// 总条数
const total = ref(0);
// 查询参数
const params = reactive<IUserListParams>({
page: 1,
pageSize: 10,
...defaultParams
});
// 加载列表
const loadList = async (resetPage = false): Promise<void> => {
if (resetPage) {
params.page = 1;
}
loading.value = true;
try {
const res = await userApi.getList({ ...params });
if (res.code === 200) {
list.value = res.data.list;
total.value = res.data.total;
}
} finally {
loading.value = false;
}
};
// 刷新
const refresh = (): Promise<void> => {
return loadList(true);
};
// 重置查询
const reset = (): Promise<void> => {
Object.assign(params, {
page: 1,
pageSize: 10,
...defaultParams
});
return loadList();
};
// 分页变化
const handlePageChange = (page: number): void => {
params.page = page;
loadList();
};
// 每页条数变化
const handleSizeChange = (size: number): void => {
params.pageSize = size;
params.page = 1;
loadList();
};
// 监听参数变化自动刷新
watch(
() => [params.page, params.pageSize],
() => {
loadList();
}
);
// 组件挂载时加载
onMounted(() => {
loadList();
});
return {
list,
loading,
total,
params,
loadList,
refresh,
reset,
handlePageChange,
handleSizeChange
};
};
```
### 2.5 通用请求 Hook 模板
```typescript
// src/composables/useRequest.ts
import { ref, computed } from 'vue';
/**
* 通用请求 Hook
*/
export const useRequest = <T = any, P extends any[] = any[]>(
requestFn: (...args: P) => Promise<T>,
options?: {
manual?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: any) => void;
defaultParams?: P;
}
) => {
const { manual = false, onSuccess, onError, defaultParams } = options || {};
const data = ref<T>();
const loading = ref(false);
const error = ref<any>();
const run = async (...args: P): Promise<T | undefined> => {
loading.value = true;
error.value = undefined;
try {
const result = await requestFn(...args);
data.value = result;
onSuccess?.(result);
return result;
} catch (err) {
error.value = err;
onError?.(err);
throw err;
} finally {
loading.value = false;
}
};
const reset = (): void => {
data.value = undefined;
loading.value = false;
error.value = undefined;
};
// 自动执行
if (!manual && defaultParams) {
run(...defaultParams);
}
return {
data,
loading,
error,
run,
reset
};
};
```
### 2.6 Pinia Store 模板
```typescript
// src/stores/modules/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { userApi } from '@/api/modules/user';
import type { IUser, IUserListParams } from '@/types';
/**
* 用户 Store
*/
export const useUserStore = defineStore('user', () => {
// 当前用户信息
const currentUser = ref<IUser | null>(null);
// 用户列表
const userList = ref<IUser[]>([]);
// 选中的用户
const selectedUsers = ref<IUser[]>([]);
// 是否有选中的用户
const hasSelected = computed(() => selectedUsers.value.length > 0);
// 选中的用户ID
const selectedIds = computed(() => selectedUsers.value.map((user) => user.id));
// 设置当前用户
const setCurrentUser = (user: IUser | null): void => {
currentUser.value = user;
};
// 加载用户列表
const loadUserList = async (params: IUserListParams): Promise<void> => {
const res = await userApi.getList(params);
if (res.code === 200) {
userList.value = res.data.list;
}
};
// 刷新用户列表
const refreshUserList = async (): Promise<void> => {
await loadUserList({ page: 1, pageSize: 10 });
};
// 清空选中
const clearSelection = (): void => {
selectedUsers.value = [];
};
// 设置选中
const setSelection = (users: IUser[]): void => {
selectedUsers.value = users;
};
// 添加选中
const addSelection = (user: IUser): void => {
const exists = selectedUsers.value.find((u) => u.id === user.id);
if (!exists) {
selectedUsers.value.push(user);
}
};
// 移除选中
const removeSelection = (userId: number): void => {
selectedUsers.value = selectedUsers.value.filter((u) => u.id !== userId);
};
return {
currentUser,
userList,
selectedUsers,
hasSelected,
selectedIds,
setCurrentUser,
loadUserList,
refreshUserList,
clearSelection,
setSelection,
addSelection,
removeSelection
};
});
```
### 2.7 Mock 数据模板
```typescript
// src/api/mock/user.ts
import type { IUser, IPageResponse } from '@/types';
/**
* 用户 Mock 数据
*/
export const mockUsers: IUser[] = [
{
id: 1,
username: 'admin',
nickname: '管理员',
email: 'admin@example.com',
phone: '13800138000',
avatar: 'https://example.com/avatar1.png',
status: 1,
createTime: Date.now(),
updateTime: Date.now()
},
{
id: 2,
username: 'user1',
nickname: '用户1',
email: 'user1@example.com',
phone: '13800138001',
avatar: 'https://example.com/avatar2.png',
status: 1,
createTime: Date.now(),
updateTime: Date.now()
}
];
/**
* Mock 用户列表接口
*/
export const mockGetUserList = (params: any): IPageResponse<IUser> => {
const { page = 1, pageSize = 10 } = params;
const start = (page - 1) * pageSize;
const end = start + pageSize;
return {
list: mockUsers.slice(start, end),
total: mockUsers.length,
page,
pageSize,
totalPages: Math.ceil(mockUsers.length / pageSize)
};
};
/**
* Mock 用户详情接口
*/
export const mockGetUserDetail = (id: number): IUser | undefined => {
return mockUsers.find((user) => user.id === id);
};
```
### 2.8 页面组件联调模板
```vue
<template>
<div class="user-list-container">
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :model="searchForm" inline @submit.prevent="handleSearch">
<el-form-item label="用户名">
<el-input
v-model="searchForm.username"
placeholder="请输入用户名"
clearable
/>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSearch">
查询
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作区域 -->
<el-card class="operation-card">
<template #header>
<div class="operation-header">
<span>用户列表</span>
<div class="operation-actions">
<el-button type="primary" @click="handleCreate">
新增
</el-button>
<el-button
type="danger"
:disabled="!hasSelected"
@click="handleBatchDelete"
>
批量删除
</el-button>
<el-button @click="handleExport">
导出
</el-button>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
stripe
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="nickname" label="昵称" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="phone" label="手机号" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleDetail(row)">
详情
</el-button>
<el-button link type="primary" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="params.page"
v-model:page-size="params.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useUserList } from '@/composables/api/useUserList';
import { userApi } from '@/api/modules/user';
import type { IUser, ICreateUserParams, IUpdateUserParams } from '@/types';
/**
* 用户列表组件
*/
// 搜索表单
const searchForm = reactive({
username: '',
status: ''
});
// 使用用户列表 Hook
const {
list,
loading,
total,
params,
loadList,
refresh
} = useUserList();
// 选中的用户
const selectedUsers = ref<IUser[]>([]);
const hasSelected = computed(() => selectedUsers.value.length > 0);
// 弹窗
const dialogVisible = ref(false);
const dialogTitle = computed(() => (isEdit.value ? '编辑用户' : '新增用户'));
const isEdit = ref(false);
const submitLoading = ref(false);
// 表单
const formRef = ref();
const formData = reactive({
id: 0,
username: '',
nickname: '',
email: '',
phone: ''
});
const formRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
};
/**
* 搜索
*/
const handleSearch = (): void => {
Object.assign(params, searchForm);
loadList(true);
};
/**
* 重置搜索
*/
const handleReset = (): void => {
Object.assign(searchForm, {
username: '',
status: ''
});
refresh();
};
/**
* 选择变化
*/
const handleSelectionChange = (selection: IUser[]): void => {
selectedUsers.value = selection;
};
/**
* 新增
*/
const handleCreate = (): void => {
isEdit.value = false;
Object.assign(formData, {
id: 0,
username: '',
nickname: '',
email: '',
phone: ''
});
dialogVisible.value = true;
};
/**
* 详情
*/
const handleDetail = (row: IUser): void => {
console.log('详情:', row);
};
/**
* 编辑
*/
const handleEdit = (row: IUser): void => {
isEdit.value = true;
Object.assign(formData, row);
dialogVisible.value = true;
};
/**
* 删除
*/
const handleDelete = async (row: IUser): Promise<void> => {
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
type: 'warning'
});
await userApi.delete(row.id);
ElMessage.success('删除成功');
await refresh();
};
/**
* 批量删除
*/
const handleBatchDelete = async (): Promise<void> => {
const ids = selectedUsers.value.map((user) => user.id);
await ElMessageBox.confirm(`确定要删除选中的 ${ids.length} 个用户吗?`, '提示', {
type: 'warning'
});
await userApi.batchDelete(ids);
ElMessage.success('删除成功');
selectedUsers.value = [];
await refresh();
};
/**
* 导出
*/
const handleExport = async (): Promise<void> => {
await userApi.export({ ...params, ...searchForm });
ElMessage.success('导出成功');
};
/**
* 提交
*/
const handleSubmit = async (): Promise<void> => {
await formRef.value.validate();
submitLoading.value = true;
try {
if (isEdit.value) {
const data: IUpdateUserParams = {
id: formData.id,
nickname: formData.nickname,
email: formData.email,
phone: formData.phone
};
await userApi.update(formData.id, data);
} else {
const data: ICreateUserParams = {
username: formData.username,
password: '123456',
nickname: formData.nickname,
email: formData.email,
phone: formData.phone
};
await userApi.create(data);
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
dialogVisible.value = false;
await refresh();
} finally {
submitLoading.value = false;
}
};
/**
* 弹窗关闭
*/
const handleDialogClose = (): void => {
formRef.value?.resetFields();
};
/**
* 分页变化
*/
const handlePageChange = (page: number): void => {
params.page = page;
loadList();
};
const handleSizeChange = (size: number): void => {
params.pageSize = size;
params.page = 1;
loadList();
};
</script>
<style lang="scss" scoped>
.user-list-container {
.search-card,
.operation-card {
margin-bottom: 20px;
}
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
.operation-actions {
display: flex;
gap: 10px;
}
}
}
</style>
```
## 3. 注意事项
### 3.1 接口设计规范
- **RESTful 风格**: 使用标准的 HTTP 方法(GET/POST/PUT/DELETE)
- **统一响应格式**: 所有接口返回统一的数据结构(code/message/data)
- **状态码规范**: 业务状态码和 HTTP 状态码分离
- **分页参数**: 统一使用 page/pageSize 参数
### 3.2 数据转换
- **前端到后端**: 数据提交前进行字段映射和格式转换
- **后端到前端**: 数据接收后进行字段映射和格式转换
- **日期处理**: 统一日期格式和时区处理
- **类型转换**: 确保数值、布尔值等类型正确
### 3.3 错误处理
- **统一错误提示**: 使用 Element Plus 消息组件统一提示
- **业务错误**: 根据业务状态码进行不同处理
- **网络错误**: 友好的网络异常提示
- **错误日志**: 开发环境打印详细错误日志
### 3.4 性能优化
- **请求防抖**: 搜索、筛选等操作添加防抖
- **请求取消**: 路由切换时取消未完成的请求
- **数据缓存**: 合理使用缓存避免重复请求
- **懒加载**: 列表分页、图片懒加载等
### 3.5 调试技巧
- **开发环境**: 打印请求和响应日志
- **Network 面板**: 使用浏览器开发者工具查看请求详情
- **Mock 数据**: 后端未完成时使用 Mock 数据开发
- **代理配置**: Vite 代理解决跨域问题
### 3.6 禁止事项
- 禁止直接在组件中使用 axios,必须使用封装后的 API
- 禁止在接口调用后不处理错误
- 禁止硬编码 API 地址
- 禁止在组件中写复杂的业务逻辑
- 禁止忽略 loading 状态
## 4. 执行步骤
1. **确认接口文档**: 获取后端接口文档,确认接口地址、参数、响应格式
2. **定义类型**: 根据接口文档定义 TypeScript 类型
3. **封装 API 模块**: 创建 API 模块,封装接口调用
4. **创建 API Hook**: 创建组合式函数,管理接口状态和逻辑
5. **创建 Store (可选)**: 使用 Pinia 管理共享状态
6. **开发页面组件**: 在组件中使用 API Hook
7. **联调测试**: 与后端进行接口联调测试
8. **Mock 数据**: 后端未完成时使用 Mock 数据开发
## 5. Vite 代理配置
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
```
## 6. 依赖安装
```bash
pnpm add axios pinia
pnpm add -D @types/node
```