@agile-team/wl-skills-kit 2.11.1 → 2.11.3
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/CHANGELOG.md +25 -0
- package/README.md +38 -21
- package/bin/wl-skills.js +27 -3
- package/files/.wl-skills/docs/jh-pagination.md +505 -505
- package/files/.wl-skills/docs/request.md +940 -940
- package/files/.wl-skills/docs/validate-exempt.md +113 -0
- package/files/.wl-skills/guides/architecture.md +1 -1
- package/files/.wl-skills/skills/_compat/headers/cursor-mdc.txt +1 -1
- package/files/.wl-skills/skills/_compat/headers/kiro.txt +1 -1
- package/files/.wl-skills/skills/_compat/headers/trae.txt +1 -1
- package/files/.wl-skills/skills/core/convention-audit/SKILL.md +3 -3
- package/files/.wl-skills/skills/core/spec-doc-parse/SKILL.md +332 -332
- package/files/.wl-skills/skills/core/spec-doc-parse/USAGE.md +97 -97
- package/files/.wl-skills/skills/sync/permission-sync/USAGE.md +107 -107
- package/files/.wl-skills/src/components/global/C_ParentView/index.vue +3 -3
- package/files/.wl-skills/src/components/global/C_RightToolbar/index.vue +157 -157
- package/files/.wl-skills/src/components/global/C_SvgIcon/index.vue +31 -31
- package/files/.wl-skills/src/components/global/C_SvgIcon/svgicon.js +10 -10
- package/files/.wl-skills/src/components/global/C_TagStatus/README.md +264 -264
- package/files/.wl-skills/src/components/global/C_TagStatus/config.ts +192 -192
- package/files/.wl-skills/src/components/global/C_TagStatus/index.vue +106 -106
- package/files/.wl-skills/src/components/global/C_TagStatus/types.ts +64 -64
- package/files/.wl-skills/src/components/global/C_Tree/README.md +153 -153
- package/files/.wl-skills/src/components/global/C_Tree/index.scss +42 -42
- package/files/.wl-skills/src/components/global/C_Tree/index.vue +78 -78
- package/files/.wl-skills/src/components/global/C_Tree/types.ts +59 -59
- package/files/.wl-skills/src/components/local/c_formModal/README.md +235 -235
- package/files/.wl-skills/src/components/local/c_formModal/data.ts +95 -95
- package/files/.wl-skills/src/components/local/c_formModal/index.scss +8 -8
- package/files/.wl-skills/src/components/local/c_formModal/index.vue +107 -107
- package/files/.wl-skills/src/components/local/c_formSections/data.ts +175 -175
- package/files/.wl-skills/src/components/local/c_formSections/index.scss +280 -280
- package/files/.wl-skills/src/components/local/c_formSections/index.vue +429 -429
- package/files/.wl-skills/src/components/local/c_listModal/data.ts +41 -41
- package/files/.wl-skills/src/components/local/c_listModal/index.vue +136 -136
- package/files/.wl-skills/src/components/local/c_spliterTitle/index.scss +25 -25
- package/files/.wl-skills/src/components/local/c_spliterTitle/index.vue +21 -21
- package/files/.wl-skills/src/components/remote/AGGrid/README.md +530 -530
- package/files/.wl-skills/src/components/remote/BaseForm/README.md +508 -508
- package/files/.wl-skills/src/components/remote/BaseQuery/README.md +865 -865
- package/files/.wl-skills/src/components/remote/BaseTable/README.md +941 -941
- package/files/.wl-skills/src/components/remote/BaseToolbar/README.md +496 -496
- package/files/.wl-skills/src/types/page.ts +24 -24
- package/files/.wl-skills/standards/04-coding-basics.md +39 -1
- package/files/.wl-skills/standards/09-typescript.md +26 -3
- package/files/.wl-skills/standards/12-base-table.md +56 -4
- package/files/.wl-skills/standards/13-platform-components.md +1 -0
- package/files/.wl-skills/standards/index.md +2 -2
- package/files/.wl-skills/templates/README.md +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/api.md +54 -54
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -346
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -28
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -115
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -43
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -338
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -28
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -115
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -43
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/api.md +88 -88
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/data.ts +601 -601
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.vue +64 -64
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/api.md +67 -67
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/data.ts +286 -286
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.scss +139 -139
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.vue +318 -318
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -98
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -543
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -52
- package/files/.wl-skills/templates/sale/demo/add-demo/data.ts +518 -518
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/data.ts +524 -524
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.scss +154 -154
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.vue +117 -117
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/data.ts +308 -308
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.scss +99 -99
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.vue +77 -77
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/data.ts +367 -367
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.scss +100 -100
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.vue +170 -170
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/meltDialog.vue +320 -320
- package/files/.wl-skills/templates/sale/demo/metallurgical-spec/data.ts +824 -824
- package/lib/ast-rules.js +395 -12
- package/mcp/config.js +46 -46
- package/mcp/registry.js +6 -1
- package/mcp/tools/projectTools.js +9 -1
- package/package.json +2 -2
|
@@ -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
|
+
|