@agile-team/wl-skills-kit 2.11.0 → 2.11.2

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +47 -9
  2. package/README.md +41 -23
  3. package/bin/wl-skills.js +133 -39
  4. package/docs/agent-pipeline-runbook.md +3 -3
  5. package/docs//345/205/250/347/233/230/345/210/206/346/236/220/344/270/216/346/231/272/350/203/275/344/275/223/346/220/255/345/273/272/346/214/207/345/215/227.md +4 -4
  6. package/files/.wl-skills/copilot-instructions-full.md +233 -233
  7. package/files/.wl-skills/docs/jh-pagination.md +505 -505
  8. package/files/.wl-skills/docs/page-spec-schema.md +109 -0
  9. package/files/.wl-skills/docs/request.md +940 -940
  10. package/files/.wl-skills/guides/architecture.md +1 -1
  11. package/files/.wl-skills/skills/core/convention-audit/SKILL.md +3 -3
  12. package/files/.wl-skills/skills/core/page-codegen/SKILL.md +10 -4
  13. package/files/.wl-skills/skills/core/spec-doc-parse/SKILL.md +332 -332
  14. package/files/.wl-skills/skills/core/spec-doc-parse/USAGE.md +97 -97
  15. package/files/.wl-skills/skills/sync/permission-sync/USAGE.md +107 -107
  16. package/files/.wl-skills/src/components/global/C_ParentView/index.vue +3 -3
  17. package/files/.wl-skills/src/components/global/C_RightToolbar/index.vue +157 -157
  18. package/files/.wl-skills/src/components/global/C_SvgIcon/index.vue +31 -31
  19. package/files/.wl-skills/src/components/global/C_SvgIcon/svgicon.js +10 -10
  20. package/files/.wl-skills/src/components/global/C_TagStatus/README.md +264 -264
  21. package/files/.wl-skills/src/components/global/C_TagStatus/config.ts +192 -192
  22. package/files/.wl-skills/src/components/global/C_TagStatus/index.vue +106 -106
  23. package/files/.wl-skills/src/components/global/C_TagStatus/types.ts +64 -64
  24. package/files/.wl-skills/src/components/global/C_Tree/README.md +153 -153
  25. package/files/.wl-skills/src/components/global/C_Tree/index.scss +42 -42
  26. package/files/.wl-skills/src/components/global/C_Tree/index.vue +78 -78
  27. package/files/.wl-skills/src/components/global/C_Tree/types.ts +59 -59
  28. package/files/.wl-skills/src/components/local/c_formModal/README.md +235 -235
  29. package/files/.wl-skills/src/components/local/c_formModal/data.ts +95 -95
  30. package/files/.wl-skills/src/components/local/c_formModal/index.scss +8 -8
  31. package/files/.wl-skills/src/components/local/c_formModal/index.vue +107 -107
  32. package/files/.wl-skills/src/components/local/c_formSections/data.ts +175 -175
  33. package/files/.wl-skills/src/components/local/c_formSections/index.scss +280 -280
  34. package/files/.wl-skills/src/components/local/c_formSections/index.vue +429 -429
  35. package/files/.wl-skills/src/components/local/c_listModal/data.ts +41 -41
  36. package/files/.wl-skills/src/components/local/c_listModal/index.vue +136 -136
  37. package/files/.wl-skills/src/components/local/c_spliterTitle/index.scss +25 -25
  38. package/files/.wl-skills/src/components/local/c_spliterTitle/index.vue +21 -21
  39. package/files/.wl-skills/src/components/remote/AGGrid/README.md +530 -530
  40. package/files/.wl-skills/src/components/remote/BaseForm/README.md +508 -508
  41. package/files/.wl-skills/src/components/remote/BaseQuery/README.md +865 -865
  42. package/files/.wl-skills/src/components/remote/BaseTable/README.md +941 -941
  43. package/files/.wl-skills/src/components/remote/BaseToolbar/README.md +496 -496
  44. package/files/.wl-skills/src/types/page.ts +24 -24
  45. package/files/.wl-skills/standards/04-coding-basics.md +39 -1
  46. package/files/.wl-skills/standards/09-typescript.md +26 -3
  47. package/files/.wl-skills/standards/14-layout-containers.md +6 -6
  48. package/files/.wl-skills/standards/index.md +2 -2
  49. package/files/.wl-skills/templates/README.md +44 -44
  50. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/api.md +54 -54
  51. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -346
  52. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -1
  53. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -28
  54. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -115
  55. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -44
  56. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -43
  57. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -338
  58. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -1
  59. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -28
  60. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -115
  61. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -44
  62. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -43
  63. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/api.md +88 -88
  64. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/data.ts +601 -601
  65. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.scss +1 -1
  66. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.vue +64 -64
  67. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/api.md +67 -67
  68. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/data.ts +286 -286
  69. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.scss +139 -139
  70. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.vue +318 -318
  71. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -98
  72. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -543
  73. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -1
  74. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -52
  75. package/files/.wl-skills/templates/sale/demo/add-demo/data.ts +518 -518
  76. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/data.ts +524 -524
  77. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.scss +154 -154
  78. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.vue +117 -117
  79. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/data.ts +308 -308
  80. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.scss +99 -99
  81. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.vue +77 -77
  82. package/files/.wl-skills/templates/sale/demo/heat-batch-return/data.ts +367 -367
  83. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.scss +100 -100
  84. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.vue +170 -170
  85. package/files/.wl-skills/templates/sale/demo/heat-batch-return/meltDialog.vue +320 -320
  86. package/files/.wl-skills/templates/sale/demo/metallurgical-spec/data.ts +824 -824
  87. package/lib/ast-rules.js +304 -9
  88. package/lib/page-spec.js +588 -0
  89. package/lib/safe-fix.js +115 -0
  90. package/mcp/config.js +47 -47
  91. package/mcp/registry.js +6 -1
  92. package/mcp/tools/projectTools.js +19 -1
  93. package/package.json +16 -11
  94. package/files/.wl-skills/src/components/global/C_Splitter/index.scss +0 -61
  95. package/files/.wl-skills/src/components/global/C_Splitter/index.vue +0 -149
