@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/dist/components/button/button.js +2 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/form-item/style2.css +17 -2
- package/dist/components/multi-tree-select/multi-tree-select.js +8 -2
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/table-editor/style2.css +27 -4
- package/dist/components/table-editor/table-editor.js +25 -10
- package/dist/components/table-editor/table-editor.js.map +1 -1
- package/dist/components/tree/tree-node.js +1 -1
- package/dist/components/tree/tree-node.js.map +1 -1
- package/dist/components/tree/tree.js +3 -6
- package/dist/components/tree/tree.js.map +1 -1
- package/dist/components/tree/use-check.js +32 -9
- package/dist/components/tree/use-check.js.map +1 -1
- package/package.json +1 -1
- package/src/components/button/button.vue +3 -2
- package/src/components/form-item/style.scss +11 -1
- package/src/components/multi-tree-select/__test__/multi-tree-select.test.ts +222 -0
- package/src/components/multi-tree-select/multi-tree-select.vue +3 -1
- package/src/components/table-editor/__test__/table-editor.test.ts +16 -8
- package/src/components/table-editor/style.scss +26 -12
- package/src/components/table-editor/table-editor.vue +30 -9
- package/src/components/tree/tree-node.vue +1 -1
- package/src/components/tree/tree.vue +8 -6
- package/src/components/tree/use-check.ts +63 -33
package/package.json
CHANGED
|
@@ -58,8 +58,9 @@ const classList = computed(() => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
const ripple = computed(() => {
|
|
61
|
-
if (props.disabled || props.loading
|
|
62
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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).
|
|
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:
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
}
|