@jahanxu/trellis 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +58 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +17 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +19 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/templates/claude/commands/trellis/handoff.md +90 -387
- package/dist/templates/claude/commands/trellis/pick-task.md +74 -444
- package/dist/templates/claude/hooks/inject-subagent-context.py +17 -101
- package/dist/templates/claude/hooks/ralph-loop.py +1 -0
- package/dist/templates/claude/hooks/session-start.py +170 -54
- package/dist/templates/iflow/commands/trellis/handoff.md +148 -0
- package/dist/templates/iflow/commands/trellis/pick-task.md +145 -0
- package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -0
- package/dist/templates/iflow/hooks/ralph-loop.py +1 -0
- package/dist/templates/iflow/hooks/session-start.py +171 -0
- package/dist/templates/markdown/index.d.ts +9 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +10 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/roles/designer/index.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/designer/mock-data-standards.md.txt +63 -0
- package/dist/templates/markdown/spec/roles/designer/prototype-guidelines.md.txt +49 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/api-integration.md.txt +63 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/index.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/prototype-to-production.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/pm/index.md.txt +45 -0
- package/dist/templates/markdown/spec/roles/pm/prd-template.md.txt +64 -0
- package/dist/templates/markdown/spec/roles/pm/requirement-checklist.md.txt +43 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +3 -2
- package/dist/templates/trellis/scripts/common/cli_adapter.py +4 -4
- package/dist/templates/trellis/scripts/common/developer.py +4 -4
- package/dist/templates/trellis/scripts/common/git_context.py +7 -7
- package/dist/templates/trellis/scripts/common/paths.py +64 -14
- package/dist/templates/trellis/scripts/common/phase.py +2 -2
- package/dist/templates/trellis/scripts/common/registry.py +16 -16
- package/dist/templates/trellis/scripts/common/task_queue.py +10 -10
- package/dist/templates/trellis/scripts/common/task_utils.py +5 -5
- package/dist/templates/trellis/scripts/common/worktree.py +8 -8
- package/dist/templates/trellis/scripts/pool.py +214 -265
- package/dist/templates/trellis/scripts/task.py +3 -116
- package/package.json +3 -3
- package/dist/templates/claude/commands/trellis/before-role-work.md +0 -364
- package/dist/templates/trellis/VERSION +0 -1
- package/dist/templates/trellis/deliverables/README.md +0 -51
- package/dist/templates/trellis/paths.README.md +0 -277
- package/dist/templates/trellis/paths.yaml +0 -41
- package/dist/templates/trellis/pool/implementations.json +0 -5
- package/dist/templates/trellis/pool/prototypes.json +0 -5
- package/dist/templates/trellis/pool/requirements.json +0 -5
- package/dist/templates/trellis/scripts/common/project_paths.py +0 -189
- package/dist/templates/trellis/scripts/handoff_generator.py +0 -380
- package/dist/templates/trellis/spec/roles/designer/index.md +0 -243
- package/dist/templates/trellis/spec/roles/designer/mock-data-standards.md +0 -481
- package/dist/templates/trellis/spec/roles/designer/prototype-guidelines.md +0 -429
- package/dist/templates/trellis/spec/roles/frontend-impl/api-integration.md +0 -565
- package/dist/templates/trellis/spec/roles/frontend-impl/index.md +0 -321
- package/dist/templates/trellis/spec/roles/frontend-impl/state-management.md +0 -599
- package/dist/templates/trellis/spec/roles/pm/index.md +0 -112
- package/dist/templates/trellis/spec/roles/pm/prd-template.md +0 -124
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
# API 集成指南
|
|
2
|
-
|
|
3
|
-
> Frontend 角色将原型 Mock API 替换为真实 API 的指南
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 核心原则
|
|
8
|
-
|
|
9
|
-
API 集成的目标是:
|
|
10
|
-
1. **定位准确** - 快速找到所有需要替换的 Mock API
|
|
11
|
-
2. **类型安全** - 使用 TypeScript 确保 API 响应类型正确
|
|
12
|
-
3. **错误处理** - 完善处理网络错误、业务错误
|
|
13
|
-
4. **复用性强** - API 调用逻辑集中管理,便于维护
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 定位 Mock API
|
|
18
|
-
|
|
19
|
-
### 1. 搜索 TODO 注释
|
|
20
|
-
|
|
21
|
-
Designer 会用 TODO 注释标注需要替换的位置:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# 搜索所有需要替换的 Mock API
|
|
25
|
-
grep -r "TODO:.*Frontend.*API" src/
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**示例输出**:
|
|
29
|
-
```
|
|
30
|
-
src/pages/LoginPage.tsx:45: // TODO: [Frontend] 替换为真实API调用 POST /api/auth/login
|
|
31
|
-
src/pages/UserList.tsx:23: // TODO: [Frontend] 替换为真实API调用 GET /api/users
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### 2. 搜索 Mock 函数调用
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# 搜索 mock 关键字
|
|
38
|
-
grep -r "mock.*API\|\.mock\." src/
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 3. 检查 HANDOFF 文档
|
|
42
|
-
|
|
43
|
-
Designer 的 HANDOFF.md 会列出所有需要替换的 API:
|
|
44
|
-
|
|
45
|
-
```markdown
|
|
46
|
-
## 需要Frontend补充的逻辑
|
|
47
|
-
|
|
48
|
-
1. **API集成点**
|
|
49
|
-
- 文件:`LoginPage.tsx:45`
|
|
50
|
-
- Mock: `mockLoginAPI()`
|
|
51
|
-
- 真实API: `POST /api/auth/login`
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## API 客户端配置
|
|
57
|
-
|
|
58
|
-
### 1. 创建 API 客户端
|
|
59
|
-
|
|
60
|
-
使用 Axios 或 Fetch 封装统一的 API 客户端:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// src/lib/api/client.ts
|
|
64
|
-
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
65
|
-
|
|
66
|
-
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
|
67
|
-
|
|
68
|
-
export const apiClient: AxiosInstance = axios.create({
|
|
69
|
-
baseURL,
|
|
70
|
-
timeout: 10000,
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// 请求拦截器:添加 token
|
|
77
|
-
apiClient.interceptors.request.use(
|
|
78
|
-
(config) => {
|
|
79
|
-
const token = localStorage.getItem('auth_token');
|
|
80
|
-
if (token) {
|
|
81
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
82
|
-
}
|
|
83
|
-
return config;
|
|
84
|
-
},
|
|
85
|
-
(error) => Promise.reject(error)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// 响应拦截器:统一错误处理
|
|
89
|
-
apiClient.interceptors.response.use(
|
|
90
|
-
(response) => response.data, // 直接返回 data
|
|
91
|
-
(error) => {
|
|
92
|
-
if (error.response?.status === 401) {
|
|
93
|
-
// Token 过期,跳转登录
|
|
94
|
-
localStorage.removeItem('auth_token');
|
|
95
|
-
window.location.href = '/login';
|
|
96
|
-
}
|
|
97
|
-
return Promise.reject(error);
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### 2. 环境变量配置
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
# .env.development
|
|
106
|
-
VITE_API_BASE_URL=http://localhost:3000
|
|
107
|
-
|
|
108
|
-
# .env.production
|
|
109
|
-
VITE_API_BASE_URL=https://api.example.com
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## API 函数封装
|
|
115
|
-
|
|
116
|
-
### 1. 按模块组织 API
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
src/lib/api/
|
|
120
|
-
├── client.ts # API 客户端配置
|
|
121
|
-
├── auth.ts # 认证相关 API
|
|
122
|
-
├── user.ts # 用户相关 API
|
|
123
|
-
├── product.ts # 产品相关 API
|
|
124
|
-
└── index.ts # 统一导出
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### 2. 定义 API 函数
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// src/lib/api/auth.ts
|
|
131
|
-
import { apiClient } from './client';
|
|
132
|
-
import type { User } from '@/types/user';
|
|
133
|
-
|
|
134
|
-
export interface LoginRequest {
|
|
135
|
-
email: string;
|
|
136
|
-
password: string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface LoginResponse {
|
|
140
|
-
user: User;
|
|
141
|
-
token: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 用户登录
|
|
146
|
-
* POST /api/auth/login
|
|
147
|
-
*/
|
|
148
|
-
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
|
149
|
-
return apiClient.post('/api/auth/login', data);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* 用户登出
|
|
154
|
-
* POST /api/auth/logout
|
|
155
|
-
*/
|
|
156
|
-
export const logout = async (): Promise<void> => {
|
|
157
|
-
return apiClient.post('/api/auth/logout');
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 获取当前用户信息
|
|
162
|
-
* GET /api/auth/me
|
|
163
|
-
*/
|
|
164
|
-
export const getCurrentUser = async (): Promise<User> => {
|
|
165
|
-
return apiClient.get('/api/auth/me');
|
|
166
|
-
};
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### 3. 列表 API 封装
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
// src/lib/api/user.ts
|
|
173
|
-
import { apiClient } from './client';
|
|
174
|
-
import type { User } from '@/types/user';
|
|
175
|
-
import type { ApiListResponse } from '@/types/api';
|
|
176
|
-
|
|
177
|
-
export interface GetUsersParams {
|
|
178
|
-
page?: number;
|
|
179
|
-
pageSize?: number;
|
|
180
|
-
keyword?: string;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 获取用户列表
|
|
185
|
-
* GET /api/users?page=1&pageSize=10&keyword=xxx
|
|
186
|
-
*/
|
|
187
|
-
export const getUsers = async (
|
|
188
|
-
params: GetUsersParams = {}
|
|
189
|
-
): Promise<ApiListResponse<User>> => {
|
|
190
|
-
return apiClient.get('/api/users', { params });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* 获取用户详情
|
|
195
|
-
* GET /api/users/:id
|
|
196
|
-
*/
|
|
197
|
-
export const getUserById = async (id: string): Promise<User> => {
|
|
198
|
-
return apiClient.get(`/api/users/${id}`);
|
|
199
|
-
};
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## 替换 Mock API
|
|
205
|
-
|
|
206
|
-
### Before(Designer 的 Mock)
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
// src/pages/LoginPage.tsx
|
|
210
|
-
import { mockLoginAPI } from '@/mocks/api/auth.mock';
|
|
211
|
-
|
|
212
|
-
const LoginPage = () => {
|
|
213
|
-
const handleSubmit = async (email: string, password: string) => {
|
|
214
|
-
try {
|
|
215
|
-
// TODO: [Frontend] 替换为真实API调用 POST /api/auth/login
|
|
216
|
-
const { user, token } = await mockLoginAPI(email, password);
|
|
217
|
-
localStorage.setItem('auth_token', token);
|
|
218
|
-
setUser(user);
|
|
219
|
-
} catch (error) {
|
|
220
|
-
setError('登录失败');
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// ...
|
|
225
|
-
};
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### After(真实 API)
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
// src/pages/LoginPage.tsx
|
|
232
|
-
import { login } from '@/lib/api/auth';
|
|
233
|
-
|
|
234
|
-
const LoginPage = () => {
|
|
235
|
-
const handleSubmit = async (email: string, password: string) => {
|
|
236
|
-
try {
|
|
237
|
-
const { user, token } = await login({ email, password });
|
|
238
|
-
localStorage.setItem('auth_token', token);
|
|
239
|
-
setUser(user);
|
|
240
|
-
navigate('/dashboard');
|
|
241
|
-
} catch (error) {
|
|
242
|
-
if (error.response?.status === 401) {
|
|
243
|
-
setError('邮箱或密码错误');
|
|
244
|
-
} else {
|
|
245
|
-
setError('登录失败,请稍后重试');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// ...
|
|
251
|
-
};
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## 错误处理规范
|
|
257
|
-
|
|
258
|
-
### 1. 错误类型定义
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
// src/lib/api/errors.ts
|
|
262
|
-
export class ApiError extends Error {
|
|
263
|
-
constructor(
|
|
264
|
-
public status: number,
|
|
265
|
-
public message: string,
|
|
266
|
-
public data?: any
|
|
267
|
-
) {
|
|
268
|
-
super(message);
|
|
269
|
-
this.name = 'ApiError';
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export class NetworkError extends Error {
|
|
274
|
-
constructor(message: string = '网络连接失败') {
|
|
275
|
-
super(message);
|
|
276
|
-
this.name = 'NetworkError';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### 2. 统一错误处理
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
// src/lib/api/client.ts(响应拦截器)
|
|
285
|
-
apiClient.interceptors.response.use(
|
|
286
|
-
(response) => response.data,
|
|
287
|
-
(error) => {
|
|
288
|
-
if (error.response) {
|
|
289
|
-
// 服务器返回错误
|
|
290
|
-
const { status, data } = error.response;
|
|
291
|
-
throw new ApiError(status, data.message || '请求失败', data);
|
|
292
|
-
} else if (error.request) {
|
|
293
|
-
// 网络错误
|
|
294
|
-
throw new NetworkError('网络连接失败,请检查网络设置');
|
|
295
|
-
} else {
|
|
296
|
-
// 其他错误
|
|
297
|
-
throw new Error('请求失败,请稍后重试');
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
);
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### 3. 组件中处理错误
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
const LoginPage = () => {
|
|
307
|
-
const [error, setError] = useState<string | null>(null);
|
|
308
|
-
|
|
309
|
-
const handleSubmit = async (email: string, password: string) => {
|
|
310
|
-
setError(null);
|
|
311
|
-
try {
|
|
312
|
-
await login({ email, password });
|
|
313
|
-
} catch (err) {
|
|
314
|
-
if (err instanceof ApiError) {
|
|
315
|
-
// 业务错误
|
|
316
|
-
if (err.status === 401) {
|
|
317
|
-
setError('邮箱或密码错误');
|
|
318
|
-
} else if (err.status === 429) {
|
|
319
|
-
setError('请求过于频繁,请稍后重试');
|
|
320
|
-
} else {
|
|
321
|
-
setError(err.message);
|
|
322
|
-
}
|
|
323
|
-
} else if (err instanceof NetworkError) {
|
|
324
|
-
// 网络错误
|
|
325
|
-
setError('网络连接失败,请检查网络设置');
|
|
326
|
-
} else {
|
|
327
|
-
// 未知错误
|
|
328
|
-
setError('登录失败,请稍后重试');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<form onSubmit={handleSubmit}>
|
|
335
|
-
{error && <p className="text-red-500">{error}</p>}
|
|
336
|
-
{/* ... */}
|
|
337
|
-
</form>
|
|
338
|
-
);
|
|
339
|
-
};
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
---
|
|
343
|
-
|
|
344
|
-
## 加载状态管理
|
|
345
|
-
|
|
346
|
-
### 1. 基本加载状态
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
const UserList = () => {
|
|
350
|
-
const [users, setUsers] = useState<User[]>([]);
|
|
351
|
-
const [loading, setLoading] = useState(false);
|
|
352
|
-
const [error, setError] = useState<string | null>(null);
|
|
353
|
-
|
|
354
|
-
useEffect(() => {
|
|
355
|
-
const fetchUsers = async () => {
|
|
356
|
-
setLoading(true);
|
|
357
|
-
setError(null);
|
|
358
|
-
try {
|
|
359
|
-
const response = await getUsers({ page: 1, pageSize: 10 });
|
|
360
|
-
setUsers(response.data.items);
|
|
361
|
-
} catch (err) {
|
|
362
|
-
setError('加载用户列表失败');
|
|
363
|
-
} finally {
|
|
364
|
-
setLoading(false);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
fetchUsers();
|
|
369
|
-
}, []);
|
|
370
|
-
|
|
371
|
-
if (loading) return <div>加载中...</div>;
|
|
372
|
-
if (error) return <div>错误: {error}</div>;
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<ul>
|
|
376
|
-
{users.map((user) => (
|
|
377
|
-
<li key={user.id}>{user.name}</li>
|
|
378
|
-
))}
|
|
379
|
-
</ul>
|
|
380
|
-
);
|
|
381
|
-
};
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### 2. 使用 React Query(推荐)
|
|
385
|
-
|
|
386
|
-
```typescript
|
|
387
|
-
import { useQuery } from '@tanstack/react-query';
|
|
388
|
-
import { getUsers } from '@/lib/api/user';
|
|
389
|
-
|
|
390
|
-
const UserList = () => {
|
|
391
|
-
const {
|
|
392
|
-
data,
|
|
393
|
-
isLoading,
|
|
394
|
-
error,
|
|
395
|
-
refetch
|
|
396
|
-
} = useQuery({
|
|
397
|
-
queryKey: ['users', { page: 1 }],
|
|
398
|
-
queryFn: () => getUsers({ page: 1, pageSize: 10 }),
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
if (isLoading) return <div>加载中...</div>;
|
|
402
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
403
|
-
|
|
404
|
-
return (
|
|
405
|
-
<div>
|
|
406
|
-
<button onClick={() => refetch()}>刷新</button>
|
|
407
|
-
<ul>
|
|
408
|
-
{data?.data.items.map((user) => (
|
|
409
|
-
<li key={user.id}>{user.name}</li>
|
|
410
|
-
))}
|
|
411
|
-
</ul>
|
|
412
|
-
</div>
|
|
413
|
-
);
|
|
414
|
-
};
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
## 类型安全保证
|
|
420
|
-
|
|
421
|
-
### 1. API 响应类型定义
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
// src/types/api.ts
|
|
425
|
-
export interface ApiResponse<T> {
|
|
426
|
-
code: number;
|
|
427
|
-
message: string;
|
|
428
|
-
data: T;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export interface ApiListResponse<T> {
|
|
432
|
-
code: number;
|
|
433
|
-
message: string;
|
|
434
|
-
data: {
|
|
435
|
-
items: T[];
|
|
436
|
-
total: number;
|
|
437
|
-
page: number;
|
|
438
|
-
pageSize: number;
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### 2. 运行时类型验证(可选)
|
|
444
|
-
|
|
445
|
-
使用 Zod 进行运行时验证:
|
|
446
|
-
|
|
447
|
-
```typescript
|
|
448
|
-
import { z } from 'zod';
|
|
449
|
-
|
|
450
|
-
const UserSchema = z.object({
|
|
451
|
-
id: z.string(),
|
|
452
|
-
name: z.string(),
|
|
453
|
-
email: z.string().email(),
|
|
454
|
-
role: z.enum(['admin', 'user', 'guest']),
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
|
458
|
-
const response = await apiClient.post('/api/auth/login', data);
|
|
459
|
-
|
|
460
|
-
// 验证响应数据
|
|
461
|
-
const user = UserSchema.parse(response.user);
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
user,
|
|
465
|
-
token: response.token,
|
|
466
|
-
};
|
|
467
|
-
};
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
---
|
|
471
|
-
|
|
472
|
-
## 检查清单
|
|
473
|
-
|
|
474
|
-
API 集成完成前,确保:
|
|
475
|
-
|
|
476
|
-
- [ ] 所有 TODO 标记的 Mock API 已替换
|
|
477
|
-
- [ ] API 客户端配置完成(baseURL、拦截器)
|
|
478
|
-
- [ ] 环境变量配置正确
|
|
479
|
-
- [ ] 所有 API 函数包含类型定义
|
|
480
|
-
- [ ] 错误处理完善(网络错误、业务错误)
|
|
481
|
-
- [ ] 加载状态管理完善
|
|
482
|
-
- [ ] Token 刷新逻辑实现(如需要)
|
|
483
|
-
- [ ] API 调用已测试(手动测试或单元测试)
|
|
484
|
-
|
|
485
|
-
---
|
|
486
|
-
|
|
487
|
-
## 常见问题
|
|
488
|
-
|
|
489
|
-
### Q: 如何处理 Token 过期?
|
|
490
|
-
A: 在响应拦截器中检测 401 状态,自动刷新 Token 或跳转登录页
|
|
491
|
-
|
|
492
|
-
```typescript
|
|
493
|
-
apiClient.interceptors.response.use(
|
|
494
|
-
(response) => response.data,
|
|
495
|
-
async (error) => {
|
|
496
|
-
if (error.response?.status === 401) {
|
|
497
|
-
const refreshToken = localStorage.getItem('refresh_token');
|
|
498
|
-
if (refreshToken) {
|
|
499
|
-
// 尝试刷新 Token
|
|
500
|
-
try {
|
|
501
|
-
const { token } = await refreshAuthToken(refreshToken);
|
|
502
|
-
localStorage.setItem('auth_token', token);
|
|
503
|
-
// 重试原请求
|
|
504
|
-
return apiClient.request(error.config);
|
|
505
|
-
} catch {
|
|
506
|
-
// 刷新失败,跳转登录
|
|
507
|
-
window.location.href = '/login';
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
window.location.href = '/login';
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return Promise.reject(error);
|
|
514
|
-
}
|
|
515
|
-
);
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
### Q: 如何处理并发请求?
|
|
519
|
-
A: 使用 Promise.all 或 React Query 的并发查询
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// Promise.all
|
|
523
|
-
const [users, products] = await Promise.all([
|
|
524
|
-
getUsers(),
|
|
525
|
-
getProducts(),
|
|
526
|
-
]);
|
|
527
|
-
|
|
528
|
-
// React Query
|
|
529
|
-
const usersQuery = useQuery(['users'], getUsers);
|
|
530
|
-
const productsQuery = useQuery(['products'], getProducts);
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Q: 如何实现请求取消?
|
|
534
|
-
A: 使用 AbortController
|
|
535
|
-
|
|
536
|
-
```typescript
|
|
537
|
-
useEffect(() => {
|
|
538
|
-
const controller = new AbortController();
|
|
539
|
-
|
|
540
|
-
const fetchData = async () => {
|
|
541
|
-
try {
|
|
542
|
-
await apiClient.get('/api/users', {
|
|
543
|
-
signal: controller.signal,
|
|
544
|
-
});
|
|
545
|
-
} catch (error) {
|
|
546
|
-
if (error.name === 'AbortError') {
|
|
547
|
-
// 请求被取消
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
fetchData();
|
|
554
|
-
|
|
555
|
-
return () => controller.abort(); // 组件卸载时取消请求
|
|
556
|
-
}, []);
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
---
|
|
560
|
-
|
|
561
|
-
## 相关文档
|
|
562
|
-
|
|
563
|
-
- [错误处理规范](./error-handling.md)
|
|
564
|
-
- [状态管理规范](./state-management.md)
|
|
565
|
-
- [Frontend 工作规范](./index.md)
|