---
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
```
