@qilitt-mickey/vue3-temp-skill 1.0.12 → 1.1.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.
- package/README.md +66 -214
- package/SKILL.md +165 -527
- package/package.json +2 -2
- package/references/api-check.md +187 -128
- package/references/base-code-dict.md +49 -48
- package/references/chart-echarts.md +61 -0
- package/references/code-quality.md +44 -15
- package/references/core-kernel.md +305 -0
- package/references/crud-pages.md +87 -261
- package/references/data-compare.md +502 -501
- package/references/data-mapping.md +218 -213
- package/references/data-screen.md +94 -79
- package/references/data-writeback.md +104 -104
- package/references/detail-page.md +100 -99
- package/references/directives-advanced.md +27 -4
- package/references/download-export.md +70 -68
- package/references/feedback-loading.md +62 -60
- package/references/feedback-ui.md +125 -111
- package/references/file-management.md +28 -7
- package/references/flowchart-g6.md +249 -244
- package/references/form-advanced.md +264 -26
- package/references/graph-relation.md +12 -7
- package/references/http-api.md +138 -103
- package/references/icons.md +326 -0
- package/references/layout-theme.md +1 -1
- package/references/mobile-h5.md +20 -0
- package/references/particles.md +143 -0
- package/references/permission-auth.md +8 -31
- package/references/project-inventory.md +175 -120
- package/references/qrcode-barcode.md +107 -92
- package/references/rich-text.md +85 -73
- package/references/seamless-scroll.md +40 -38
- package/references/table-vxe.md +114 -0
- package/references/tree-table.md +2 -1
- package/references/ui-components.md +5 -1
- package/references/verify-captcha.md +110 -96
- package/references/websocket-realtime.md +2 -1
- package/references/wechat-js.md +58 -0
- package/references/workflow-bpmn.md +207 -206
- package/references/advanced-ui.md +0 -302
- package/references/build-optim.md +0 -282
- package/references/vue-core.md +0 -209
package/references/crud-pages.md
CHANGED
|
@@ -1,316 +1,142 @@
|
|
|
1
1
|
---
|
|
2
2
|
skill: crud-pages
|
|
3
|
-
description:
|
|
3
|
+
description: 查询列表页、表单页、标准 CRUD 流程。含 useTableSearch(onSearch/reset)、ReGrid 查询区、表格工具栏。务必按照本文件完整示例编写。
|
|
4
4
|
scope: project
|
|
5
5
|
tags: [vue3, crud, list, form, detail, table, useTableSearch, page]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# CRUD 页面开发规范
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
### 标准结构
|
|
10
|
+
> 以下示例即规范真相。
|
|
11
|
+
> 须同时遵守 `core-kernel` / `http-api`。详情跳转见 `detail-page`,回显见 `data-writeback`。
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```vue
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { useTableSearch } from "@/hooks/useTableSearch";
|
|
19
|
-
import { getXxxListApi } from "@/api/xxx";
|
|
20
|
-
import type { XxxQueryForm, XxxListItem } from "@/api/types/xxx";
|
|
21
|
-
|
|
22
|
-
defineOptions({ name: "XxxList" });
|
|
13
|
+
## 查询列表页
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
const formQuery = reactive<XxxQueryForm>({
|
|
26
|
-
keyword: "",
|
|
27
|
-
status: undefined,
|
|
28
|
-
dateRange: [],
|
|
29
|
-
});
|
|
15
|
+
### useTableSearch 真实签名
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
handleSearch, // 查询
|
|
37
|
-
handleReset, // 重置
|
|
38
|
-
handlePageChange, // 翻页
|
|
39
|
-
handleSizeChange, // 切换每页条数
|
|
40
|
-
} = useTableSearch(formQuery, getXxxListApi);
|
|
17
|
+
```typescript
|
|
18
|
+
useTableSearch(formQuery, searchFn, url)
|
|
19
|
+
// searchFn: (query: CombinedQueryData, url: string) => Promise<Result<T>>
|
|
20
|
+
// 返回含:content, tableData, pagination, loading, formRef, onSearch, reset, tableHeight, ...
|
|
21
|
+
```
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
- 查询:`onSearch()`
|
|
24
|
+
- 重置:`reset()`(内部 `formRef.resetFields()`)
|
|
25
|
+
- 列表:`tableData.value.tableList`(或经 computed 断言)
|
|
26
|
+
- 分页:请求带 `pageNo` / `pageSize`;响应 `data.content` / `data.totalCount`
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
<div class="re-xxx-list">
|
|
48
|
-
<!-- 查询区域 -->
|
|
49
|
-
<el-form :model="formQuery" inline>
|
|
50
|
-
<el-form-item label="关键词">
|
|
51
|
-
<el-input v-model="formQuery.keyword" placeholder="请输入" clearable />
|
|
52
|
-
</el-form-item>
|
|
53
|
-
<el-form-item label="状态">
|
|
54
|
-
<el-select v-model="formQuery.status" placeholder="请选择" clearable>
|
|
55
|
-
<el-option label="启用" :value="1" />
|
|
56
|
-
<el-option label="禁用" :value="0" />
|
|
57
|
-
</el-select>
|
|
58
|
-
</el-form-item>
|
|
59
|
-
<el-form-item>
|
|
60
|
-
<el-button type="primary" @click="handleSearch">查询</el-button>
|
|
61
|
-
<el-button @click="handleReset">重置</el-button>
|
|
62
|
-
</el-form-item>
|
|
63
|
-
</el-form>
|
|
28
|
+
### 完整列表示例
|
|
64
29
|
|
|
65
|
-
|
|
66
|
-
<ReTableBar>
|
|
67
|
-
<template #left>
|
|
68
|
-
<el-button type="primary" @click="handleAdd">新增</el-button>
|
|
69
|
-
</template>
|
|
70
|
-
</ReTableBar>
|
|
30
|
+
见 `core-kernel.md`「列表黄金样板」。本模块补充约定:
|
|
71
31
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@page-change="handlePageChange"
|
|
78
|
-
@size-change="handleSizeChange"
|
|
79
|
-
>
|
|
80
|
-
<el-table-column prop="name" label="名称" />
|
|
81
|
-
<el-table-column prop="status" label="状态">
|
|
82
|
-
<template #default="{ row }">
|
|
83
|
-
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
|
84
|
-
{{ row.status === 1 ? "启用" : "禁用" }}
|
|
85
|
-
</el-tag>
|
|
86
|
-
</template>
|
|
87
|
-
</el-table-column>
|
|
88
|
-
<el-table-column label="操作" width="200" fixed="right">
|
|
89
|
-
<template #default="{ row }">
|
|
90
|
-
<ReTableOperate
|
|
91
|
-
:buttons="[
|
|
92
|
-
{ label: '编辑', type: 'primary', onClick: () => handleEdit(row) },
|
|
93
|
-
{ label: '删除', type: 'danger', onClick: () => handleDelete(row) },
|
|
94
|
-
]"
|
|
95
|
-
/>
|
|
96
|
-
</template>
|
|
97
|
-
</el-table-column>
|
|
98
|
-
</ReGrid>
|
|
99
|
-
</div>
|
|
100
|
-
</template>
|
|
101
|
-
```
|
|
32
|
+
1. 查询区用 **`ReGrid` + `ReGridItem`**,折叠与 `collapsed` / `showCollapse` / `collapsedToggle` 配套。
|
|
33
|
+
2. 工具栏用 **`ReTableBar`**,行操作用 **`ReTableOperate`**,分页用 **`RePagination`**。
|
|
34
|
+
3. 查询 / 重置按钮文案 `$t('查询')` / `$t('重置')`,点击绑定 **`onSearch` / `reset`**。
|
|
35
|
+
4. `defineOptions({ name })` 与路由 `name` 一致(keep-alive / 标签)。
|
|
36
|
+
5. 需要权限的按钮加 `v-auth` / `ReAuth`。
|
|
102
37
|
|
|
103
|
-
###
|
|
38
|
+
### API 配套
|
|
104
39
|
|
|
105
40
|
```typescript
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
options?: {
|
|
110
|
-
immediate?: boolean; // 是否立即加载(默认 true)
|
|
111
|
-
defaultPageSize?: number; // 默认每页条数(默认 10)
|
|
112
|
-
}
|
|
113
|
-
): {
|
|
114
|
-
content: Ref<TResult["list"]>;
|
|
115
|
-
pagination: { currentPage: number; pageSize: number; total: number };
|
|
116
|
-
loading: Ref<boolean>;
|
|
117
|
-
handleSearch: () => Promise<void>;
|
|
118
|
-
handleReset: () => void;
|
|
119
|
-
handlePageChange: (page: number) => void;
|
|
120
|
-
handleSizeChange: (size: number) => void;
|
|
41
|
+
// src/api/customer.ts — 详见 http-api
|
|
42
|
+
export function getCustomerList(data: CustomerQuery, url: string) {
|
|
43
|
+
return http.request<Result<CustomerPage>>("post", url, { data });
|
|
121
44
|
}
|
|
122
45
|
```
|
|
123
46
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
## 表单页
|
|
47
|
+
```typescript
|
|
48
|
+
const { onSearch, reset, tableData, loading, pagination, ... } = useTableSearch(
|
|
49
|
+
formQuery,
|
|
50
|
+
getCustomerList,
|
|
51
|
+
`${import.meta.env.VITE_API_BASE_URL}/console/customer/list`,
|
|
52
|
+
);
|
|
53
|
+
onMounted(() => onSearch());
|
|
54
|
+
```
|
|
134
55
|
|
|
135
|
-
|
|
56
|
+
## 表单页(新增 / 编辑)
|
|
136
57
|
|
|
137
58
|
```vue
|
|
138
59
|
<script setup lang="ts">
|
|
139
60
|
import type { FormInstance, FormRules } from "element-plus";
|
|
140
|
-
import {
|
|
141
|
-
import
|
|
61
|
+
import type { Customer } from "@/api/customer";
|
|
62
|
+
import { saveCustomer } from "@/api/customer";
|
|
63
|
+
import { message } from "@/utils/message";
|
|
64
|
+
import { checkReeorPosition } from "@/utils/utils";
|
|
142
65
|
|
|
143
|
-
defineOptions({ name: "
|
|
66
|
+
defineOptions({ name: "CustomerForm" });
|
|
144
67
|
|
|
145
|
-
const route = useRoute();
|
|
146
|
-
const router = useRouter();
|
|
147
68
|
const formRef = ref<FormInstance>();
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
name: "",
|
|
69
|
+
const submitLoading = ref(false);
|
|
70
|
+
const formData = reactive<Customer>({
|
|
71
|
+
id: "",
|
|
152
72
|
code: "",
|
|
153
|
-
|
|
154
|
-
remark: "",
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const rules = reactive<FormRules<XxxForm>>({
|
|
158
|
-
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
|
|
159
|
-
code: [{ required: true, message: "请输入编码", trigger: "blur" }],
|
|
160
|
-
status: [{ required: true, message: "请选择状态", trigger: "change" }],
|
|
73
|
+
name: "",
|
|
161
74
|
});
|
|
162
75
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const { data } = await getXxxDetailApi(id);
|
|
168
|
-
Object.assign(formData, data);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
76
|
+
const rules: FormRules<Customer> = {
|
|
77
|
+
code: [{ required: true, message: $t("请输入编号"), trigger: "blur" }],
|
|
78
|
+
name: [{ required: true, message: $t("请输入名称"), trigger: "blur" }],
|
|
79
|
+
};
|
|
171
80
|
|
|
172
|
-
|
|
173
|
-
async function handleSubmit() {
|
|
81
|
+
async function onSubmit() {
|
|
174
82
|
const valid = await formRef.value?.validate().catch(() => false);
|
|
175
|
-
if (!valid)
|
|
176
|
-
|
|
177
|
-
|
|
83
|
+
if (!valid) {
|
|
84
|
+
checkReeorPosition();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
submitLoading.value = true;
|
|
178
88
|
try {
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
89
|
+
const res = await saveCustomer(formData);
|
|
90
|
+
if (res.status === 200) {
|
|
91
|
+
message($t("保存成功"), { type: "success" });
|
|
92
|
+
} else {
|
|
93
|
+
message(res.statusText || $t("保存失败"), { type: "error" });
|
|
94
|
+
}
|
|
182
95
|
} finally {
|
|
183
|
-
|
|
96
|
+
submitLoading.value = false;
|
|
184
97
|
}
|
|
185
98
|
}
|
|
186
|
-
|
|
187
|
-
// 重置
|
|
188
|
-
function handleReset() {
|
|
189
|
-
formRef.value?.resetFields();
|
|
190
|
-
}
|
|
191
99
|
</script>
|
|
192
100
|
|
|
193
101
|
<template>
|
|
194
|
-
<
|
|
195
|
-
<el-form
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<el-input v-model="formData.code" placeholder="请输入" />
|
|
206
|
-
</el-form-item>
|
|
207
|
-
<el-form-item label="状态" prop="status">
|
|
208
|
-
<el-radio-group v-model="formData.status">
|
|
209
|
-
<el-radio :value="1">启用</el-radio>
|
|
210
|
-
<el-radio :value="0">禁用</el-radio>
|
|
211
|
-
</el-radio-group>
|
|
212
|
-
</el-form-item>
|
|
213
|
-
<el-form-item label="备注" prop="remark">
|
|
214
|
-
<el-input v-model="formData.remark" type="textarea" :rows="3" />
|
|
215
|
-
</el-form-item>
|
|
216
|
-
<el-form-item>
|
|
217
|
-
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
|
218
|
-
保存
|
|
219
|
-
</el-button>
|
|
220
|
-
<el-button @click="handleReset">重置</el-button>
|
|
221
|
-
</el-form-item>
|
|
222
|
-
</el-form>
|
|
223
|
-
</div>
|
|
102
|
+
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
|
|
103
|
+
<el-form-item :label="$t('编号')" prop="code">
|
|
104
|
+
<el-input v-model="formData.code" />
|
|
105
|
+
</el-form-item>
|
|
106
|
+
<el-form-item :label="$t('名称')" prop="name">
|
|
107
|
+
<el-input v-model="formData.name" />
|
|
108
|
+
</el-form-item>
|
|
109
|
+
<el-button type="primary" :loading="submitLoading" @click="onSubmit">
|
|
110
|
+
{{ $t("提交") }}
|
|
111
|
+
</el-button>
|
|
112
|
+
</el-form>
|
|
224
113
|
</template>
|
|
225
114
|
```
|
|
226
115
|
|
|
227
116
|
### 表单约定
|
|
228
117
|
|
|
229
|
-
1.
|
|
230
|
-
2.
|
|
231
|
-
3.
|
|
232
|
-
4.
|
|
233
|
-
5. 重置使用 `formRef.resetFields()`,不要手动清空字段。
|
|
234
|
-
6. 提交按钮使用 `loading` 状态防止重复提交。
|
|
118
|
+
1. `reactive` + `FormRules` + `formRef.validate()`
|
|
119
|
+
2. 提交按钮必须有 **loading** 防重复
|
|
120
|
+
3. 编辑回填:`Object.assign(formData, detail)`(见 `data-writeback`)
|
|
121
|
+
4. 远程下拉:`ReSelectQuery`(`url` + `value` + `label`,回写用 `edit-data`),见 `form-advanced`
|
|
235
122
|
|
|
236
|
-
##
|
|
237
|
-
|
|
238
|
-
### 页面跳转传参
|
|
123
|
+
## 路由片段
|
|
239
124
|
|
|
240
125
|
```typescript
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 列表页跳转到编辑页
|
|
250
|
-
function handleEdit(row: XxxListItem) {
|
|
251
|
-
router.push({
|
|
252
|
-
path: "/xxx/edit",
|
|
253
|
-
query: { id: row.id },
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// 列表页跳转到新增页
|
|
258
|
-
function handleAdd() {
|
|
259
|
-
router.push({ path: "/xxx/add" });
|
|
126
|
+
{
|
|
127
|
+
path: "/customer/list",
|
|
128
|
+
name: "CustomerList",
|
|
129
|
+
component: () => import("@/views/customer/list/index.vue"),
|
|
130
|
+
meta: { title: "客户列表", roles: ["admin", "common"] },
|
|
260
131
|
}
|
|
261
132
|
```
|
|
262
133
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// 编辑页加载数据并回填
|
|
267
|
-
onMounted(async () => {
|
|
268
|
-
const id = route.query.id as string;
|
|
269
|
-
if (!id) return;
|
|
270
|
-
|
|
271
|
-
loading.value = true;
|
|
272
|
-
try {
|
|
273
|
-
const { data } = await getXxxDetailApi(id);
|
|
274
|
-
// 使用 Object.assign 保持 reactive 响应性
|
|
275
|
-
Object.assign(formData, data);
|
|
276
|
-
} finally {
|
|
277
|
-
loading.value = false;
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### 多表单校验
|
|
283
|
-
|
|
284
|
-
当一个页面有多个表单区域时,需要统一校验:
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
async function handleSubmitAll() {
|
|
288
|
-
const results = await Promise.all([
|
|
289
|
-
formRef1.value?.validate().catch(() => false),
|
|
290
|
-
formRef2.value?.validate().catch(() => false),
|
|
291
|
-
formRef3.value?.validate().catch(() => false),
|
|
292
|
-
]);
|
|
293
|
-
|
|
294
|
-
if (results.some(r => r === false)) {
|
|
295
|
-
ElMessage.warning("请完善表单信息");
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// 合并数据提交
|
|
300
|
-
const submitData = {
|
|
301
|
-
...formData1,
|
|
302
|
-
...formData2,
|
|
303
|
-
...formData3,
|
|
304
|
-
};
|
|
305
|
-
await saveApi(submitData);
|
|
306
|
-
}
|
|
307
|
-
```
|
|
134
|
+
禁止业务模块再挂 Layout。
|
|
308
135
|
|
|
309
136
|
## 常见反例
|
|
310
137
|
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
316
|
-
- 操作列直接写 `el-button` 而不使用 `ReTableOperate`。
|
|
138
|
+
- 使用 `handleSearch` / `handleReset`(错误命名)
|
|
139
|
+
- 不用 `useTableSearch` 手写一套分页 loading
|
|
140
|
+
- 提交无 loading
|
|
141
|
+
- 业务路由二次 Layout
|
|
142
|
+
- ReSelectQuery 写成不存在的 `:api` / `label-key` API(真实 props 见 form-advanced)
|