@@ -1,940 +1,940 @@
1
- # request - HTTP 请求工具
2
-
3
- > 基于 axios 封装的统一请求工具,自动处理响应数据解包、错误拦截、token 注入等
4
-
5
- ## 导入方式
6
-
7
- ```typescript
8
- import request from "@jhlc/common-core/src/util/request";
9
- ```
10
-
11
- ## 基本用法
12
-
13
- ### GET 请求
14
-
15
- ```typescript
16
- // 基础 GET 请求
17
- const data = await request({
18
- url: "/api/user/list",
19
- method: "get"
20
- });
21
-
22
- // 带查询参数
23
- const data = await request({
24
- url: "/api/user/detail",
25
- method: "get",
26
- params: { id: "123" }
27
- });
28
- ```
29
-
30
- ### POST 请求
31
-
32
- ```typescript
33
- // 创建数据
34
- const result = await request({
35
- url: "/api/user/save",
36
- method: "post",
37
- data: {
38
- name: "张三",
39
- age: 25
40
- }
41
- });
42
-
43
- // 带查询参数的 POST
44
- const result = await request({
45
- url: "/auth/oauth/token",
46
- method: "post",
47
- params: {
48
- grant_type: "password",
49
- username: "admin"
50
- }
51
- });
52
- ```
53
-
54
- ### PUT 请求
55
-
56
- ```typescript
57
- // 更新数据
58
- const result = await request({
59
- url: "/api/user/update",
60
- method: "put",
61
- data: {
62
- id: "123",
63
- name: "李四"
64
- }
65
- });
66
- ```
67
-
68
- ### DELETE 请求
69
-
70
- ```typescript
71
- // 删除数据
72
- const result = await request({
73
- url: "/api/user/delete",
74
- method: "delete",
75
- params: { id: "123" }
76
- });
77
- ```
78
-
79
- ## 响应数据结构
80
-
81
- `request()` 的 response interceptor 执行 `return cloneDeep(res.data)`,直接返回 HTTP 响应体,即 `ApiResult<T>` 结构:
82
-
83
- ```typescript
84
- // ApiResult<T> 类型定义(来自 @jhlc/types)
85
- interface ApiResult<T = any> {
86
- code: number; // 2000 = 成功,200 也被接受(mock 常用)
87
- message: string; // 提示信息
88
- data: T; // 实际业务数据
89
- }
90
-
91
- // 后端返回的 HTTP 响应体(request() 直接返回这层,不再多一层 .data)
92
- {
93
- code: 2000,
94
- message: "操作成功",
95
- data: { id: 1, name: "张三" }
96
- }
97
-
98
- // 使用 request 后直接拿到
99
- const result = await request({ url: "/api/xxx", method: "get" });
100
- console.log(result.code); // 2000
101
- console.log(result.message); // "操作成功"
102
- console.log(result.data); // { id: 1, name: "张三" }(业务数据)
103
- ```
104
-
105
- > **与 `getAction`/`postAction` 的关系**:这些 action 函数内部调用 `request()`,返回值结构**完全相同**,类型签名为 `Promise<ApiResult<T>>`。`ApiResult<T>` 就是上面的 `{ code, message, data: T }`,两者等价。
106
-
107
- ## 常见场景
108
-
109
- ### 场景 1:列表查询
110
-
111
- ```typescript
112
- // src/views/your-module/data.ts
113
- async select() {
114
- const params = {
115
- current: this.page.current,
116
- size: this.page.size,
117
- ...this.queryParam.value
118
- };
119
-
120
- const result = await request({
121
- url: "/api/order/list",
122
- method: "get",
123
- params
124
- });
125
-
126
- this.list.value = result.data.records;
127
- this.page.total = result.data.total;
128
- }
129
- ```
130
-
131
- ### 场景 2:表单保存
132
-
133
- ```typescript
134
- // 新增/编辑统一处理
135
- async save() {
136
- const result = await request({
137
- url: this.isEdit ? "/api/user/update" : "/api/user/save",
138
- method: this.isEdit ? "put" : "post",
139
- data: this.form.value
140
- });
141
-
142
- this.msgSuccess(result.message || "操作成功");
143
- }
144
- ```
145
-
146
- ### 场景 3:获取详情
147
-
148
- ```typescript
149
- async getById(id: string) {
150
- const result = await request({
151
- url: "/api/order/getOneById",
152
- method: "get",
153
- params: { id }
154
- });
155
-
156
- this.form.value = result.data;
157
- }
158
- ```
159
-
160
- ### 场景 4:删除数据
161
-
162
- ```typescript
163
- async remove(id: string) {
164
- const result = await request({
165
- url: "/api/order/remove",
166
- method: "delete",
167
- params: { id }
168
- });
169
-
170
- this.msgSuccess(result.message || "删除成功");
171
- this.select(); // 刷新列表
172
- }
173
- ```
174
-
175
- ### 场景 5:文件下载
176
-
177
- ```typescript
178
- // 下载文件需要设置 responseType
179
- async downloadFile() {
180
- const res = await request({
181
- url: "/api/file/download",
182
- method: "get",
183
- params: { id: "123" },
184
- responseType: "arraybuffer"
185
- });
186
-
187
- // 创建 Blob 并下载
188
- const blob = new Blob([res], { type: "application/vnd.ms-excel" });
189
- const downloadElement = document.createElement("a");
190
- const href = window.URL.createObjectURL(blob);
191
- downloadElement.href = href;
192
- downloadElement.download = "文件名.xlsx";
193
- document.body.appendChild(downloadElement);
194
- downloadElement.click();
195
- document.body.removeChild(downloadElement);
196
- }
197
- ```
198
-
199
- ### 场景 6:获取字典数据
200
-
201
- ```typescript
202
- // 获取下拉选项
203
- async loadDictOptions() {
204
- const result = await request({
205
- url: "/system/dictDtl/getListByDicSn",
206
- method: "get",
207
- params: { strSn: "ORDER_STATUS" }
208
- });
209
-
210
- this.statusOptions = result.data;
211
- }
212
- ```
213
-
214
- ## 配置选项
215
-
216
- ```typescript
217
- interface RequestConfig {
218
- url: string; // 请求地址(必填)
219
- method: string; // 请求方法:get、post、put、delete(必填)
220
- params?: object; // URL 查询参数
221
- data?: object; // 请求体数据(POST/PUT)
222
- headers?: object; // 自定义请求头
223
- responseType?: string; // 响应类型:json、arraybuffer、blob
224
- timeout?: number; // 超时时间(毫秒)
225
- }
226
- ```
227
-
228
- ## 与原生 axios 对比
229
-
230
- ### 原生 axios(啰嗦)
231
-
232
- ```typescript
233
- import axios from "axios";
234
-
235
- // 需要手动解包 res.data
236
- const res = await axios.get("/api/user/list");
237
- console.log(res.data.data); // 两层 data
238
-
239
- // POST 请求参数位置不一致
240
- await axios.post("/api/user/save", formData);
241
- await axios.get("/api/user/detail", { params: { id: 1 } });
242
- ```
243
-
244
- ### 封装 request(统一)
245
-
246
- ```typescript
247
- import request from "@jhlc/common-core/src/util/request";
248
-
249
- // 自动解包,直接拿到数据
250
- const result = await request({
251
- url: "/api/user/list",
252
- method: "get"
253
- });
254
- console.log(result.data); // 一层 data
255
-
256
- // 所有请求配置统一
257
- await request({ url: "/api/user/save", method: "post", data: formData });
258
- await request({ url: "/api/user/detail", method: "get", params: { id: 1 } });
259
- ```
260
-
261
- ## 自动处理功能
262
-
263
- 1. **自动解包**:返回 `{ code, message, data }` 格式,无需 `res.data.data`
264
- 2. **Token 注入**:自动从 localStorage 读取 token 并添加到请求头
265
- 3. **错误拦截**:统一处理 401/403/500 等错误,自动弹出提示
266
- 4. **双码成功**:`code: 2000`(真实后端业务码)和 `code: 200`(mock 常用值)均视为成功
267
- 5. **i18n 支持**:自动在请求头注入当前语言(`Lang` header)
268
-
269
- ## 在组件中使用
270
-
271
- ### 页面 Hook 中
272
-
273
- ```typescript
274
- // src/views/your-module/data.ts
275
- import request from "@jhlc/common-core/src/util/request";
276
-
277
- export function createPage() {
278
- const Page = new (class extends AbstractPageQueryHook {
279
- async customMethod() {
280
- const result = await request({
281
- url: "/api/custom",
282
- method: "post",
283
- data: { key: "value" }
284
- });
285
- return result.data;
286
- }
287
- })();
288
-
289
- return Page.create();
290
- }
291
- ```
292
-
293
- ### API 文件中
294
-
295
- ```typescript
296
- // src/api/user.ts
297
- import request from "@jhlc/common-core/src/util/request";
298
-
299
- export function getUserList(params: any) {
300
- return request({
301
- url: "/api/user/list",
302
- method: "get",
303
- params
304
- });
305
- }
306
-
307
- export function saveUser(data: any) {
308
- return request({
309
- url: "/api/user/save",
310
- method: "post",
311
- data
312
- });
313
- }
314
- ```
315
-
316
- ## 错误处理
317
-
318
- ```typescript
319
- try {
320
- const result = await request({
321
- url: "/api/user/save",
322
- method: "post",
323
- data: formData
324
- });
325
- console.log("成功:", result.message);
326
- } catch (error) {
327
- console.error("失败:", error);
328
- // request 会自动弹出错误提示,这里可以做额外处理
329
- }
330
- ```
331
-
332
- ## 注意事项
333
-
334
- 1. **method 必填**:必须明确指定请求方法(get、post、put、delete)
335
- 2. **params vs data**:
336
- - `params`:URL 查询参数,适用于 GET/DELETE
337
- - `data`:请求体数据,适用于 POST/PUT
338
- 3. **返回值**:直接返回 `ApiResult<T>` = `{ code, message, data }`,不是 axios 的 response 对象(已自动解包一层)
339
- 4. **错误处理**:request 会自动处理错误并提示,通常不需要手动 catch
340
- 5. **mock 的 code 值**:response interceptor 同时接受 `code: 200` 和 `code: 2000` 作为成功。项目 mock 文件通常写 `code: 200`(HTTP 标准),真实后端使用业务码 `code: 2000`,两者均正常工作
341
-
342
- ## 实际案例
343
-
344
- ### 案例 1:登录
345
-
346
- ```typescript
347
- // src/api/login.ts
348
- export function login(username: string, password: string) {
349
- return request({
350
- url: "/auth/oauth/token",
351
- method: "post",
352
- params: {
353
- grant_type: "password",
354
- username,
355
- password,
356
- client_id: "c1",
357
- client_secret: "secret"
358
- }
359
- });
360
- }
361
-
362
- // 使用
363
- const result = await login("admin", "123456");
364
- localStorage.setItem("token", result.data.access_token);
365
- ```
366
-
367
- ### 案例 2:CRUD 完整流程
368
-
369
- ```typescript
370
- // src/views/order/data.ts
371
- import request from "@jhlc/common-core/src/util/request";
372
-
373
- export function createPage(modalRef: Ref<any>) {
374
- const Page = new (class extends AbstractPageQueryHook {
375
- // 查询列表
376
- async select() {
377
- const result = await request({
378
- url: "/api/order/list",
379
- method: "get",
380
- params: {
381
- current: this.page.current,
382
- size: this.page.size
383
- }
384
- });
385
- this.list.value = result.data.records;
386
- }
387
-
388
- // 删除
389
- async remove(id: string) {
390
- const result = await request({
391
- url: "/api/order/remove",
392
- method: "delete",
393
- params: { id }
394
- });
395
- this.msgSuccess(result.message);
396
- this.select();
397
- }
398
- })();
399
-
400
- return Page.create();
401
- }
402
-
403
- // Modal 弹窗
404
- export function createFormModal(props, mode, emit) {
405
- const Page = new (class extends AbstractFormHook {
406
- // 保存(新增/编辑)
407
- async save() {
408
- const result = await request({
409
- url: mode.value === "add" ? props.api.save : props.api.update,
410
- method: mode.value === "add" ? "post" : "put",
411
- data: this.form.value
412
- });
413
- this.msgSuccess(result.message);
414
- emit("ok");
415
- }
416
-
417
- // 获取详情
418
- async getById(id: string) {
419
- const result = await request({
420
- url: props.api.getById,
421
- method: "get",
422
- params: { id }
423
- });
424
- this.form.value = result.data;
425
- }
426
- })();
427
-
428
- return Page.create();
429
- }
430
- ```
431
-
432
- ---
433
-
434
- ## 📌 AbstractPageQueryHook 基类内置方法
435
-
436
- > 在继承 `AbstractPageQueryHook` 的页面中,可以直接使用以下内置方法,**无需单独创建 API 层文件**。
437
- > 这些方法(`getAction`/`postAction`/`putAction`/`deleteAction`)也可以单独导入使用:
438
- > ```typescript
439
- > import { getAction, postAction, putAction, deleteAction } from "@jhlc/common-core/src/api/action";
440
- > ```
441
- > 返回值均为 `Promise<ApiResult<T>>`,即 `{ code: number, message: string, data: T }`,与直接调用 `request()` 完全相同。
442
-
443
- ### 可用方法一览
444
-
445
- | 方法 | 作用 | 推荐场景 |
446
- |------|------|---------|
447
- | `this.getAction` | GET 请求 | 查询详情、获取下拉选项 |
448
- | `this.postAction` | POST 请求 | 新增、批量审批、自定义操作 |
449
- | `this.putAction` | PUT 请求 | 修改、批量更新 |
450
- | `this.deleteAction` | DELETE 请求 | 删除(单条/批量) |
451
- | `this.actionBatch` | 批量操作封装 | 带确认框的批量POST/PUT |
452
- | `this.postBatch` | POST批量快捷方法 | 批量审批、批量导入 |
453
- | `this.putBatch` | PUT批量快捷方法 | 批量修改状态 |
454
- | `this.deleteBatch` | DELETE批量快捷方法 | 批量删除 |
455
-
456
- ---
457
-
458
- ### 方法签名与参数说明
459
-
460
- #### getAction
461
- ```typescript
462
- /**
463
- * GET 请求
464
- * @param url - 接口地址
465
- * @param params - 查询参数(拼接到URL上)
466
- * @param headers - 请求头配置(可选)
467
- */
468
- this.getAction<T>(url: string, params?: object, headers?: RequestHeader): Promise<ApiResult<T>>
469
-
470
- // 示例
471
- const res = await this.getAction("/api/order/detail", { id: "123" });
472
- console.log(res.data);
473
- ```
474
-
475
- #### postAction
476
- ```typescript
477
- /**
478
- * POST 请求
479
- * @param url - 接口地址
480
- * @param data - 请求体数据
481
- * @param query - URL查询参数(可选)
482
- * @param headers - 请求头配置(可选)
483
- */
484
- this.postAction<T>(url: string, data?: any, query?: object, headers?: RequestHeader): Promise<ApiResult<T>>
485
-
486
- // 示例:审批操作
487
- this.postAction("/api/order/approve", { ids: [row.id], status: "approved" });
488
-
489
- // 示例:带query参数的POST
490
- this.postAction("/api/user/import", fileData, { type: "excel" });
491
- ```
492
-
493
- #### putAction
494
- ```typescript
495
- /**
496
- * PUT 请求
497
- * @param url - 接口地址
498
- * @param data - 请求体数据
499
- * @param query - URL查询参数(可选)
500
- * @param headers - 请求头配置(可选)
501
- */
502
- this.putAction<T>(url: string, data?: any, query?: object, headers?: RequestHeader): Promise<ApiResult<T>>
503
-
504
- // 示例:修改状态
505
- this.putAction("/api/order/updateStatus", { id: row.id, status: "completed" });
506
- ```
507
-
508
- #### deleteAction ⚠️
509
- ```typescript
510
- /**
511
- * DELETE 请求
512
- * @param url - 接口地址
513
- * @param params - URL查询参数(第二个参数)
514
- * @param data - 请求体数据(第三个参数)⚠️
515
- * @param headers - 请求头配置(可选)
516
- */
517
- this.deleteAction<T>(url: string, params?: any, data?: any, headers?: RequestHeader): Promise<ApiResult<T>>
518
-
519
- // ✅ 正确用法:把 ids 放在 data(第三个参数)
520
- this.deleteAction("/api/order/remove", {}, { ids: [row.id] });
521
-
522
- // ❌ 错误用法:ids 会被当作 params(query参数)
523
- this.deleteAction("/api/order/remove", { ids: [row.id] }); // Mock/后端期望body时会失败
524
- ```
525
-
526
- #### actionBatch
527
- ```typescript
528
- /**
529
- * 批量操作封装(自动处理选择、确认、刷新)
530
- * @param action - 要执行的方法(this.postAction / this.putAction)
531
- * @param url - 接口地址
532
- * @param tip - 确认提示文字
533
- * @param idList - ID数组(可选,默认获取选中行)
534
- * @param autoTipSuccess - 是否自动提示成功(默认false)
535
- */
536
- this.actionBatch(
537
- action: Function,
538
- url: string,
539
- tip: string,
540
- idList?: string[],
541
- autoTipSuccess?: boolean
542
- ): Promise<ApiResult>
543
-
544
- // 示例:批量审批
545
- this.actionBatch(
546
- this.postAction,
547
- "/api/order/batchApprove",
548
- "确定审批选中的订单吗?",
549
- this.getSelection().map(i => i.id)
550
- );
551
-
552
- // ⚠️ 注意:不要用于 deleteAction(参数位置不匹配)
553
- // ❌ this.actionBatch(this.deleteAction, url, tip, ids) // ids会变成params
554
- ```
555
-
556
- #### postBatch / putBatch / deleteBatch
557
- ```typescript
558
- /**
559
- * 快捷批量方法(自动处理选择、确认、刷新、成功提示)
560
- * @param url - 接口地址
561
- * @param tip - 确认提示
562
- * @param idList - ID数组(可选)
563
- */
564
- this.postBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
565
- this.putBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
566
- this.deleteBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
567
-
568
- // 示例
569
- this.postBatch("/api/order/batchApprove", "确定审批选中数据?");
570
- ```
571
-
572
- ---
573
-
574
- ### 📝 实战示例
575
-
576
- #### 示例1:单行操作按钮
577
-
578
- ```typescript
579
- // data.ts
580
- columnsDef(): TableColumnDesc[] {
581
- return [
582
- // ... 其他列
583
- {
584
- label: "操作",
585
- width: 200,
586
- fixed: "right",
587
- operations: [
588
- {
589
- name: "approve",
590
- label: "审批",
591
- onClick: (row: any) =>
592
- this.confirm("确定审批该记录吗?", "提示").then(() => {
593
- this.postAction(API_CONFIG.approve, { id: row.id, status: "approved" })
594
- .then(res => {
595
- this.msgSuccess(res.message);
596
- this.select(); // 刷新列表
597
- });
598
- })
599
- },
600
- {
601
- name: "delete",
602
- label: "删除",
603
- onClick: (row: any) =>
604
- this.confirm("确定删除吗?", "警告").then(() => {
605
- // ✅ 注意:deleteAction 的 ids 放在第三个参数(data)
606
- this.deleteAction(API_CONFIG.remove, {}, { ids: [row.id] })
607
- .then(res => {
608
- this.msgSuccess(res.message);
609
- this.select();
610
- });
611
- })
612
- }
613
- ]
614
- }
615
- ];
616
- }
617
- ```
618
-
619
- #### 示例2:工具栏批量操作
620
-
621
- ```typescript
622
- // data.ts
623
- toolbarDef(): ActionButtonDesc[] {
624
- return [
625
- {
626
- label: "批量审批",
627
- type: "primary",
628
- icon: "Check",
629
- onClick: () => {
630
- const ids = this.getSelection().map(i => i.id);
631
- if (!ids.length) {
632
- this.msgWarning("请选择数据");
633
- return;
634
- }
635
-
636
- // ✅ 方式1:使用 actionBatch(推荐)
637
- this.actionBatch(
638
- this.postAction,
639
- API_CONFIG.batchApprove,
640
- "确定审批选中数据吗?",
641
- ids
642
- );
643
- }
644
- },
645
- {
646
- label: "批量删除",
647
- type: "danger",
648
- icon: "Delete",
649
- onClick: () => {
650
- const ids = this.getSelection().map(i => i.id);
651
- if (!ids.length) {
652
- this.msgWarning("请选择数据");
653
- return;
654
- }
655
-
656
- // ✅ 方式2:手动调用(DELETE需要这样)
657
- this.confirm("确定删除选中数据吗?", "警告").then(() => {
658
- this.deleteAction(API_CONFIG.remove, {}, { ids }).then(res => {
659
- this.msgSuccess(res.message);
660
- this.select();
661
- });
662
- });
663
- }
664
- },
665
- {
666
- label: "批量发布",
667
- type: "success",
668
- icon: "Upload",
669
- onClick: () => {
670
- // ✅ 方式3:使用快捷方法 postBatch
671
- this.postBatch(API_CONFIG.batchPublish, "确定发布选中数据吗?");
672
- }
673
- }
674
- ];
675
- }
676
- ```
677
-
678
- #### 示例3:获取详情/下拉选项
679
-
680
- ```typescript
681
- // data.ts
682
- async loadOptions() {
683
- // 获取字典选项
684
- const res = await this.getAction("/system/dictDtl/getListByDicSn", { strSn: "ORDER_STATUS" });
685
- this.statusOptions = res.data;
686
- }
687
-
688
- async viewDetail(id: string) {
689
- // 查看详情
690
- const res = await this.getAction(API_CONFIG.getById, { id });
691
- this.detailData.value = res.data;
692
- }
693
- ```
694
-
695
- #### 示例4:复杂参数场景
696
-
697
- ```typescript
698
- // 带复杂参数的批量操作
699
- onClick: () => {
700
- const rows = this.getSelection();
701
- if (!rows.length) {
702
- this.msgWarning("请选择数据");
703
- return;
704
- }
705
-
706
- this.confirm("确定提交选中数据吗?", "提示").then(() => {
707
- // 自定义参数结构
708
- this.postAction(API_CONFIG.batchSubmit, {
709
- ids: rows.map(r => r.id),
710
- submitTime: new Date().toISOString(),
711
- operator: "admin",
712
- remark: "批量提交"
713
- }).then(res => {
714
- this.msgSuccess(res.message);
715
- this.select();
716
- });
717
- });
718
- }
719
- ```
720
-
721
- ---
722
-
723
- ### ⚠️ 常见错误与注意事项
724
-
725
- #### 错误1:deleteAction 参数位置错误
726
-
727
- ```typescript
728
- // ❌ 错误:ids被当作params(query参数)
729
- this.deleteAction(API_CONFIG.remove, { ids: [row.id] });
730
- // 实际请求:DELETE /api/remove?ids=xxx (Mock/后端期望body时会失败)
731
-
732
- // ✅ 正确:ids放在第三个参数(data/body)
733
- this.deleteAction(API_CONFIG.remove, {}, { ids: [row.id] });
734
- // 实际请求:DELETE /api/remove Body: { ids: ["xxx"] }
735
- ```
736
-
737
- #### 错误2:actionBatch 用于 deleteAction
738
-
739
- ```typescript
740
- // ❌ 错误:actionBatch会把ids传给deleteAction的第二个参数(params)
741
- this.actionBatch(this.deleteAction, API_CONFIG.remove, "确定删除?", ids);
742
- // 等价于:this.deleteAction(url, ids) ← ids变成了params
743
-
744
- // ✅ 正确:手动调用
745
- this.confirm("确定删除?", "警告").then(() => {
746
- this.deleteAction(API_CONFIG.remove, {}, { ids }).then(res => {
747
- this.msgSuccess(res.message);
748
- this.select();
749
- });
750
- });
751
- ```
752
-
753
- #### 错误3:忘记刷新列表
754
-
755
- ```typescript
756
- // ❌ 错误:操作成功后没有刷新
757
- onClick: (row) =>
758
- this.postAction(API_CONFIG.approve, { id: row.id })
759
- .then(res => this.msgSuccess(res.message));
760
-
761
- // ✅ 正确:调用 this.select() 刷新
762
- onClick: (row) =>
763
- this.postAction(API_CONFIG.approve, { id: row.id })
764
- .then(res => {
765
- this.msgSuccess(res.message);
766
- this.select(); // ← 刷新列表
767
- });
768
- ```
769
-
770
- #### 错误4:没有选中数据检查
771
-
772
- ```typescript
773
- // ❌ 错误:没有检查是否选中数据
774
- onClick: () => {
775
- const ids = this.getSelection().map(i => i.id);
776
- this.postBatch(API_CONFIG.batchApprove, "确定审批?", ids);
777
- }
778
-
779
- // ✅ 正确:先检查
780
- onClick: () => {
781
- const ids = this.getSelection().map(i => i.id);
782
- if (!ids.length) {
783
- this.msgWarning("请选择数据");
784
- return;
785
- }
786
- this.postBatch(API_CONFIG.batchApprove, "确定审批?", ids);
787
- }
788
- ```
789
-
790
- ---
791
-
792
- ### 🎯 最佳实践
793
-
794
- #### 1. 统一使用 API_CONFIG 管理路径
795
-
796
- ```typescript
797
- // data.ts
798
- export const API_CONFIG = {
799
- list: "/api/order/list",
800
- remove: "/api/order/remove",
801
- approve: "/api/order/approve",
802
- batchSubmit: "/api/order/batchSubmit"
803
- } as const;
804
-
805
- // 使用时引用配置
806
- this.postAction(API_CONFIG.approve, data);
807
- ```
808
-
809
- #### 2. 单行操作用 confirm + action
810
-
811
- ```typescript
812
- // 单行删除、审批等
813
- onClick: (row) =>
814
- this.confirm("确定操作吗?", "提示").then(() => {
815
- this.postAction(API_CONFIG.xxx, { id: row.id })
816
- .then(res => {
817
- this.msgSuccess(res.message);
818
- this.select();
819
- });
820
- })
821
- ```
822
-
823
- #### 3. 批量操作用 actionBatch 或 xxxBatch
824
-
825
- ```typescript
826
- // 批量POST/PUT:使用 actionBatch
827
- this.actionBatch(this.postAction, API_CONFIG.batchApprove, "确定审批?", ids);
828
-
829
- // 批量DELETE:手动调用
830
- this.confirm("确定删除?", "警告").then(() => {
831
- this.deleteAction(API_CONFIG.remove, {}, { ids }).then(/* ... */);
832
- });
833
-
834
- // 或使用快捷方法
835
- this.postBatch(API_CONFIG.batchApprove, "确定审批?");
836
- ```
837
-
838
- #### 4. 复杂场景可封装方法
839
-
840
- ```typescript
841
- // data.ts
842
- export function createPage() {
843
- return new class extends AbstractPageQueryHook {
844
- // 封装复杂的批量操作
845
- async batchApproveWithRemark() {
846
- const rows = this.getSelection();
847
- if (!rows.length) {
848
- this.msgWarning("请选择数据");
849
- return;
850
- }
851
-
852
- // 弹出输入框获取备注
853
- const remark = await this.prompt("请输入审批意见", "审批");
854
- if (!remark) return;
855
-
856
- return this.postAction(API_CONFIG.batchApprove, {
857
- ids: rows.map(r => r.id),
858
- remark,
859
- approveTime: new Date()
860
- }).then(res => {
861
- this.msgSuccess(res.message);
862
- this.select();
863
- });
864
- }
865
- }
866
- }
867
- ```
868
-
869
- ---
870
-
871
- ### 📚 何时需要独立 API 文件?
872
-
873
- 虽然基类方法已覆盖大部分场景,但以下情况仍建议创建 `api/*.ts`:
874
-
875
- 1. **复杂参数转换**
876
- ```typescript
877
- // 需要复杂的前置数据处理
878
- export function submitOrder(form: OrderForm) {
879
- const params = {
880
- ...form,
881
- items: form.items.map(transformItem), // 复杂转换
882
- attachments: await uploadFiles(form.files) // 异步前置
883
- };
884
- return request({ url: "/api/order/submit", method: "post", data: params });
885
- }
886
- ```
887
-
888
- 2. **特殊 Header 或 Content-Type**
889
- ```typescript
890
- // 文件上传、multipart等
891
- export function uploadFile(file: File) {
892
- const formData = new FormData();
893
- formData.append("file", file);
894
- return request({
895
- url: "/api/file/upload",
896
- method: "post",
897
- data: formData,
898
- headers: { "Content-Type": "multipart/form-data" }
899
- });
900
- }
901
- ```
902
-
903
- 3. **多个接口组合调用**
904
- ```typescript
905
- // 需要先后调用多个接口
906
- export async function publishOrder(id: string) {
907
- await request({ url: `/api/order/validate/${id}`, method: "get" });
908
- await request({ url: `/api/order/lock/${id}`, method: "post" });
909
- return request({ url: `/api/order/publish/${id}`, method: "post" });
910
- }
911
- ```
912
-
913
- 4. **跨模块复用**
914
- ```typescript
915
- // 多个页面都要调用的通用接口
916
- export function getUserInfo() {
917
- return request({ url: "/api/user/info", method: "get" });
918
- }
919
- ```
920
-
921
- 对于**简单的 CRUD + 按钮操作**,直接在 `data.ts` 中使用基类方法即可,**无需创建 API 文件**。
922
-
923
- ---
924
-
925
- ## 总结
926
-
927
- - **统一配置**:所有请求使用相同的配置格式
928
- - **自动解包**:无需关心 axios 的 response 结构
929
- - **类型安全**:配合 TypeScript 获得完整类型提示
930
- - **易于维护**:所有 HTTP 请求逻辑统一管理
931
- - **项目规范**:与团队其他成员保持一致的代码风格
932
- - **基类优先**:继承 `AbstractPageQueryHook` 时优先使用内置方法,减少 API 层文件
933
-
934
- ---
935
-
936
- **相关文档**:
937
- - [AbstractPageQueryHook 页面开发最佳实践](./page-query-hook-best-practices.md)
938
-
939
- ---
940
-
1
+ # request - HTTP 请求工具
2
+
3
+ > 基于 axios 封装的统一请求工具,自动处理响应数据解包、错误拦截、token 注入等
4
+
5
+ ## 导入方式
6
+
7
+ ```typescript
8
+ import request from "@jhlc/common-core/src/util/request";
9
+ ```
10
+
11
+ ## 基本用法
12
+
13
+ ### GET 请求
14
+
15
+ ```typescript
16
+ // 基础 GET 请求
17
+ const data = await request({
18
+ url: "/api/user/list",
19
+ method: "get"
20
+ });
21
+
22
+ // 带查询参数
23
+ const data = await request({
24
+ url: "/api/user/detail",
25
+ method: "get",
26
+ params: { id: "123" }
27
+ });
28
+ ```
29
+
30
+ ### POST 请求
31
+
32
+ ```typescript
33
+ // 创建数据
34
+ const result = await request({
35
+ url: "/api/user/save",
36
+ method: "post",
37
+ data: {
38
+ name: "张三",
39
+ age: 25
40
+ }
41
+ });
42
+
43
+ // 带查询参数的 POST
44
+ const result = await request({
45
+ url: "/auth/oauth/token",
46
+ method: "post",
47
+ params: {
48
+ grant_type: "password",
49
+ username: "admin"
50
+ }
51
+ });
52
+ ```
53
+
54
+ ### PUT 请求
55
+
56
+ ```typescript
57
+ // 更新数据
58
+ const result = await request({
59
+ url: "/api/user/update",
60
+ method: "put",
61
+ data: {
62
+ id: "123",
63
+ name: "李四"
64
+ }
65
+ });
66
+ ```
67
+
68
+ ### DELETE 请求
69
+
70
+ ```typescript
71
+ // 删除数据
72
+ const result = await request({
73
+ url: "/api/user/delete",
74
+ method: "delete",
75
+ params: { id: "123" }
76
+ });
77
+ ```
78
+
79
+ ## 响应数据结构
80
+
81
+ `request()` 的 response interceptor 执行 `return cloneDeep(res.data)`,直接返回 HTTP 响应体,即 `ApiResult<T>` 结构:
82
+
83
+ ```typescript
84
+ // ApiResult<T> 类型定义(来自 @jhlc/types)
85
+ interface ApiResult<T = any> {
86
+ code: number; // 2000 = 成功,200 也被接受(mock 常用)
87
+ message: string; // 提示信息
88
+ data: T; // 实际业务数据
89
+ }
90
+
91
+ // 后端返回的 HTTP 响应体(request() 直接返回这层,不再多一层 .data)
92
+ {
93
+ code: 2000,
94
+ message: "操作成功",
95
+ data: { id: 1, name: "张三" }
96
+ }
97
+
98
+ // 使用 request 后直接拿到
99
+ const result = await request({ url: "/api/xxx", method: "get" });
100
+ console.log(result.code); // 2000
101
+ console.log(result.message); // "操作成功"
102
+ console.log(result.data); // { id: 1, name: "张三" }(业务数据)
103
+ ```
104
+
105
+ > **与 `getAction`/`postAction` 的关系**:这些 action 函数内部调用 `request()`,返回值结构**完全相同**,类型签名为 `Promise<ApiResult<T>>`。`ApiResult<T>` 就是上面的 `{ code, message, data: T }`,两者等价。
106
+
107
+ ## 常见场景
108
+
109
+ ### 场景 1:列表查询
110
+
111
+ ```typescript
112
+ // src/views/your-module/data.ts
113
+ async select() {
114
+ const params = {
115
+ current: this.page.current,
116
+ size: this.page.size,
117
+ ...this.queryParam.value
118
+ };
119
+
120
+ const result = await request({
121
+ url: "/api/order/list",
122
+ method: "get",
123
+ params
124
+ });
125
+
126
+ this.list.value = result.data.records;
127
+ this.page.total = result.data.total;
128
+ }
129
+ ```
130
+
131
+ ### 场景 2:表单保存
132
+
133
+ ```typescript
134
+ // 新增/编辑统一处理
135
+ async save() {
136
+ const result = await request({
137
+ url: this.isEdit ? "/api/user/update" : "/api/user/save",
138
+ method: this.isEdit ? "put" : "post",
139
+ data: this.form.value
140
+ });
141
+
142
+ this.msgSuccess(result.message || "操作成功");
143
+ }
144
+ ```
145
+
146
+ ### 场景 3:获取详情
147
+
148
+ ```typescript
149
+ async getById(id: string) {
150
+ const result = await request({
151
+ url: "/api/order/getOneById",
152
+ method: "get",
153
+ params: { id }
154
+ });
155
+
156
+ this.form.value = result.data;
157
+ }
158
+ ```
159
+
160
+ ### 场景 4:删除数据
161
+
162
+ ```typescript
163
+ async remove(id: string) {
164
+ const result = await request({
165
+ url: "/api/order/remove",
166
+ method: "delete",
167
+ params: { id }
168
+ });
169
+
170
+ this.msgSuccess(result.message || "删除成功");
171
+ this.select(); // 刷新列表
172
+ }
173
+ ```
174
+
175
+ ### 场景 5:文件下载
176
+
177
+ ```typescript
178
+ // 下载文件需要设置 responseType
179
+ async downloadFile() {
180
+ const res = await request({
181
+ url: "/api/file/download",
182
+ method: "get",
183
+ params: { id: "123" },
184
+ responseType: "arraybuffer"
185
+ });
186
+
187
+ // 创建 Blob 并下载
188
+ const blob = new Blob([res], { type: "application/vnd.ms-excel" });
189
+ const downloadElement = document.createElement("a");
190
+ const href = window.URL.createObjectURL(blob);
191
+ downloadElement.href = href;
192
+ downloadElement.download = "文件名.xlsx";
193
+ document.body.appendChild(downloadElement);
194
+ downloadElement.click();
195
+ document.body.removeChild(downloadElement);
196
+ }
197
+ ```
198
+
199
+ ### 场景 6:获取字典数据
200
+
201
+ ```typescript
202
+ // 获取下拉选项
203
+ async loadDictOptions() {
204
+ const result = await request({
205
+ url: "/system/dictDtl/getListByDicSn",
206
+ method: "get",
207
+ params: { strSn: "ORDER_STATUS" }
208
+ });
209
+
210
+ this.statusOptions = result.data;
211
+ }
212
+ ```
213
+
214
+ ## 配置选项
215
+
216
+ ```typescript
217
+ interface RequestConfig {
218
+ url: string; // 请求地址(必填)
219
+ method: string; // 请求方法:get、post、put、delete(必填)
220
+ params?: object; // URL 查询参数
221
+ data?: object; // 请求体数据(POST/PUT)
222
+ headers?: object; // 自定义请求头
223
+ responseType?: string; // 响应类型:json、arraybuffer、blob
224
+ timeout?: number; // 超时时间(毫秒)
225
+ }
226
+ ```
227
+
228
+ ## 与原生 axios 对比
229
+
230
+ ### 原生 axios(啰嗦)
231
+
232
+ ```typescript
233
+ import axios from "axios";
234
+
235
+ // 需要手动解包 res.data
236
+ const res = await axios.get("/api/user/list");
237
+ console.log(res.data.data); // 两层 data
238
+
239
+ // POST 请求参数位置不一致
240
+ await axios.post("/api/user/save", formData);
241
+ await axios.get("/api/user/detail", { params: { id: 1 } });
242
+ ```
243
+
244
+ ### 封装 request(统一)
245
+
246
+ ```typescript
247
+ import request from "@jhlc/common-core/src/util/request";
248
+
249
+ // 自动解包,直接拿到数据
250
+ const result = await request({
251
+ url: "/api/user/list",
252
+ method: "get"
253
+ });
254
+ console.log(result.data); // 一层 data
255
+
256
+ // 所有请求配置统一
257
+ await request({ url: "/api/user/save", method: "post", data: formData });
258
+ await request({ url: "/api/user/detail", method: "get", params: { id: 1 } });
259
+ ```
260
+
261
+ ## 自动处理功能
262
+
263
+ 1. **自动解包**:返回 `{ code, message, data }` 格式,无需 `res.data.data`
264
+ 2. **Token 注入**:自动从 localStorage 读取 token 并添加到请求头
265
+ 3. **错误拦截**:统一处理 401/403/500 等错误,自动弹出提示
266
+ 4. **双码成功**:`code: 2000`(真实后端业务码)和 `code: 200`(mock 常用值)均视为成功
267
+ 5. **i18n 支持**:自动在请求头注入当前语言(`Lang` header)
268
+
269
+ ## 在组件中使用
270
+
271
+ ### 页面 Hook 中
272
+
273
+ ```typescript
274
+ // src/views/your-module/data.ts
275
+ import request from "@jhlc/common-core/src/util/request";
276
+
277
+ export function createPage() {
278
+ const Page = new (class extends AbstractPageQueryHook {
279
+ async customMethod() {
280
+ const result = await request({
281
+ url: "/api/custom",
282
+ method: "post",
283
+ data: { key: "value" }
284
+ });
285
+ return result.data;
286
+ }
287
+ })();
288
+
289
+ return Page.create();
290
+ }
291
+ ```
292
+
293
+ ### API 文件中
294
+
295
+ ```typescript
296
+ // src/api/user.ts
297
+ import request from "@jhlc/common-core/src/util/request";
298
+
299
+ export function getUserList(params: any) {
300
+ return request({
301
+ url: "/api/user/list",
302
+ method: "get",
303
+ params
304
+ });
305
+ }
306
+
307
+ export function saveUser(data: any) {
308
+ return request({
309
+ url: "/api/user/save",
310
+ method: "post",
311
+ data
312
+ });
313
+ }
314
+ ```
315
+
316
+ ## 错误处理
317
+
318
+ ```typescript
319
+ try {
320
+ const result = await request({
321
+ url: "/api/user/save",
322
+ method: "post",
323
+ data: formData
324
+ });
325
+ console.log("成功:", result.message);
326
+ } catch (error) {
327
+ console.error("失败:", error);
328
+ // request 会自动弹出错误提示,这里可以做额外处理
329
+ }
330
+ ```
331
+
332
+ ## 注意事项
333
+
334
+ 1. **method 必填**:必须明确指定请求方法(get、post、put、delete)
335
+ 2. **params vs data**:
336
+ - `params`:URL 查询参数,适用于 GET/DELETE
337
+ - `data`:请求体数据,适用于 POST/PUT
338
+ 3. **返回值**:直接返回 `ApiResult<T>` = `{ code, message, data }`,不是 axios 的 response 对象(已自动解包一层)
339
+ 4. **错误处理**:request 会自动处理错误并提示,通常不需要手动 catch
340
+ 5. **mock 的 code 值**:response interceptor 同时接受 `code: 200` 和 `code: 2000` 作为成功。项目 mock 文件通常写 `code: 200`(HTTP 标准),真实后端使用业务码 `code: 2000`,两者均正常工作
341
+
342
+ ## 实际案例
343
+
344
+ ### 案例 1:登录
345
+
346
+ ```typescript
347
+ // src/api/login.ts
348
+ export function login(username: string, password: string) {
349
+ return request({
350
+ url: "/auth/oauth/token",
351
+ method: "post",
352
+ params: {
353
+ grant_type: "password",
354
+ username,
355
+ password,
356
+ client_id: "c1",
357
+ client_secret: "secret"
358
+ }
359
+ });
360
+ }
361
+
362
+ // 使用
363
+ const result = await login("admin", "123456");
364
+ localStorage.setItem("token", result.data.access_token);
365
+ ```
366
+
367
+ ### 案例 2:CRUD 完整流程
368
+
369
+ ```typescript
370
+ // src/views/order/data.ts
371
+ import request from "@jhlc/common-core/src/util/request";
372
+
373
+ export function createPage(modalRef: Ref<any>) {
374
+ const Page = new (class extends AbstractPageQueryHook {
375
+ // 查询列表
376
+ async select() {
377
+ const result = await request({
378
+ url: "/api/order/list",
379
+ method: "get",
380
+ params: {
381
+ current: this.page.current,
382
+ size: this.page.size
383
+ }
384
+ });
385
+ this.list.value = result.data.records;
386
+ }
387
+
388
+ // 删除
389
+ async remove(id: string) {
390
+ const result = await request({
391
+ url: "/api/order/remove",
392
+ method: "delete",
393
+ params: { id }
394
+ });
395
+ this.msgSuccess(result.message);
396
+ this.select();
397
+ }
398
+ })();
399
+
400
+ return Page.create();
401
+ }
402
+
403
+ // Modal 弹窗
404
+ export function createFormModal(props, mode, emit) {
405
+ const Page = new (class extends AbstractFormHook {
406
+ // 保存(新增/编辑)
407
+ async save() {
408
+ const result = await request({
409
+ url: mode.value === "add" ? props.api.save : props.api.update,
410
+ method: mode.value === "add" ? "post" : "put",
411
+ data: this.form.value
412
+ });
413
+ this.msgSuccess(result.message);
414
+ emit("ok");
415
+ }
416
+
417
+ // 获取详情
418
+ async getById(id: string) {
419
+ const result = await request({
420
+ url: props.api.getById,
421
+ method: "get",
422
+ params: { id }
423
+ });
424
+ this.form.value = result.data;
425
+ }
426
+ })();
427
+
428
+ return Page.create();
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ## 📌 AbstractPageQueryHook 基类内置方法
435
+
436
+ > 在继承 `AbstractPageQueryHook` 的页面中,可以直接使用以下内置方法,**无需单独创建 API 层文件**。
437
+ > 这些方法(`getAction`/`postAction`/`putAction`/`deleteAction`)也可以单独导入使用:
438
+ > ```typescript
439
+ > import { getAction, postAction, putAction, deleteAction } from "@jhlc/common-core/src/api/action";
440
+ > ```
441
+ > 返回值均为 `Promise<ApiResult<T>>`,即 `{ code: number, message: string, data: T }`,与直接调用 `request()` 完全相同。
442
+
443
+ ### 可用方法一览
444
+
445
+ | 方法 | 作用 | 推荐场景 |
446
+ |------|------|---------|
447
+ | `this.getAction` | GET 请求 | 查询详情、获取下拉选项 |
448
+ | `this.postAction` | POST 请求 | 新增、批量审批、自定义操作 |
449
+ | `this.putAction` | PUT 请求 | 修改、批量更新 |
450
+ | `this.deleteAction` | DELETE 请求 | 删除(单条/批量) |
451
+ | `this.actionBatch` | 批量操作封装 | 带确认框的批量POST/PUT |
452
+ | `this.postBatch` | POST批量快捷方法 | 批量审批、批量导入 |
453
+ | `this.putBatch` | PUT批量快捷方法 | 批量修改状态 |
454
+ | `this.deleteBatch` | DELETE批量快捷方法 | 批量删除 |
455
+
456
+ ---
457
+
458
+ ### 方法签名与参数说明
459
+
460
+ #### getAction
461
+ ```typescript
462
+ /**
463
+ * GET 请求
464
+ * @param url - 接口地址
465
+ * @param params - 查询参数(拼接到URL上)
466
+ * @param headers - 请求头配置(可选)
467
+ */
468
+ this.getAction<T>(url: string, params?: object, headers?: RequestHeader): Promise<ApiResult<T>>
469
+
470
+ // 示例
471
+ const res = await this.getAction("/api/order/detail", { id: "123" });
472
+ console.log(res.data);
473
+ ```
474
+
475
+ #### postAction
476
+ ```typescript
477
+ /**
478
+ * POST 请求
479
+ * @param url - 接口地址
480
+ * @param data - 请求体数据
481
+ * @param query - URL查询参数(可选)
482
+ * @param headers - 请求头配置(可选)
483
+ */
484
+ this.postAction<T>(url: string, data?: any, query?: object, headers?: RequestHeader): Promise<ApiResult<T>>
485
+
486
+ // 示例:审批操作
487
+ this.postAction("/api/order/approve", { ids: [row.id], status: "approved" });
488
+
489
+ // 示例:带query参数的POST
490
+ this.postAction("/api/user/import", fileData, { type: "excel" });
491
+ ```
492
+
493
+ #### putAction
494
+ ```typescript
495
+ /**
496
+ * PUT 请求
497
+ * @param url - 接口地址
498
+ * @param data - 请求体数据
499
+ * @param query - URL查询参数(可选)
500
+ * @param headers - 请求头配置(可选)
501
+ */
502
+ this.putAction<T>(url: string, data?: any, query?: object, headers?: RequestHeader): Promise<ApiResult<T>>
503
+
504
+ // 示例:修改状态
505
+ this.putAction("/api/order/updateStatus", { id: row.id, status: "completed" });
506
+ ```
507
+
508
+ #### deleteAction ⚠️
509
+ ```typescript
510
+ /**
511
+ * DELETE 请求
512
+ * @param url - 接口地址
513
+ * @param params - URL查询参数(第二个参数)
514
+ * @param data - 请求体数据(第三个参数)⚠️
515
+ * @param headers - 请求头配置(可选)
516
+ */
517
+ this.deleteAction<T>(url: string, params?: any, data?: any, headers?: RequestHeader): Promise<ApiResult<T>>
518
+
519
+ // ✅ 正确用法:把 ids 放在 data(第三个参数)
520
+ this.deleteAction("/api/order/remove", {}, { ids: [row.id] });
521
+
522
+ // ❌ 错误用法:ids 会被当作 params(query参数)
523
+ this.deleteAction("/api/order/remove", { ids: [row.id] }); // Mock/后端期望body时会失败
524
+ ```
525
+
526
+ #### actionBatch
527
+ ```typescript
528
+ /**
529
+ * 批量操作封装(自动处理选择、确认、刷新)
530
+ * @param action - 要执行的方法(this.postAction / this.putAction)
531
+ * @param url - 接口地址
532
+ * @param tip - 确认提示文字
533
+ * @param idList - ID数组(可选,默认获取选中行)
534
+ * @param autoTipSuccess - 是否自动提示成功(默认false)
535
+ */
536
+ this.actionBatch(
537
+ action: Function,
538
+ url: string,
539
+ tip: string,
540
+ idList?: string[],
541
+ autoTipSuccess?: boolean
542
+ ): Promise<ApiResult>
543
+
544
+ // 示例:批量审批
545
+ this.actionBatch(
546
+ this.postAction,
547
+ "/api/order/batchApprove",
548
+ "确定审批选中的订单吗?",
549
+ this.getSelection().map(i => i.id)
550
+ );
551
+
552
+ // ⚠️ 注意:不要用于 deleteAction(参数位置不匹配)
553
+ // ❌ this.actionBatch(this.deleteAction, url, tip, ids) // ids会变成params
554
+ ```
555
+
556
+ #### postBatch / putBatch / deleteBatch
557
+ ```typescript
558
+ /**
559
+ * 快捷批量方法(自动处理选择、确认、刷新、成功提示)
560
+ * @param url - 接口地址
561
+ * @param tip - 确认提示
562
+ * @param idList - ID数组(可选)
563
+ */
564
+ this.postBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
565
+ this.putBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
566
+ this.deleteBatch(url: string, tip: string, idList?: string[]): Promise<ApiResult>
567
+
568
+ // 示例
569
+ this.postBatch("/api/order/batchApprove", "确定审批选中数据?");
570
+ ```
571
+
572
+ ---
573
+
574
+ ### 📝 实战示例
575
+
576
+ #### 示例1:单行操作按钮
577
+
578
+ ```typescript
579
+ // data.ts
580
+ columnsDef(): TableColumnDesc[] {
581
+ return [
582
+ // ... 其他列
583
+ {
584
+ label: "操作",
585
+ width: 200,
586
+ fixed: "right",
587
+ operations: [
588
+ {
589
+ name: "approve",
590
+ label: "审批",
591
+ onClick: (row: any) =>
592
+ this.confirm("确定审批该记录吗?", "提示").then(() => {
593
+ this.postAction(API_CONFIG.approve, { id: row.id, status: "approved" })
594
+ .then(res => {
595
+ this.msgSuccess(res.message);
596
+ this.select(); // 刷新列表
597
+ });
598
+ })
599
+ },
600
+ {
601
+ name: "delete",
602
+ label: "删除",
603
+ onClick: (row: any) =>
604
+ this.confirm("确定删除吗?", "警告").then(() => {
605
+ // ✅ 注意:deleteAction 的 ids 放在第三个参数(data)
606
+ this.deleteAction(API_CONFIG.remove, {}, { ids: [row.id] })
607
+ .then(res => {
608
+ this.msgSuccess(res.message);
609
+ this.select();
610
+ });
611
+ })
612
+ }
613
+ ]
614
+ }
615
+ ];
616
+ }
617
+ ```
618
+
619
+ #### 示例2:工具栏批量操作
620
+
621
+ ```typescript
622
+ // data.ts
623
+ toolbarDef(): ActionButtonDesc[] {
624
+ return [
625
+ {
626
+ label: "批量审批",
627
+ type: "primary",
628
+ icon: "Check",
629
+ onClick: () => {
630
+ const ids = this.getSelection().map(i => i.id);
631
+ if (!ids.length) {
632
+ this.msgWarning("请选择数据");
633
+ return;
634
+ }
635
+
636
+ // ✅ 方式1:使用 actionBatch(推荐)
637
+ this.actionBatch(
638
+ this.postAction,
639
+ API_CONFIG.batchApprove,
640
+ "确定审批选中数据吗?",
641
+ ids
642
+ );
643
+ }
644
+ },
645
+ {
646
+ label: "批量删除",
647
+ type: "danger",
648
+ icon: "Delete",
649
+ onClick: () => {
650
+ const ids = this.getSelection().map(i => i.id);
651
+ if (!ids.length) {
652
+ this.msgWarning("请选择数据");
653
+ return;
654
+ }
655
+
656
+ // ✅ 方式2:手动调用(DELETE需要这样)
657
+ this.confirm("确定删除选中数据吗?", "警告").then(() => {
658
+ this.deleteAction(API_CONFIG.remove, {}, { ids }).then(res => {
659
+ this.msgSuccess(res.message);
660
+ this.select();
661
+ });
662
+ });
663
+ }
664
+ },
665
+ {
666
+ label: "批量发布",
667
+ type: "success",
668
+ icon: "Upload",
669
+ onClick: () => {
670
+ // ✅ 方式3:使用快捷方法 postBatch
671
+ this.postBatch(API_CONFIG.batchPublish, "确定发布选中数据吗?");
672
+ }
673
+ }
674
+ ];
675
+ }
676
+ ```
677
+
678
+ #### 示例3:获取详情/下拉选项
679
+
680
+ ```typescript
681
+ // data.ts
682
+ async loadOptions() {
683
+ // 获取字典选项
684
+ const res = await this.getAction("/system/dictDtl/getListByDicSn", { strSn: "ORDER_STATUS" });
685
+ this.statusOptions = res.data;
686
+ }
687
+
688
+ async viewDetail(id: string) {
689
+ // 查看详情
690
+ const res = await this.getAction(API_CONFIG.getById, { id });
691
+ this.detailData.value = res.data;
692
+ }
693
+ ```
694
+
695
+ #### 示例4:复杂参数场景
696
+
697
+ ```typescript
698
+ // 带复杂参数的批量操作
699
+ onClick: () => {
700
+ const rows = this.getSelection();
701
+ if (!rows.length) {
702
+ this.msgWarning("请选择数据");
703
+ return;
704
+ }
705
+
706
+ this.confirm("确定提交选中数据吗?", "提示").then(() => {
707
+ // 自定义参数结构
708
+ this.postAction(API_CONFIG.batchSubmit, {
709
+ ids: rows.map(r => r.id),
710
+ submitTime: new Date().toISOString(),
711
+ operator: "admin",
712
+ remark: "批量提交"
713
+ }).then(res => {
714
+ this.msgSuccess(res.message);
715
+ this.select();
716
+ });
717
+ });
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ### ⚠️ 常见错误与注意事项
724
+
725
+ #### 错误1:deleteAction 参数位置错误
726
+
727
+ ```typescript
728
+ // ❌ 错误:ids被当作params(query参数)
729
+ this.deleteAction(API_CONFIG.remove, { ids: [row.id] });
730
+ // 实际请求:DELETE /api/remove?ids=xxx (Mock/后端期望body时会失败)
731
+
732
+ // ✅ 正确:ids放在第三个参数(data/body)
733
+ this.deleteAction(API_CONFIG.remove, {}, { ids: [row.id] });
734
+ // 实际请求:DELETE /api/remove Body: { ids: ["xxx"] }
735
+ ```
736
+
737
+ #### 错误2:actionBatch 用于 deleteAction
738
+
739
+ ```typescript
740
+ // ❌ 错误:actionBatch会把ids传给deleteAction的第二个参数(params)
741
+ this.actionBatch(this.deleteAction, API_CONFIG.remove, "确定删除?", ids);
742
+ // 等价于:this.deleteAction(url, ids) ← ids变成了params
743
+
744
+ // ✅ 正确:手动调用
745
+ this.confirm("确定删除?", "警告").then(() => {
746
+ this.deleteAction(API_CONFIG.remove, {}, { ids }).then(res => {
747
+ this.msgSuccess(res.message);
748
+ this.select();
749
+ });
750
+ });
751
+ ```
752
+
753
+ #### 错误3:忘记刷新列表
754
+
755
+ ```typescript
756
+ // ❌ 错误:操作成功后没有刷新
757
+ onClick: (row) =>
758
+ this.postAction(API_CONFIG.approve, { id: row.id })
759
+ .then(res => this.msgSuccess(res.message));
760
+
761
+ // ✅ 正确:调用 this.select() 刷新
762
+ onClick: (row) =>
763
+ this.postAction(API_CONFIG.approve, { id: row.id })
764
+ .then(res => {
765
+ this.msgSuccess(res.message);
766
+ this.select(); // ← 刷新列表
767
+ });
768
+ ```
769
+
770
+ #### 错误4:没有选中数据检查
771
+
772
+ ```typescript
773
+ // ❌ 错误:没有检查是否选中数据
774
+ onClick: () => {
775
+ const ids = this.getSelection().map(i => i.id);
776
+ this.postBatch(API_CONFIG.batchApprove, "确定审批?", ids);
777
+ }
778
+
779
+ // ✅ 正确:先检查
780
+ onClick: () => {
781
+ const ids = this.getSelection().map(i => i.id);
782
+ if (!ids.length) {
783
+ this.msgWarning("请选择数据");
784
+ return;
785
+ }
786
+ this.postBatch(API_CONFIG.batchApprove, "确定审批?", ids);
787
+ }
788
+ ```
789
+
790
+ ---
791
+
792
+ ### 🎯 最佳实践
793
+
794
+ #### 1. 统一使用 API_CONFIG 管理路径
795
+
796
+ ```typescript
797
+ // data.ts
798
+ export const API_CONFIG = {
799
+ list: "/api/order/list",
800
+ remove: "/api/order/remove",
801
+ approve: "/api/order/approve",
802
+ batchSubmit: "/api/order/batchSubmit"
803
+ } as const;
804
+
805
+ // 使用时引用配置
806
+ this.postAction(API_CONFIG.approve, data);
807
+ ```
808
+
809
+ #### 2. 单行操作用 confirm + action
810
+
811
+ ```typescript
812
+ // 单行删除、审批等
813
+ onClick: (row) =>
814
+ this.confirm("确定操作吗?", "提示").then(() => {
815
+ this.postAction(API_CONFIG.xxx, { id: row.id })
816
+ .then(res => {
817
+ this.msgSuccess(res.message);
818
+ this.select();
819
+ });
820
+ })
821
+ ```
822
+
823
+ #### 3. 批量操作用 actionBatch 或 xxxBatch
824
+
825
+ ```typescript
826
+ // 批量POST/PUT:使用 actionBatch
827
+ this.actionBatch(this.postAction, API_CONFIG.batchApprove, "确定审批?", ids);
828
+
829
+ // 批量DELETE:手动调用
830
+ this.confirm("确定删除?", "警告").then(() => {
831
+ this.deleteAction(API_CONFIG.remove, {}, { ids }).then(/* ... */);
832
+ });
833
+
834
+ // 或使用快捷方法
835
+ this.postBatch(API_CONFIG.batchApprove, "确定审批?");
836
+ ```
837
+
838
+ #### 4. 复杂场景可封装方法
839
+
840
+ ```typescript
841
+ // data.ts
842
+ export function createPage() {
843
+ return new class extends AbstractPageQueryHook {
844
+ // 封装复杂的批量操作
845
+ async batchApproveWithRemark() {
846
+ const rows = this.getSelection();
847
+ if (!rows.length) {
848
+ this.msgWarning("请选择数据");
849
+ return;
850
+ }
851
+
852
+ // 弹出输入框获取备注
853
+ const remark = await this.prompt("请输入审批意见", "审批");
854
+ if (!remark) return;
855
+
856
+ return this.postAction(API_CONFIG.batchApprove, {
857
+ ids: rows.map(r => r.id),
858
+ remark,
859
+ approveTime: new Date()
860
+ }).then(res => {
861
+ this.msgSuccess(res.message);
862
+ this.select();
863
+ });
864
+ }
865
+ }
866
+ }
867
+ ```
868
+
869
+ ---
870
+
871
+ ### 📚 何时需要独立 API 文件?
872
+
873
+ 虽然基类方法已覆盖大部分场景,但以下情况仍建议创建 `api/*.ts`:
874
+
875
+ 1. **复杂参数转换**
876
+ ```typescript
877
+ // 需要复杂的前置数据处理
878
+ export function submitOrder(form: OrderForm) {
879
+ const params = {
880
+ ...form,
881
+ items: form.items.map(transformItem), // 复杂转换
882
+ attachments: await uploadFiles(form.files) // 异步前置
883
+ };
884
+ return request({ url: "/api/order/submit", method: "post", data: params });
885
+ }
886
+ ```
887
+
888
+ 2. **特殊 Header 或 Content-Type**
889
+ ```typescript
890
+ // 文件上传、multipart等
891
+ export function uploadFile(file: File) {
892
+ const formData = new FormData();
893
+ formData.append("file", file);
894
+ return request({
895
+ url: "/api/file/upload",
896
+ method: "post",
897
+ data: formData,
898
+ headers: { "Content-Type": "multipart/form-data" }
899
+ });
900
+ }
901
+ ```
902
+
903
+ 3. **多个接口组合调用**
904
+ ```typescript
905
+ // 需要先后调用多个接口
906
+ export async function publishOrder(id: string) {
907
+ await request({ url: `/api/order/validate/${id}`, method: "get" });
908
+ await request({ url: `/api/order/lock/${id}`, method: "post" });
909
+ return request({ url: `/api/order/publish/${id}`, method: "post" });
910
+ }
911
+ ```
912
+
913
+ 4. **跨模块复用**
914
+ ```typescript
915
+ // 多个页面都要调用的通用接口
916
+ export function getUserInfo() {
917
+ return request({ url: "/api/user/info", method: "get" });
918
+ }
919
+ ```
920
+
921
+ 对于**简单的 CRUD + 按钮操作**,直接在 `data.ts` 中使用基类方法即可,**无需创建 API 文件**。
922
+
923
+ ---
924
+
925
+ ## 总结
926
+
927
+ - **统一配置**:所有请求使用相同的配置格式
928
+ - **自动解包**:无需关心 axios 的 response 结构
929
+ - **类型安全**:配合 TypeScript 获得完整类型提示
930
+ - **易于维护**:所有 HTTP 请求逻辑统一管理
931
+ - **项目规范**:与团队其他成员保持一致的代码风格
932
+ - **基类优先**:继承 `AbstractPageQueryHook` 时优先使用内置方法,减少 API 层文件
933
+
934
+ ---
935
+
936
+ **相关文档**:
937
+ - [AbstractPageQueryHook 页面开发最佳实践](./page-query-hook-best-practices.md)
938
+
939
+ ---
940
+