API 类型
API 类型定义用于前后端数据交互,确保请求和响应的类型安全。
🎯 核心 API 类型
1. Result 类型
统一的 API 响应格式,采用元组形式处理错误和数据。
typescript
// 定义
type Result<T = any> = Promise<[Error | null, T | null]>
// 使用场景
export const getUser = (id: string): Result<UserVo> => {
return http.get<UserVo>(`/system/user/${id}`)
}
// 组件中使用
const [err, data] = await getUser('123')
if (err) {
console.error('获取用户失败', err)
return
}
console.log('用户信息', data)优势:
- ✅ 强制错误处理(解构时必须处理 err)
- ✅ 类型安全(data 自动推导类型)
- ✅ 简洁优雅(无需 try-catch)
- ✅ 避免异常穿透(不会意外中断程序)
- ✅ 便于测试(明确的错误和数据分离)
2. R 类型
后端标准响应结构。
typescript
// 定义
interface R<T = any> {
/** 响应状态码 */
code: number
/** 响应消息 */
msg: string
/** 响应数据 */
data: T
}
// 使用示例
const response: R<UserVo> = {
code: 200,
msg: '操作成功',
data: {
id: '1',
username: 'admin',
nickname: '管理员'
}
}3. PageResult 类型
分页响应数据结构。
typescript
// 定义
interface PageResult<T = any> {
/** 数据记录列表 */
records: T[]
/** 总记录数 */
total: number
/** 总页数 */
pages: number
/** 当前页码 */
current: number
/** 每页大小 */
size: number
/** 是否为最后一页 */
last: boolean
}
// 使用示例
export const pageUsers = (query?: UserQuery): Result<PageResult<UserVo>> => {
return http.get<PageResult<UserVo>>('/system/user/page', query)
}
// 组件中使用
const [err, data] = await pageUsers({ pageNum: 1, pageSize: 10 })
if (!err && data) {
tableData.value = data.records
total.value = data.total
currentPage.value = data.current
}4. PageQuery 类型
分页查询参数。
typescript
// 定义
interface PageQuery {
/** 当前页码,从1开始 */
pageNum?: number
/** 每页显示记录数 */
pageSize?: number
/** 排序字段 */
orderByColumn?: string
/** 排序方向 asc/desc */
isAsc?: string
/** 模糊搜索关键词 */
searchValue?: string
/** 扩展查询参数 */
params?: Record<string, any>
}
// 使用示例
const query: PageQuery = {
pageNum: 1,
pageSize: 10,
orderByColumn: 'createTime',
isAsc: 'desc',
searchValue: '张三',
params: {
beginCreateTime: '2024-01-01',
endCreateTime: '2024-12-31'
}
}📦 业务对象类型
1. BO (Business Object)
业务对象,用于表单提交和业务逻辑处理。
typescript
// 定义
interface UserBo {
id?: string | number
username: string
nickname: string
email?: string
phone?: string
status: string
roleIds?: number[]
}
// 使用示例
export const addUser = (data: UserBo): Result<string | number> => {
return http.post<string | number>('/system/user/add', data)
}
export const updateUser = (data: UserBo): Result<void> => {
return http.put<void>('/system/user/update', data)
}命名规范:
- 新增:
add{EntityName} - 修改:
update{EntityName} - 接口参数类型:
{EntityName}Bo
2. VO (View Object)
视图对象,用于数据展示。
typescript
// 定义
interface UserVo {
id: string | number
username: string
nickname: string
email?: string
phone?: string
status: string
statusName: string
createTime: string
updateTime?: string
roles?: RoleVo[]
deptName?: string
}
// 使用示例
export const getUser = (id: string): Result<UserVo> => {
return http.get<UserVo>(`/system/user/${id}`)
}VO 特点:
- 包含关联对象(如
roles) - 包含显示名称(如
statusName) - 包含时间戳(如
createTime) - 只读,不用于提交
3. Query 类型
查询参数对象,继承 PageQuery。
typescript
// 定义
interface UserQuery extends PageQuery {
username?: string
nickname?: string
phone?: string
status?: string
deptId?: string | number
}
// 使用示例
export const pageUsers = (query?: UserQuery): Result<PageResult<UserVo>> => {
return http.get<PageResult<UserVo>>('/system/user/page', query)
}🔧 HTTP 请求类型
CustomHeaders 接口
自定义请求头配置。
typescript
// 定义 (http.d.ts)
export interface CustomHeaders {
/** 是否需要认证,默认 true */
auth?: boolean
/** 是否需要租户ID,默认 true */
tenant?: boolean
/** 是否防止重复提交,默认 true */
repeatSubmit?: boolean
/** 是否加密请求数据 */
isEncrypt?: boolean
/** 其他自定义头部 */
[key: string]: any
}
// 使用示例
export const login = (data: LoginBo): Result<LoginVo> => {
return http.post<LoginVo>('/auth/login', data, {
headers: {
auth: false, // 登录接口不需要认证
tenant: true,
repeatSubmit: false
}
})
}📋 API 函数定义规范
1. CRUD 操作
typescript
// 分页查询
export const pageAds = (query?: AdQuery): Result<PageResult<AdVo>> => {
return http.get<PageResult<AdVo>>('/base/ad/pageAds', query)
}
// 列表查询
export const listAds = (query?: AdQuery): Result<AdVo[]> => {
return http.get<AdVo[]>('/base/ad/listAds', query)
}
// 详情查询
export const getAd = (id: string | number): Result<AdVo> => {
return http.get<AdVo>(`/base/ad/getAd/${id}`)
}
// 新增
export const addAd = (data: AdBo): Result<string | number> => {
return http.post<string | number>('/base/ad/addAd', data)
}
// 修改
export const updateAd = (data: AdBo): Result<void> => {
return http.put<void>('/base/ad/updateAd', data)
}
// 删除
export const deleteAds = (ids: (string | number)[]): Result<void> => {
return http.delete<void>(`/base/ad/deleteAds/${ids}`)
}
// 批量操作
export const batchUpdateStatus = (
ids: (string | number)[],
status: string
): Result<void> => {
return http.put<void>('/base/ad/batchUpdateStatus', { ids, status })
}2. 文件上传
typescript
// 单文件上传
export const uploadFile = (file: File): Result<string> => {
const formData = new FormData()
formData.append('file', file)
return http.post<string>('/system/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 多文件上传
export const uploadFiles = (files: File[]): Result<string[]> => {
const formData = new FormData()
files.forEach(file => formData.append('files', file))
return http.post<string[]>('/system/uploadMulti', formData)
}
// 带进度的文件上传
export const uploadWithProgress = (
file: File,
onProgress: (progress: number) => void
): Result<string> => {
const formData = new FormData()
formData.append('file', file)
return http.post<string>('/system/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
)
onProgress(percentCompleted)
}
})
}3. 导出导入
typescript
// 导出(返回 Blob)
export const exportAds = (query?: AdQuery): Promise<Blob> => {
return http.get<Blob>('/base/ad/export', query, {
responseType: 'blob'
})
}
// 导入
export const importAds = (file: File): Result<void> => {
const formData = new FormData()
formData.append('file', file)
return http.post<void>('/base/ad/import', formData)
}
// 下载模板
export const downloadTemplate = (): Promise<Blob> => {
return http.get<Blob>('/base/ad/template', {}, {
responseType: 'blob'
})
}
// 导出处理示例
async function handleExport() {
const blob = await exportAds(queryParams.value)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `广告数据_${Date.now()}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
}4. 特殊操作类型
typescript
// 启用/禁用
export const changeStatus = (
id: string | number,
status: string
): Result<void> => {
return http.put<void>(`/system/user/changeStatus/${id}`, { status })
}
// 重置密码
export const resetPassword = (
id: string | number,
password: string
): Result<void> => {
return http.put<void>(`/system/user/resetPassword/${id}`, { password })
}
// 修改自己的密码
export const updatePassword = (
oldPassword: string,
newPassword: string
): Result<void> => {
return http.put<void>('/system/user/updatePassword', {
oldPassword,
newPassword
})
}
// 获取验证码
export const getCaptcha = (): Result<CaptchaVo> => {
return http.get<CaptchaVo>('/auth/captcha')
}
// 发送短信验证码
export const sendSms = (phone: string, scene: string): Result<void> => {
return http.post<void>('/auth/sendSms', { phone, scene })
}🎯 组件中使用 API
1. 基础使用
typescript
<script setup lang="ts">
import { ref } from 'vue'
import { pageAds, getAd, addAd, updateAd, deleteAds } from '@/api/business/base/ad/adApi'
import type { AdVo, AdBo, AdQuery } from '@/api/business/base/ad/types'
// 列表数据
const tableData = ref<AdVo[]>([])
const total = ref(0)
const loading = ref(false)
// 查询参数
const queryParams = ref<AdQuery>({
pageNum: 1,
pageSize: 10
})
// 查询列表
async function getList() {
loading.value = true
const [err, data] = await pageAds(queryParams.value)
loading.value = false
if (err) {
console.error('查询失败', err)
return
}
if (data) {
tableData.value = data.records
total.value = data.total
}
}
// 新增/修改
async function handleSubmit(form: AdBo) {
const api = form.id ? updateAd : addAd
const [err] = await api(form)
if (err) {
ElMessage.error('操作失败')
return
}
ElMessage.success('操作成功')
getList()
}
// 删除
async function handleDelete(ids: (string | number)[]) {
const [err] = await deleteAds(ids)
if (err) {
ElMessage.error('删除失败')
return
}
ElMessage.success('删除成功')
getList()
}
</script>2. 错误处理
typescript
// 统一错误处理
async function handleRequest<T>(
request: Result<T>,
successMsg?: string,
errorMsg?: string
): Promise<T | null> {
const [err, data] = await request
if (err) {
ElMessage.error(errorMsg || '操作失败')
return null
}
if (successMsg) {
ElMessage.success(successMsg)
}
return data
}
// 使用
const data = await handleRequest(
getAd('123'),
undefined,
'获取详情失败'
)
if (data) {
form.value = data
}3. 并发请求
typescript
// 同时请求多个接口
async function initData() {
const [
[err1, userData],
[err2, roleData],
[err3, deptData]
] = await Promise.all([
getUser('123'),
getRoleList(),
getDeptTree()
])
if (err1 || err2 || err3) {
ElMessage.error('数据加载失败')
return
}
// 处理数据
user.value = userData
roleOptions.value = roleData || []
deptTree.value = deptData || []
}4. 防抖节流请求
typescript
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
// 防抖搜索
const debouncedSearch = useDebounceFn((keyword: string) => {
queryParams.value.searchValue = keyword
queryParams.value.pageNum = 1
getList()
}, 500)
// 节流滚动加载
const throttledLoadMore = useThrottleFn(async () => {
if (loading.value || !hasMore.value) return
queryParams.value.pageNum++
const [err, data] = await pageAds(queryParams.value)
if (!err && data) {
tableData.value.push(...data.records)
hasMore.value = !data.last
}
}, 1000)5. 轮询请求
typescript
import { useIntervalFn } from '@vueuse/core'
// 轮询查询订单状态
const { pause, resume } = useIntervalFn(async () => {
const [err, data] = await getOrderStatus(orderId.value)
if (!err && data) {
orderStatus.value = data.status
// 支付成功或失败时停止轮询
if (data.status === 'SUCCESS' || data.status === 'FAILED') {
pause()
}
}
}, 2000)
// 组件卸载时停止轮询
onUnmounted(() => {
pause()
})📝 类型定义文件示例
adApi.ts
typescript
import type { Result, PageResult } from '@/types/global'
import type { AdVo, AdBo, AdQuery } from './types'
import http from '@/utils/http'
/**
* 广告配置 API
*/
// 分页查询
export const pageAds = (query?: AdQuery): Result<PageResult<AdVo>> => {
return http.get<PageResult<AdVo>>('/base/ad/pageAds', query)
}
// 详情查询
export const getAd = (id: string | number): Result<AdVo> => {
return http.get<AdVo>(`/base/ad/getAd/${id}`)
}
// 新增
export const addAd = (data: AdBo): Result<string | number> => {
return http.post<string | number>('/base/ad/addAd', data)
}
// 修改
export const updateAd = (data: AdBo): Result<void> => {
return http.put<void>('/base/ad/updateAd', data)
}
// 删除
export const deleteAds = (ids: (string | number)[]): Result<void> => {
return http.delete<void>(`/base/ad/deleteAds/${ids}`)
}types.ts
typescript
/**
* 广告配置业务对象
*/
export interface AdBo {
id?: string | number
adName: string
adPosition: string
adLink?: string
adImage?: string
adSort?: number
status: string
beginTime?: string
endTime?: string
}
/**
* 广告配置视图对象
*/
export interface AdVo {
id: string | number
adName: string
adPosition: string
adPositionName: string
adLink?: string
adImage?: string
adSort: number
status: string
statusName: string
beginTime?: string
endTime?: string
createTime: string
}
/**
* 广告配置查询参数
*/
export interface AdQuery extends PageQuery {
adName?: string
adPosition?: string
status?: string
}🔄 请求与响应拦截
请求拦截器
typescript
// http.ts
import axios from 'axios'
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
const http: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000
})
// 请求拦截器
http.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 添加 token
const token = useUserStore().getToken()
if (token && config.headers.auth !== false) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加租户 ID
const tenantId = useUserStore().getTenantId()
if (tenantId && config.headers.tenant !== false) {
config.headers['Tenant-Id'] = tenantId
}
// 防重复提交
if (config.headers.repeatSubmit !== false) {
const url = config.url
const data = JSON.stringify(config.data)
const requestKey = `${url}_${data}`
const lastRequest = sessionStorage.getItem(requestKey)
if (lastRequest && Date.now() - Number(lastRequest) < 1000) {
return Promise.reject(new Error('请勿重复提交'))
}
sessionStorage.setItem(requestKey, String(Date.now()))
}
return config
},
(error) => {
return Promise.reject(error)
}
)响应拦截器
typescript
// 响应拦截器
http.interceptors.response.use(
(response) => {
const res: R = response.data
// 处理文件下载
if (response.config.responseType === 'blob') {
return response.data
}
// 处理业务错误码
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败')
// 401 未授权
if (res.code === 401) {
useUserStore().logout()
router.push('/login')
}
return [new Error(res.msg), null]
}
// 成功返回
return [null, res.data]
},
(error) => {
let message = '请求失败'
if (error.response) {
const { status, data } = error.response
switch (status) {
case 400:
message = data.msg || '请求参数错误'
break
case 401:
message = '未授权,请重新登录'
useUserStore().logout()
router.push('/login')
break
case 403:
message = '没有权限访问该资源'
break
case 404:
message = '请求的资源不存在'
break
case 500:
message = '服务器错误'
break
default:
message = data.msg || '请求失败'
}
} else if (error.request) {
message = '网络请求失败,请检查网络连接'
}
ElMessage.error(message)
return [error, null]
}
)🎨 高级用法
1. 泛型 API 函数
typescript
// 通用 CRUD 工厂函数
function createCrudApi<VO, BO, Query extends PageQuery = PageQuery>(
baseUrl: string
) {
return {
page: (query?: Query): Result<PageResult<VO>> => {
return http.get<PageResult<VO>>(`${baseUrl}/page`, query)
},
list: (query?: Query): Result<VO[]> => {
return http.get<VO[]>(`${baseUrl}/list`, query)
},
get: (id: string | number): Result<VO> => {
return http.get<VO>(`${baseUrl}/${id}`)
},
add: (data: BO): Result<string | number> => {
return http.post<string | number>(baseUrl, data)
},
update: (data: BO): Result<void> => {
return http.put<void>(baseUrl, data)
},
delete: (ids: (string | number)[]): Result<void> => {
return http.delete<void>(`${baseUrl}/${ids}`)
}
}
}
// 使用
const userApi = createCrudApi<UserVo, UserBo, UserQuery>('/system/user')
// 调用
const [err, data] = await userApi.page({ pageNum: 1, pageSize: 10 })2. API 组合与复用
typescript
// 基础 API 函数
const baseApi = {
changeStatus: (id: string | number, status: string): Result<void> => {
return http.put<void>(`/base/changeStatus/${id}`, { status })
},
batchDelete: (ids: (string | number)[]): Result<void> => {
return http.delete<void>(`/base/batchDelete/${ids}`)
}
}
// 继承基础 API
const userApi = {
...baseApi,
...createCrudApi<UserVo, UserBo, UserQuery>('/system/user'),
// 用户特有的方法
resetPassword: (id: string | number): Result<void> => {
return http.put<void>(`/system/user/resetPassword/${id}`)
},
assignRoles: (userId: string | number, roleIds: number[]): Result<void> => {
return http.put<void>(`/system/user/assignRoles/${userId}`, { roleIds })
}
}3. 缓存与优化
typescript
import { useStorage } from '@vueuse/core'
// API 缓存包装器
function withCache<T>(
apiFunc: () => Result<T>,
cacheKey: string,
ttl: number = 5 * 60 * 1000 // 5分钟
): Result<T> {
const cached = useStorage<{ data: T; timestamp: number }>(cacheKey, null)
// 检查缓存是否有效
if (cached.value && Date.now() - cached.value.timestamp < ttl) {
return Promise.resolve([null, cached.value.data])
}
// 请求新数据
return apiFunc().then(([err, data]) => {
if (!err && data) {
cached.value = {
data,
timestamp: Date.now()
}
}
return [err, data]
})
}
// 使用
const getDictList = (): Result<DictItem[]> => {
return withCache(
() => http.get<DictItem[]>('/system/dict/list'),
'dict_list',
10 * 60 * 1000 // 10分钟缓存
)
}✅ API 类型最佳实践
1. 明确返回类型
所有 API 函数必须指定返回类型:
typescript
// ✅ 正确
export const getUser = (id: string): Result<UserVo> => {
return http.get<UserVo>(`/system/user/${id}`)
}
// ❌ 错误 - 缺少返回类型
export const getUser = (id: string) => {
return http.get(`/system/user/${id}`)
}2. 使用 Result 包装
统一使用 Result<T> 处理异步响应:
typescript
// ✅ 正确
const [err, data] = await getUser('123')
if (err) {
console.error('获取失败', err)
return
}
// ❌ 错误 - 使用 try-catch
try {
const data = await getUser('123')
} catch (err) {
console.error('获取失败', err)
}3. BO/VO 分离
提交用 BO,展示用 VO:
typescript
// ✅ 正确
interface UserBo {
username: string
password: string
roleIds: number[]
}
interface UserVo {
id: string
username: string
nickname: string
roles: RoleVo[]
}
// ❌ 错误 - 混用
interface User {
id?: string
username: string
password?: string
roleIds?: number[]
roles?: RoleVo[]
}4. Query 继承 PageQuery
查询参数统一继承分页参数:
typescript
// ✅ 正确
interface UserQuery extends PageQuery {
username?: string
status?: string
}
// ❌ 错误 - 重复定义分页参数
interface UserQuery {
pageNum?: number
pageSize?: number
username?: string
status?: string
}5. 类型导出
在 types.ts 中集中导出业务类型:
typescript
// types.ts
export interface UserVo { ... }
export interface UserBo { ... }
export interface UserQuery { ... }
export interface RoleVo { ... }
// api.ts
import type { UserVo, UserBo, UserQuery } from './types'6. 错误处理
始终处理 Result 的错误情况:
typescript
// ✅ 正确
const [err, data] = await getUser('123')
if (err) {
ElMessage.error('获取用户失败')
return
}
// ❌ 错误 - 未处理错误
const [, data] = await getUser('123')
tableData.value = data.records // data 可能为 null7. 泛型约束
合理使用泛型提供类型推导:
typescript
// ✅ 正确
function request<T>(url: string): Result<T> {
return http.get<T>(url)
}
// 使用时自动推导类型
const [err, user] = await request<UserVo>('/system/user/1')
// ❌ 错误 - 返回 any
function request(url: string): Promise<any> {
return http.get(url)
}8. 命名规范
API 函数命名应符合语义:
typescript
// ✅ 正确命名
getUser // 获取单个
pageUsers // 分页查询
listUsers // 列表查询
addUser // 新增
updateUser // 修改
deleteUsers // 删除(复数表示可批量)
changeUserStatus // 修改状态
// ❌ 错误命名
fetchUser // 不明确
userList // 应该是动词开头
saveUser // 不明确是新增还是修改9. 参数简化
当只有一个参数时,可以直接传递:
typescript
// ✅ 推荐
export const getUser = (id: string | number): Result<UserVo> => {
return http.get<UserVo>(`/system/user/${id}`)
}
// ⚠️ 可以但不推荐
export const getUser = (params: { id: string | number }): Result<UserVo> => {
return http.get<UserVo>(`/system/user/${params.id}`)
}10. 请求配置
将请求配置抽取为常量:
typescript
// config.ts
export const API_TIMEOUT = 30000
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
// http.ts
const http = axios.create({
baseURL: API_BASE_URL,
timeout: API_TIMEOUT
})📚 总结
API 类型系统提供了完整的前后端交互类型安全保障:
- Result 类型 - 统一的异步错误处理模式
- 业务对象分离 - BO/VO 各司其职,提升类型安全
- 泛型支持 - 灵活的类型推导和复用
- 拦截器增强 - 统一处理认证、错误、防重
- 最佳实践 - 规范化的 API 定义和使用方式
完整的 API 类型定义确保前后端数据交互的类型安全和开发效率。
