@veltra/desktop 1.7.15 → 1.7.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.7.15",
3
+ "version": "1.7.17",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -58,8 +58,9 @@ const classList = computed(() => {
58
58
  })
59
59
 
60
60
  const ripple = computed(() => {
61
- if (props.disabled || props.loading || props.text) return false
62
- if (props.plain && props.type) {
61
+ if (props.disabled || props.loading) return false
62
+ // plain / text 变体无底色,波纹用类型色才能看清
63
+ if ((props.plain || props.text) && props.type) {
63
64
  return bem.is(`ripple-${props.type}`)
64
65
  }
65
66
  return true
@@ -52,7 +52,17 @@ $root-name: form-item;
52
52
  #{fn.bem(multi-tree-select)},
53
53
  #{fn.bem(auto-complete)},
54
54
  #{fn.bem(textarea--more)} {
55
- border-color: fn.use-var(color, danger);
55
+ // hover 的 primary 描边与错误态同权重,按源序分胜负:错误态显式压住,避免悬浮时描边回蓝
56
+ &,
57
+ &:hover,
58
+ &.is-focus {
59
+ border-color: fn.use-var(color, danger);
60
+ }
61
+
62
+ // 聚焦光晕与描边同色,错误态转红
63
+ &.is-focus {
64
+ box-shadow: fn.use-var(focus-ring, danger);
65
+ }
56
66
  }
57
67
  }
58
68
 
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h, nextTick, ref, type Ref } from 'vue'
3
+
4
+ import UMultiTreeSelect from '../multi-tree-select.vue'
5
+
6
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7
+
8
+ const treeData = [
9
+ {
10
+ label: '亚洲',
11
+ value: 'asia',
12
+ children: [
13
+ { label: '中国', value: 'cn' },
14
+ { label: '日本', value: 'jp' }
15
+ ]
16
+ },
17
+ {
18
+ label: '欧洲',
19
+ value: 'europe',
20
+ children: [
21
+ { label: '英国', value: 'uk' },
22
+ { label: '法国', value: 'fr' }
23
+ ]
24
+ }
25
+ ]
26
+
27
+ function mountMultiTreeSelect(props: Record<string, unknown> = {}) {
28
+ const host = document.createElement('div')
29
+ document.body.appendChild(host)
30
+
31
+ const model = ref(props.modelValue ?? []) as Ref<(string | number)[]>
32
+ const data = ref(props.data ?? treeData)
33
+
34
+ const app = createApp({
35
+ render() {
36
+ return h(UMultiTreeSelect, {
37
+ ...props,
38
+ modelValue: model.value,
39
+ data: data.value,
40
+ expandAll: true,
41
+ 'onUpdate:modelValue': (value: (string | number)[]) => {
42
+ model.value = value
43
+ }
44
+ })
45
+ }
46
+ })
47
+
48
+ app.mount(host)
49
+
50
+ return {
51
+ host,
52
+ model,
53
+ data,
54
+ unmount() {
55
+ app.unmount()
56
+ host.remove()
57
+ }
58
+ }
59
+ }
60
+
61
+ async function openDropdown(host: HTMLElement) {
62
+ const trigger = host.querySelector('.u-multi-tree-select') as HTMLElement
63
+ trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
64
+ for (let attempt = 0; attempt < 20; attempt++) {
65
+ await nextTick()
66
+ await sleep(25)
67
+ if (queryNodeContents().length > 0) return
68
+ }
69
+ }
70
+
71
+ function queryNodeContents() {
72
+ return [...document.body.querySelectorAll<HTMLElement>('.u-tree__node-content')]
73
+ }
74
+
75
+ function queryCheckboxes() {
76
+ return [...document.body.querySelectorAll<HTMLInputElement>('.u-tree .u-checkbox__native')]
77
+ }
78
+
79
+ async function clickNodeByLabel(label: string, ctrlKey = false) {
80
+ const node = queryNodeContents().find((el) => el.textContent?.includes(label))
81
+ expect(node, `tree node "${label}" not found`).toBeTruthy()
82
+ node!.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey }))
83
+ await nextTick()
84
+ await sleep(50)
85
+ }
86
+
87
+ describe('UMultiTreeSelect check-strictly and ctrl-key behavior', () => {
88
+ it('cascades check and uncheck to children by default (checkStrictly = false)', async () => {
89
+ const { host, model, unmount } = mountMultiTreeSelect()
90
+
91
+ try {
92
+ await openDropdown(host)
93
+ await clickNodeByLabel('亚洲')
94
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
95
+
96
+ // 取消勾选「亚洲」时级联取消子节点
97
+ await clickNodeByLabel('亚洲')
98
+ expect(model.value).toEqual([])
99
+ } finally {
100
+ unmount()
101
+ }
102
+ })
103
+
104
+ it('independently checks node when pressing ctrl in default mode', async () => {
105
+ const { host, model, unmount } = mountMultiTreeSelect()
106
+
107
+ try {
108
+ await openDropdown(host)
109
+ // 按住 ctrl 点击「亚洲」,仅勾选「亚洲」自身
110
+ await clickNodeByLabel('亚洲', true)
111
+ expect(model.value).toEqual(['asia'])
112
+
113
+ // 按住 ctrl 再次点击「亚洲」,仅取消「亚洲」自身
114
+ await clickNodeByLabel('亚洲', true)
115
+ expect(model.value).toEqual([])
116
+ } finally {
117
+ unmount()
118
+ }
119
+ })
120
+
121
+ it('cascades children on check, but only unchecks self on uncheck in checkStrictly mode', async () => {
122
+ const { host, model, unmount } = mountMultiTreeSelect({ 'check-strictly': true })
123
+
124
+ try {
125
+ await openDropdown(host)
126
+ // 勾选父节点「亚洲」-> 把所有子节点都勾上
127
+ await clickNodeByLabel('亚洲')
128
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
129
+
130
+ // 取消勾选父节点「亚洲」-> 只取消自己,子节点保持勾选
131
+ await clickNodeByLabel('亚洲')
132
+ expect(model.value).toEqual(['cn', 'jp'])
133
+ } finally {
134
+ unmount()
135
+ }
136
+ })
137
+
138
+ it('does not show indeterminate on parent when only child is checked in checkStrictly mode', async () => {
139
+ const { host, unmount } = mountMultiTreeSelect({ checkStrictly: true })
140
+
141
+ try {
142
+ await openDropdown(host)
143
+ await clickNodeByLabel('中国')
144
+
145
+ // 父节点「亚洲」不应有半选状态
146
+ const asiaNodeContent = queryNodeContents().find((el) => el.textContent?.includes('亚洲'))
147
+ const asiaCheckbox = asiaNodeContent?.closest('.u-tree__node')?.querySelector('.u-checkbox')
148
+ expect(asiaCheckbox?.classList.contains('is-indeterminate')).toBe(false)
149
+ expect(asiaCheckbox?.querySelector('.u-checkbox--indeterminate')).toBeNull()
150
+ } finally {
151
+ unmount()
152
+ }
153
+ })
154
+
155
+ it('selects all nodes when clicking select all in checkStrictly mode', async () => {
156
+ const { host, model, unmount } = mountMultiTreeSelect({ checkStrictly: true })
157
+
158
+ try {
159
+ await openDropdown(host)
160
+ // 点击全选 checkbox
161
+ const allCheckboxInput = document.body.querySelector(
162
+ '.u-multi-tree-select__content-header .u-checkbox__native'
163
+ ) as HTMLInputElement
164
+ expect(allCheckboxInput).toBeTruthy()
165
+ allCheckboxInput.click()
166
+ await nextTick()
167
+ await sleep(50)
168
+
169
+ expect(model.value).toEqual(['asia', 'cn', 'jp', 'europe', 'uk', 'fr'])
170
+
171
+ // 再次点击取消全选
172
+ allCheckboxInput.click()
173
+ await nextTick()
174
+ await sleep(50)
175
+ expect(model.value).toEqual([])
176
+ } finally {
177
+ unmount()
178
+ }
179
+ })
180
+
181
+ it('echoes only parent node when initialized with parent id in checkStrictly mode', async () => {
182
+ const { host, model, unmount } = mountMultiTreeSelect({
183
+ checkStrictly: true,
184
+ modelValue: ['asia']
185
+ })
186
+
187
+ try {
188
+ await openDropdown(host)
189
+ expect(model.value).toEqual(['asia'])
190
+
191
+ // tags 只有「亚洲」
192
+ const tags = host.querySelectorAll('.u-tag')
193
+ expect(tags.length).toBe(1)
194
+ expect(tags[0].textContent).toContain('亚洲')
195
+ } finally {
196
+ unmount()
197
+ }
198
+ })
199
+
200
+ it('directly clicking checkbox cascades on check and only unchecks self on uncheck in checkStrictly mode', async () => {
201
+ const { host, model, unmount } = mountMultiTreeSelect({ checkStrictly: true })
202
+
203
+ try {
204
+ await openDropdown(host)
205
+ const checkboxes = queryCheckboxes()
206
+ expect(checkboxes.length).toBeGreaterThan(0)
207
+ // 点击第一个 checkbox(亚洲)-> 勾选所有子节点
208
+ checkboxes[0].click()
209
+ await nextTick()
210
+ await sleep(50)
211
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
212
+
213
+ // 再次点击第一个 checkbox(亚洲)-> 仅取消亚洲自身
214
+ checkboxes[0].click()
215
+ await nextTick()
216
+ await sleep(50)
217
+ expect(model.value).toEqual(['cn', 'jp'])
218
+ } finally {
219
+ unmount()
220
+ }
221
+ })
222
+ })
@@ -131,7 +131,9 @@ const props = withDefaults(defineProps<MultiTreeSelectProps>(), {
131
131
  readonly: undefined,
132
132
  filterable: false,
133
133
  visibilityLimit: 3,
134
- minWidth: '280px'
134
+ minWidth: '280px',
135
+ checkStrictly: false,
136
+ checkOnClickNode: true
135
137
  })
