@qilitt-mickey/vue3-temp-skill 1.1.74 → 1.1.76

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 (65) hide show
  1. package/SKILL.md +137 -137
  2. package/mapping/basic/color.json +35 -35
  3. package/mapping/basic/radius.json +17 -17
  4. package/mapping/basic/semantic-values.json +72 -72
  5. package/mapping/basic/spacing.json +21 -21
  6. package/mapping/basic/typography.json +21 -21
  7. package/mapping/component/button.json +90 -90
  8. package/mapping/component/card.json +31 -31
  9. package/mapping/component/input.json +42 -42
  10. package/mapping/component/overlay.json +79 -79
  11. package/mapping/component/pagination.json +34 -34
  12. package/mapping/component/table.json +42 -42
  13. package/mapping/component/tag.json +48 -48
  14. package/mapping/layout/breakpoint.json +20 -20
  15. package/mapping/scenes/admin/content.json +497 -494
  16. package/mapping/scenes/admin/layout.json +577 -574
  17. package/package.json +1 -1
  18. package/parser/fixtures/changeset-legacy.json +11 -11
  19. package/parser/fixtures/changeset-neg1.json +8 -8
  20. package/parser/fixtures/changeset-neg2.json +10 -10
  21. package/parser/fixtures/changeset-neg3.json +22 -22
  22. package/parser/fixtures/changeset-neg5-g8-stub.json +26 -26
  23. package/parser/fixtures/changeset-neg7-g10-dims.json +57 -57
  24. package/parser/fixtures/changeset-neg9-g6-token-chain.json +34 -0
  25. package/parser/fixtures/changeset-pos-g6-token-chain.json +35 -0
  26. package/parser/fixtures/changeset-pos-theme-nometa.json +260 -260
  27. package/parser/fixtures/changeset-registry-gap.json +29 -29
  28. package/parser/fixtures/fake-project/src/styles/app.scss +5 -5
  29. package/parser/fixtures/fake-project/src/views/list.vue +13 -13
  30. package/parser/fixtures/registry-bad.json +11 -11
  31. package/parser/fixtures/registry-g6-token-chain.json +10 -0
  32. package/parser/fixtures/verify/actual-clean.json +35 -35
  33. package/parser/fixtures/verify/actual.json +33 -33
  34. package/parser/fixtures/verify/mockup-g6-content.html +12 -0
  35. package/parser/fixtures/verify/mockup-neutral.html +26 -26
  36. package/parser/fixtures/verify/mockup-nometa.html +26 -26
  37. package/parser/fixtures/verify/snapshot-latest.json +102 -102
  38. package/parser/index.js +32 -1
  39. package/parser/loader.js +66 -66
  40. package/parser/validator.js +206 -206
  41. package/references/auth-pages.md +217 -217
  42. package/references/base-code-dict.md +56 -56
  43. package/references/data-compare.md +509 -509
  44. package/references/data-writeback.md +124 -124
  45. package/references/design-apply.md +167 -167
  46. package/references/detail-page.md +119 -119
  47. package/references/download-export.md +97 -97
  48. package/references/feedback-loading.md +104 -104
  49. package/references/feedback-ui.md +134 -134
  50. package/references/i18n-localization.md +230 -230
  51. package/references/layout-theme.md +2 -0
  52. package/references/project-inventory.md +438 -438
  53. package/references/rich-text.md +94 -94
  54. package/references/seamless-scroll.md +47 -47
  55. package/references/steps/step0-preflight.md +58 -58
  56. package/references/steps/step1-match.md +5 -2
  57. package/references/steps/step3-style.md +38 -33
  58. package/references/steps/step3b-shell.md +2 -0
  59. package/references/steps/step3c-priority.md +26 -26
  60. package/references/steps/step4-l3.md +14 -14
  61. package/references/steps/step5-gates.md +65 -63
  62. package/references/steps/step6-verify.md +77 -77
  63. package/references/verify-captcha.md +119 -119
  64. package/scripts/check.mjs +24 -0
  65. package/scripts/design-audit.mjs +208 -3
