@qilitt-mickey/vue3-temp-skill 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -0
- package/SKILL.md +621 -0
- package/bin/cli.js +579 -0
- package/package.json +46 -0
- package/references/advanced-ui.md +302 -0
- package/references/api-check.md +272 -0
- package/references/base-code-dict.md +48 -0
- package/references/build-optim.md +282 -0
- package/references/code-quality.md +235 -0
- package/references/crud-pages.md +316 -0
- package/references/data-compare.md +501 -0
- package/references/data-mapping.md +213 -0
- package/references/data-screen.md +79 -0
- package/references/data-writeback.md +104 -0
- package/references/detail-page.md +99 -0
- package/references/directives-advanced.md +93 -0
- package/references/download-export.md +68 -0
- package/references/feedback-loading.md +60 -0
- package/references/feedback-ui.md +111 -0
- package/references/file-management.md +132 -0
- package/references/flowchart-g6.md +244 -0
- package/references/form-advanced.md +137 -0
- package/references/graph-relation.md +253 -0
- package/references/http-api.md +188 -0
- package/references/layout-theme.md +540 -0
- package/references/mobile-h5.md +271 -0
- package/references/permission-auth.md +235 -0
- package/references/project-inventory.md +326 -0
- package/references/qrcode-barcode.md +92 -0
- package/references/rich-text.md +73 -0
- package/references/seamless-scroll.md +38 -0
- package/references/tree-table.md +111 -0
- package/references/ui-components.md +161 -0
- package/references/verify-captcha.md +96 -0
- package/references/vue-core.md +209 -0
- package/references/websocket-realtime.md +176 -0
- package/references/workflow-bpmn.md +206 -0
|
@@ -0,0 +1,501 @@
|
|
|
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`(编辑页骨架)。
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# JsPlumb 数据映射
|
|
2
|
+
|
|
3
|
+
## 依赖
|
|
4
|
+
|
|
5
|
+
`jsplumb` — 拖拽连线、字段映射组件。
|
|
6
|
+
|
|
7
|
+
## 页面结构
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { jsPlumb } from 'jsplumb'
|
|
12
|
+
import { message } from '@/utils/message'
|
|
13
|
+
|
|
14
|
+
defineOptions({ name: 'FieldMapping' })
|
|
15
|
+
|
|
16
|
+
interface Field {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
type: string
|
|
20
|
+
value?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sourceFields = ref<Field[]>([
|
|
24
|
+
{ id: 'src_1', name: 'id', type: 'int', value: '1001' },
|
|
25
|
+
{ id: 'src_2', name: 'organizationid', type: 'int', value: 'ORG_01' },
|
|
26
|
+
{ id: 'src_3', name: 'brand_id', type: 'int' },
|
|
27
|
+
{ id: 'src_4', name: 'source', type: 'string' },
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
const targetFields = ref<Field[]>([
|
|
31
|
+
{ id: 'target_1', name: 'id', type: 'BIGINT', value: '' },
|
|
32
|
+
{ id: 'target_2', name: 'pid', type: 'BIGINT', value: '' },
|
|
33
|
+
{ id: 'target_3', name: 'tn_id', type: 'BIGINT' },
|
|
34
|
+
{ id: 'target_4', name: 'vpdm', type: 'STRING' },
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
let jsPlumbInstance: any = null
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 初始化 jsPlumb
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const initJsPlumb = () => {
|
|
45
|
+
jsPlumbInstance = jsPlumb.getInstance({
|
|
46
|
+
// 连接线:Straight(直线) / Bezier(贝塞尔) / Flowchart(折线) / StateMachine(状态机)
|
|
47
|
+
Connector: ['Straight', { stub: 0, gap: 1 }],
|
|
48
|
+
PaintStyle: { stroke: '#409eff', strokeWidth: 2 },
|
|
49
|
+
HoverPaintStyle: { stroke: '#66b1ff', strokeWidth: 3 },
|
|
50
|
+
Endpoint: ['Dot', { radius: 6 }],
|
|
51
|
+
EndpointStyle: { fill: 'transparent', stroke: 'transparent' },
|
|
52
|
+
EndpointHoverStyle: { fill: '#fff', stroke: '#66b1ff' },
|
|
53
|
+
Container: 'mapping-container', // 必须指定容器 ID
|
|
54
|
+
ConnectionOverlays: [
|
|
55
|
+
['Arrow', {
|
|
56
|
+
width: 10, length: 10, location: 1,
|
|
57
|
+
id: 'arrow', foldback: 0.8,
|
|
58
|
+
}],
|
|
59
|
+
],
|
|
60
|
+
ConnectionsDetachable: true, // 允许拖动重连
|
|
61
|
+
ReattachConnections: false,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// 点击连线删除
|
|
65
|
+
jsPlumbInstance.bind('click', (conn: any) => {
|
|
66
|
+
jsPlumbInstance.deleteConnection(conn)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// 注册源和目标
|
|
70
|
+
nextTick(() => {
|
|
71
|
+
sourceFields.value.forEach((item) => {
|
|
72
|
+
jsPlumbInstance.makeSource(item.id, {
|
|
73
|
+
anchor: 'Right',
|
|
74
|
+
allowLoopback: false,
|
|
75
|
+
maxConnections: 1,
|
|
76
|
+
filter: 'input, select, textarea, .el-input, .el-select',
|
|
77
|
+
filterExclude: true,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
targetFields.value.forEach((item) => {
|
|
81
|
+
jsPlumbInstance.makeTarget(item.id, {
|
|
82
|
+
anchor: 'Left',
|
|
83
|
+
allowLoopback: false,
|
|
84
|
+
maxConnections: 2,
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 常用操作
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// 同行映射(按顺序一一对应)
|
|
95
|
+
const handleSameLineMapping = () => {
|
|
96
|
+
jsPlumbInstance.deleteEveryConnection()
|
|
97
|
+
const len = Math.min(sourceFields.value.length, targetFields.value.length)
|
|
98
|
+
for (let i = 0; i < len; i++) {
|
|
99
|
+
jsPlumbInstance.connect({
|
|
100
|
+
source: sourceFields.value[i].id,
|
|
101
|
+
target: targetFields.value[i].id,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 清空所有连线
|
|
107
|
+
const clearAll = () => {
|
|
108
|
+
jsPlumbInstance.deleteEveryConnection()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 获取映射结果
|
|
112
|
+
const getMappingResult = () => {
|
|
113
|
+
const connections = jsPlumbInstance.getAllConnections()
|
|
114
|
+
return connections.map((conn: any) => ({
|
|
115
|
+
source: sourceFields.value.find(f => f.id === conn.sourceId),
|
|
116
|
+
target: targetFields.value.find(f => f.id === conn.targetId),
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 模板结构
|
|
122
|
+
|
|
123
|
+
```vue
|
|
124
|
+
<template>
|
|
125
|
+
<div class="mapping-page p-4 flex flex-col">
|
|
126
|
+
<!-- 操作栏 -->
|
|
127
|
+
<el-card shadow="never" class="mb-4">
|
|
128
|
+
<div class="flex justify-center gap-4 mt-4">
|
|
129
|
+
<el-button @click="handleSameLineMapping">同行映射</el-button>
|
|
130
|
+
<el-button type="danger" plain @click="clearAll">清空所有</el-button>
|
|
131
|
+
<el-button type="primary" @click="getMappingResult">保存映射</el-button>
|
|
132
|
+
</div>
|
|
133
|
+
</el-card>
|
|
134
|
+
|
|
135
|
+
<!-- 映射区域 -->
|
|
136
|
+
<div id="mapping-container" class="flex-1 relative flex justify-between bg-white p-8">
|
|
137
|
+
<!-- 左侧源字段 -->
|
|
138
|
+
<div class="w-[40%] flex flex-col border rounded">
|
|
139
|
+
<div v-for="item in sourceFields" :id="item.id" :key="item.id"
|
|
140
|
+
class="field-item flex border-b hover:bg-gray-50 cursor-pointer relative">
|
|
141
|
+
<div class="flex-1 p-2">{{ item.name }}</div>
|
|
142
|
+
<div class="w-[80px] p-2 text-center">{{ item.type }}</div>
|
|
143
|
+
<!-- 连接点 -->
|
|
144
|
+
<div :id="`handle-${item.id}`"
|
|
145
|
+
class="connector-handle-right absolute right-0 top-1/2 -mt-[6px] translate-x-1/2" />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- 中间留白给连线 -->
|
|
150
|
+
<div class="w-[20%]" />
|
|
151
|
+
|
|
152
|
+
<!-- 右侧目标字段 -->
|
|
153
|
+
<div class="w-[40%] flex flex-col border rounded">
|
|
154
|
+
<div v-for="item in targetFields" :id="item.id" :key="item.id"
|
|
155
|
+
class="field-item flex border-b hover:bg-gray-50 cursor-pointer relative">
|
|
156
|
+
<div :id="`handle-${item.id}`"
|
|
157
|
+
class="connector-handle-left absolute left-0 top-1/2 -mt-[6px] -translate-x-1/2" />
|
|
158
|
+
<div class="flex-1 p-2">{{ item.name }}</div>
|
|
159
|
+
<div class="w-[80px] p-2 text-center">{{ item.type }}</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 样式
|
|
168
|
+
|
|
169
|
+
```scss
|
|
170
|
+
.connector-handle-right,
|
|
171
|
+
.connector-handle-left {
|
|
172
|
+
z-index: 10;
|
|
173
|
+
width: 12px;
|
|
174
|
+
height: 12px;
|
|
175
|
+
background-color: #fff;
|
|
176
|
+
border: 1px solid #409eff;
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
}
|
|
179
|
+
.connector-handle-right:hover,
|
|
180
|
+
.connector-handle-left:hover {
|
|
181
|
+
background-color: #e6f7ff;
|
|
182
|
+
border-color: #1890ff;
|
|
183
|
+
}
|
|
184
|
+
:deep(.jtk-endpoint) { z-index: 20; }
|
|
185
|
+
:deep(.jtk-connector) { z-index: 10; }
|
|
186
|
+
:deep(.jtk-connector.jtk-hover) { z-index: 30; }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 生命周期
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
onMounted(() => {
|
|
193
|
+
nextTick(() => initJsPlumb())
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
onUnmounted(() => {
|
|
197
|
+
jsPlumbInstance?.reset()
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 关键约定
|
|
202
|
+
|
|
203
|
+
1. `Container` 必须指定为映射区域的 DOM ID,否则连线定位错误。
|
|
204
|
+
2. 源端点用 `makeSource`,目标端点用 `makeTarget`,锚点分别在 `Right` 和 `Left`。
|
|
205
|
+
3. `filter` 排除表单元素,避免点击输入框时触发连线拖动。
|
|
206
|
+
4. 连线删除通过 `bind('click')` 事件实现,点击连线即删除。
|
|
207
|
+
5. `onUnmounted` 中必须调用 `jsPlumbInstance.reset()` 清理。
|
|
208
|
+
6. 字段行必须有唯一 `id`,jsPlumb 通过 ID 绑定源/目标。
|
|
209
|
+
7. 中间留白区域(`w-[20%]`)给连线绘制空间。
|
|
210
|
+
|
|
211
|
+
## 关联文件
|
|
212
|
+
|
|
213
|
+
- [src/views/workflow/jsplumb/index.vue](file:///e:/work/vue3-web-temp/src/views/workflow/jsplumb/index.vue)
|