136
138
 
137
139
  const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
@@ -266,8 +266,10 @@ describe('UTableEditor 按列校验', () => {
266
266
  const [row0] = getDataRows()
267
267
  const headerText = () =>
268
268
  findHeader(host, '姓名')?.querySelector('.u-table-editor__header-text')
269
+ const nameCellError = () =>
270
+ row0.querySelector('[data-test="name-input"]')?.parentElement?.className
269
271
 
270
- // 输入过程(input 事件)不触发校验:清空后表头无错误指示
272
+ // 输入过程(input 事件)不触发校验:清空后表头无错误指示,单元格无错误标记
271
273
  const input = row0.querySelector('input')!
272
274
  input.value = ''
273
275
  input.dispatchEvent(new Event('input', { bubbles: true }))
@@ -275,14 +277,15 @@ describe('UTableEditor 按列校验', () => {
275
277
  await nextTick()
276
278
  expect(headerText()?.className).not.toContain('is-error')
277
279
  expect(findHeader(host, '姓名')?.querySelector('.u-table-editor__header-icon')).toBeFalsy()
280
+ expect(nameCellError()).not.toContain('is-error')
278
281
 
279
- // change 事件(如失焦提交)触发校验:该列表头出现错误指示,单元格自身无错误样式
282
+ // change 事件(如失焦提交)触发校验:该列表头出现错误指示,单元格挂 is-error
280
283
  input.dispatchEvent(new Event('change', { bubbles: true }))
281
284
  await nextTick()
282
285
  await nextTick()
283
286
  expect(headerText()?.className).toContain('is-error')
284
287
  expect(findHeader(host, '姓名')?.querySelector('.u-table-editor__header-icon')).toBeTruthy()
285
- expect(row0.querySelector('.u-table-editor__cell-error')).toBeFalsy()
288
+ expect(nameCellError()).toContain('is-error')
286
289
 
287
290
  // 填回合法值并 change,错误消失
288
291
  input.value = 'Alice'
@@ -291,12 +294,13 @@ describe('UTableEditor 按列校验', () => {
291
294
  await nextTick()
292
295
  await nextTick()
293
296
  expect(headerText()?.className).not.toContain('is-error')
297
+ expect(nameCellError()).not.toContain('is-error')
294
298
  } finally {
295
299
  unmount()
296
300
  }
297
301
  })
298
302
 
299
- it('validate 懒校验:某行存在错误即停止校验其后的行', async () => {
303
+ it('validate 全量校验:某行存在错误仍继续校验其后的行', async () => {
300
304
  const { host, getEditor, unmount } = mountTableEditor(
301
305
  {},
302
306
  [
@@ -312,20 +316,21 @@ describe('UTableEditor 按列校验', () => {
312
316
  await expect(getEditor().validate()).resolves.toBe(false)
313
317
  await nextTick()
314
318
 
315
- // 第 2 行 name 校验失败:该列表头标红并出现感叹号图标
319
+ // 第 2、3 行 name 均校验失败:该列表头标红并出现感叹号图标,失败单元格全部挂 is-error
316
320
  const nameHeader = findHeader(host, '姓名')!
317
321
  expect(nameHeader.querySelector('.u-table-editor__header-text')?.className).toContain(
318
322
  'is-error'
319
323
  )
320
324
  const icon = nameHeader.querySelector('.u-table-editor__header-icon')!
321
325
  expect(icon).toBeTruthy()
326
+ expect(host.querySelectorAll('td .is-error')).toHaveLength(2)
322
327
 
323
- // 悬停图标查看气泡:仅含第 2 行明细,第 3 行未校验不出现
328
+ // 悬停图标查看气泡:包含第 2、3 行明细
324
329
  icon.dispatchEvent(new MouseEvent('mouseenter'))
325
330
  await nextTick()
326
331
  const bubble = document.querySelector('.u-tip__content')
327
332
  expect(bubble?.textContent).toContain('第 2 行')
328
- expect(bubble?.textContent).not.toContain('第 3 行')
333
+ expect(bubble?.textContent).toContain('第 3 行')
329
334
  // 等弹层异步定位计算完成,避免与卸载竞争产生未处理拒绝
330
335
  await new Promise((resolve) => setTimeout(resolve))
331
336
  } finally {
@@ -423,14 +428,17 @@ describe('UTableEditor 按列校验', () => {
423
428
  await nextTick()
424
429
  await expect(getEditor().validate()).resolves.toBe(false)
425
430
  await nextTick()
426
- // validate 后失败列的表头直接出现错误指示
431
+ // validate 后失败列的表头直接出现错误指示,失败单元格挂 is-error
427
432
  const headerText = () =>
428
433
  findHeader(host, '姓名')?.querySelector('.u-table-editor__header-text')
429
434
  expect(headerText()?.className).toContain('is-error')
435
+ expect(host.querySelector('td .is-error')).toBeTruthy()
430
436
 
431
437
  setModel([{ name: 'Alice', age: 18, city: '杭州' }])
432
438
  await nextTick()
433
439
  await expect(getEditor().validate()).resolves.toBe(true)
440
+ await nextTick()
441
+ expect(host.querySelector('td .is-error')).toBeFalsy()
434
442
  } finally {
435
443
  unmount()
436
444
  }
@@ -8,23 +8,37 @@ $root-name: table-editor;
8
8
  @include m.b($root-name) {
9
9
  @include m.bem(table, body) {
10
10
  @include m.bem(table, cell) {
11
- padding: 2px;
12
-
13
- // 行高锚定常驻输入控件的高度:td 的 height 即行的最小高度
14
- // 4px = 单元格上下 padding;1px = u-table 行分隔线(border-bottom,border-box 计入高度)
15
- // 注意:主题 token 只有带尺寸后缀的变量(--u-form-component-height-default 等),
16
- // 这里锚定 default 尺寸,与单元格内默认尺寸的输入控件对齐
17
- height: calc(#{fn.use-var(form-component-height, default)} + 4px + 1px);
18
-
19
- // 单元格内行内级控件(inline-flex 输入框)参与基线对齐
20
- // 会在行盒下方留出 descender 空隙,顶对齐抵消,保证行高恰好为锚定值
21
- & > [class^='u-'],
22
- [class^='u-input'] {
11
+ padding: 4px;
12
+
13
+ // 单元格内行内级控件(inline-flex 输入框)参与基线对齐,
14
+ // 会在行盒下方留出 descender 空隙,顶对齐抵消
15
+ [class^='u-'] {
23
16
  vertical-align: top;
24
17
  }
25
18
  }
26
19
  }
27
20
 
21
+ // 校验未通过的单元格:与表单项错误态一致,内嵌控件描红边(含 hover)、聚焦光晕转红,纯文本标红
22
+ .is-error {
23
+ color: fn.use-var(color, danger);
24
+
25
+ #{fn.bem(input)},
26
+ #{fn.bem(multi-select)},
27
+ #{fn.bem(multi-tree-select)},
28
+ #{fn.bem(auto-complete)},
29
+ #{fn.bem(textarea--more)} {
30
+ &,
31
+ &:hover,
32
+ &.is-focus {
33
+ border-color: fn.use-var(color, danger);
34
+ }
35
+
36
+ &.is-focus {
37
+ box-shadow: fn.use-var(focus-ring, danger);
38
+ }
39
+ }
40
+ }
41
+
28
42
  // 操作列按钮容器:fixed right 列由 u-table 独立分层渲染,此处只做按钮排布
29
43
  @include m.bem($root-name, operations) {
30
44
  display: flex;
@@ -182,6 +182,8 @@ function setCellError(rowData: Record<string, any>, key: string, message: string
182
182
 
183
183
  if (!message) {
184
184
  if (!rowErrors?.delete(key)) return
185
+ // 行错误清空后移除条目,保证「size > 0 即存在错误」可作整体判定
186
+ if (!rowErrors.size) cellErrors.value.delete(row)
185
187
  } else {
186
188
  if (rowErrors?.get(key) === message) return
187
189
  if (rowErrors) rowErrors.set(key, message)
@@ -221,18 +223,19 @@ async function validateCell(
221
223
  }
222
224
 
223
225
  /**
224
- * 整表校验:自上而下逐行校验,某行存在未通过项即停止校验其后的行(懒校验)。
226
+ * 整表校验:并行校验所有行的规则列(不懒校验),未通过的单元格全部标出。
225
227
  * 全部通过 resolve true,否则 resolve false。
226
228
  */
227
229
  async function validate() {
228
230
  const ruleColumns = columns.value.filter((column) => column.rules)
229
231
 
230
- for (const row of modelValue.value) {
231
- await Promise.all(ruleColumns.map((column) => validateCell(row, column.key, column.rules)))
232
- if (cellErrors.value.get(errorRowKey(row))?.size) return false
233
- }
232
+ await Promise.all(
233
+ modelValue.value.map((row) =>
234
+ Promise.all(ruleColumns.map((column) => validateCell(row, column.key, column.rules)))
235
+ )
236
+ )
234
237
 
235
- return true
238
+ return cellErrors.value.size === 0
236
239
  }
237
240
 
238
241
  // 行删除(或整体替换)后,同步清理已不存在的行数据上的错误
@@ -270,14 +273,32 @@ const columnErrors = computed(() => {
270
273
 
271
274
  // --- 单元格渲染 ---
272
275
 
273
- /** 声明了 `#column:key` 插槽的列调用编辑插槽(注入 model),未声明的渲染字段原始值 */
276
+ /**
277
+ * 声明了 `#column:key` 插槽的列调用编辑插槽(注入 model),未声明的渲染字段原始值。
278
+ * 配置了 rules 的列在渲染期间读取 cellErrors 建立依赖,错误出现/消失即时更新:
279
+ * 插槽内容常驻包一层容器、仅切换 is-error 类(与表单项错误态同类,不增删 DOM,
280
+ * 控件不因错误态变化重挂丢焦点);纯文本列仅在出错时包一层标红原始值。
281
+ */
274
282
  function editCell(column: TableEditorColumn, ctx: TableColumnRenderContext): RenderReturn {
275
283
  const editSlot = slots[`column:${column.key}`]
276
284
  // 内部列 key 带只读态后缀,u-table 按 key 取到的 val 失效,这里按原始列 key 重取
277
285
  const val = o(ctx.rowData).get(column.key)
278
- if (!editSlot) return val
286
+
287
+ if (!column.rules) {
288
+ if (!editSlot) return val
289
+ const scope = { ...ctx, val }
290
+ return editSlot({ ...scope, model: createCellModel(column, scope) })
291
+ }
292
+
293
+ const hasError = !!cellErrors.value.get(errorRowKey(ctx.rowData))?.has(column.key)
294
+ if (!editSlot) return hasError ? h('span', { class: bem.is('error') }, val) : val
295
+
279
296
  const scope = { ...ctx, val }
280
- return editSlot({ ...scope, model: createCellModel(column, scope) })
297
+ return h(
298
+ 'span',
299
+ { class: bem.is('error', hasError) },
300
+ editSlot({ ...scope, model: createCellModel(column, scope) })
301
+ )
281
302
  }
282
303
 
283
304
  /**
@@ -27,7 +27,7 @@
27
27
  v-if="treeProps.checkable"
28
28
  :class="cls.e('checkbox')"
29
29
  :model-value="node.checked"
30
- :indeterminate="node.indeterminate"
30
+ :indeterminate="!treeProps.checkStrictly && node.indeterminate"
31
31
  :disabled="node.disabled"
32
32
  @change="handleChange"
33
33
  @click.stop
@@ -102,7 +102,13 @@ const { filter } = useFilter({ forest, getFlattedNodes })
102
102
 
103
103
  const { handleSelect, selectedData } = useSelect({ props, emit, nodeDict, getFlattedNodes })
104
104
 
105
- const { checkedData, toggleCheck } = useCheck({ props, emit, nodeDict, getFlattedNodes })
105
+ const { checkedData, toggleCheck, checkAll } = useCheck({
106
+ props,
107
+ emit,
108
+ forest,
109
+ nodeDict,
110
+ getFlattedNodes
111
+ })
106
112
 
107
113
  const estimateSize = (): number => {
108
114
  // 基于 style.scss 中节点稳态高度推算的默认值;
@@ -196,11 +202,7 @@ defineExpose<_TreeExposed>({
196
202
  nodes,
197
203
  checkNode: toggleCheck,
198
204
  selectNode: handleSelect,
199
- checkAll(check: boolean) {
200
- forest.value.roots.forEach((node) => {
201
- toggleCheck(node, check)
202
- })
203
- },
205
+ checkAll,
204
206
  getSelected() {
205
207
  return selectedData
206
208
  },
@@ -1,3 +1,4 @@
1
+ import type { Forest } from '@cat-kit/core'
1
2
  import { dfs, o } from '@cat-kit/core'
2
3
  import { fieldKey } from '@veltra/utils'
3
4
  import { nextTick, watch, type ComputedRef } from 'vue'
@@ -8,6 +9,7 @@ import type { TreeNode } from './tree-node'
8
9
  interface Options {
9
10
  emit: TreeEmit
10
11
  props: TreeProps
12
+ forest: ComputedRef<Forest<Record<string, unknown>, any>>
11
13
  nodeDict: ComputedRef<Map<any, TreeNode>>
12
14
  getFlattedNodes: () => void
13
15
  }
@@ -15,6 +17,7 @@ interface Options {
15
17
  interface UseCheckReturned {
16
18
  checkedData: Set<Record<string, any>>
17
19
  toggleCheck: (node: TreeNode, check: boolean, ctrlKey?: boolean) => void
20
+ checkAll: (check: boolean) => void
18
21
  }
19
22
 
20
23
  /**
@@ -33,7 +36,7 @@ function expandAncestors(node: TreeNode): boolean {
33
36
  }
34
37
 
35
38
  export function useCheck(options: Options): UseCheckReturned {
36
- const { emit, props, nodeDict, getFlattedNodes } = options
39
+ const { emit, props, forest, nodeDict, getFlattedNodes } = options
37
40
 
38
41
  const checkedData = new Set<Record<string, any>>()
39
42
 
@@ -48,7 +51,7 @@ export function useCheck(options: Options): UseCheckReturned {
48
51
  function checkNode(node: TreeNode): void {
49
52
  if (node.checked) return
50
53
  node.checked = true
51
- if (node.parent) {
54
+ if (!props.checkStrictly && node.parent) {
52
55
  node.parent.childrenCheckCount++
53
56
  }
54
57
  checkedData.add(node.data)
@@ -57,7 +60,7 @@ export function useCheck(options: Options): UseCheckReturned {
57
60
  function uncheckNode(node: TreeNode): void {
58
61
  if (!node.checked) return
59
62
  node.checked = false
60
- if (node.parent) {
63
+ if (!props.checkStrictly && node.parent) {
61
64
  node.parent.childrenCheckCount--
62
65
  }
63
66
  checkedData.delete(node.data)
@@ -129,18 +132,19 @@ export function useCheck(options: Options): UseCheckReturned {
129
132
  const { checkStrictly } = props
130
133
 
131
134
  if (ctrlKey) {
132
- checkNode(node)
133
- } else {
134
- dfs(
135
- node as unknown as Record<string, unknown>,
136
- (n) => {
137
- const tn = n as unknown as TreeNode
138
- !tn.disabled && checkNode(tn)
139
- },
140
- props.childrenKey ?? 'children'
141
- )
135
+ !node.disabled && checkNode(node)
136
+ return
142
137
  }
143
138
 
139
+ dfs(
140
+ node as unknown as Record<string, unknown>,
141
+ (n) => {
142
+ const tn = n as unknown as TreeNode
143
+ !tn.disabled && checkNode(tn)
144
+ },
145
+ props.childrenKey ?? 'children'
146
+ )
147
+
144
148
  // 非严格选择时还需要更新祖先节点,
145
149
  // 一旦子节点全部选中,父节点也要设置为选中状态
146
150
  if (!checkStrictly) {
@@ -158,28 +162,28 @@ export function useCheck(options: Options): UseCheckReturned {
158
162
 
159
163
  function handleUncheck(node: TreeNode, ctrlKey?: boolean) {
160
164
  const { checkStrictly } = props
161
- if (ctrlKey) {
162
- uncheckNode(node)
163
- } else {
164
- dfs(
165
- node as unknown as Record<string, unknown>,
166
- (n) => {
167
- const tn = n as unknown as TreeNode
168
- !tn.disabled && uncheckNode(tn)
169
- },
170
- props.childrenKey ?? 'children'
171
- )
165
+
166
+ if (ctrlKey || checkStrictly) {
167
+ !node.disabled && uncheckNode(node)
168
+ return
172
169
  }
173
170
 
171
+ dfs(
172
+ node as unknown as Record<string, unknown>,
173
+ (n) => {
174
+ const tn = n as unknown as TreeNode
175
+ !tn.disabled && uncheckNode(tn)
176
+ },
177
+ props.childrenKey ?? 'children'
178
+ )
179
+
174
180
  // 非严格模式下,取消选中时,需要更新父节点
175
- if (!checkStrictly) {
176
- node.bubbleSet((node) => {
177
- const { parent } = node
178
- if (parent) {
179
- uncheckNode(parent)
180
- }
181
- })
182
- }
181
+ node.bubbleSet((node) => {
182
+ const { parent } = node
183
+ if (parent) {
184
+ uncheckNode(parent)
185
+ }
186
+ })
183
187
  }
184
188
 
185
189
  function toggleCheck(node: TreeNode, check: boolean, ctrlKey?: boolean) {
@@ -202,5 +206,31 @@ export function useCheck(options: Options): UseCheckReturned {
202
206
  })
203
207
  }
204
208
 
205
- return { checkedData, toggleCheck }
209
+ function checkAll(check: boolean) {
210
+ checkedByEvent = true
211
+
212
+ forest.value.dfs((node) => {
213
+ const tn = node as unknown as TreeNode
214
+ if (!tn.disabled) {
215
+ check ? checkNode(tn) : uncheckNode(tn)
216
+ }
217
+ })
218
+
219
+ const checkedArr = Array.from(checkedData)
220
+
221
+ const valueKey = fieldKey(props.valueKey, 'value')
222
+ const nextValues = checkedArr.map((item) => o(item).get(valueKey) as unknown)
223
+
224
+ lastCheckedSet = new Set(nextValues)
225
+
226
+ emit('update:checked', nextValues as any[], checkedArr)
227
+
228
+ getFlattedNodes()
229
+
230
+ nextTick(() => {
231
+ checkedByEvent = false
232
+ })
233
+ }
234
+
235
+ return { checkedData, toggleCheck, checkAll }
206
236
  }