@agile-team/wl-skills-kit 2.3.0 → 2.3.1

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 (93) hide show
  1. package/CHANGELOG.md +33 -63
  2. package/README.md +15 -148
  3. package/bin/wl-skills.js +2 -100
  4. package/files/.github/guides/README.md +13 -13
  5. package/files/.github/guides/architecture.md +555 -576
  6. package/files/.github/guides/usage.md +176 -176
  7. package/files/.github/reports/README.md +65 -65
  8. package/files/.github/reports/SYS_DICT_INFO.md +50 -50
  9. package/files/.github/reports/SYS_MENU_INFO.md +247 -247
  10. package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
  11. package/files/.github/reports//347/273/204/344/273/266/346/217/220/345/217/226/345/273/272/350/256/256.md +33 -33
  12. package/files/.github/reports//350/247/204/350/214/203/345/256/241/346/237/245/346/212/245/345/221/212.md +44 -44
  13. package/files/.github/skills/_compat/README.md +108 -108
  14. package/files/.github/skills/_compat/headers/agents.txt +8 -8
  15. package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
  16. package/files/.github/skills/_compat/headers/cline.txt +7 -7
  17. package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
  18. package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
  19. package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
  20. package/files/.github/skills/_compat/headers/kiro.txt +10 -10
  21. package/files/.github/skills/_compat/headers/trae.txt +11 -11
  22. package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
  23. package/files/.github/skills/_registry.md +81 -81
  24. package/files/.github/skills/core/api-contract/SKILL.md +344 -344
  25. package/files/.github/skills/core/api-contract/USAGE.md +110 -110
  26. package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
  27. package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
  28. package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
  29. package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
  30. package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
  31. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
  32. package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
  33. package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
  34. package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
  35. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
  36. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +124 -124
  37. package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
  38. package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
  39. package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
  40. package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
  41. package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
  42. package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
  43. package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
  44. package/files/.github/skills/core/template-extract/SKILL.md +139 -139
  45. package/files/.github/skills/core/template-extract/USAGE.md +93 -93
  46. package/files/.github/skills/domain/README.md +51 -51
  47. package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
  48. package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
  49. package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
  50. package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
  51. package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
  52. package/files/.github/standards/01-toolchain.md +57 -57
  53. package/files/.github/standards/02-code-structure.md +111 -111
  54. package/files/.github/standards/03-comments.md +53 -53
  55. package/files/.github/standards/04-coding-basics.md +33 -33
  56. package/files/.github/standards/05-logging.md +38 -38
  57. package/files/.github/standards/06-security.md +44 -44
  58. package/files/.github/standards/07-config.md +52 -52
  59. package/files/.github/standards/08-git.md +60 -60
  60. package/files/.github/standards/09-typescript.md +71 -71
  61. package/files/.github/standards/10-pinia.md +57 -57
  62. package/files/.github/standards/11-form-validation.md +81 -81
  63. package/files/.github/standards/12-base-table.md +153 -153
  64. package/files/.github/standards/13-platform-components.md +123 -123
  65. package/files/.github/standards/index.md +89 -89
  66. package/files/demo/produce/aiflow/mmwr-customer-apply-add/api.md +1 -1
  67. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
  68. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
  69. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
  70. package/files/docs/jh-date-range.md +257 -257
  71. package/files/docs/jh-date.md +222 -222
  72. package/files/docs/jh-dept-picker.md +190 -190
  73. package/files/docs/jh-drag-row.md +590 -590
  74. package/files/docs/jh-file-upload.md +216 -216
  75. package/files/docs/jh-picker.md +218 -218
  76. package/files/docs/jh-select.md +148 -148
  77. package/files/docs/jh-text.md +248 -248
  78. package/files/docs/jh-user-picker.md +197 -197
  79. package/files/docs/request.md +24 -9
  80. package/files/src/components/global/C_RightToolbar/data.ts +228 -0
  81. package/files/src/components/global/C_RightToolbar/index.scss +44 -0
  82. package/files/src/components/global/C_RightToolbar/index.vue +34 -336
  83. package/files/src/components/global/C_Splitter/index.scss +61 -0
  84. package/files/src/components/global/C_Splitter/index.vue +2 -64
  85. package/files/src/components/global/C_SvgIcon/index.scss +15 -0
  86. package/files/src/components/global/C_SvgIcon/index.vue +20 -50
  87. package/files/src/components/global/C_TagStatus/index.scss +20 -0
  88. package/files/src/components/global/C_TagStatus/index.vue +1 -22
  89. package/files/src/components/global/C_Tree/data.ts +61 -0
  90. package/files/src/components/global/C_Tree/index.vue +12 -53
  91. package/files/src/components/local/c_listModal/index.scss +4 -0
  92. package/files/src/components/local/c_listModal/index.vue +1 -1
  93. package/package.json +5 -9
