@veltra/desktop 1.7.16 → 1.7.18

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