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