@@ -1,376 +1,376 @@
1
- # RECORD_FORM:录入型实绩页
2
-
3
- > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
-
5
-
6
- > 适用场景:通过 BaseQuery 选定主记录(如炉号、生产计划号),展示可编辑的 BaseForm 字段区 + BaseTable 明细行,**无分页**。
7
- > 典型于生产域实绩录入(转炉实绩、精炼实绩、连铸实绩等)。
8
- > 参考实现:`src/views/produce/production-omom/lgsj/mmsm-convert-progress/`
9
-
10
- #### 识别规则
11
-
12
- - 顶部 **BaseQuery** 仅用于"选择主记录"(1~3 个字段),**不用于列表翻页**
13
- - 中部 **BaseForm**(editable),随查询结果填充,用户修改后保存
14
- - 底部 **BaseTable**(明细行),**无分页**(`jh-pagination` 不需要)
15
- - BaseToolbar 在 BaseForm 上方,含**保存 / 重置**等按钮,通过函数调用传入 `formRef`
16
- - data.ts **不使用 `AbstractPageQueryHook`**,改为直接导出 `ref` + 函数(Composable 风格)
17
- - 用 `c_spliterTitle` 将表单字段按业务分区(钢种信息 / 进程信息 / 实绩信息)
18
-
19
- #### data.ts
20
-
21
- ```typescript
22
- import {
23
- BaseQueryItemDesc,
24
- ActionButtonDesc,
25
- TableColumnDesc,
26
- BusLogicDataType
27
- } from "@/types/page";
28
- import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";
29
- import c_spliterTitle from "@/components/local/c_spliterTitle/index.vue";
30
- import { getAction, postAction } from "@jhlc/common-core/src/api/action";
31
- import { debounce } from "lodash-es";
32
-
33
- export const API_CONFIG = {
34
- getByKey: "/[服务缩写]/[资源名]/getBy[Key]", // 按主键查询(炉号/计划号/熔炼号)
35
- saveOrUpdate: "/[服务缩写]/[资源名]/saveOrUpdate" // 保存实绩
36
- } as const;
37
-
38
- // ─────────────── 查询区 ───────────────
39
- /** 查询参数(1~3 个主键字段) */
40
- export const queryParam = ref<any>({});
41
-
42
- /** 查询项配置 */
43
- export const queryItems: BaseQueryItemDesc<any>[] = [
44
- {
45
- name: "[keyField]",
46
- label: "[主键名]",
47
- placeholder: "请输入[主键名]"
48
- }
49
- // 如需关联 Picker,使用 componentVNode 渲染(参考 mmsm-convert-progress PlanMainPicker)
50
- ];
51
-
52
- /** 查询 → 加载主记录到表单 */
53
- export const select = async () => {
54
- const res = await getAction(API_CONFIG.getByKey, queryParam.value);
55
- form.value = {
56
- ...queryParam.value,
57
- ...(res.data?.[主数据字段] || {})
58
- };
59
- bottomTableData.value = res.data?.[明细字段] || [];
60
- };
61
-
62
- /** 重置 */
63
- export const reset = (formRef: any) => {
64
- queryParam.value = {};
65
- resetForm();
66
- formRef?.resetFields();
67
- };
68
-
69
- // ─────────────── 表单区 ───────────────
70
- /** 表单数据 */
71
- export const form = ref<any>({});
72
-
73
- /** 重置表单到默认值 */
74
- export const resetForm = () => {
75
- form.value = {
76
- // [有默认值的字段,如厂别等]
77
- };
78
- };
79
-
80
- /** 表单项配置(支持 c_spliterTitle 分区 + 各种 logicType) */
81
- export const formItems: BaseFormItemDesc<any>[] = [
82
- // 分区标题(span=列数,通常为表单总列数)
83
- {
84
- name: "divider1",
85
- label: "",
86
- labelWidth: "0px",
87
- span: 4,
88
- componentVNode: () => h(c_spliterTitle, { title: "[分区名称]" })
89
- },
90
- // 普通文本
91
- { label: "[字段名]", name: "[fieldName]", placeholder: "请输入[字段名]" },
92
- // 字典下拉
93
- {
94
- label: "[字典字段]",
95
- name: "[dictField]",
96
- placeholder: "请选择[字典字段]",
97
- logicType: BusLogicDataType.dict,
98
- logicValue: "[dictCode]",
99
- required: true
100
- },
101
- // 时间
102
- {
103
- label: "[时间字段]",
104
- name: "[timeField]",
105
- placeholder: "请选择[时间字段]",
106
- logicType: BusLogicDataType.datetime
107
- },
108
- // 数值
109
- {
110
- label: "[数值字段]",
111
- name: "[numField]",
112
- placeholder: "请输入[数值字段]",
113
- logicType: BusLogicDataType.number
114
- }
115
- ];
116
-
117
- /** 工具栏(需传入 formRef 以触发校验) */
118
- export const toolbars = (formRef: any): ActionButtonDesc[] => [
119
- {
120
- label: "保存",
121
- type: "primary",
122
- icon: "Save",
123
- onClick: debounce(() => {
124
- formRef?.validate((valid: boolean) => {
125
- if (!valid) return ElMessage.error("请完善表单信息");
126
- ElMessageBox.confirm("确定保存吗?", "提示", {
127
- confirmButtonText: "确定",
128
- cancelButtonText: "取消",
129
- type: "warning"
130
- }).then(async () => {
131
- const res = await postAction(API_CONFIG.saveOrUpdate, { ...form.value });
132
- ElMessage.success(res?.message || "保存成功");
133
- });
134
- });
135
- }, 600)
136
- },
137
- {
138
- label: "重置",
139
- icon: "Refresh",
140
- type: "default",
141
- onClick: () => {
142
- resetForm();
143
- formRef?.resetFields();
144
- }
145
- }
146
- ];
147
-
148
- // ─────────────── 明细表格区 ───────────────
149
- /** 明细表格数据(全量,无分页) */
150
- export const bottomTableData = ref<any[]>([]);
151
-
152
- /** 明细表格列配置 */
153
- export const bottomTableColumns: TableColumnDesc<any>[] = [
154
- { type: "index", width: 55 },
155
- {
156
- label: "[列名]",
157
- name: "[fieldName]",
158
- minWidth: 100,
159
- sortable: true,
160
- filterable: true
161
- }
162
- // 按原型顺序添加明细列,通常为只读(无操作列)
163
- ];
164
- ```
165
-
166
- #### index.vue
167
-
168
- ```vue
169
- <template>
170
- <div class="app-container app-page-container">
171
- <!-- 查询区(选择主记录) -->
172
- <BaseQuery
173
- :form="queryParam"
174
- :items="queryItems"
175
- :columns="[n]"
176
- :auto-select="false"
177
- @select="select"
178
- @reset="reset(formRef)"
179
- />
180
- <div class="form-table-content">
181
- <!-- 工具栏(传入 formRef 用于校验) -->
182
- <BaseToolbar :items="toolbars(formRef)" />
183
- <!-- 表单区(可编辑字段) -->
184
- <BaseForm
185
- ref="formRef"
186
- :form="form"
187
- :items="formItems"
188
- :columns="[n]"
189
- :label-width="[n]"
190
- />
191
- <!-- 明细表格(无分页) -->
192
- <BaseTable
193
- :data="bottomTableData"
194
- :columns="bottomTableColumns"
195
- :height="300"
196
- />
197
- </div>
198
- </div>
199
- </template>
200
-
201
- <script setup lang="ts">
202
- import {
203
- toolbars,
204
- form,
205
- formItems,
206
- queryParam,
207
- queryItems,
208
- select,
209
- reset,
210
- bottomTableData,
211
- bottomTableColumns,
212
- resetForm
213
- } from "./data";
214
-
215
- const formRef = ref<any>(null);
216
-
217
- onMounted(() => {
218
- resetForm();
219
- });
220
- </script>
221
-
222
- <style scoped lang="scss">
223
- @import "./index.scss";
224
- </style>
225
- ```
226
-
227
- #### index.scss
228
-
229
- ```scss
230
- .app-page-container {
231
- overflow-y: auto;
232
-
233
- .form-table-content {
234
- display: flex;
235
- flex-direction: column;
236
- gap: 8px;
237
- margin-top: 8px;
238
- }
239
-
240
- // 统一表单控件宽度
241
- :deep(.jh-select),
242
- :deep(.jh-date),
243
- :deep(.el-input-number) {
244
- width: 100%;
245
- }
246
- }
247
- ```
248
-
249
- #### 关键约束
250
-
251
- | 约束 | 说明 |
252
- |------|------|
253
- | **不用 AbstractPageQueryHook** | 直接导出 `ref` + 函数,无 `createPage()` 包装 |
254
- | **无分页** | `bottomTableData` 绑定全量数据,不加 `jh-pagination` |
255
- | **auto-select: false** | BaseQuery 查询是"选择主记录",不自动触发,用户主动点击 |
256
- | **toolbars(formRef)** | template 中调用函数传入 `formRef`,不是直接绑定数组 |
257
- | **debounce 保存** | 防止连点,来自 `lodash-es`,600ms |
258
- | **c_spliterTitle 分区** | 表单字段按业务分组,每组前插分隔标题 |
259
-
260
- #### Mock 文件要点
261
-
262
- ```typescript
263
- // mock/[page-kebab-name].ts
264
- import type { MockMethod } from "vite-plugin-mock";
265
-
266
- const mockData = {
267
- [主数据字段]: {
268
- [keyField]: "MOCK-001",
269
- [field1]: "值1"
270
- // ...表单全部字段
271
- },
272
- [明细字段]: [
273
- { id: "1", [col1]: "值A", [col2]: "值B" }
274
- ]
275
- };
276
-
277
- const mockApi: MockMethod[] = [
278
- {
279
- url: "/dev-api/[服务缩写]/[资源名]/getBy[Key]",
280
- method: "get",
281
- response: ({ query }: any) => ({
282
- code: 200,
283
- msg: "操作成功",
284
- data: {
285
- ...mockData,
286
- [主数据字段]: {
287
- ...mockData[主数据字段],
288
- [keyField]: query.[keyField] || mockData[主数据字段].[keyField]
289
- }
290
- }
291
- })
292
- },
293
- {
294
- url: "/dev-api/[服务缩写]/[资源名]/saveOrUpdate",
295
- method: "post",
296
- response: () => ({ code: 200, msg: "保存成功", data: null })
297
- }
298
- ];
299
-
300
- export default mockApi;
301
- ```
302
-
303
- ---
304
-
305
- #### 变更比对(Inline Diff)
306
-
307
- > 原型中若出现表单字段右侧显示带删除线的旧值(橙色文字),或表格行下方出现旧版对比行,则需要实现"变更比对"能力。
308
-
309
- **业务域 Tabs 组件需要提供以下 diff 契约**(以 `c_customerTabs` 为参考实现):
310
-
311
- ```
312
- defineExpose({ loadData, collectFormData, validate, loadDiffData, clearDiffData })
313
- ```
314
-
315
- | 方法 | 说明 |
316
- |------|------|
317
- | `loadDiffData(prevData)` | 接收旧版数据,组件内部对比并渲染差异指示 |
318
- | `clearDiffData()` | 清除比对状态 |
319
-
320
- **组件内部实现要点**:
321
-
322
- 1. **表单字段 diff**:用 `div.diff-field-col` 包裹 `jh-select` + `<span class="diff-old-value">`,旧值出现在字段**下方**(不破坏原布局):
323
- ```html
324
- <el-form-item label="纳税类型" prop="taxCategory">
325
- <div class="diff-field-col">
326
- <jh-select v-model="basicInfo.taxCategory" dict="tax_category" label="" placeholder="请选择" />
327
- <span v-if="diffBasicInfo && diffBasicInfo.taxCategory !== basicInfo.taxCategory"
328
- class="diff-old-value">{{ diffBasicInfo.taxCategory }}</span>
329
- </div>
330
- </el-form-item>
331
- ```
332
-
333
- > **数据约定**:`diffBasicInfo` 中存储显示标签(如 `"小规模纳税人"`),不存储 dict code,与 `basicInfo` 保持一致格式,才能正确比对和展示。
334
-
335
- 2. **表格行 diff**:使用 `computed` 在每条主数据行后插入带 `_isDiffRow: true` 标记的旧版行:
336
- ```typescript
337
- const displayList = computed(() => {
338
- if (!diffList.value) return mainList.value;
339
- const result: any[] = [];
340
- mainList.value.forEach((row, i) => {
341
- result.push({ ...row, _seq: i + 1 });
342
- const old = diffList.value![i];
343
- if (old) {
344
- const changed = Object.keys(old).filter(k => !k.startsWith('_') && String(old[k]) !== String(row[k]));
345
- if (changed.length) result.push({ ...old, _isDiffRow: true, _changedFields: changed });
346
- }
347
- });
348
- return result;
349
- });
350
- ```
351
-
352
- 3. **单元格级高亮**:每个 view 模式列的 `<span>` 加上 `diffCellClass(row, 'fieldName')`。
353
-
354
- 4. **CSS 样式**(在组件 scoped style 中):
355
- ```scss
356
- /* 表单字段 diff 包装器:列方向 flex,使旧值出现在 jh-select 下方 */
357
- .diff-field-col {
358
- display: flex; flex-direction: column; width: 100%;
359
- :deep(.el-select) { width: 100% !important; }
360
- }
361
- /* 表单字段旧值:在字段下方,● 前缀不加删除线,文字橙色 + 删除线 */
362
- .diff-old-value {
363
- display: block; font-size: 12px; color: #e6a23c;
364
- text-decoration: line-through; margin-top: 2px; line-height: 1.4;
365
- &::before { content: "● "; text-decoration: none; display: inline-block; }
366
- }
367
- /* 表格对比行:已变更字段 —— 橙色 + 删除线 */
368
- .diff-changed { color: #e6a23c !important; text-decoration: line-through; }
369
- .diff-row-marker { color: #e6a23c; font-size: 12px; }
370
- /* 表格对比行:整行浅红背景 + 未变字段灰色,已变字段橙色覆盖 */
371
- :deep(.el-table .is-diff-row) {
372
- background-color: #fef0f0 !important;
373
- td { background-color: #fef0f0 !important; color: #c0c4cc; }
374
- .diff-changed { color: #e6a23c !important; }
375
- }
376
- ```
1
+ # RECORD_FORM:录入型实绩页
2
+
3
+ > 见 SKILL.md 主文件(约束 + 按钮规则 + Mock 规范等共用规则)。
4
+
5
+
6
+ > 适用场景:通过 BaseQuery 选定主记录(如炉号、生产计划号),展示可编辑的 BaseForm 字段区 + BaseTable 明细行,**无分页**。
7
+ > 典型于生产域实绩录入(转炉实绩、精炼实绩、连铸实绩等)。
8
+ > 参考实现:`src/views/produce/production-omom/lgsj/mmsm-convert-progress/`
9
+
10
+ #### 识别规则
11
+
12
+ - 顶部 **BaseQuery** 仅用于"选择主记录"(1~3 个字段),**不用于列表翻页**
13
+ - 中部 **BaseForm**(editable),随查询结果填充,用户修改后保存
14
+ - 底部 **BaseTable**(明细行),**无分页**(`jh-pagination` 不需要)
15
+ - BaseToolbar 在 BaseForm 上方,含**保存 / 重置**等按钮,通过函数调用传入 `formRef`
16
+ - data.ts **不使用 `AbstractPageQueryHook`**,改为直接导出 `ref` + 函数(Composable 风格)
17
+ - 用 `c_spliterTitle` 将表单字段按业务分区(钢种信息 / 进程信息 / 实绩信息)
18
+
19
+ #### data.ts
20
+
21
+ ```typescript
22
+ import {
23
+ BaseQueryItemDesc,
24
+ ActionButtonDesc,
25
+ TableColumnDesc,
26
+ BusLogicDataType
27
+ } from "@/types/page";
28
+ import type { BaseFormItemDesc } from "@jhlc/common-core/src/components/form/common/type";
29
+ import c_spliterTitle from "@/components/local/c_spliterTitle/index.vue";
30
+ import { getAction, postAction } from "@jhlc/common-core/src/api/action";
31
+ import { debounce } from "lodash-es";
32
+
33
+ export const API_CONFIG = {
34
+ getByKey: "/[服务缩写]/[资源名]/getBy[Key]", // 按主键查询(炉号/计划号/熔炼号)
35
+ saveOrUpdate: "/[服务缩写]/[资源名]/saveOrUpdate" // 保存实绩
36
+ } as const;
37
+
38
+ // ─────────────── 查询区 ───────────────
39
+ /** 查询参数(1~3 个主键字段) */
40
+ export const queryParam = ref<any>({});
41
+
42
+ /** 查询项配置 */
43
+ export const queryItems: BaseQueryItemDesc<any>[] = [
44
+ {
45
+ name: "[keyField]",
46
+ label: "[主键名]",
47
+ placeholder: "请输入[主键名]"
48
+ }
49
+ // 如需关联 Picker,使用 componentVNode 渲染(参考 mmsm-convert-progress PlanMainPicker)
50
+ ];
51
+
52
+ /** 查询 → 加载主记录到表单 */
53
+ export const select = async () => {
54
+ const res = await getAction(API_CONFIG.getByKey, queryParam.value);
55
+ form.value = {
56
+ ...queryParam.value,
57
+ ...(res.data?.[主数据字段] || {})
58
+ };
59
+ bottomTableData.value = res.data?.[明细字段] || [];
60
+ };
61
+
62
+ /** 重置 */
63
+ export const reset = (formRef: any) => {
64
+ queryParam.value = {};
65
+ resetForm();
66
+ formRef?.resetFields();
67
+ };
68
+
69
+ // ─────────────── 表单区 ───────────────
70
+ /** 表单数据 */
71
+ export const form = ref<any>({});
72
+
73
+ /** 重置表单到默认值 */
74
+ export const resetForm = () => {
75
+ form.value = {
76
+ // [有默认值的字段,如厂别等]
77
+ };
78
+ };
79
+
80
+ /** 表单项配置(支持 c_spliterTitle 分区 + 各种 logicType) */
81
+ export const formItems: BaseFormItemDesc<any>[] = [
82
+ // 分区标题(span=列数,通常为表单总列数)
83
+ {
84
+ name: "divider1",
85
+ label: "",
86
+ labelWidth: "0px",
87
+ span: 4,
88
+ componentVNode: () => h(c_spliterTitle, { title: "[分区名称]" })
89
+ },
90
+ // 普通文本
91
+ { label: "[字段名]", name: "[fieldName]", placeholder: "请输入[字段名]" },
92
+ // 字典下拉
93
+ {
94
+ label: "[字典字段]",
95
+ name: "[dictField]",
96
+ placeholder: "请选择[字典字段]",
97
+ logicType: BusLogicDataType.dict,
98
+ logicValue: "[dictCode]",
99
+ required: true
100
+ },
101
+ // 时间
102
+ {
103
+ label: "[时间字段]",
104
+ name: "[timeField]",
105
+ placeholder: "请选择[时间字段]",
106
+ logicType: BusLogicDataType.datetime
107
+ },
108
+ // 数值
109
+ {
110
+ label: "[数值字段]",
111
+ name: "[numField]",
112
+ placeholder: "请输入[数值字段]",
113
+ logicType: BusLogicDataType.number
114
+ }
115
+ ];
116
+
117
+ /** 工具栏(需传入 formRef 以触发校验) */
118
+ export const toolbars = (formRef: any): ActionButtonDesc[] => [
119
+ {
120
+ label: "保存",
121
+ type: "primary",
122
+ icon: "Save",
123
+ onClick: debounce(() => {
124
+ formRef?.validate((valid: boolean) => {
125
+ if (!valid) return ElMessage.error("请完善表单信息");
126
+ ElMessageBox.confirm("确定保存吗?", "提示", {
127
+ confirmButtonText: "确定",
128
+ cancelButtonText: "取消",
129
+ type: "warning"
130
+ }).then(async () => {
131
+ const res = await postAction(API_CONFIG.saveOrUpdate, { ...form.value });
132
+ ElMessage.success(res?.message || "保存成功");
133
+ });
134
+ });
135
+ }, 600)
136
+ },
137
+ {
138
+ label: "重置",
139
+ icon: "Refresh",
140
+ type: "default",
141
+ onClick: () => {
142
+ resetForm();
143
+ formRef?.resetFields();
144
+ }
145
+ }
146
+ ];
147
+
148
+ // ─────────────── 明细表格区 ───────────────
149
+ /** 明细表格数据(全量,无分页) */
150
+ export const bottomTableData = ref<any[]>([]);
151
+
152
+ /** 明细表格列配置 */
153
+ export const bottomTableColumns: TableColumnDesc<any>[] = [
154
+ { type: "index", width: 55 },
155
+ {
156
+ label: "[列名]",
157
+ name: "[fieldName]",
158
+ minWidth: 100,
159
+ sortable: true,
160
+ filterable: true
161
+ }
162
+ // 按原型顺序添加明细列,通常为只读(无操作列)
163
+ ];
164
+ ```
165
+
166
+ #### index.vue
167
+
168
+ ```vue
169
+ <template>
170
+ <div class="app-container app-page-container">
171
+ <!-- 查询区(选择主记录) -->
172
+ <BaseQuery
173
+ :form="queryParam"
174
+ :items="queryItems"
175
+ :columns="[n]"
176
+ :auto-select="false"
177
+ @select="select"
178
+ @reset="reset(formRef)"
179
+ />
180
+ <div class="form-table-content">
181
+ <!-- 工具栏(传入 formRef 用于校验) -->
182
+ <BaseToolbar :items="toolbars(formRef)" />
183
+ <!-- 表单区(可编辑字段) -->
184
+ <BaseForm
185
+ ref="formRef"
186
+ :form="form"
187
+ :items="formItems"
188
+ :columns="[n]"
189
+ :label-width="[n]"
190
+ />
191
+ <!-- 明细表格(无分页) -->
192
+ <BaseTable
193
+ :data="bottomTableData"
194
+ :columns="bottomTableColumns"
195
+ :height="300"
196
+ />
197
+ </div>
198
+ </div>
199
+ </template>
200
+
201
+ <script setup lang="ts">
202
+ import {
203
+ toolbars,
204
+ form,
205
+ formItems,
206
+ queryParam,
207
+ queryItems,
208
+ select,
209
+ reset,
210
+ bottomTableData,
211
+ bottomTableColumns,
212
+ resetForm
213
+ } from "./data";
214
+
215
+ const formRef = ref<any>(null);
216
+
217
+ onMounted(() => {
218
+ resetForm();
219
+ });
220
+ </script>
221
+
222
+ <style scoped lang="scss">
223
+ @import "./index.scss";
224
+ </style>
225
+ ```
226
+
227
+ #### index.scss
228
+
229
+ ```scss
230
+ .app-page-container {
231
+ overflow-y: auto;
232
+
233
+ .form-table-content {
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 8px;
237
+ margin-top: 8px;
238
+ }
239
+
240
+ // 统一表单控件宽度
241
+ :deep(.jh-select),
242
+ :deep(.jh-date),
243
+ :deep(.el-input-number) {
244
+ width: 100%;
245
+ }
246
+ }
247
+ ```
248
+
249
+ #### 关键约束
250
+
251
+ | 约束 | 说明 |
252
+ |------|------|
253
+ | **不用 AbstractPageQueryHook** | 直接导出 `ref` + 函数,无 `createPage()` 包装 |
254
+ | **无分页** | `bottomTableData` 绑定全量数据,不加 `jh-pagination` |
255
+ | **auto-select: false** | BaseQuery 查询是"选择主记录",不自动触发,用户主动点击 |
256
+ | **toolbars(formRef)** | template 中调用函数传入 `formRef`,不是直接绑定数组 |
257
+ | **debounce 保存** | 防止连点,来自 `lodash-es`,600ms |
258
+ | **c_spliterTitle 分区** | 表单字段按业务分组,每组前插分隔标题 |
259
+
260
+ #### Mock 文件要点
261
+
262
+ ```typescript
263
+ // mock/[page-kebab-name].ts
264
+ import type { MockMethod } from "vite-plugin-mock";
265
+
266
+ const mockData = {
267
+ [主数据字段]: {
268
+ [keyField]: "MOCK-001",
269
+ [field1]: "值1"
270
+ // ...表单全部字段
271
+ },
272
+ [明细字段]: [
273
+ { id: "1", [col1]: "值A", [col2]: "值B" }
274
+ ]
275
+ };
276
+
277
+ const mockApi: MockMethod[] = [
278
+ {
279
+ url: "/dev-api/[服务缩写]/[资源名]/getBy[Key]",
280
+ method: "get",
281
+ response: ({ query }: any) => ({
282
+ code: 2000,
283
+ message: "操作成功",
284
+ data: {
285
+ ...mockData,
286
+ [主数据字段]: {
287
+ ...mockData[主数据字段],
288
+ [keyField]: query.[keyField] || mockData[主数据字段].[keyField]
289
+ }
290
+ }
291
+ })
292
+ },
293
+ {
294
+ url: "/dev-api/[服务缩写]/[资源名]/saveOrUpdate",
295
+ method: "post",
296
+ response: () => ({ code: 2000, message: "保存成功", data: null })
297
+ }
298
+ ];
299
+
300
+ export default mockApi;
301
+ ```
302
+
303
+ ---
304
+
305
+ #### 变更比对(Inline Diff)
306
+
307
+ > 原型中若出现表单字段右侧显示带删除线的旧值(橙色文字),或表格行下方出现旧版对比行,则需要实现"变更比对"能力。
308
+
309
+ **业务域 Tabs 组件需要提供以下 diff 契约**(以 `c_customerTabs` 为参考实现):
310
+
311
+ ```
312
+ defineExpose({ loadData, collectFormData, validate, loadDiffData, clearDiffData })
313
+ ```
314
+
315
+ | 方法 | 说明 |
316
+ |------|------|
317
+ | `loadDiffData(prevData)` | 接收旧版数据,组件内部对比并渲染差异指示 |
318
+ | `clearDiffData()` | 清除比对状态 |
319
+
320
+ **组件内部实现要点**:
321
+
322
+ 1. **表单字段 diff**:用 `div.diff-field-col` 包裹 `jh-select` + `<span class="diff-old-value">`,旧值出现在字段**下方**(不破坏原布局):
323
+ ```html
324
+ <el-form-item label="纳税类型" prop="taxCategory">
325
+ <div class="diff-field-col">
326
+ <jh-select v-model="basicInfo.taxCategory" dict="tax_category" label="" placeholder="请选择" />
327
+ <span v-if="diffBasicInfo && diffBasicInfo.taxCategory !== basicInfo.taxCategory"
328
+ class="diff-old-value">{{ diffBasicInfo.taxCategory }}</span>
329
+ </div>
330
+ </el-form-item>
331
+ ```
332
+
333
+ > **数据约定**:`diffBasicInfo` 中存储显示标签(如 `"小规模纳税人"`),不存储 dict code,与 `basicInfo` 保持一致格式,才能正确比对和展示。
334
+
335
+ 2. **表格行 diff**:使用 `computed` 在每条主数据行后插入带 `_isDiffRow: true` 标记的旧版行:
336
+ ```typescript
337
+ const displayList = computed(() => {
338
+ if (!diffList.value) return mainList.value;
339
+ const result: any[] = [];
340
+ mainList.value.forEach((row, i) => {
341
+ result.push({ ...row, _seq: i + 1 });
342
+ const old = diffList.value![i];
343
+ if (old) {
344
+ const changed = Object.keys(old).filter(k => !k.startsWith('_') && String(old[k]) !== String(row[k]));
345
+ if (changed.length) result.push({ ...old, _isDiffRow: true, _changedFields: changed });
346
+ }
347
+ });
348
+ return result;
349
+ });
350
+ ```
351
+
352
+ 3. **单元格级高亮**:每个 view 模式列的 `<span>` 加上 `diffCellClass(row, 'fieldName')`。
353
+
354
+ 4. **CSS 样式**(在组件 scoped style 中):
355
+ ```scss
356
+ /* 表单字段 diff 包装器:列方向 flex,使旧值出现在 jh-select 下方 */
357
+ .diff-field-col {
358
+ display: flex; flex-direction: column; width: 100%;
359
+ :deep(.el-select) { width: 100% !important; }
360
+ }
361
+ /* 表单字段旧值:在字段下方,● 前缀不加删除线,文字橙色 + 删除线 */
362
+ .diff-old-value {
363
+ display: block; font-size: 12px; color: #e6a23c;
364
+ text-decoration: line-through; margin-top: 2px; line-height: 1.4;
365
+ &::before { content: "● "; text-decoration: none; display: inline-block; }
366
+ }
367
+ /* 表格对比行:已变更字段 —— 橙色 + 删除线 */
368
+ .diff-changed { color: #e6a23c !important; text-decoration: line-through; }
369
+ .diff-row-marker { color: #e6a23c; font-size: 12px; }
370
+ /* 表格对比行:整行浅红背景 + 未变字段灰色,已变字段橙色覆盖 */
371
+ :deep(.el-table .is-diff-row) {
372
+ background-color: #fef0f0 !important;
373
+ td { background-color: #fef0f0 !important; color: #c0c4cc; }
374
+ .diff-changed { color: #e6a23c !important; }
375
+ }
376
+ ```