@veltra/desktop 1.7.17 → 1.7.19
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/dist/components/batch-edit/batch-edit-form.js +6 -1
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +45 -27
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +2 -2
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/di.js.map +1 -1
- package/dist/components/batch-edit/use-edit-state.js +14 -10
- package/dist/components/batch-edit/use-edit-state.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +1 -0
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/dialog/style2.css +1 -1
- package/dist/components/expression-editor/core/chip.js +1 -15
- package/dist/components/expression-editor/core/chip.js.map +1 -1
- package/dist/components/expression-editor/core/editor.js +0 -18
- package/dist/components/expression-editor/core/editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +0 -4
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/style2.css +0 -23
- package/dist/index.d.ts +2 -2
- package/dist/types/batch-edit.d.ts +0 -3
- package/dist/types/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/batch-edit/__test__/batch-edit.test.ts +165 -38
- package/src/components/batch-edit/batch-edit-form.vue +7 -1
- package/src/components/batch-edit/batch-edit-list.vue +53 -22
- package/src/components/batch-edit/batch-edit.vue +2 -1
- package/src/components/batch-edit/di.ts +2 -0
- package/src/components/batch-edit/use-edit-state.ts +25 -32
- package/src/components/batch-edit/use-handlers.ts +2 -0
- package/src/components/dialog/style.scss +2 -1
- package/src/components/expression-editor/__test__/chip.test.ts +3 -14
- package/src/components/expression-editor/core/chip.ts +3 -29
- package/src/components/expression-editor/core/editor.ts +0 -21
- package/src/components/expression-editor/expression-editor.vue +0 -4
- package/src/components/expression-editor/style.scss +0 -29
- package/src/types/batch-edit.ts +0 -4
|
@@ -107,6 +107,10 @@ function queryRows(host: HTMLElement) {
|
|
|
107
107
|
return [...host.querySelectorAll<HTMLElement>('tr.u-table__row')]
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function queryEditButton(host: HTMLElement, index: number) {
|
|
111
|
+
return queryRows(host)[index]?.querySelector<HTMLElement>('button[title="编辑"]')
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
async function clickRow(host: HTMLElement, index: number) {
|
|
111
115
|
const row = queryRows(host)[index]
|
|
112
116
|
expect(row, `第 ${index} 行应存在`).toBeTruthy()
|
|
@@ -115,19 +119,27 @@ async function clickRow(host: HTMLElement, index: number) {
|
|
|
115
119
|
await nextTick()
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
async function clickEditRow(host: HTMLElement, index: number) {
|
|
123
|
+
const btn = queryEditButton(host, index)
|
|
124
|
+
expect(btn, `第 ${index} 行的编辑按钮应存在`).toBeTruthy()
|
|
125
|
+
btn!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
126
|
+
await nextTick()
|
|
127
|
+
await nextTick()
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
119
131
|
it('切换行时先重置表单再回显,不污染行数据', async () => {
|
|
120
132
|
const { host, data, model, unmount } = mountBatchEdit()
|
|
121
133
|
|
|
122
|
-
// 第一次点击第 1
|
|
123
|
-
await
|
|
134
|
+
// 第一次点击第 1 行的编辑按钮:正常回显
|
|
135
|
+
await clickEditRow(host, 0)
|
|
124
136
|
expect(model.label).toBe('存草稿')
|
|
125
137
|
expect(model.behavior).toBe('common-resource')
|
|
126
138
|
expect(model.submitType).toBe('DRAFT')
|
|
127
139
|
expect(model.apiMethod).toBeUndefined()
|
|
128
140
|
|
|
129
|
-
// 再点击第 2
|
|
130
|
-
await
|
|
141
|
+
// 再点击第 2 行的编辑按钮:先重置回 model 初始快照,再回显第 2 行数据
|
|
142
|
+
await clickEditRow(host, 1)
|
|
131
143
|
expect(model.label).toBe('调接口')
|
|
132
144
|
expect(model.behavior).toBe('api')
|
|
133
145
|
expect(model.submitType).toBe('SUBMIT')
|
|
@@ -142,14 +154,33 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
142
154
|
unmount()
|
|
143
155
|
})
|
|
144
156
|
|
|
145
|
-
it('
|
|
157
|
+
it('点击行打开表单,再次点击当前行关闭表单', async () => {
|
|
158
|
+
const { host, model, unmount } = mountBatchEdit()
|
|
159
|
+
|
|
160
|
+
const aside = host.querySelector<HTMLElement>('.u-batch-edit__form')!
|
|
161
|
+
|
|
162
|
+
await clickRow(host, 0)
|
|
163
|
+
expect(aside.style.display).not.toBe('none')
|
|
164
|
+
expect(model.label).toBe('存草稿')
|
|
165
|
+
|
|
166
|
+
// 再次点击当前行:取消选中并关闭表单,model 恢复初始值
|
|
167
|
+
await clickRow(host, 0)
|
|
168
|
+
expect(aside.style.display).toBe('none')
|
|
169
|
+
expect(model.label).toBe('')
|
|
170
|
+
|
|
171
|
+
unmount()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('关闭表单后表单数据重置为 model 初始值', async () => {
|
|
146
175
|
const { host, data, model, unmount } = mountBatchEdit()
|
|
147
176
|
|
|
148
|
-
await
|
|
177
|
+
await clickEditRow(host, 1)
|
|
149
178
|
expect(model.label).toBe('调接口')
|
|
150
179
|
|
|
151
|
-
//
|
|
152
|
-
|
|
180
|
+
// 点击「取消」关闭表单,model 恢复到初始快照
|
|
181
|
+
clickEl(queryCancelButton(host)!)
|
|
182
|
+
await nextTick()
|
|
183
|
+
await nextTick()
|
|
153
184
|
expect(model.label).toBe('')
|
|
154
185
|
expect(model.behavior).toBe('event')
|
|
155
186
|
expect(model.submitType).toBeUndefined()
|
|
@@ -169,16 +200,16 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
169
200
|
it('表单常驻挂载(v-show 控制显隐),切换行不重建表单', async () => {
|
|
170
201
|
const { host, unmount } = mountBatchEdit()
|
|
171
202
|
|
|
172
|
-
// model
|
|
203
|
+
// model 存在即挂载表单,未编辑行时仅隐藏(快照只在首次挂载时拍一次)
|
|
173
204
|
const aside = host.querySelector<HTMLElement>('.u-batch-edit__form')
|
|
174
205
|
expect(aside).toBeTruthy()
|
|
175
206
|
expect(aside!.style.display).toBe('none')
|
|
176
207
|
|
|
177
|
-
await
|
|
208
|
+
await clickEditRow(host, 0)
|
|
178
209
|
const formBody = host.querySelector('.u-batch-edit__form-body')
|
|
179
210
|
expect(aside!.style.display).not.toBe('none')
|
|
180
211
|
|
|
181
|
-
await
|
|
212
|
+
await clickEditRow(host, 1)
|
|
182
213
|
// 同一 DOM 节点:表单未因切行销毁重建,初始快照不会被重新生成
|
|
183
214
|
expect(host.querySelector('.u-batch-edit__form-body')).toBe(formBody)
|
|
184
215
|
|
|
@@ -188,7 +219,7 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
188
219
|
it('quick-edit 下用户输入仍实时写回行数据', async () => {
|
|
189
220
|
const { host, data, unmount } = mountBatchEdit()
|
|
190
221
|
|
|
191
|
-
await
|
|
222
|
+
await clickEditRow(host, 0)
|
|
192
223
|
|
|
193
224
|
const input = host.querySelector<HTMLInputElement>('.u-input input')
|
|
194
225
|
expect(input).toBeTruthy()
|
|
@@ -205,18 +236,71 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
205
236
|
})
|
|
206
237
|
})
|
|
207
238
|
|
|
239
|
+
describe('UBatchEdit 操作列', () => {
|
|
240
|
+
it('按钮顺序:编辑在最前,上/下插入在最后', async () => {
|
|
241
|
+
const { host, unmount } = mountBatchEdit({ props: { tree: true } })
|
|
242
|
+
await nextTick()
|
|
243
|
+
|
|
244
|
+
const titles = [...queryRows(host)[0]!.querySelectorAll<HTMLElement>('button[title]')].map(
|
|
245
|
+
(btn) => btn.title
|
|
246
|
+
)
|
|
247
|
+
expect(titles).toEqual(['编辑', '添加子级', '删除', '在上方插入', '在下方插入'])
|
|
248
|
+
|
|
249
|
+
unmount()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('readonly 模式操作列只保留「查看」按钮,点击打开只读表单', async () => {
|
|
253
|
+
const { host, unmount } = mountBatchEdit({ props: { readonly: true } })
|
|
254
|
+
await nextTick()
|
|
255
|
+
|
|
256
|
+
const row = queryRows(host)[0]!
|
|
257
|
+
const viewBtn = row.querySelector<HTMLElement>('button[title="查看"]')
|
|
258
|
+
expect(viewBtn).toBeTruthy()
|
|
259
|
+
expect(row.querySelector('button[title="删除"]')).toBeNull()
|
|
260
|
+
expect(row.querySelector('button[title="在上方插入"]')).toBeNull()
|
|
261
|
+
expect(host.querySelector('.u-batch-edit__add-btn')).toBeNull()
|
|
262
|
+
|
|
263
|
+
viewBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
264
|
+
await nextTick()
|
|
265
|
+
await nextTick()
|
|
266
|
+
|
|
267
|
+
expect(host.querySelector('.u-batch-edit__form-title-text')!.textContent).toBe('查看详情')
|
|
268
|
+
|
|
269
|
+
unmount()
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
208
273
|
function queryDialog() {
|
|
209
274
|
return document.body.querySelector<HTMLElement>('.u-batch-edit__form-dialog')
|
|
210
275
|
}
|
|
211
276
|
|
|
212
|
-
/** 等弹框(teleport 到 body
|
|
213
|
-
async function
|
|
277
|
+
/** 等弹框(teleport 到 body)打开:进入过渡在 CI 高负载下可能超过固定时长,轮询等待 */
|
|
278
|
+
async function waitDialogOpen() {
|
|
279
|
+
await vi.waitFor(() => {
|
|
280
|
+
expect(queryDialog()).toBeTruthy()
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 等弹框关闭卸载:离开过渡在 CI 高负载下可能超过固定时长,轮询等待 */
|
|
285
|
+
async function waitDialogClosed() {
|
|
286
|
+
await vi.waitFor(() => {
|
|
287
|
+
expect(queryDialog()).toBeNull()
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** 等一次校验 / 保存等异步处理落地(固定短等,避免两次点击的 handleSave 重叠) */
|
|
292
|
+
async function waitDialogSettled() {
|
|
214
293
|
await nextTick()
|
|
215
294
|
await nextTick()
|
|
216
295
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
217
296
|
await nextTick()
|
|
218
297
|
}
|
|
219
298
|
|
|
299
|
+
/** 当前用例弹框的遮罩(body 上可能残留前序用例未卸载的弹框,必须按当前弹框定位) */
|
|
300
|
+
function currentOverlay() {
|
|
301
|
+
return queryDialog()!.closest<HTMLElement>('.u-dialog__overlay')!
|
|
302
|
+
}
|
|
303
|
+
|
|
220
304
|
function clickEl(el: HTMLElement) {
|
|
221
305
|
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
222
306
|
}
|
|
@@ -232,8 +316,8 @@ function querySaveButton(dialog: HTMLElement) {
|
|
|
232
316
|
return dialog.querySelector<HTMLElement>('.u-batch-edit__form-actions button[title="保存"]')
|
|
233
317
|
}
|
|
234
318
|
|
|
235
|
-
function queryCancelButton(
|
|
236
|
-
return [...
|
|
319
|
+
function queryCancelButton(container: HTMLElement) {
|
|
320
|
+
return [...container.querySelectorAll<HTMLElement>('.u-batch-edit__form-actions button')].find(
|
|
237
321
|
(btn) => btn.textContent?.includes('取消')
|
|
238
322
|
)
|
|
239
323
|
}
|
|
@@ -242,7 +326,7 @@ describe('UBatchEdit formMode', () => {
|
|
|
242
326
|
it('默认(不传 formMode)仍渲染右侧面板,不出现弹框', async () => {
|
|
243
327
|
const { host, unmount } = mountBatchEdit()
|
|
244
328
|
|
|
245
|
-
await
|
|
329
|
+
await clickEditRow(host, 0)
|
|
246
330
|
const aside = host.querySelector<HTMLElement>('.u-batch-edit__form')
|
|
247
331
|
expect(aside).toBeTruthy()
|
|
248
332
|
expect(aside!.style.display).not.toBe('none')
|
|
@@ -259,8 +343,8 @@ describe('UBatchEdit formMode', () => {
|
|
|
259
343
|
expect(host.querySelector('.u-batch-edit__form')).toBeNull()
|
|
260
344
|
expect(queryDialog()).toBeNull()
|
|
261
345
|
|
|
262
|
-
await
|
|
263
|
-
await
|
|
346
|
+
await clickEditRow(host, 0)
|
|
347
|
+
await waitDialogOpen()
|
|
264
348
|
|
|
265
349
|
const dialog = queryDialog()
|
|
266
350
|
expect(dialog).toBeTruthy()
|
|
@@ -285,12 +369,11 @@ describe('UBatchEdit formMode', () => {
|
|
|
285
369
|
|
|
286
370
|
// 打开新增弹框
|
|
287
371
|
clickEl(host.querySelector<HTMLElement>('.u-batch-edit__add-btn')!)
|
|
288
|
-
await
|
|
289
|
-
expect(queryDialog()).toBeTruthy()
|
|
372
|
+
await waitDialogOpen()
|
|
290
373
|
|
|
291
374
|
// 必填校验失败:不调用 saveMethod,弹框保持打开
|
|
292
375
|
clickEl(querySaveButton(queryDialog()!)!)
|
|
293
|
-
await
|
|
376
|
+
await waitDialogSettled()
|
|
294
377
|
expect(saveMethod).not.toHaveBeenCalled()
|
|
295
378
|
expect(queryDialog()).toBeTruthy()
|
|
296
379
|
|
|
@@ -299,14 +382,13 @@ describe('UBatchEdit formMode', () => {
|
|
|
299
382
|
await nextTick()
|
|
300
383
|
await nextTick()
|
|
301
384
|
clickEl(querySaveButton(queryDialog()!)!)
|
|
302
|
-
await
|
|
385
|
+
await waitDialogClosed()
|
|
303
386
|
|
|
304
387
|
expect(saveMethod).toHaveBeenCalledTimes(1)
|
|
305
388
|
expect(saveMethod.mock.calls[0]![0]).toMatchObject({ label: '新名称' })
|
|
306
389
|
expect(saveMethod.mock.calls[0]![1]).toBe('create')
|
|
307
390
|
expect(data.value).toHaveLength(3)
|
|
308
391
|
expect(data.value[2]).toMatchObject({ label: '新名称' })
|
|
309
|
-
expect(queryDialog()).toBeNull()
|
|
310
392
|
|
|
311
393
|
unmount()
|
|
312
394
|
})
|
|
@@ -318,24 +400,23 @@ describe('UBatchEdit formMode', () => {
|
|
|
318
400
|
})
|
|
319
401
|
|
|
320
402
|
// 编辑第 1 行后取消
|
|
321
|
-
await
|
|
322
|
-
await
|
|
403
|
+
await clickEditRow(host, 0)
|
|
404
|
+
await waitDialogOpen()
|
|
323
405
|
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '改过的名称')
|
|
324
406
|
await nextTick()
|
|
325
407
|
await nextTick()
|
|
326
408
|
clickEl(queryCancelButton(queryDialog()!)!)
|
|
327
|
-
await
|
|
409
|
+
await waitDialogClosed()
|
|
328
410
|
|
|
329
411
|
expect(saveMethod).not.toHaveBeenCalled()
|
|
330
412
|
expect(data.value[0]).toMatchObject({ label: '存草稿' })
|
|
331
413
|
// model 恢复初始快照,而不是残留上一行数据
|
|
332
414
|
expect(model.label).toBe('')
|
|
333
415
|
expect(model.submitType).toBeUndefined()
|
|
334
|
-
expect(queryDialog()).toBeNull()
|
|
335
416
|
|
|
336
417
|
// 取消后再新增:表单应为初始值(快照不被上一行污染)
|
|
337
418
|
clickEl(host.querySelector<HTMLElement>('.u-batch-edit__add-btn')!)
|
|
338
|
-
await
|
|
419
|
+
await waitDialogOpen()
|
|
339
420
|
expect(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!.value).toBe('')
|
|
340
421
|
|
|
341
422
|
// 表单头部的关闭按钮同样不保存
|
|
@@ -343,27 +424,73 @@ describe('UBatchEdit formMode', () => {
|
|
|
343
424
|
await nextTick()
|
|
344
425
|
await nextTick()
|
|
345
426
|
clickEl(queryDialog()!.querySelector<HTMLElement>('.u-batch-edit__form-close')!)
|
|
346
|
-
await
|
|
427
|
+
await waitDialogClosed()
|
|
347
428
|
expect(saveMethod).not.toHaveBeenCalled()
|
|
348
429
|
expect(model.label).toBe('')
|
|
349
430
|
expect(data.value).toHaveLength(2)
|
|
350
|
-
expect(queryDialog()).toBeNull()
|
|
351
431
|
|
|
352
432
|
// 弹框自身关闭交互(点击遮罩)也不保存
|
|
353
|
-
await
|
|
354
|
-
await
|
|
433
|
+
await clickEditRow(host, 1)
|
|
434
|
+
await waitDialogOpen()
|
|
355
435
|
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '改过的名称')
|
|
356
436
|
await nextTick()
|
|
357
437
|
await nextTick()
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }))
|
|
361
|
-
await flushDialog()
|
|
438
|
+
currentOverlay().dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }))
|
|
439
|
+
await waitDialogClosed()
|
|
362
440
|
|
|
363
441
|
expect(saveMethod).not.toHaveBeenCalled()
|
|
364
442
|
expect(data.value[1]).toMatchObject({ label: '调接口' })
|
|
365
443
|
expect(model.label).toBe('')
|
|
366
|
-
|
|
444
|
+
|
|
445
|
+
unmount()
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
describe('UBatchEdit 重入保护', () => {
|
|
450
|
+
it('保存进行中重复触发(快捷键/连点)被忽略:saveMethod 只调一次、只插一行、弹框正常关闭', async () => {
|
|
451
|
+
const saveMethod = vi.fn()
|
|
452
|
+
const { host, data, unmount } = mountBatchEdit({
|
|
453
|
+
props: { formMode: 'dialog', quickEdit: false, saveMethod },
|
|
454
|
+
labelRules: { required: true }
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
clickEl(host.querySelector<HTMLElement>('.u-batch-edit__add-btn')!)
|
|
458
|
+
await waitDialogOpen()
|
|
459
|
+
|
|
460
|
+
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '新名称')
|
|
461
|
+
await nextTick()
|
|
462
|
+
await nextTick()
|
|
463
|
+
|
|
464
|
+
// 同一 tick 内连续两次触发保存(模拟保存进行中再按 Ctrl/Cmd+S),中间故意不等待
|
|
465
|
+
const saveBtn = querySaveButton(queryDialog()!)!
|
|
466
|
+
clickEl(saveBtn)
|
|
467
|
+
clickEl(saveBtn)
|
|
468
|
+
|
|
469
|
+
await waitDialogClosed()
|
|
470
|
+
|
|
471
|
+
expect(saveMethod).toHaveBeenCalledTimes(1)
|
|
472
|
+
expect(data.value).toHaveLength(3)
|
|
473
|
+
expect(data.value[2]).toMatchObject({ label: '新名称' })
|
|
474
|
+
|
|
475
|
+
unmount()
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('删除进行中重复触发被忽略:deleteMethod 只调一次、只删一行', async () => {
|
|
479
|
+
const deleteMethod = vi.fn()
|
|
480
|
+
const { host, data, unmount } = mountBatchEdit({ props: { deleteMethod } })
|
|
481
|
+
await nextTick()
|
|
482
|
+
|
|
483
|
+
// 同一 tick 内连续两次点击删除,中间故意不等待
|
|
484
|
+
const deleteBtn = queryRows(host)[0]!.querySelector<HTMLElement>('button[title="删除"]')!
|
|
485
|
+
clickEl(deleteBtn)
|
|
486
|
+
clickEl(deleteBtn)
|
|
487
|
+
|
|
488
|
+
await waitDialogSettled()
|
|
489
|
+
|
|
490
|
+
expect(deleteMethod).toHaveBeenCalledTimes(1)
|
|
491
|
+
expect(deleteMethod.mock.calls[0]![0]).toHaveLength(1)
|
|
492
|
+
expect(data.value).toHaveLength(1)
|
|
493
|
+
expect(data.value[0]).toMatchObject({ id: '2' })
|
|
367
494
|
|
|
368
495
|
unmount()
|
|
369
496
|
})
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
v-show="state.formVisible"
|
|
8
8
|
>
|
|
9
9
|
<header :class="cls.e('form-header')">
|
|
10
|
-
<span :class="[cls.e('form-icon'),
|
|
10
|
+
<span :class="[cls.e('form-icon'), iconToneCls]">
|
|
11
11
|
<u-icon>
|
|
12
12
|
<component :is="headerInfo.icon" />
|
|
13
13
|
</u-icon>
|
|
@@ -152,4 +152,10 @@ const showSaveBtn = computed(() => {
|
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
const headerInfo = computed(() => FORM_ACTION_HEADER_MAP[state.formActionType])
|
|
155
|
+
|
|
156
|
+
/** 头部图标色调修饰类:样式中 createChild 对应的修饰类为 is-child */
|
|
157
|
+
const iconToneCls = computed(() => {
|
|
158
|
+
const type = state.formActionType
|
|
159
|
+
return bem.is(type === 'createChild' ? 'child' : type)
|
|
160
|
+
})
|
|
155
161
|
</script>
|
|
@@ -11,21 +11,16 @@
|
|
|
11
11
|
@update:selected="emit('update:selected', $event)"
|
|
12
12
|
>
|
|
13
13
|
<template #column:__action__="{ row }">
|
|
14
|
-
<u-action-group :
|
|
14
|
+
<u-action-group :max="5" circle @click.stop>
|
|
15
15
|
<u-action
|
|
16
|
-
v-if="allowed('
|
|
17
|
-
:icon="
|
|
18
|
-
title="
|
|
19
|
-
@run="
|
|
20
|
-
|
|
21
|
-
<u-action
|
|
22
|
-
v-if="allowed('create', row)"
|
|
23
|
-
:icon="InsertToNext"
|
|
24
|
-
title="在下方插入"
|
|
25
|
-
@run="handleInsertToNext(row)"
|
|
16
|
+
v-if="allowed('update', row)"
|
|
17
|
+
:icon="props.readonly ? View : EditPen"
|
|
18
|
+
:title="props.readonly ? '查看' : '编辑'"
|
|
19
|
+
@run="startEdit(row)"
|
|
20
|
+
v-bind="props.actionsProps?.update"
|
|
26
21
|
/>
|
|
27
22
|
<u-action
|
|
28
|
-
v-if="!!props.tree && allowed('createChild', row)"
|
|
23
|
+
v-if="!props.readonly && !!props.tree && allowed('createChild', row)"
|
|
29
24
|
:icon="AddChild"
|
|
30
25
|
title="添加子级"
|
|
31
26
|
@run="handleInsertChild(row)"
|
|
@@ -38,6 +33,18 @@
|
|
|
38
33
|
@run="handleDelete(row)"
|
|
39
34
|
v-bind="props.actionsProps?.delete"
|
|
40
35
|
/>
|
|
36
|
+
<u-action
|
|
37
|
+
v-if="!props.readonly && allowed('create', row)"
|
|
38
|
+
:icon="InsertToPrev"
|
|
39
|
+
title="在上方插入"
|
|
40
|
+
@run="handleInsertToPrev(row)"
|
|
41
|
+
/>
|
|
42
|
+
<u-action
|
|
43
|
+
v-if="!props.readonly && allowed('create', row)"
|
|
44
|
+
:icon="InsertToNext"
|
|
45
|
+
title="在下方插入"
|
|
46
|
+
@run="handleInsertToNext(row)"
|
|
47
|
+
/>
|
|
41
48
|
</u-action-group>
|
|
42
49
|
</template>
|
|
43
50
|
|
|
@@ -61,7 +68,15 @@
|
|
|
61
68
|
|
|
62
69
|
<script setup lang="ts">
|
|
63
70
|
import { o } from '@cat-kit/core'
|
|
64
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
AddChild,
|
|
73
|
+
Delete,
|
|
74
|
+
EditPen,
|
|
75
|
+
InsertToNext,
|
|
76
|
+
InsertToPrev,
|
|
77
|
+
Plus,
|
|
78
|
+
View
|
|
79
|
+
} from '@veltra/icons/normal'
|
|
65
80
|
import { computed, inject, type Slots } from 'vue'
|
|
66
81
|
|
|
67
82
|
import type { BatchEditFeature, TableRow } from '../../types'
|
|
@@ -81,6 +96,8 @@ const {
|
|
|
81
96
|
emit,
|
|
82
97
|
staticFeatures,
|
|
83
98
|
dynamicFeatures,
|
|
99
|
+
startEdit,
|
|
100
|
+
handleClose,
|
|
84
101
|
handleCreate,
|
|
85
102
|
handleDelete,
|
|
86
103
|
handleInsertToNext,
|
|
@@ -97,9 +114,11 @@ const tableProps = computed(() => {
|
|
|
97
114
|
'deleteMethod',
|
|
98
115
|
'saveMethod',
|
|
99
116
|
'features',
|
|
100
|
-
'mode',
|
|
101
117
|
'beforeCreate',
|
|
102
|
-
'formMode'
|
|
118
|
+
'formMode',
|
|
119
|
+
'quickEdit',
|
|
120
|
+
'labelWidth',
|
|
121
|
+
'actionsProps'
|
|
103
122
|
])
|
|
104
123
|
})
|
|
105
124
|
|
|
@@ -110,7 +129,11 @@ const allowed = (feature: BatchEditFeature, row?: TableRow) => {
|
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
const columns = computed(() => {
|
|
113
|
-
|
|
132
|
+
// 只读模式的操作列只保留「查看」入口
|
|
133
|
+
const related: BatchEditFeature[] = props.readonly
|
|
134
|
+
? ['update']
|
|
135
|
+
: ['create', 'delete', 'createChild', 'update']
|
|
136
|
+
if (hasNot(related)) return props.columns
|
|
114
137
|
|
|
115
138
|
return (props.columns ?? []).concat({
|
|
116
139
|
name: '操作',
|
|
@@ -122,16 +145,24 @@ const columns = computed(() => {
|
|
|
122
145
|
})
|
|
123
146
|
})
|
|
124
147
|
|
|
125
|
-
function handleUpdateCurrentRow(row?: TableRow) {
|
|
126
|
-
if (allowed('update', row)) {
|
|
127
|
-
state.row = row
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
148
|
function canDelete(row: TableRow) {
|
|
132
149
|
return !props.readonly && allowed('delete', row)
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 点击行打开编辑/查看表单;再次点击当前行取消选中并关闭表单。
|
|
154
|
+
* update 特性关闭时点击行不响应
|
|
155
|
+
*/
|
|
156
|
+
function handleUpdateCurrentRow(row?: TableRow) {
|
|
157
|
+
if (!allowed('update', row)) return
|
|
158
|
+
|
|
159
|
+
if (row) {
|
|
160
|
+
startEdit(row)
|
|
161
|
+
} else {
|
|
162
|
+
handleClose()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
const showCreateBtn = computed(() => {
|
|
136
167
|
if (props.readonly) return false
|
|
137
168
|
|
|
@@ -64,7 +64,7 @@ const cls = bem('batch-edit')
|
|
|
64
64
|
|
|
65
65
|
const formRef = useTemplateRef<FormExposed>('form')
|
|
66
66
|
|
|
67
|
-
const { state, resetState, syncing } = useEditState({ props, formRef })
|
|
67
|
+
const { state, resetState, startEdit, syncing } = useEditState({ props, formRef })
|
|
68
68
|
|
|
69
69
|
const { staticFeatures, dynamicFeatures } = useFeatures({ props })
|
|
70
70
|
|
|
@@ -91,6 +91,7 @@ provide(BatchEditDIKey, {
|
|
|
91
91
|
dynamicFeatures,
|
|
92
92
|
focused,
|
|
93
93
|
syncing,
|
|
94
|
+
startEdit,
|
|
94
95
|
...handlers
|
|
95
96
|
})
|
|
96
97
|
</script>
|
|
@@ -24,5 +24,7 @@ export const BatchEditDIKey: InjectionKey<
|
|
|
24
24
|
focused: ShallowRef<boolean>
|
|
25
25
|
/** 是否正在以编程方式重置/回显表单,此期间禁止 quick-edit 回写行数据 */
|
|
26
26
|
syncing: ShallowRef<boolean>
|
|
27
|
+
/** 开始编辑/查看一行(点击行或操作列「编辑」按钮触发) */
|
|
28
|
+
startEdit: (row: TableRow) => void
|
|
27
29
|
} & EditReturned
|
|
28
30
|
> = Symbol('BatchEditDIKey')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copy, o } from '@cat-kit/core'
|
|
2
2
|
import { nextTick, shallowReactive, shallowRef, watch, type ShallowRef } from 'vue'
|
|
3
3
|
|
|
4
|
-
import type { BatchEditProps, BatchEditStates, FormExposed } from '../../types'
|
|
4
|
+
import type { BatchEditProps, BatchEditStates, FormExposed, TableRow } from '../../types'
|
|
5
5
|
|
|
6
6
|
interface Options {
|
|
7
7
|
props: BatchEditProps
|
|
@@ -56,10 +56,6 @@ export function useEditState(options: Options) {
|
|
|
56
56
|
function resetState() {
|
|
57
57
|
resetModel()
|
|
58
58
|
|
|
59
|
-
if (state.row) {
|
|
60
|
-
state.row.isCurrent = false
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
Object.keys(state).forEach((key) => {
|
|
64
60
|
delete state[key]
|
|
65
61
|
})
|
|
@@ -67,33 +63,30 @@ export function useEditState(options: Options) {
|
|
|
67
63
|
Object.assign(state, defaultState)
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
o(props.model).deepExtend(row.data)
|
|
83
|
-
}
|
|
84
|
-
state.formActionType = props.readonly ? 'view' : 'update'
|
|
85
|
-
state.formVisible = true
|
|
86
|
-
state.depth = row.depth
|
|
87
|
-
state.indexPath = [...row.indexes]
|
|
88
|
-
} else {
|
|
89
|
-
resetState()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
nextTick(() => {
|
|
93
|
-
syncing.value = false
|
|
94
|
-
})
|
|
66
|
+
/**
|
|
67
|
+
* 开始编辑/查看一行:先重置回 model 初始快照,再回显行数据。
|
|
68
|
+
* 同一 tick 内的连续写入会被表单字段 watcher 合并,
|
|
69
|
+
* 重置产生的默认值不会触发 field:update 回写行数据
|
|
70
|
+
*/
|
|
71
|
+
function startEdit(row: TableRow) {
|
|
72
|
+
syncing.value = true
|
|
73
|
+
|
|
74
|
+
resetModel()
|
|
75
|
+
|
|
76
|
+
if (props.model) {
|
|
77
|
+
o(props.model).deepExtend(row.data)
|
|
95
78
|
}
|
|
96
|
-
)
|
|
97
79
|
|
|
98
|
-
|
|
80
|
+
state.row = row
|
|
81
|
+
state.formActionType = props.readonly ? 'view' : 'update'
|
|
82
|
+
state.formVisible = true
|
|
83
|
+
state.depth = row.depth
|
|
84
|
+
state.indexPath = [...row.indexes]
|
|
85
|
+
|
|
86
|
+
nextTick(() => {
|
|
87
|
+
syncing.value = false
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { state, resetState, startEdit, syncing }
|
|
99
92
|
}
|
|
@@ -135,6 +135,8 @@ export function useHandlers(options: Options): EditReturned {
|
|
|
135
135
|
|
|
136
136
|
function runWithLoading<Arg extends any[]>(fn: (...args: Arg) => Promise<void> | void) {
|
|
137
137
|
return async (...args: Arg) => {
|
|
138
|
+
// 重入保护:进行中(快捷键/按钮并发触发)直接忽略
|
|
139
|
+
if (state.loading) return
|
|
138
140
|
state.loading = true
|
|
139
141
|
if (state.row) {
|
|
140
142
|
state.row.operating = true
|
|
@@ -81,7 +81,8 @@ $root-name: dialog;
|
|
|
81
81
|
flex-grow: 1;
|
|
82
82
|
|
|
83
83
|
// padding 加在滚动内容而非 body 本身:u-scroll 的 overflow 会裁切子元素 box-shadow
|
|
84
|
-
|
|
84
|
+
// 用子代链限定 body 自己的滚动内容,避免命中弹框内嵌套的 u-scroll
|
|
85
|
+
> #{fn.bem(scroll, container)} > #{fn.bem(scroll, content)} {
|
|
85
86
|
padding: 8px;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
@@ -14,7 +14,6 @@ const cls = {
|
|
|
14
14
|
} as any
|
|
15
15
|
|
|
16
16
|
function makeChip(overrides: Partial<{ value: string; label: string; type?: string }> = {}) {
|
|
17
|
-
const onRemove = vi.fn<(el: HTMLElement) => void>()
|
|
18
17
|
const onReselect = vi.fn<(el: HTMLElement) => void>()
|
|
19
18
|
const segment = {
|
|
20
19
|
kind: 'var' as const,
|
|
@@ -22,8 +21,8 @@ function makeChip(overrides: Partial<{ value: string; label: string; type?: stri
|
|
|
22
21
|
label: overrides.label ?? '用户姓名',
|
|
23
22
|
...(overrides.type ? { type: overrides.type } : {})
|
|
24
23
|
}
|
|
25
|
-
const el = createChip(segment, { cls,
|
|
26
|
-
return { el,
|
|
24
|
+
const el = createChip(segment, { cls, onReselect })
|
|
25
|
+
return { el, onReselect, segment }
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
describe('createChip', () => {
|
|
@@ -54,21 +53,11 @@ describe('createChip', () => {
|
|
|
54
53
|
})
|
|
55
54
|
|
|
56
55
|
it('mousedown on chip body triggers onReselect', () => {
|
|
57
|
-
const { el, onReselect
|
|
56
|
+
const { el, onReselect } = makeChip()
|
|
58
57
|
const label = el.querySelector(`.${cls.e('chip-label')}`) as HTMLElement
|
|
59
58
|
label.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
60
59
|
expect(onReselect).toHaveBeenCalledTimes(1)
|
|
61
60
|
expect(onReselect).toHaveBeenCalledWith(el)
|
|
62
|
-
expect(onRemove).not.toHaveBeenCalled()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('mousedown on close (×) triggers onRemove and not onReselect', () => {
|
|
66
|
-
const { el, onReselect, onRemove } = makeChip()
|
|
67
|
-
const close = el.querySelector(`[${ChipAttrs.close}]`) as HTMLElement
|
|
68
|
-
close.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
69
|
-
expect(onRemove).toHaveBeenCalledTimes(1)
|
|
70
|
-
expect(onRemove).toHaveBeenCalledWith(el)
|
|
71
|
-
expect(onReselect).not.toHaveBeenCalled()
|
|
72
61
|
})
|
|
73
62
|
|
|
74
63
|
it('readChipValue returns the bound value', () => {
|