Robot Admin 接口调用实践指南
写在前面
本文档基于 Robot Admin 项目实践,介绍如何使用 OpenAPI + SDK 自动生成 的现代化接口管理方案。相比传统手写 API 层,这种方式能减少 70% 的重复代码,提升 90% 的类型安全性,并实现 零维护成本 的接口更新。
🎯 为什么要用 SDK 自动生成?
传统方式的痛点
| 痛点场景 | 问题描述 | 时间浪费 | 风险等级 |
|---|---|---|---|
| 手写接口层 | 每个接口都要写 TS 类型、请求函数 | 30分钟/接口 | ⭐⭐⭐⭐ |
| 类型不一致 | 前后端类型定义分离,容易不同步 | 2小时/次修复 | ⭐⭐⭐⭐⭐ |
| 接口变更 | 后端修改接口,前端手动同步更新 | 1天/次对接 | ⭐⭐⭐⭐⭐ |
| 文档过时 | 接口文档更新不及时,对着错的调 | 半天/次排查 | ⭐⭐⭐⭐ |
| 重复劳动 | 增删改查重复写相似代码 | 1小时/模块 | ⭐⭐⭐ |
SDK 方案的优势
typescript
// api/user.ts - 需要手动编写和维护
export interface User {
id: number
name: string
email: string
// 后端加了新字段?手动加上...
}
export interface UserListParams {
page?: number
pageSize?: number
keyword?: string
// 参数变了?手动改...
}
export const getUserList = (params: UserListParams) => {
return request.get<ApiResponse<User[]>>('/api/users', { params })
}
export const getUserById = (id: number) => {
return request.get<User>(`/api/users/${id}`)
}
export const createUser = (data: Partial<User>) => {
return request.post<User>('/api/users', data)
}
// ... 每个接口都要这样写,累死了 😵1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typescript
// 1. 后端提供 OpenAPI 规范文件(Swagger JSON)
// 2. 运行命令:bun run generate-api
// 3. 自动生成完整的 SDK 代码和类型定义
// 直接使用,开箱即用 🚀
import {
getEmployeesList, // ✅ 自动生成
getEmployeesById, // ✅ 自动生成
putEmployeesById, // ✅ 自动生成
deleteEmployeesById, // ✅ 自动生成
type Employee, // ✅ 类型自动同步
} from '@/api/generated'
// 完美的类型提示和自动补全
const { data, error } = await getEmployeesList({
query: {
page: 1,
pageSize: 10,
keyword: '张三', // 💡 IDE 自动提示所有可用参数
},
})
// 零维护成本:后端接口更新 → 重新生成 → 前端自动同步 ✨1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
收益对比
| 对比维度 | 传统方式 | SDK 方式 | 提升幅度 |
|---|---|---|---|
| 开发效率 | 30分钟/接口 | 0秒(自动生成) | ∞ |
| 类型安全 | 50%(容易漏) | 100%(自动同步) | 100% ⬆️ |
| 维护成本 | 1天/次接口变更 | 1分钟(重新生成) | 99% ⬇️ |
| 代码质量 | 依赖个人水平 | 统一标准 | 80% ⬆️ |
| 学习成本 | 需要熟悉项目规范 | 看文档即可 | 70% ⬇️ |
关键收益
- 70% 代码减少(不用写接口层)
- 90% 类型安全提升(自动同步)
- 99% 维护成本降低(一键生成)
- ∞ 开发效率提升(零手写)
🔧 技术选型与架构
技术栈
| 工具 | 版本 | 作用 | 官方文档 |
|---|---|---|---|
| @hey-api/openapi-ts | ^0.81.1 | OpenAPI → TypeScript SDK 生成器 | 文档 |
| OpenAPI 3.0 | 3.0+ | 接口规范标准 | 规范 |
| Axios | ^1.9.0 | HTTP 请求库 | 文档 |
| TypeScript | ~5.8.0 | 类型系统 | 文档 |
架构设计
mermaid
graph LR
A[后端 API] -->|生成| B[openapi.json]
B -->|@hey-api/openapi-ts| C[SDK 代码生成]
C --> D[sdk.gen.ts<br/>API 函数]
C --> E[types.gen.ts<br/>TypeScript 类型]
C --> F[client-config.ts<br/>客户端配置]
D --> G[业务组件]
E --> G
F --> G1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
目录结构
bash
src/api/
├── generated/ # 🤖 自动生成目录(不要手动修改)
│ ├── sdk.gen.ts # SDK API 函数(137行)
│ ├── types.gen.ts # TypeScript 类型定义(469行)
│ ├── client-config.ts # 客户端配置(baseURL等)
│ ├── client.gen.ts # HTTP 客户端实例
│ └── index.ts # 统一导出
│
├── auth.ts # 认证相关接口(手动维护)
├── permission-manage.ts # 权限管理接口(手动维护)
│
scripts/
└── download-openapi.js # 下载 OpenAPI 规范文件脚本
│
openapi-ts.config.ts # SDK 生成配置文件
openapi.json # OpenAPI 规范文件(从后端获取)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
文件说明
- generated/ 目录下的所有文件都是自动生成的,不要手动修改
- 每次运行
bun run generate-api都会覆盖重新生成 - 手动维护的接口(如 auth.ts)放在
api/根目录
📦 快速开始
1. 配置 OpenAPI 生成器
typescript
// 项目根目录配置文件
export default {
// 输入:OpenAPI 规范文件
input: './openapi.json',
// 输出:生成代码目录
output: 'src/api/generated',
// 插件配置
plugins: [
'@hey-api/typescript', // 生成 TypeScript 类型定义
{
name: '@hey-api/sdk', // 生成 SDK 函数
asClass: false, // 使用函数导出(支持 tree-shaking)
},
],
// 类型配置
types: {
enums: 'javascript', // 枚举用对象而不是 TS enum(更灵活)
},
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
json
{
"scripts": {
// 下载 OpenAPI 规范 + 生成 SDK
"generate-api": "node scripts/download-openapi.js && openapi-ts"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.81.1"
},
"dependencies": {
"axios": "^1.9.0"
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2. 下载 OpenAPI 规范文件
javascript
// 从后端 Swagger 接口下载最新的 OpenAPI 规范
const fs = require('fs')
const https = require('https')
const OPENAPI_URL = process.env.VITE_API_BASE + '/v3/api-docs'
const OUTPUT_FILE = './openapi.json'
console.log('📥 正在下载 OpenAPI 规范...')
console.log(`🔗 URL: ${OPENAPI_URL}`)
https
.get(OPENAPI_URL, res => {
let data = ''
res.on('data', chunk => {
data += chunk
})
res.on('end', () => {
fs.writeFileSync(OUTPUT_FILE, data)
console.log('✅ OpenAPI 规范下载成功!')
console.log(`📁 保存位置: ${OUTPUT_FILE}`)
})
})
.on('error', err => {
console.error('❌ 下载失败:', err.message)
process.exit(1)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3. 生成 SDK 代码
bash
# 方式一:直接运行生成命令
bun run generate-api
# 输出示例:
# 📥 正在下载 OpenAPI 规范...
# 🔗 URL: https://api.example.com/v3/api-docs
# ✅ OpenAPI 规范下载成功!
# 📁 保存位置: ./openapi.json
# 🚀 正在生成 SDK...
# ✅ 生成完成!生成文件:
# - src/api/generated/sdk.gen.ts (137行)
# - src/api/generated/types.gen.ts (469行)
# - src/api/generated/client-config.ts
# 方式二:分步执行
node scripts/download-openapi.js # 下载 OpenAPI 规范
openapi-ts # 生成 SDK 代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. 配置客户端
typescript
// 自动生成的配置文件,设置 baseURL
import { client } from './client.gen'
const { VITE_API_BASE } = import.meta.env
// 配置 SDK 客户端的 baseURL
client.setConfig({
baseUrl: VITE_API_BASE || '',
})
export { client }1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
typescript
// 在应用启动时导入配置
import '@/api/generated/client-config' // ⚠️ 确保全局导入
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
重要提示
必须在 main.ts 中导入 client-config.ts,否则 SDK 无法正确配置 baseURL!
🚀 推荐用法:SDK 方式(80% 场景)
基础用法:数据加载
vue
<script setup lang="ts">
import { getEmployeesList, type Employee } from '@/api/generated'
// 方式一:手动调用
const employees = ref<Employee[]>([])
const loading = ref(false)
const loadData = async () => {
loading.value = true
try {
const { data, error } = await getEmployeesList({
query: {
page: 1,
pageSize: 10,
keyword: '',
},
})
if (error) {
console.error('加载失败:', error)
return
}
if (data?.code === '0') {
employees.value = data.data || []
}
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vue
<script setup lang="ts">
import { getEmployeesList } from '@/api/generated'
import { useTableData } from '@/composables/Table/useTableData'
// 方式二:使用 useTableData 组合式函数(推荐)
const { tableData, loading, refresh } = useTableData(params =>
getEmployeesList({ query: params })
)
// tableData - 响应式数据
// loading - 加载状态
// refresh - 刷新函数
// 自动处理分页、排序、筛选等
</script>
<template>
<C_Table
:data="tableData"
:loading="loading"
@refresh="refresh"
/>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
进阶用法:完整 CRUD
vue
<script setup lang="ts">
import {
getEmployeesList,
getEmployeesById,
putEmployeesById,
deleteEmployeesById,
type Employee,
} from '@/api/generated'
import { useTableData } from '@/composables/Table/useTableData'
import { createTableActions } from '@/composables/Table/createTableActions'
// ============ 数据加载 ============
const { tableData, loading, refresh } = useTableData(params =>
getEmployeesList({ query: params })
)
// ============ CRUD 操作配置 ============
const tableActions = createTableActions<Employee>({
apis: {
update: putEmployeesById, // 编辑
delete: deleteEmployeesById, // 删除
detail: getEmployeesById, // 详情
},
custom: [
{
key: 'copy',
label: '复制',
icon: 'mdi:content-copy',
type: 'default',
onClick: handleCopy,
},
{
key: 'authorize',
label: '授权',
icon: 'mdi:shield-key',
type: 'primary',
onClick: handleAuthorize,
},
],
})
// ============ 自定义操作 ============
const handleCopy = (row: Employee) => {
const newRow = {
...row,
id: Date.now(),
name: `${row.name}_副本`,
}
tableData.value.unshift(newRow)
window.$message?.success('复制成功')
}
const handleAuthorize = async (row: Employee) => {
// 自定义授权逻辑
console.log('授权用户:', row)
}
</script>
<template>
<C_Table
v-model:data="tableData"
:loading="loading"
:actions="tableActions"
@refresh="refresh"
/>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
SDK 响应格式说明
typescript
// SDK 自动生成的函数返回格式
const { data, error, response, request } = await getEmployeesList({
query: { page: 1, pageSize: 10 },
})
// ✅ data - 响应数据(成功时)
// ✅ error - 错误对象(失败时)
// ✅ response - 原始响应对象
// ✅ request - 请求配置
// 类型定义
interface SdkResponse<T> {
data: T | null // 响应数据
error: Error | null // 错误对象
response: Response // 原始响应
request: RequestInit // 请求配置
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript
// 方式一:检查 error
const { data, error } = await getEmployeesList({ query: params })
if (error) {
console.error('请求失败:', error)
window.$message?.error('加载失败,请重试')
return
}
// 方式二:使用 try-catch(配合 unwrapSdk)
import { unwrapSdk } from '@/utils/sdk-helper'
try {
// unwrapSdk 自动解包 data,出错时抛出异常
const result = await unwrapSdk(getEmployeesList)({
query: params,
})
console.log('数据:', result)
} catch (error) {
console.error('请求失败:', error)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
辅助工具:unwrapSdk
typescript
/**
* 包装 SDK 函数,自动解包 { data, error } 响应格式
* @param fn SDK 生成的 API 函数
* @returns 解包后的函数,直接返回 data 或抛出 error
*/
export const unwrapSdk = <Fn extends (...args: any[]) => Promise<any>>(
fn: Fn
) => {
return async (...args: Parameters<Fn>) => {
const { data, error } = await fn(...args)
if (error) throw error
return data
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript
import { deleteEmployeesById } from '@/api/generated'
import { unwrapSdk } from '@/utils/sdk-helper'
// 原始方式
const { data, error } = await deleteEmployeesById({ path: { id: 1 } })
if (error) throw error
console.log(data)
// 使用 unwrapSdk(更简洁)
const result = await unwrapSdk(deleteEmployeesById)({ path: { id: 1 } })
console.log(result) // 直接得到 data,出错自动抛出异常1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
工厂函数:createTableActions
typescript
/**
* 创建表格操作配置
* 自动包装 SDK API 调用,处理 { data, error } 响应格式
*/
export const createTableActions = <T extends Record<string, any>>(
config: TableActionsConfig<T>
): ComputedRef<TableActions<T>> => {
const { apis, custom = [], idField = 'id' as keyof T } = config
return computed(() => {
const actions: TableActions<T> = {}
// 编辑操作
if (apis.update) {
actions.edit = async (row: T) => {
return unwrapSdk(apis.update!)({
path: { id: row[idField] },
body: row as any,
})
}
}
// 删除操作
if (apis.delete) {
actions.delete = async (row: T) => {
return unwrapSdk(apis.delete!)({
path: { id: row[idField] },
})
}
}
// 详情操作
if (apis.detail) {
actions.detail = async (row: T) => {
return unwrapSdk(apis.detail!)({
path: { id: row[idField] },
})
}
}
// 自定义操作
if (custom.length > 0) {
actions.custom = custom
}
return actions
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typescript
import { createTableActions } from '@/composables/Table/createTableActions'
import {
putEmployeesById,
deleteEmployeesById,
getEmployeesById,
} from '@/api/generated'
// 配置前:40 行代码
const tableActions = computed(() => ({
edit: async row => {
const { data, error } = await putEmployeesById({
path: { id: row.id },
body: row,
})
if (error) throw error
return data
},
delete: async row => {
const { data, error } = await deleteEmployeesById({
path: { id: row.id },
})
if (error) throw error
return data
},
// ... 更多重复代码
}))
// 配置后:13 行代码(减少 68%)
const tableActions = createTableActions<Employee>({
apis: {
update: putEmployeesById,
delete: deleteEmployeesById,
detail: getEmployeesById,
},
custom: [
{
key: 'copy',
label: '复制',
icon: 'mdi:content-copy',
onClick: handleCopy,
},
],
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🔄 传统用法:手写 API 层(20% 场景)
适用场景
什么时候需要手写 API 层?
- ❌ 后端没有提供 OpenAPI 规范(强烈建议推动后端提供)
- ❌ 临时接口/Mock 数据(还未对接真实后端)
- ❌ 特殊业务逻辑(需要在接口层做复杂处理)
- ❌ 第三方 API(非本项目后端接口)
手写示例
typescript
// 手动定义接口类型
export interface User {
id: number
name: string
email: string
role: string
createdAt: string
}
export interface UserListParams {
page?: number
pageSize?: number
keyword?: string
status?: 'active' | 'inactive'
}
export interface UserListResponse {
code: string
message: string
data: User[]
total: number
}
// 手动编写 API 函数
import request from '@/axios/request'
export const userApi = {
// 获取用户列表
list: (params: UserListParams): Promise<UserListResponse> => {
return request.get('/api/users', { params })
},
// 获取用户详情
detail: (id: number): Promise<User> => {
return request.get(`/api/users/${id}`)
},
// 创建用户
create: (data: Partial<User>): Promise<User> => {
return request.post('/api/users', data)
},
// 更新用户
update: (id: number, data: Partial<User>): Promise<User> => {
return request.put(`/api/users/${id}`, data)
},
// 删除用户
delete: (id: number): Promise<void> => {
return request.delete(`/api/users/${id}`)
},
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
vue
<script setup lang="ts">
import { userApi, type User } from '@/api/modules/user'
const users = ref<User[]>([])
const loading = ref(false)
// 加载数据
const loadData = async () => {
loading.value = true
try {
const response = await userApi.list({
page: 1,
pageSize: 10,
keyword: '',
})
if (response.code === '0') {
users.value = response.data
}
} catch (error) {
console.error('加载失败:', error)
window.$message?.error('加载失败,请重试')
} finally {
loading.value = false
}
}
// 删除用户
const handleDelete = async (id: number) => {
try {
await userApi.delete(id)
window.$message?.success('删除成功')
loadData() // 重新加载数据
} catch (error) {
console.error('删除失败:', error)
window.$message?.error('删除失败,请重试')
}
}
onMounted(() => {
loadData()
})
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
手写 vs SDK 对比
| 维度 | 手写 API 层 | SDK 自动生成 |
|---|---|---|
| 开发时间 | 30分钟/接口 | 0秒(自动) |
| 类型安全 | 50%(手动维护) | 100%(自动同步) |
| 维护成本 | 高(每次接口变更都要改) | 低(重新生成即可) |
| 代码量 | 200+ 行/模块 | 0 行(自动生成) |
| 出错概率 | 高(人工容易遗漏) | 低(机器生成) |
| 学习成本 | 需要了解项目规范 | 看文档即可 |
| 适用场景 | 特殊业务逻辑 | 标准 CRUD |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
建议
- 新项目:强烈推荐使用 SDK 方式
- 老项目:逐步迁移到 SDK,减少维护成本
- 特殊场景:手写 API 层作为补充
📚 实战案例:表格演示页面
场景描述
实现一个员工管理表格,包含以下功能:
- ✅ 数据加载(分页、排序、筛选)
- ✅ 新增员工
- ✅ 编辑员工
- ✅ 删除员工
- ✅ 查看详情
- ✅ 批量操作
- ✅ 自定义操作(复制、授权)
完整代码
vue
<template>
<div class="table-demo-page">
<NH1>表格组件场景示例</NH1>
<NCard>
<NSpace
vertical
:size="20"
>
<!-- 操作按钮区 -->
<NCard
title="操作区"
size="small"
>
<div class="controls-row">
<!-- 编辑模式选择 -->
<NRadioGroup v-model:value="editMode">
<NRadioButton
v-for="mode in EDIT_MODES"
:key="mode.value"
:value="mode.value"
>
{{ mode.label }}
</NRadioButton>
</NRadioGroup>
<!-- 添加新行 -->
<NButton
@click="addNewRow"
type="primary"
:disabled="editMode === 'none'"
>
<template #icon>
<C_Icon name="mdi:plus" />
</template>
添加新行
</NButton>
<!-- 刷新按钮 -->
<NButton
@click="refresh"
type="info"
:loading="loading"
>
<template #icon>
<C_Icon name="mdi:refresh" />
</template>
刷新数据
</NButton>
</div>
</NCard>
<!-- 表格组件 -->
<C_Table
ref="tableRef"
v-model:data="tableData"
:columns="tableColumns"
:loading="loading"
:edit-mode="editMode"
:actions="tableActions"
:pagination="paginationConfig"
@save="handleSave"
@cancel="handleCancel"
@row-delete="handleRowDelete"
@view-detail="handleViewDetail"
/>
</NSpace>
</NCard>
<!-- 详情模态框 -->
<c_detail
v-model:visible="detailModalVisible"
:data="currentEmployee || {}"
:config="detailConfig"
:title="detailModalTitle"
@close="handleDetailClose"
/>
</div>
</template>
<script setup lang="ts">
import type { EditMode, PaginationConfig } from '@/types/modules/table'
import {
EDIT_MODES,
MODE_CONFIG,
getTableColumns,
createNewEmployee,
detailConfig,
type Employee,
} from './data'
import {
getEmployeesList,
deleteEmployeesById,
putEmployeesById,
getEmployeesById,
} from '@/api/generated'
import { useTableData } from '@/composables/Table/useTableData'
import { createTableActions } from '@/composables/Table/createTableActions'
// ================= 组合式函数 =================
const message = useMessage()
const dialog = useDialog()
// 数据加载
const { tableData, loading, refresh } = useTableData(params =>
getEmployeesList({ query: params })
)
// ================= 响应式状态 =================
const tableRef = ref()
const editMode = ref<EditMode>('modal')
const paginationEnabled = ref(true)
const currentPage = ref(1)
const defaultPageSize = ref(10)
const detailModalVisible = ref(false)
const detailModalTitle = ref('')
const currentEmployee = ref<Employee | null>(null)
// ================= 计算属性 =================
const tableColumns = computed(() => getTableColumns())
const paginationConfig = computed((): PaginationConfig | boolean => {
if (!paginationEnabled.value) return false
return {
enabled: true,
page: currentPage.value,
pageSize: defaultPageSize.value,
}
})
// ================= CRUD 操作配置 =================
const tableActions = createTableActions<Employee>({
apis: {
update: putEmployeesById,
delete: deleteEmployeesById,
detail: getEmployeesById,
},
custom: [
{
key: 'copy',
label: '复制',
icon: 'mdi:content-copy',
type: 'default',
onClick: handleCopy,
},
{
key: 'authorize',
label: '授权',
icon: 'mdi:shield-key',
type: 'primary',
onClick: handleAuthorize,
},
],
})
// ================= 详情处理 =================
const handleViewDetail = (data: any): void => {
const employee = data as Employee
currentEmployee.value = employee
detailModalTitle.value = `员工详情 - ${employee.name}`
detailModalVisible.value = true
}
const handleDetailClose = (): void => {
detailModalVisible.value = false
currentEmployee.value = null
}
// ================= 自定义操作 =================
const handleCopy = (row: Employee): void => {
const newRow: Employee = {
...row,
id: Date.now(),
name: `${row.name}_副本`,
}
tableData.value.unshift(newRow)
message.success('复制成功')
}
const handleAuthorize = (row: Employee): void => {
dialog.info({
title: '授权确认',
content: `确定要授权给 ${row.name} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success(`已授权给 ${row.name}`)
},
})
}
// ================= 新增行 =================
const addNewRow = (): void => {
const newRow = createNewEmployee()
tableData.value.unshift(newRow)
message.success('已添加新行,请填写信息后保存')
}
// ================= 保存/取消 =================
const handleSave = (row: Employee): void => {
message.success('保存成功')
}
const handleCancel = (row: Employee): void => {
message.info('已取消编辑')
}
// ================= 删除行 =================
const handleRowDelete = (row: Employee): void => {
message.success('删除成功')
}
</script>
<style scoped lang="scss">
.table-demo-page {
padding: 20px;
}
.controls-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
typescript
import type { Employee } from '@/api/generated'
// 编辑模式配置
export const EDIT_MODES = [
{ value: 'none', label: '不可编辑', icon: 'mdi:lock' },
{ value: 'inline', label: '行内编辑', icon: 'mdi:pencil' },
{ value: 'modal', label: '弹窗编辑', icon: 'mdi:window-maximize' },
] as const
export type EditMode = (typeof EDIT_MODES)[number]['value']
// 表格列配置
export const getTableColumns = () => [
{
type: 'selection',
width: 50,
},
{
title: 'ID',
key: 'id',
width: 80,
sorter: true,
},
{
title: '姓名',
key: 'name',
width: 120,
ellipsis: { tooltip: true },
},
{
title: '邮箱',
key: 'email',
width: 200,
ellipsis: { tooltip: true },
},
{
title: '角色',
key: 'role',
width: 120,
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
]
// 详情配置
export const detailConfig = [
{ label: 'ID', key: 'id' },
{ label: '姓名', key: 'name' },
{ label: '邮箱', key: 'email' },
{ label: '角色', key: 'role' },
{ label: '创建时间', key: 'createdAt' },
]
// 创建新员工
export const createNewEmployee = (): Employee => ({
id: Date.now(),
name: '新员工',
email: 'new@example.com',
role: 'employee',
createdAt: new Date().toISOString(),
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
代码解析
1. 数据加载
typescript
// 使用 useTableData 组合式函数
const { tableData, loading, refresh } = useTableData(params =>
getEmployeesList({ query: params })
)
// 自动处理:
// - 分页参数
// - 加载状态
// - 响应格式解包
// - 错误处理1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2. CRUD 操作配置
typescript
// 使用 createTableActions 工厂函数
const tableActions = createTableActions<Employee>({
apis: {
update: putEmployeesById, // SDK 函数
delete: deleteEmployeesById, // SDK 函数
detail: getEmployeesById, // SDK 函数
},
custom: [
// 自定义操作
{
key: 'copy',
label: '复制',
icon: 'mdi:content-copy',
onClick: handleCopy,
},
],
})
// 自动生成:
// - edit 函数(自动解包响应)
// - delete 函数(自动解包响应)
// - detail 函数(自动解包响应)
// - custom 操作列表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
3. 类型安全
typescript
// 从 SDK 导入类型
import type { Employee } from '@/api/generated'
// 完美的类型提示
const currentEmployee = ref<Employee | null>(null)
// IDE 自动提示所有字段
currentEmployee.value?.name
currentEmployee.value?.email
currentEmployee.value?.role1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
效果展示
| 功能点 | 代码行数 | 类型安全 | 维护成本 |
|---|---|---|---|
| 数据加载 | 3 行 | ✅ 100% | ⭐ 极低 |
| CRUD 配置 | 13 行 | ✅ 100% | ⭐ 极低 |
| 自定义操作 | 10 行 | ✅ 100% | ⭐⭐ 低 |
| 总计 | 26 行 | ✅ 100% | ⭐ 极低 |
对比传统方式
- 代码减少:70%(从 80 行降到 26 行)
- 类型安全:100%(完全类型推导)
- 维护成本:接口变更只需重新生成 SDK
🔥 最佳实践
1. 目录组织
bash
src/
├── api/
│ ├── generated/ # SDK 自动生成(不要修改)
│ ├── auth.ts # 手动维护的特殊接口
│ └── permission.ts # 手动维护的特殊接口
│
├── composables/
│ └── Table/
│ ├── useTableData.ts # 表格数据加载
│ └── createTableActions.ts # CRUD 操作工厂
│
├── utils/
│ └── sdk-helper.ts # SDK 辅助工具
│
└── views/
└── demo/
└── 10-table/
├── index.vue # 页面组件
└── data.ts # 配置数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2. 命名规范
typescript
// ✅ 推荐:语义化命名
import {
getEmployeesList, // 获取列表
getEmployeesById, // 获取详情
putEmployeesById, // 更新
deleteEmployeesById, // 删除
postEmployees, // 创建
} from '@/api/generated'
// ❌ 不推荐:缩写或不清晰的命名
import {
getList, // 什么的列表?
update, // 更新什么?
del, // 删除什么?
} from '@/api/generated'1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 错误处理
typescript
// ✅ 推荐:统一错误处理
const { data, error } = await getEmployeesList({ query: params })
if (error) {
console.error('加载失败:', error)
window.$message?.error('加载失败,请重试')
return
}
// 或使用 unwrapSdk + try-catch
try {
const result = await unwrapSdk(getEmployeesList)({ query: params })
console.log('数据:', result)
} catch (error) {
console.error('加载失败:', error)
window.$message?.error('加载失败,请重试')
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. 类型导入
typescript
// ✅ 推荐:明确导入类型
import {
getEmployeesList,
type Employee, // 使用 type 关键字
type EmployeeListParams,
} from '@/api/generated'
// ❌ 不推荐:混合导入
import {
getEmployeesList,
Employee, // 会被误认为是值
} from '@/api/generated'1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
5. 响应验证
typescript
// ✅ 推荐:验证响应数据
const { data, error } = await getEmployeesList({ query: params })
if (error) {
// 处理错误
return
}
// 验证业务状态码
if (data?.code === '0') {
employees.value = data.data || []
} else {
console.warn('业务错误:', data?.message)
}
// ❌ 不推荐:直接使用 data
employees.value = data.data // 可能为 undefined1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🚨 常见问题
1. SDK 生成失败
问题:运行 generate-api 报错
原因:
- OpenAPI 规范文件下载失败
- 后端接口返回格式不正确
- 网络问题
解决方案:
bash
# 检查后端接口是否可访问
curl https://api.example.com/v3/api-docs
# 手动下载 OpenAPI 规范
node scripts/download-openapi.js
# 检查 openapi.json 文件是否正确
cat openapi.json
# 重新生成 SDK
openapi-ts1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2. 类型提示不准确
问题:IDE 无法正确推导类型
原因:
- 生成的类型文件未被 TypeScript 识别
- IDE 缓存问题
解决方案:
bash
# 重新生成类型文件
bun run generate-api
# 重启 TypeScript 服务
# VS Code: Ctrl+Shift+P → TypeScript: Restart TS Server
# 检查 tsconfig.json 是否包含生成目录
{
"include": [
"src/**/*",
"src/api/generated/**/*" // 确保包含
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
3. baseURL 配置无效
问题:接口请求 404,baseURL 未生效
原因:
- 未在 main.ts 中导入 client-config.ts
- 环境变量未正确设置
解决方案:
typescript
// main.ts - 确保全局导入
import '@/api/generated/client-config' // ⚠️ 必须导入
// 检查环境变量
console.log(import.meta.env.VITE_API_BASE)
// 手动设置 baseURL(调试用)
import { client } from '@/api/generated'
client.setConfig({ baseUrl: 'https://api.example.com' })1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
4. 接口变更后类型不同步
问题:后端接口更新,前端类型未更新
原因:
- 未重新生成 SDK
解决方案:
bash
# 重新生成 SDK
bun run generate-api
# 检查 git diff,确认类型已更新
git diff src/api/generated/types.gen.ts
# 如果没有变化,检查 openapi.json 是否最新
node scripts/download-openapi.js1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
5. 响应格式不兼容
问题:useTableData 报错,响应格式不匹配
原因:
- 接口返回格式不符合标准
- 未正确处理 SDK 响应格式
解决方案:
typescript
// 方式一:使用 useTableData(自动处理双格式)
const { tableData, loading, refresh } = useTableData(params =>
getEmployeesList({ query: params })
)
// 方式二:手动处理 SDK 格式
const { data, error } = await getEmployeesList({ query: params })
if (error) {
// 处理错误
}
if (data?.code === '0') {
tableData.value = data.data || []
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
📊 收益评估
开发效率提升
| 开发阶段 | 传统方式 | SDK 方式 | 提升幅度 |
|---|---|---|---|
| 接口层开发 | 2小时 | 0分钟 | ∞ |
| 类型定义 | 1小时 | 0分钟(自动) | ∞ |
| CRUD 配置 | 40分钟 | 5分钟 | 88% ⬆️ |
| 错误处理 | 30分钟 | 5分钟 | 83% ⬆️ |
| 接口对接 | 4小时 | 1小时 | 75% ⬆️ |
| 维护更新 | 2小时/次 | 1分钟/次 | 99% ⬆️ |
| 总计 | 10小时 | 1.2小时 | 88% ⬆️ |
代码质量提升
| 质量指标 | 传统方式 | SDK 方式 | 改进 |
|---|---|---|---|
| 类型覆盖率 | 50% | 100% | 100% ⬆️ |
| 类型错误率 | 15% | 0% | 100% ⬇️ |
| 接口一致性 | 60% | 100% | 67% ⬆️ |
| 维护成本 | 高 | 极低 | 90% ⬇️ |
| 出错概率 | 中 | 极低 | 80% ⬇️ |
团队协作提升
| 协作场景 | 传统方式 | SDK 方式 | 改进 |
|---|---|---|---|
| 新人上手 | 2天 | 2小时 | 92% ⬇️ |
| 前后端对接 | 4小时/次 | 1小时/次 | 75% ⬇️ |
| 接口文档 | 手动维护 | 自动生成 | 100% ⬇️ |
| 类型同步 | 手动对齐 | 自动同步 | 100% ⬇️ |
🎯 总结
核心优势
SDK 自动生成的核心价值
- 零手写代码 - 接口层完全自动生成
- 100% 类型安全 - 前后端类型自动同步
- 零维护成本 - 一键更新,无需手动修改
- 开发效率提升 88% - 从 10 小时降到 1.2 小时
- 团队协作优化 - 统一标准,降低沟通成本
使用建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新项目 | ⭐⭐⭐⭐⭐ SDK 方式 | 从零开始,最佳实践 |
| 老项目改造 | ⭐⭐⭐⭐ SDK 方式 | 逐步迁移,降低风险 |
| 标准 CRUD | ⭐⭐⭐⭐⭐ SDK 方式 | 零代码,完美适配 |
| 复杂业务逻辑 | ⭐⭐⭐ SDK + 手写 | 混合使用,灵活应对 |
| 第三方 API | ⭐⭐⭐ 手写方式 | 无 OpenAPI 规范 |
| 临时 Mock | ⭐⭐ 手写方式 | 快速验证想法 |
快速上手
bash
# 1. 配置 OpenAPI 生成器
# 编辑 openapi-ts.config.ts
# 2. 生成 SDK
bun run generate-api
# 3. 导入使用
import { getEmployeesList } from '@/api/generated'
# 4. 开始开发 🚀
const { data, error } = await getEmployeesList({ query: params })1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