@@ -1,509 +1,509 @@
1
- ---
2
- name: data-compare
3
- description: 编辑页数据变更对比:v-compare、compareStore、ChangeSummaryService
4
- scope: project
5
- tags: [v-compare, 变更汇总, 变更对比, 数据对比, 字段对比, 高亮变更]
6
- ---
7
-
8
- # 数据变更对比
9
-
10
- > 按本文件示例编写,复用 `v-compare` / `compareStore` / `ChangeSummaryService` 等已有能力,勿另起一套对比实现。
11
-
12
- 编辑页对比「当前值 vs 编辑前原值」:字段高亮、悬停原值提示、变更汇总弹窗、表格行增删改矩阵。
13
-
14
- > 示例:以本文件完整规范为准。
15
- > 核心落点:`src/directives/compare/`、`src/store/modules/compareStore.ts`、`src/utils/changeSummaryService.ts`、
16
-
17
- ---
18
-
19
- ## 一、架构总览
20
-
21
- ```
22
- 页面 provide(oldDataInfo, compareEnabled)
23
- └─ 子表单 inject → v-compare / compare-config
24
- ├─ 高亮 .is-compare-changed + tooltip「原值:…」
25
- └─ useCompareStore.addOrUpdateRecord
26
- └─ ChangeSummaryDialog ← ChangeSummaryService.getChangeSummaryData()
27
- └─ 定位:scrollIntoView + data-compare-* DOM 回退
28
- ```
29
-
30
- | 层级 | 职责 |
31
- |------|------|
32
- | `v-compare` | 读 DOM/Vue 当前值,与 `oldValue` 比(映射后显示值),写样式与 Store |
33
- | `useCompareStore` | 按模块聚合变更项;去重键 = module+label+context+tableFirstValue+tableRowOrder |
34
- | `ChangeSummaryService` | 读 Store;元素引用失效时用 `data-compare-label/module` 修复 |
35
-
36
-
37
- **对比的是映射后的显示文案**,不是原始 code(除非未配置 map)。空值统一显示为 `空`(`''` / `null` / `undefined` 视为相等)。
38
-
39
- ---
40
-
41
- ## 二、页面编排(必须先做)
42
-
43
- ### 1. 父页 provide
44
-
45
- ```typescript
46
- const formData = reactive<DataInfo>({ /* 当前编辑数据 */ })
47
- const oldDataInfo = ref<DataInfo | null>(null) // 编辑前快照(接口返回后赋值)
48
- const compareEnabled = ref(true) // true = 修改模式启用对比;新增模式设 false
49
-
50
- provide('oldDataInfo', oldDataInfo)
51
- provide('compareDisabled', compareEnabled) // 历史命名:变量叫 Disabled,语义却是「是否启用」
52
-
53
- // 打开编辑:拉取原数据
54
- async function fetchDetail(id: string) {
55
- const res = await getDetailApi({ id })
56
- Object.assign(formData, res.data)
57
- oldDataInfo.value = cloneDeep(res.data)
58
- }
59
- ```
60
-
61
- > **命名陷阱**:示例里 inject 的 key 是 `compareDisabled`,但 `enabled: compareDisabled` 时 **true 表示对比已启用**。新代码建议本地命名为 `compareEnabled`,provide key 仍用 `compareDisabled` 以兼容现有子组件。
62
-
63
- ### 2. 子表单 inject
64
-
65
- ```typescript
66
- const oldFormInfos = inject('oldDataInfo') as Ref<DataInfo | null>
67
- const compareDisabled = inject<Ref<boolean>>('compareDisabled', ref(false))
68
- ```
69
-
70
- ### 3. 变更汇总入口
71
-
72
- ```vue
73
- <el-button :disabled="!compareDisabled" @click="showChangeSummary = true">
74
- 变更汇总
75
- </el-button>
76
- <ChangeSummaryDialog
77
- v-model="showChangeSummary"
78
- :module-order="['基本信息', '项目成员', '项目列表']"
79
- />
80
- ```
81
-
82
- 离开编辑页时建议 `useCompareStoreHook().clearAllRecords()`。
83
-
84
- ### 4. 嵌套弹窗
85
-
86
- 弹窗内再 `provide('oldDataInfo', localOld)` / `provide('compareDisabled', …)`,与主表单同一套指令即可(见 `MultiFormDialog.vue`)。
87
-
88
- ---
89
-
90
- ## 三、指令 API
91
-
92
- ### 绑定值 `CompareBindingValue`
93
-
94
- | 字段 | 说明 |
95
- |------|------|
96
- | `oldValue` | 编辑前原值(必填语义) |
97
- | `value` | **覆盖当前显示值**(树/级联等用名称字段,不用 code) |
98
- | `title` | 模块分组名(汇总按此分组;运行时优先于 `module`) |
99
- | `label` | 字段中文名(汇总列 + DOM `data-compare-label`) |
100
- | `enabled` | `false` 跳过对比(推荐开关) |
101
- | `showInSummary` | `false` 只高亮,不进汇总 |
102
- | `map` | 配合 `.map`:对象映射 `{ 1: '是' }` |
103
- | `mapArr` | 配合 `.mapArr`:选项映射 / 多选顺序无关比较 |
104
- | `format` | `(v) => string` 自定义显示 |
105
- | `tableFirstLabel` | 表格模式:业务主键列名 |
106
- | `tableFirstValue` | 表格模式:该行业务主键值 |
107
- | `tableRowStatus` | `I` 新增 / `U` 修改 / `D`\|`B` 删除(强制记入汇总) |
108
- | `tableRowOrder` | 行序号(与页面顺序一致) |
109
-
110
- ### 修饰符 / 参数
111
-
112
- | 写法 | 作用 |
113
- |------|------|
114
- | `v-compare:fieldKey` | `arg` 为字段名(可读性;逻辑不依赖 arg) |
115
- | `.map` | 使用 `map` 对象做显示映射 |
116
- | `.mapArr` | 使用 `mapArr`;多选按集合比较(忽略顺序) |
117
- | `.table` | 表格行上下文,汇总走矩阵视图 |
118
-
119
- ### 行为摘要
120
-
121
- - 变更 → 加 `.is-compare-changed`,悬停全局 tooltip「原值:xxx」
122
- - 写入 Store;改回原值 → `removeRecordByLabel`
123
- - DOM 标记:`data-v-compare`、`data-compare-label`、`data-compare-module`(定位回退用)
124
- - 全局恢复:`window.restoreCompareValue(el, originalValue)`
125
-
126
- ---
127
-
128
- ## 四、控件用法对照(逐类)
129
-
130
- ### 1. 普通输入 `el-input`
131
-
132
- 指令挂在组件根上即可。
133
-
134
- ```vue
135
- <el-input
136
- v-model="formInfos.projectName"
137
- v-compare:projectName="{
138
- oldValue: oldFormInfos?.projectName,
139
- title: props.item.title,
140
- label: '项目名称',
141
- enabled: compareDisabled,
142
- }"
143
- />
144
- ```
145
-
146
- `showInSummary: false` → 仍高亮,但不进汇总弹窗。
147
-
148
- ### 2. 文本域 `el-input type="textarea"`
149
-
150
- 同 `el-input`,挂在输入组件上。
151
-
152
- ### 3. 日期/时间 `el-date-picker` / `el-time-picker`
153
-
154
- **必须包一层 `div` 挂指令**(picker 根节点才稳定可读 DOM)。
155
-
156
- ```vue
157
- <div
158
- v-compare:planStart="{
159
- oldValue: oldFormInfos?.planStart,
160
- title: props.item.title,
161
- label: '计划开始时间',
162
- enabled: compareDisabled,
163
- }"
164
- class="!w-[100%]"
165
- >
166
- <el-date-picker v-model="formInfos.planStart" type="date" class="!w-[100%]" />
167
- </div>
168
- ```
169
-
170
- 适用:`date` / `datetime` / `month` / `year` / `daterange` / `datetimerange` / `time`。
171
- 范围值会按分隔符拼成字符串再比。
172
-
173
- ### 4. 本地下拉 `el-select`(单选)→ `.map` + `composeOptions`
174
-
175
- ```vue
176
- <el-select
177
- v-model="formInfos.projectStatus"
178
- v-compare:projectStatus.map="{
179
- oldValue: oldFormInfos?.projectStatus,
180
- title: props.item.title,
181
- label: '项目状态',
182
- map: composeOptions(localSelectOptions.projectStatus, 'value', 'label'),
183
- enabled: compareDisabled,
184
- }"
185
- >
186
- <el-option v-for="s in localSelectOptions.projectStatus" :key="s.value" :label="s.label" :value="s.value" />
187
- </el-select>
188
- ```
189
-
190
- `composeOptions(list, codeKey, labelKey)` → `{ [code]: label }`(见 `src/utils/utils.ts`)。
191
-
192
- 异步选项列表加载完后同样用 `composeOptions(saleUnitList)`(默认 key 为 `code`/`value`)。
193
-
194
- ### 5. 多选下拉 `el-select multiple` → `.mapArr`
195
-
196
- ```vue
197
- <el-select
198
- v-model="formInfos.multiSelectStatus"
199
- multiple
200
- v-compare:multiSelectStatus.mapArr="{
201
- oldValue: oldFormInfos?.multiSelectStatus,
202
- title: props.item.title,
203
- label: '多选状态',
204
- mapArr: composeOptions(localSelectOptions.projectStatus, 'value', 'label'),
205
- enabled: compareDisabled,
206
- }"
207
- />
208
- ```
209
-
210
- 比较时忽略选项顺序;显示为逗号拼接的中文标签。
211
-
212
- ### 6. `ReSelectQuery`(异步分页选择)→ props 桥接
213
-
214
- 模板不能直接写 `v-compare` 时,用组件内置 props(内部 `withDirectives`)。
215
-
216
- ```vue
217
- <!-- 单选:oldValue 用名称字段 -->
218
- <ReSelectQuery
219
- v-model="formInfos.asyncSelectUser"
220
- :compare-config="{
221
- oldValue: oldFormInfos?.asyncSelectUserName,
222
- label: '异步单选用户',
223
- title: props.item.title,
224
- enabled: compareDisabled,
225
- }"
226
- compare-key="asyncSelectUser"
227
- :edit-data="formInfos.single1BackData"
228
- url="..."
229
- value="userCode"
230
- label="userName"
231
- />
232
-
233
- <!-- 多选 -->
234
- <ReSelectQuery
235
- v-model="formInfos.asyncMultiSelectUser"
236
- multiple
237
- :compare-config="{
238
- oldValue: oldFormInfos?.asyncMultiSelectUserNames,
239
- label: '异步多选用户',
240
- title: props.item.title,
241
- enabled: compareDisabled,
242
- }"
243
- compare-key="asyncMultiSelectUser"
244
- :compare-modifiers="{ mapArr: true }"
245
- />
246
- ```
247
-
248
- | Prop | 说明 |
249
- |------|------|
250
- | `compareConfig` | 同指令 binding value |
251
- | `compareKey` | 同 `v-compare:key` 的 arg |
252
- | `compareModifiers` | 如 `{ mapArr: true }` |
253
-
254
- ### 7. `el-tree-select` → 用 `value` 覆盖显示名
255
-
256
- 对比名称字符串,不要对比节点 code。
257
-
258
- ```vue
259
- <el-tree-select
260
- v-model="formInfos.value3"
261
- v-compare:value3="{
262
- oldValue: oldFormInfos?.value3Name,
263
- value: formInfos.value3Name,
264
- title: props.item.title,
265
- label: '单选树',
266
- enabled: compareDisabled,
267
- }"
268
- :cache-data="formInfos.value3Text"
269
- ...
270
- />
271
-
272
- <!-- 多选树 -->
273
- v-compare:multiple1.mapArr="{
274
- oldValue: oldFormInfos?.multiple1Name,
275
- value: formInfos.multiple1Name,
276
- title, label, enabled: compareDisabled,
277
- }"
278
- ```
279
-
280
- 需在业务里维护 `*Name` / `*Text` 回写字段(与 data-writeback 模块配合)。
281
-
282
- ### 8. `ReCascader` → 同 ReSelectQuery 桥接 + `value` 名称
283
-
284
- ```vue
285
- <ReCascader
286
- v-model="formInfos.singleBack"
287
- :compare-config="{
288
- oldValue: oldFormInfos?.singleBackName,
289
- value: formInfos.singleBackName,
290
- label: '级联器单选',
291
- title: props.item.title,
292
- enabled: compareDisabled,
293
- }"
294
- compare-key="singleBack"
295
- @change="getSingleBackName"
296
- />
297
-
298
- <!-- 多选加 modifiers -->
299
- :compare-modifiers="{ mapArr: true }"
300
- ```
301
-
302
- ### 9. `el-checkbox` / `el-switch` → `.map` 布尔/枚举文案
303
-
304
- ```vue
305
- <el-checkbox
306
- v-model="formInfos.isUrgent"
307
- true-value="1"
308
- false-value="0"
309
- v-compare:isUrgent.map="{
310
- oldValue: oldFormInfos?.isUrgent,
311
- title: props.item.title,
312
- label: '是否紧急',
313
- map: { 1: '是', 0: '否' },
314
- enabled: compareDisabled,
315
- }"
316
- >
317
- 紧急项目
318
- </el-checkbox>
319
- ```
320
-
321
- `el-switch` 同理,读取 `active-value` / `inactive-value`。
322
-
323
- ### 10. `el-radio-group` / `el-checkbox-group` → `.map` / `.mapArr`
324
-
325
- ```vue
326
- <el-radio-group
327
- v-model="formInfos.approvalStatus"
328
- v-compare:approvalStatus.map="{
329
- oldValue: oldFormInfos?.approvalStatus,
330
- title: props.item.title,
331
- label: '审批状态',
332
- map: composeOptions(options, 'value', 'label'),
333
- enabled: compareDisabled,
334
- }"
335
- >
336
- <el-radio v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</el-radio>
337
- </el-radio-group>
338
-
339
- <el-checkbox-group
340
- v-model="formInfos.tags"
341
- v-compare:tags.mapArr="{
342
- oldValue: oldFormInfos?.tags,
343
- title: props.item.title,
344
- label: '标签',
345
- mapArr: composeOptions(tagOptions, 'value', 'label'),
346
- enabled: compareDisabled,
347
- }"
348
- >
349
- ...
350
- </el-checkbox-group>
351
- ```
352
-
353
- ### 11. `el-input-number` / `el-rate` → `format`
354
-
355
- ```vue
356
- <el-input-number
357
- v-model="formInfos.completionRate"
358
- v-compare:completionRate="{
359
- oldValue: oldFormInfos?.completionRate,
360
- title: props.item.title,
361
- label: '完成率',
362
- enabled: compareDisabled,
363
- format: (v) => (v === null || v === undefined || v === '' ? '未设置' : `${v}%`),
364
- }"
365
- />
366
-
367
- <el-rate
368
- v-model="formInfos.projectRating"
369
- v-compare:projectRating="{
370
- oldValue: oldFormInfos?.projectRating,
371
- title: props.item.title,
372
- label: '项目评分',
373
- enabled: compareDisabled,
374
- format: (v) => (v == null || v === '' ? '未评分' : `${v}分`),
375
- }"
376
- />
377
- ```
378
-
379
- ---
380
-
381
- ## 五、表格行对比(`.table`)
382
-
383
- ### 行标志约定
384
-
385
- | `flag` / `tableRowStatus` | 含义 | 行样式(`getRowClassByFlag`) | 汇总 |
386
- |---------------------------|------|------------------------------|------|
387
- | `I` | 新增 | `row-added` | 记入;类型「新增」 |
388
- | `U` | 修改 | `row-modified` | 按字段 diff |
389
- | `D` / `B` | 删除 | `row-deleted` | **强制记入**(值相等也保留) |
390
-
391
- ```vue
392
- <el-table :data="formInfos.projectList" :row-class-name="({ row }) => getRowClassByFlag(row?.flag)">
393
- <el-table-column label="项目公司">
394
- <template #default="{ row, $index }">
395
- <el-input
396
- v-model="row.companyManager"
397
- :disabled="row.flag === 'D' || row.flag === 'B'"
398
- v-compare:companyManager.table="{
399
- oldValue: oldFormInfos?.projectList?.[$index]?.companyManager,
400
- title: props.item.title,
401
- label: '项目公司',
402
- tableFirstLabel: '名称',
403
- tableFirstValue: row.name,
404
- tableRowStatus: row.flag,
405
- tableRowOrder: $index,
406
- enabled: compareDisabled,
407
- }"
408
- />
409
- </template>
410
- </el-table-column>
411
- </el-table>
412
- ```
413
-
414
- 要点:
415
-
416
- - 每个可编辑列都挂 `.table`,共用同一 `tableFirstValue` / `tableRowOrder`
417
- - `tableFirstLabel` + `tableFirstValue` 决定汇总矩阵「业务名称」列
418
- - 新增行 `oldValue` 可为空;删除行即使字段未改也会进汇总
419
-
420
- ---
421
-
422
- ## 六、变更汇总
423
-
424
- ### Store
425
-
426
- ```typescript
427
- import { useCompareStoreHook } from '@/store/modules/compareStore'
428
-
429
- const store = useCompareStoreHook()
430
- store.getChangeSummaryData // 按 module 分组
431
- store.getStatsNum // { total, modules }
432
- store.clearAllRecords()
433
- store.clearModuleRecords(module)
434
- ```
435
-
436
- ### Service
437
-
438
- ```typescript
439
- import { ChangeSummaryService } from '@/utils/changeSummaryService'
440
-
441
- const data = ChangeSummaryService.getChangeSummaryData()
442
- // 自动修复失效 element;失败则 DOM 回退扫描 [data-v-compare]
443
- ```
444
-
445
- ### Dialog
446
-
447
- ```vue
448
- <ChangeSummaryDialog
449
- v-model="visible"
450
- :module-order="moduleTitles" // 可选:模块展示顺序
451
- />
452
- ```
453
-
454
- - 普通字段:模块标题 + 表(字段 / 原值 / 新值 / 定位)
455
- - `.table` 项:按行折叠为矩阵(变更类型 + 各列旧→新)
456
- - 定位:`scrollIntoView` + 临时类 `locate-highlight`;引用失效时用
457
- `[data-v-compare][data-compare-label="…"][data-compare-module="…"]`
458
- (`escapeAttrSelector` 防特殊字符)
459
-
460
- 可将变更汇总弹窗抽到业务模块或 `components/`。
461
-
462
- ---
463
-
464
- ## 七、封装组件内挂指令(TSX/render)
465
-
466
- ```typescript
467
- import { resolveDirective, withDirectives } from 'vue'
468
-
469
- const compareDirective = resolveDirective('compare')
470
- return withDirectives(vnode, [[
471
- compareDirective,
472
- compareConfig,
473
- compareKey,
474
- compareModifiers || {},
475
- ]])
476
- ```
477
-
478
- `ReSelectQuery` / `ReCascader` 已实现;新封装选择器应同样暴露三件套 props。
479
-
480
- ---
481
-
482
- ## 八、生成检查清单
483
-
484
- - [ ] 父页 `provide` 了 `oldDataInfo` + 启用开关;编辑拉取后写入 `oldDataInfo`
485
- - [ ] 新增模式 `enabled: false`;修改模式 `enabled: true`
486
- - [ ] 每个对比字段有稳定 `title`(模块)+ `label`(字段名)
487
- - [ ] 码值类控件用了 `.map` / `.mapArr` + `composeOptions`(或等价 map)
488
- - [ ] 日期类指令挂在外层 `div`,不是直接挂在 picker 不稳定节点
489
- - [ ] 树/级联/异步选择对比的是 **名称**(`oldValue`/`value` 用 `*Name`)
490
- - [ ] `ReSelectQuery`/`ReCascader` 用 `compare-config` + `compare-key`,多选加 `compare-modifiers.mapArr`
491
- - [ ] 表格列用 `.table` + `tableFirst*` + `tableRowStatus` + `tableRowOrder` + `getRowClassByFlag`
492
- - [ ] 接了汇总弹窗;离开页 `clearAllRecords`
493
- - [ ] 未使用文档里的错误写法:`newValue`(不存在)、数值比较数组语法(那是错误旧文档)
494
-
495
- ---
496
-
497
- ## 九、关联资产
498
-
499
- | 路径 | 说明 |
500
- |------|------|
501
- | `src/directives/compare/index.ts` | 指令实现 |
502
- | `src/directives/compare/types.ts` | 类型 |
503
- | `src/directives/compare/styles.css` | 高亮与 tooltip |
504
- | `src/store/modules/compareStore.ts` | 变更 Store |
505
- | `src/utils/changeSummaryService.ts` | 汇总读取/修复 |
506
- | `src/utils/utils.ts` | `composeOptions` / `getRowClassByFlag` / `escapeAttrSelector` |
507
- | `ReSelectQuery` / `ReCascader` | 指令 props 桥接 |
508
-
509
- 相关模块:`data-writeback`(名称回写)、`directives-advanced`(其它指令)、`crud-pages` / `detail-page`(编辑页骨架)。
1
+ ---
2
+ name: data-compare
3
+ description: 编辑页数据变更对比:v-compare、compareStore、ChangeSummaryService
4
+ scope: project
5
+ tags: [v-compare, 变更汇总, 变更对比, 数据对比, 字段对比, 高亮变更]
6
+ ---
7
+
8
+ # 数据变更对比
9
+
10
+ > 按本文件示例编写,复用 `v-compare` / `compareStore` / `ChangeSummaryService` 等已有能力,勿另起一套对比实现。
11
+
12
+ 编辑页对比「当前值 vs 编辑前原值」:字段高亮、悬停原值提示、变更汇总弹窗、表格行增删改矩阵。
13
+
14
+ > 示例:以本文件完整规范为准。
15
+ > 核心落点:`src/directives/compare/`、`src/store/modules/compareStore.ts`、`src/utils/changeSummaryService.ts`、
16
+
17
+ ---
18
+
19
+ ## 一、架构总览
20
+
21
+ ```
22
+ 页面 provide(oldDataInfo, compareEnabled)
23
+ └─ 子表单 inject → v-compare / compare-config
24
+ ├─ 高亮 .is-compare-changed + tooltip「原值:…」
25
+ └─ useCompareStore.addOrUpdateRecord
26
+ └─ ChangeSummaryDialog ← ChangeSummaryService.getChangeSummaryData()
27
+ └─ 定位:scrollIntoView + data-compare-* DOM 回退
28
+ ```
29
+
30
+ | 层级 | 职责 |
31
+ |------|------|
32
+ | `v-compare` | 读 DOM/Vue 当前值,与 `oldValue` 比(映射后显示值),写样式与 Store |
33
+ | `useCompareStore` | 按模块聚合变更项;去重键 = module+label+context+tableFirstValue+tableRowOrder |
34
+ | `ChangeSummaryService` | 读 Store;元素引用失效时用 `data-compare-label/module` 修复 |
35
+
36
+
37
+ **对比的是映射后的显示文案**,不是原始 code(除非未配置 map)。空值统一显示为 `空`(`''` / `null` / `undefined` 视为相等)。
38
+
39
+ ---
40
+
41
+ ## 二、页面编排(必须先做)
42
+
43
+ ### 1. 父页 provide
44
+
45
+ ```typescript
46
+ const formData = reactive<DataInfo>({ /* 当前编辑数据 */ })
47
+ const oldDataInfo = ref<DataInfo | null>(null) // 编辑前快照(接口返回后赋值)
48
+ const compareEnabled = ref(true) // true = 修改模式启用对比;新增模式设 false
49
+
50
+ provide('oldDataInfo', oldDataInfo)
51
+ provide('compareDisabled', compareEnabled) // 历史命名:变量叫 Disabled,语义却是「是否启用」
52
+
53
+ // 打开编辑:拉取原数据
54
+ async function fetchDetail(id: string) {
55
+ const res = await getDetailApi({ id })
56
+ Object.assign(formData, res.data)
57
+ oldDataInfo.value = cloneDeep(res.data)
58
+ }
59
+ ```
60
+
61
+ > **命名陷阱**:示例里 inject 的 key 是 `compareDisabled`,但 `enabled: compareDisabled` 时 **true 表示对比已启用**。新代码建议本地命名为 `compareEnabled`,provide key 仍用 `compareDisabled` 以兼容现有子组件。
62
+
63
+ ### 2. 子表单 inject
64
+
65
+ ```typescript
66
+ const oldFormInfos = inject('oldDataInfo') as Ref<DataInfo | null>
67
+ const compareDisabled = inject<Ref<boolean>>('compareDisabled', ref(false))
68
+ ```
69
+
70
+ ### 3. 变更汇总入口
71
+
72
+ ```vue
73
+ <el-button :disabled="!compareDisabled" @click="showChangeSummary = true">
74
+ 变更汇总
75
+ </el-button>
76
+ <ChangeSummaryDialog
77
+ v-model="showChangeSummary"
78
+ :module-order="['基本信息', '项目成员', '项目列表']"
79
+ />
80
+ ```
81
+
82
+ 离开编辑页时建议 `useCompareStoreHook().clearAllRecords()`。
83
+
84
+ ### 4. 嵌套弹窗
85
+
86
+ 弹窗内再 `provide('oldDataInfo', localOld)` / `provide('compareDisabled', …)`,与主表单同一套指令即可(见 `MultiFormDialog.vue`)。
87
+
88
+ ---
89
+
90
+ ## 三、指令 API
91
+
92
+ ### 绑定值 `CompareBindingValue`
93
+
94
+ | 字段 | 说明 |
95
+ |------|------|
96
+ | `oldValue` | 编辑前原值(必填语义) |
97
+ | `value` | **覆盖当前显示值**(树/级联等用名称字段,不用 code) |
98
+ | `title` | 模块分组名(汇总按此分组;运行时优先于 `module`) |
99
+ | `label` | 字段中文名(汇总列 + DOM `data-compare-label`) |
100
+ | `enabled` | `false` 跳过对比(推荐开关) |
101
+ | `showInSummary` | `false` 只高亮,不进汇总 |
102
+ | `map` | 配合 `.map`:对象映射 `{ 1: '是' }` |
103
+ | `mapArr` | 配合 `.mapArr`:选项映射 / 多选顺序无关比较 |
104
+ | `format` | `(v) => string` 自定义显示 |
105
+ | `tableFirstLabel` | 表格模式:业务主键列名 |
106
+ | `tableFirstValue` | 表格模式:该行业务主键值 |
107
+ | `tableRowStatus` | `I` 新增 / `U` 修改 / `D`\|`B` 删除(强制记入汇总) |
108
+ | `tableRowOrder` | 行序号(与页面顺序一致) |
109
+
110
+ ### 修饰符 / 参数
111
+
112
+ | 写法 | 作用 |
113
+ |------|------|
114
+ | `v-compare:fieldKey` | `arg` 为字段名(可读性;逻辑不依赖 arg) |
115
+ | `.map` | 使用 `map` 对象做显示映射 |
116
+ | `.mapArr` | 使用 `mapArr`;多选按集合比较(忽略顺序) |
117
+ | `.table` | 表格行上下文,汇总走矩阵视图 |
118
+
119
+ ### 行为摘要
120
+
121
+ - 变更 → 加 `.is-compare-changed`,悬停全局 tooltip「原值:xxx」
122
+ - 写入 Store;改回原值 → `removeRecordByLabel`
123
+ - DOM 标记:`data-v-compare`、`data-compare-label`、`data-compare-module`(定位回退用)
124
+ - 全局恢复:`window.restoreCompareValue(el, originalValue)`
125
+
126
+ ---
127
+
128
+ ## 四、控件用法对照(逐类)
129
+
130
+ ### 1. 普通输入 `el-input`
131
+
132
+ 指令挂在组件根上即可。
133
+
134
+ ```vue
135
+ <el-input
136
+ v-model="formInfos.projectName"
137
+ v-compare:projectName="{
138
+ oldValue: oldFormInfos?.projectName,
139
+ title: props.item.title,
140
+ label: '项目名称',
141
+ enabled: compareDisabled,
142
+ }"
143
+ />
144
+ ```
145
+
146
+ `showInSummary: false` → 仍高亮,但不进汇总弹窗。
147
+
148
+ ### 2. 文本域 `el-input type="textarea"`
149
+
150
+ 同 `el-input`,挂在输入组件上。
151
+
152
+ ### 3. 日期/时间 `el-date-picker` / `el-time-picker`
153
+
154
+ **必须包一层 `div` 挂指令**(picker 根节点才稳定可读 DOM)。
155
+
156
+ ```vue
157
+ <div
158
+ v-compare:planStart="{
159
+ oldValue: oldFormInfos?.planStart,
160
+ title: props.item.title,
161
+ label: '计划开始时间',
162
+ enabled: compareDisabled,
163
+ }"
164
+ class="!w-[100%]"
165
+ >
166
+ <el-date-picker v-model="formInfos.planStart" type="date" class="!w-[100%]" />
167
+ </div>
168
+ ```
169
+
170
+ 适用:`date` / `datetime` / `month` / `year` / `daterange` / `datetimerange` / `time`。
171
+ 范围值会按分隔符拼成字符串再比。
172
+
173
+ ### 4. 本地下拉 `el-select`(单选)→ `.map` + `composeOptions`
174
+
175
+ ```vue
176
+ <el-select
177
+ v-model="formInfos.projectStatus"
178
+ v-compare:projectStatus.map="{
179
+ oldValue: oldFormInfos?.projectStatus,
180
+ title: props.item.title,
181
+ label: '项目状态',
182
+ map: composeOptions(localSelectOptions.projectStatus, 'value', 'label'),
183
+ enabled: compareDisabled,
184
+ }"
185
+ >
186
+ <el-option v-for="s in localSelectOptions.projectStatus" :key="s.value" :label="s.label" :value="s.value" />
187
+ </el-select>
188
+ ```
189
+
190
+ `composeOptions(list, codeKey, labelKey)` → `{ [code]: label }`(见 `src/utils/utils.ts`)。
191
+
192
+ 异步选项列表加载完后同样用 `composeOptions(saleUnitList)`(默认 key 为 `code`/`value`)。
193
+
194
+ ### 5. 多选下拉 `el-select multiple` → `.mapArr`
195
+
196
+ ```vue
197
+ <el-select
198
+ v-model="formInfos.multiSelectStatus"
199
+ multiple
200
+ v-compare:multiSelectStatus.mapArr="{
201
+ oldValue: oldFormInfos?.multiSelectStatus,
202
+ title: props.item.title,
203
+ label: '多选状态',
204
+ mapArr: composeOptions(localSelectOptions.projectStatus, 'value', 'label'),
205
+ enabled: compareDisabled,
206
+ }"
207
+ />
208
+ ```
209
+
210
+ 比较时忽略选项顺序;显示为逗号拼接的中文标签。
211
+
212
+ ### 6. `ReSelectQuery`(异步分页选择)→ props 桥接
213
+
214
+ 模板不能直接写 `v-compare` 时,用组件内置 props(内部 `withDirectives`)。
215
+
216
+ ```vue
217
+ <!-- 单选:oldValue 用名称字段 -->
218
+ <ReSelectQuery
219
+ v-model="formInfos.asyncSelectUser"
220
+ :compare-config="{
221
+ oldValue: oldFormInfos?.asyncSelectUserName,
222
+ label: '异步单选用户',
223
+ title: props.item.title,
224
+ enabled: compareDisabled,
225
+ }"
226
+ compare-key="asyncSelectUser"
227
+ :edit-data="formInfos.single1BackData"
228
+ url="..."
229
+ value="userCode"
230
+ label="userName"
231
+ />
232
+
233
+ <!-- 多选 -->
234
+ <ReSelectQuery
235
+ v-model="formInfos.asyncMultiSelectUser"
236
+ multiple
237
+ :compare-config="{
238
+ oldValue: oldFormInfos?.asyncMultiSelectUserNames,
239
+ label: '异步多选用户',
240
+ title: props.item.title,
241
+ enabled: compareDisabled,
242
+ }"
243
+ compare-key="asyncMultiSelectUser"
244
+ :compare-modifiers="{ mapArr: true }"
245
+ />
246
+ ```
247
+
248
+ | Prop | 说明 |
249
+ |------|------|
250
+ | `compareConfig` | 同指令 binding value |
251
+ | `compareKey` | 同 `v-compare:key` 的 arg |
252
+ | `compareModifiers` | 如 `{ mapArr: true }` |
253
+
254
+ ### 7. `el-tree-select` → 用 `value` 覆盖显示名
255
+
256
+ 对比名称字符串,不要对比节点 code。
257
+
258
+ ```vue
259
+ <el-tree-select
260
+ v-model="formInfos.value3"
261
+ v-compare:value3="{
262
+ oldValue: oldFormInfos?.value3Name,
263
+ value: formInfos.value3Name,
264
+ title: props.item.title,
265
+ label: '单选树',
266
+ enabled: compareDisabled,
267
+ }"
268
+ :cache-data="formInfos.value3Text"
269
+ ...
270
+ />
271
+
272
+ <!-- 多选树 -->
273
+ v-compare:multiple1.mapArr="{
274
+ oldValue: oldFormInfos?.multiple1Name,
275
+ value: formInfos.multiple1Name,
276
+ title, label, enabled: compareDisabled,
277
+ }"
278
+ ```
279
+
280
+ 需在业务里维护 `*Name` / `*Text` 回写字段(与 data-writeback 模块配合)。
281
+
282
+ ### 8. `ReCascader` → 同 ReSelectQuery 桥接 + `value` 名称
283
+
284
+ ```vue
285
+ <ReCascader
286
+ v-model="formInfos.singleBack"
287
+ :compare-config="{
288
+ oldValue: oldFormInfos?.singleBackName,
289
+ value: formInfos.singleBackName,
290
+ label: '级联器单选',
291
+ title: props.item.title,
292
+ enabled: compareDisabled,
293
+ }"
294
+ compare-key="singleBack"
295
+ @change="getSingleBackName"
296
+ />
297
+
298
+ <!-- 多选加 modifiers -->
299
+ :compare-modifiers="{ mapArr: true }"
300
+ ```
301
+
302
+ ### 9. `el-checkbox` / `el-switch` → `.map` 布尔/枚举文案
303
+
304
+ ```vue
305
+ <el-checkbox
306
+ v-model="formInfos.isUrgent"
307
+ true-value="1"
308
+ false-value="0"
309
+ v-compare:isUrgent.map="{
310
+ oldValue: oldFormInfos?.isUrgent,
311
+ title: props.item.title,
312
+ label: '是否紧急',
313
+ map: { 1: '是', 0: '否' },
314
+ enabled: compareDisabled,
315
+ }"
316
+ >
317
+ 紧急项目
318
+ </el-checkbox>
319
+ ```
320
+
321
+ `el-switch` 同理,读取 `active-value` / `inactive-value`。
322
+
323
+ ### 10. `el-radio-group` / `el-checkbox-group` → `.map` / `.mapArr`
324
+
325
+ ```vue
326
+ <el-radio-group
327
+ v-model="formInfos.approvalStatus"
328
+ v-compare:approvalStatus.map="{
329
+ oldValue: oldFormInfos?.approvalStatus,
330
+ title: props.item.title,
331
+ label: '审批状态',
332
+ map: composeOptions(options, 'value', 'label'),
333
+ enabled: compareDisabled,
334
+ }"
335
+ >
336
+ <el-radio v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</el-radio>
337
+ </el-radio-group>
338
+
339
+ <el-checkbox-group
340
+ v-model="formInfos.tags"
341
+ v-compare:tags.mapArr="{
342
+ oldValue: oldFormInfos?.tags,
343
+ title: props.item.title,
344
+ label: '标签',
345
+ mapArr: composeOptions(tagOptions, 'value', 'label'),
346
+ enabled: compareDisabled,
347
+ }"
348
+ >
349
+ ...
350
+ </el-checkbox-group>
351
+ ```
352
+
353
+ ### 11. `el-input-number` / `el-rate` → `format`
354
+
355
+ ```vue
356
+ <el-input-number
357
+ v-model="formInfos.completionRate"
358
+ v-compare:completionRate="{
359
+ oldValue: oldFormInfos?.completionRate,
360
+ title: props.item.title,
361
+ label: '完成率',
362
+ enabled: compareDisabled,
363
+ format: (v) => (v === null || v === undefined || v === '' ? '未设置' : `${v}%`),
364
+ }"
365
+ />
366
+
367
+ <el-rate
368
+ v-model="formInfos.projectRating"
369
+ v-compare:projectRating="{
370
+ oldValue: oldFormInfos?.projectRating,
371
+ title: props.item.title,
372
+ label: '项目评分',
373
+ enabled: compareDisabled,
374
+ format: (v) => (v == null || v === '' ? '未评分' : `${v}分`),
375
+ }"
376
+ />
377
+ ```
378
+
379
+ ---
380
+
381
+ ## 五、表格行对比(`.table`)
382
+
383
+ ### 行标志约定
384
+
385
+ | `flag` / `tableRowStatus` | 含义 | 行样式(`getRowClassByFlag`) | 汇总 |
386
+ |---------------------------|------|------------------------------|------|
387
+ | `I` | 新增 | `row-added` | 记入;类型「新增」 |
388
+ | `U` | 修改 | `row-modified` | 按字段 diff |
389
+ | `D` / `B` | 删除 | `row-deleted` | **强制记入**(值相等也保留) |
390
+
391
+ ```vue
392
+ <el-table :data="formInfos.projectList" :row-class-name="({ row }) => getRowClassByFlag(row?.flag)">
393
+ <el-table-column label="项目公司">
394
+ <template #default="{ row, $index }">
395
+ <el-input
396
+ v-model="row.companyManager"
397
+ :disabled="row.flag === 'D' || row.flag === 'B'"
398
+ v-compare:companyManager.table="{
399
+ oldValue: oldFormInfos?.projectList?.[$index]?.companyManager,
400
+ title: props.item.title,
401
+ label: '项目公司',
402
+ tableFirstLabel: '名称',
403
+ tableFirstValue: row.name,
404
+ tableRowStatus: row.flag,
405
+ tableRowOrder: $index,
406
+ enabled: compareDisabled,
407
+ }"
408
+ />
409
+ </template>
410
+ </el-table-column>
411
+ </el-table>
412
+ ```
413
+
414
+ 要点:
415
+
416
+ - 每个可编辑列都挂 `.table`,共用同一 `tableFirstValue` / `tableRowOrder`
417
+ - `tableFirstLabel` + `tableFirstValue` 决定汇总矩阵「业务名称」列
418
+ - 新增行 `oldValue` 可为空;删除行即使字段未改也会进汇总
419
+
420
+ ---
421
+
422
+ ## 六、变更汇总
423
+
424
+ ### Store
425
+
426
+ ```typescript
427
+ import { useCompareStoreHook } from '@/store/modules/compareStore'
428
+
429
+ const store = useCompareStoreHook()
430
+ store.getChangeSummaryData // 按 module 分组
431
+ store.getStatsNum // { total, modules }
432
+ store.clearAllRecords()
433
+ store.clearModuleRecords(module)
434
+ ```
435
+
436
+ ### Service
437
+
438
+ ```typescript
439
+ import { ChangeSummaryService } from '@/utils/changeSummaryService'
440
+
441
+ const data = ChangeSummaryService.getChangeSummaryData()
442
+ // 自动修复失效 element;失败则 DOM 回退扫描 [data-v-compare]
443
+ ```
444
+
445
+ ### Dialog
446
+
447
+ ```vue
448
+ <ChangeSummaryDialog
449
+ v-model="visible"
450
+ :module-order="moduleTitles" // 可选:模块展示顺序
451
+ />
452
+ ```
453
+
454
+ - 普通字段:模块标题 + 表(字段 / 原值 / 新值 / 定位)
455
+ - `.table` 项:按行折叠为矩阵(变更类型 + 各列旧→新)
456
+ - 定位:`scrollIntoView` + 临时类 `locate-highlight`;引用失效时用
457
+ `[data-v-compare][data-compare-label="…"][data-compare-module="…"]`
458
+ (`escapeAttrSelector` 防特殊字符)
459
+
460
+ 可将变更汇总弹窗抽到业务模块或 `components/`。
461
+
462
+ ---
463
+
464
+ ## 七、封装组件内挂指令(TSX/render)
465
+
466
+ ```typescript
467
+ import { resolveDirective, withDirectives } from 'vue'
468
+
469
+ const compareDirective = resolveDirective('compare')
470
+ return withDirectives(vnode, [[
471
+ compareDirective,
472
+ compareConfig,
473
+ compareKey,
474
+ compareModifiers || {},
475
+ ]])
476
+ ```
477
+
478
+ `ReSelectQuery` / `ReCascader` 已实现;新封装选择器应同样暴露三件套 props。
479
+
480
+ ---
481
+
482
+ ## 八、生成检查清单
483
+
484
+ - [ ] 父页 `provide` 了 `oldDataInfo` + 启用开关;编辑拉取后写入 `oldDataInfo`
485
+ - [ ] 新增模式 `enabled: false`;修改模式 `enabled: true`
486
+ - [ ] 每个对比字段有稳定 `title`(模块)+ `label`(字段名)
487
+ - [ ] 码值类控件用了 `.map` / `.mapArr` + `composeOptions`(或等价 map)
488
+ - [ ] 日期类指令挂在外层 `div`,不是直接挂在 picker 不稳定节点
489
+ - [ ] 树/级联/异步选择对比的是 **名称**(`oldValue`/`value` 用 `*Name`)
490
+ - [ ] `ReSelectQuery`/`ReCascader` 用 `compare-config` + `compare-key`,多选加 `compare-modifiers.mapArr`
491
+ - [ ] 表格列用 `.table` + `tableFirst*` + `tableRowStatus` + `tableRowOrder` + `getRowClassByFlag`
492
+ - [ ] 接了汇总弹窗;离开页 `clearAllRecords`
493
+ - [ ] 未使用文档里的错误写法:`newValue`(不存在)、数值比较数组语法(那是错误旧文档)
494
+
495
+ ---
496
+
497
+ ## 九、关联资产
498
+
499
+ | 路径 | 说明 |
500
+ |------|------|
501
+ | `src/directives/compare/index.ts` | 指令实现 |
502
+ | `src/directives/compare/types.ts` | 类型 |
503
+ | `src/directives/compare/styles.css` | 高亮与 tooltip |
504
+ | `src/store/modules/compareStore.ts` | 变更 Store |
505
+ | `src/utils/changeSummaryService.ts` | 汇总读取/修复 |
506
+ | `src/utils/utils.ts` | `composeOptions` / `getRowClassByFlag` / `escapeAttrSelector` |
507
+ | `ReSelectQuery` / `ReCascader` | 指令 props 桥接 |
508
+
509
+ 相关模块:`data-writeback`(名称回写)、`directives-advanced`(其它指令)、`crud-pages` / `detail-page`(编辑页骨架)。