@veltra/desktop 1.7.10 → 1.7.12
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 +2 -6
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +0 -1
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/button/button.js +0 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/drawer/drawer.js +18 -10
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/drawer/style2.css +15 -0
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +12 -13
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/use-node-interceptor.js +31 -3
- package/dist/components/form/use-node-interceptor.js.map +1 -1
- package/dist/components/form-item/form-item.js.map +1 -1
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +14 -8
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/types/action.d.ts +6 -1
- package/dist/types/batch-edit.d.ts +0 -2
- package/dist/types/button.d.ts +5 -1
- package/dist/types/drawer.d.ts +6 -3
- package/dist/types/form.d.ts +0 -2
- package/dist/types/layout.d.ts +2 -0
- package/package.json +5 -5
- package/src/components/batch-edit/__test__/batch-edit.test.ts +0 -138
- package/src/components/batch-edit/batch-edit-form.vue +2 -16
- package/src/components/button/__test__/button.test.ts +48 -0
- package/src/components/button/button.vue +1 -8
- package/src/components/drawer/__test__/drawer.test.ts +118 -0
- package/src/components/drawer/drawer.vue +5 -2
- package/src/components/drawer/style.scss +15 -0
- package/src/components/form/__test__/form.test.ts +167 -31
- package/src/components/form/form.vue +10 -11
- package/src/components/form/use-node-interceptor.ts +36 -2
- package/src/components/form-item/__test__/form-item.test.ts +40 -10
- package/src/components/form-item/form-item.vue +14 -17
- package/src/types/action.ts +6 -1
- package/src/types/batch-edit.ts +0 -2
- package/src/types/button.ts +5 -1
- package/src/types/drawer.ts +6 -3
- package/src/types/form.ts +0 -2
- package/src/types/layout.ts +2 -0
|
@@ -1,41 +1,22 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createApp, h, nextTick, reactive } from 'vue'
|
|
2
|
+
import { createApp, defineComponent, h, nextTick, reactive, ref } from 'vue'
|
|
3
3
|
|
|
4
|
+
import { UInput } from '../../input'
|
|
4
5
|
import { USelect } from '../../select'
|
|
5
6
|
import { UForm } from '../index'
|
|
6
7
|
|
|
7
|
-
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8
|
-
|
|
9
8
|
const behaviorOptions = [
|
|
10
9
|
{ label: '事件', value: 'event' },
|
|
11
10
|
{ label: '接口', value: 'api' }
|
|
12
11
|
]
|
|
13
12
|
|
|
14
|
-
async function openSelect(host: HTMLElement) {
|
|
15
|
-
const input = host.querySelector('input')!
|
|
16
|
-
input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
17
|
-
await nextTick()
|
|
18
|
-
await nextTick()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function clickSelectOption(label: string) {
|
|
22
|
-
const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
|
|
23
|
-
(el) => el.textContent === label
|
|
24
|
-
)
|
|
25
|
-
expect(option, `选项「${label}」应存在`).toBeTruthy()
|
|
26
|
-
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
27
|
-
await nextTick()
|
|
28
|
-
await sleep(50)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
13
|
describe('UForm field events', () => {
|
|
32
|
-
it('field:update
|
|
14
|
+
it('编程写入触发 field:update,不依赖用户操作', async () => {
|
|
33
15
|
const host = document.createElement('div')
|
|
34
16
|
document.body.appendChild(host)
|
|
35
17
|
|
|
36
18
|
const model = reactive({ behavior: 'event' })
|
|
37
19
|
const updates: { field: string; value: unknown }[] = []
|
|
38
|
-
const changes: { field: string; args: unknown[] }[] = []
|
|
39
20
|
|
|
40
21
|
const app = createApp({
|
|
41
22
|
render() {
|
|
@@ -45,9 +26,6 @@ describe('UForm field events', () => {
|
|
|
45
26
|
model,
|
|
46
27
|
'onField:update': (field: string, value: unknown) => {
|
|
47
28
|
updates.push({ field, value })
|
|
48
|
-
},
|
|
49
|
-
'onField:change': (field: string, ...args: unknown[]) => {
|
|
50
|
-
changes.push({ field, args })
|
|
51
29
|
}
|
|
52
30
|
},
|
|
53
31
|
{
|
|
@@ -60,19 +38,177 @@ describe('UForm field events', () => {
|
|
|
60
38
|
|
|
61
39
|
app.mount(host)
|
|
62
40
|
updates.length = 0
|
|
63
|
-
changes.length = 0
|
|
64
41
|
|
|
65
42
|
model.behavior = 'api'
|
|
66
43
|
await nextTick()
|
|
67
44
|
|
|
68
45
|
expect(updates.some((item) => item.field === 'behavior' && item.value === 'api')).toBe(true)
|
|
69
|
-
expect(changes).toHaveLength(0)
|
|
70
46
|
|
|
71
|
-
|
|
72
|
-
|
|
47
|
+
app.unmount()
|
|
48
|
+
host.remove()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('父级重渲染时动态 props 同步到插槽控件', async () => {
|
|
52
|
+
const host = document.createElement('div')
|
|
53
|
+
document.body.appendChild(host)
|
|
54
|
+
|
|
55
|
+
const model = reactive({ username: '' })
|
|
56
|
+
const disabled = ref(false)
|
|
57
|
+
const placeholder = ref('第一次')
|
|
58
|
+
|
|
59
|
+
const app = createApp({
|
|
60
|
+
render() {
|
|
61
|
+
return h(
|
|
62
|
+
UForm,
|
|
63
|
+
{ model },
|
|
64
|
+
{
|
|
65
|
+
default: () =>
|
|
66
|
+
h(UInput, {
|
|
67
|
+
field: 'username',
|
|
68
|
+
label: '账号',
|
|
69
|
+
disabled: disabled.value,
|
|
70
|
+
placeholder: placeholder.value
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
app.mount(host)
|
|
78
|
+
|
|
79
|
+
const input = () => host.querySelector('input')!
|
|
80
|
+
expect(input().disabled).toBe(false)
|
|
81
|
+
expect(input().placeholder).toBe('第一次')
|
|
82
|
+
|
|
83
|
+
disabled.value = true
|
|
84
|
+
placeholder.value = '第二次'
|
|
85
|
+
await nextTick()
|
|
86
|
+
|
|
87
|
+
expect(input().disabled).toBe(true)
|
|
88
|
+
expect(input().placeholder).toBe('第二次')
|
|
89
|
+
|
|
90
|
+
app.unmount()
|
|
91
|
+
host.remove()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/** 用 setup 期间生成的 id 断言实例身份 */
|
|
96
|
+
let probeSeq = 0
|
|
97
|
+
const Probe = defineComponent({
|
|
98
|
+
name: 'Probe',
|
|
99
|
+
props: { field: String, label: String, modelValue: String },
|
|
100
|
+
setup(props) {
|
|
101
|
+
const id = `p${++probeSeq}`
|
|
102
|
+
return () =>
|
|
103
|
+
h('div', { class: 'probe', 'data-field': props.field, 'data-id': id }, [
|
|
104
|
+
h('span', { class: 'probe-value' }, props.modelValue ?? '')
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('UForm slot identity', () => {
|
|
110
|
+
it('中间字段卸载后不复用相邻控件实例', async () => {
|
|
111
|
+
probeSeq = 0
|
|
112
|
+
const host = document.createElement('div')
|
|
113
|
+
document.body.appendChild(host)
|
|
114
|
+
|
|
115
|
+
const model = reactive({ a: 'A', b: 'B', c: 'C' })
|
|
116
|
+
const showB = ref(true)
|
|
117
|
+
|
|
118
|
+
const app = createApp({
|
|
119
|
+
render() {
|
|
120
|
+
return h(
|
|
121
|
+
UForm,
|
|
122
|
+
{ model },
|
|
123
|
+
{
|
|
124
|
+
default: () => [
|
|
125
|
+
h(Probe, { field: 'a', label: 'A' }),
|
|
126
|
+
showB.value ? h(Probe, { field: 'b', label: 'B' }) : null,
|
|
127
|
+
h(Probe, { field: 'c', label: 'C' })
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
app.mount(host)
|
|
135
|
+
|
|
136
|
+
const idOf = (field: string) =>
|
|
137
|
+
host.querySelector(`[data-field="${field}"]`)?.getAttribute('data-id')
|
|
138
|
+
|
|
139
|
+
const cId = idOf('c')
|
|
140
|
+
expect(cId).toBe('p3')
|
|
141
|
+
|
|
142
|
+
showB.value = false
|
|
143
|
+
await nextTick()
|
|
144
|
+
|
|
145
|
+
expect(host.querySelector('[data-field="b"]')).toBeNull()
|
|
146
|
+
expect(idOf('c')).toBe(cId)
|
|
147
|
+
expect(host.querySelector('[data-field="c"] .probe-value')!.textContent).toBe('C')
|
|
148
|
+
|
|
149
|
+
app.unmount()
|
|
150
|
+
host.remove()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('重复 field 各自保有独立实例', async () => {
|
|
154
|
+
probeSeq = 0
|
|
155
|
+
const host = document.createElement('div')
|
|
156
|
+
document.body.appendChild(host)
|
|
157
|
+
|
|
158
|
+
const model = reactive({ name: '同名' })
|
|
159
|
+
|
|
160
|
+
const app = createApp({
|
|
161
|
+
render() {
|
|
162
|
+
return h(
|
|
163
|
+
UForm,
|
|
164
|
+
{ model },
|
|
165
|
+
{
|
|
166
|
+
default: () => [
|
|
167
|
+
h(Probe, { field: 'name', label: '左' }),
|
|
168
|
+
h(Probe, { field: 'name', label: '右' })
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
app.mount(host)
|
|
176
|
+
|
|
177
|
+
const probes = [...host.querySelectorAll('.probe')]
|
|
178
|
+
expect(probes).toHaveLength(2)
|
|
179
|
+
expect(probes[0]!.getAttribute('data-id')).toBe('p1')
|
|
180
|
+
expect(probes[1]!.getAttribute('data-id')).toBe('p2')
|
|
181
|
+
expect(probes[0]!.querySelector('.probe-value')!.textContent).toBe('同名')
|
|
182
|
+
expect(probes[1]!.querySelector('.probe-value')!.textContent).toBe('同名')
|
|
183
|
+
|
|
184
|
+
app.unmount()
|
|
185
|
+
host.remove()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('showModified 变更前副本不抢走编辑控件', async () => {
|
|
189
|
+
const host = document.createElement('div')
|
|
190
|
+
document.body.appendChild(host)
|
|
191
|
+
|
|
192
|
+
const model = reactive({ name: '新值' })
|
|
193
|
+
const initialModel = { name: '旧值' }
|
|
194
|
+
|
|
195
|
+
const app = createApp({
|
|
196
|
+
render() {
|
|
197
|
+
return h(
|
|
198
|
+
UForm,
|
|
199
|
+
{ model, initialModel, showModified: true },
|
|
200
|
+
{ default: () => h(UInput, { field: 'name', label: '名称' }) }
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
app.mount(host)
|
|
206
|
+
await nextTick()
|
|
73
207
|
|
|
74
|
-
|
|
75
|
-
expect(
|
|
208
|
+
const live = host.querySelector('input') as HTMLInputElement | null
|
|
209
|
+
expect(live).toBeTruthy()
|
|
210
|
+
expect(live!.value).toBe('新值')
|
|
211
|
+
expect(host.querySelector('.u-form__data-before')?.textContent).toContain('旧值')
|
|
76
212
|
|
|
77
213
|
app.unmount()
|
|
78
214
|
host.remove()
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
:class="[cls.b, bem.is('readonly', readonly)]"
|
|
9
9
|
>
|
|
10
10
|
<template
|
|
11
|
-
v-for="{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()"
|
|
12
|
-
:key="
|
|
11
|
+
v-for="{ node, isFormItem, formItemProps, field, modelValue, renderKey } of getSlotsNodes()"
|
|
12
|
+
:key="renderKey"
|
|
13
13
|
>
|
|
14
14
|
<u-form-item v-if="!isFormItem && field" v-bind="formItemProps">
|
|
15
15
|
<component
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
/>
|
|
20
20
|
<div v-if="shouldShowModified(field)" :class="cls.e('data-before')">
|
|
21
21
|
<span :class="cls.e('changed-tag')">{{ modifiedLabel }}</span>
|
|
22
|
-
<component :is="node
|
|
22
|
+
<component :is="createModifiedPreview(node, field)" />
|
|
23
23
|
</div>
|
|
24
24
|
</u-form-item>
|
|
25
25
|
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
import { o } from '@cat-kit/core'
|
|
33
33
|
import { bem } from '@veltra/utils'
|
|
34
34
|
import { provideFormContext } from '@veltra/utils'
|
|
35
|
-
import { nextTick, toRef, useTemplateRef } from 'vue'
|
|
35
|
+
import { nextTick, toRef, useTemplateRef, type VNode } from 'vue'
|
|
36
36
|
|
|
37
37
|
import type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'
|
|
38
38
|
import { UFormItem } from '../form-item'
|
|
39
39
|
import { UGrid } from '../grid'
|
|
40
40
|
import { isFieldModified } from './is-field-modified'
|
|
41
41
|
import { useFormFields } from './use-form-fields'
|
|
42
|
-
import { useNodeInterceptor } from './use-node-interceptor'
|
|
42
|
+
import { forkUnmountedVNode, useNodeInterceptor } from './use-node-interceptor'
|
|
43
43
|
|
|
44
44
|
defineOptions({ name: 'UForm' })
|
|
45
45
|
|
|
@@ -81,18 +81,13 @@ function handleFieldUpdate(field: string, value: any) {
|
|
|
81
81
|
emit('field:update', field, value)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function handleFieldChange(field: string, ...args: any[]) {
|
|
85
|
-
emit('field:change', field, ...args)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
provideFormContext({
|
|
89
85
|
formProps: props,
|
|
90
86
|
registerField,
|
|
91
87
|
unregisterField,
|
|
92
88
|
validateFields: validate,
|
|
93
89
|
shouldValidate,
|
|
94
|
-
handleFieldUpdate
|
|
95
|
-
handleFieldChange
|
|
90
|
+
handleFieldUpdate
|
|
96
91
|
})
|
|
97
92
|
|
|
98
93
|
const { getSlotsNodes } = useNodeInterceptor()
|
|
@@ -106,6 +101,10 @@ function getBaselineFieldValue(field: string) {
|
|
|
106
101
|
return o(getBaselineModel() ?? {}).get(field)
|
|
107
102
|
}
|
|
108
103
|
|
|
104
|
+
function createModifiedPreview(node: VNode, field: string) {
|
|
105
|
+
return forkUnmountedVNode(node, { readonly: true, modelValue: getBaselineFieldValue(field) })
|
|
106
|
+
}
|
|
107
|
+
|
|
109
108
|
function shouldShowModified(field: string) {
|
|
110
109
|
if (!props.showModified) return false
|
|
111
110
|
const baseline = getBaselineModel()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { o } from '@cat-kit/core'
|
|
2
2
|
import { extractNormalVNodes } from '@veltra/utils'
|
|
3
|
-
import { useSlots, type VNode } from 'vue'
|
|
3
|
+
import { cloneVNode, useSlots, type VNode } from 'vue'
|
|
4
4
|
|
|
5
5
|
export type SlotRenderItem = {
|
|
6
6
|
isFormItem: boolean
|
|
@@ -8,6 +8,28 @@ export type SlotRenderItem = {
|
|
|
8
8
|
node: VNode
|
|
9
9
|
field?: string
|
|
10
10
|
modelValue?: any
|
|
11
|
+
/** v-for 身份:调用方 :key 优先,否则 type+field+出现次序 */
|
|
12
|
+
renderKey: string | number | symbol
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getVNodeTypeName(node: VNode): string {
|
|
16
|
+
const type = node.type as { name?: string; __name?: string } | string
|
|
17
|
+
if (typeof type === 'string') return type
|
|
18
|
+
if (typeof type === 'object' && type) return type.name || type.__name || ''
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 从源 vnode 派生一份未挂载副本。
|
|
24
|
+
* Vue 的 cloneVNode 会拷贝 el/component,同一源 vnode 渲两次会把实例从编辑控件抢走。
|
|
25
|
+
*/
|
|
26
|
+
export function forkUnmountedVNode(node: VNode, extra?: Record<string, unknown>): VNode {
|
|
27
|
+
const forked = extra ? cloneVNode(node, extra) : cloneVNode(node)
|
|
28
|
+
forked.el = null
|
|
29
|
+
forked.component = null
|
|
30
|
+
forked.anchor = null
|
|
31
|
+
forked.suspense = null
|
|
32
|
+
return forked
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
/**
|
|
@@ -24,6 +46,7 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
|
|
|
24
46
|
const flattedNodes = extractNormalVNodes(nodes)
|
|
25
47
|
|
|
26
48
|
const results: SlotRenderItem[] = []
|
|
49
|
+
const seen = new Map<string, number>()
|
|
27
50
|
|
|
28
51
|
let i = 0
|
|
29
52
|
while (i < flattedNodes.length) {
|
|
@@ -43,12 +66,23 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
|
|
|
43
66
|
'field'
|
|
44
67
|
]) as Record<string, any>
|
|
45
68
|
|
|
69
|
+
let renderKey: string | number | symbol
|
|
70
|
+
if (node.key != null) {
|
|
71
|
+
renderKey = node.key
|
|
72
|
+
} else {
|
|
73
|
+
const base = `${getVNodeTypeName(node)}:${field ?? ''}`
|
|
74
|
+
const n = seen.get(base) ?? 0
|
|
75
|
+
seen.set(base, n + 1)
|
|
76
|
+
renderKey = n === 0 ? `uform:${base}` : `uform:${base}#${n}`
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
results.push({
|
|
47
80
|
isFormItem,
|
|
48
81
|
formItemProps,
|
|
49
82
|
node,
|
|
50
83
|
field,
|
|
51
|
-
modelValue: props?.['model-value'] ?? props?.modelValue
|
|
84
|
+
modelValue: props?.['model-value'] ?? props?.modelValue,
|
|
85
|
+
renderKey
|
|
52
86
|
})
|
|
53
87
|
}
|
|
54
88
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createApp, h, nextTick, reactive } from 'vue'
|
|
2
|
+
import { createApp, h, nextTick, reactive, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import { UForm } from '../../form'
|
|
5
|
+
import { UInput } from '../../input'
|
|
5
6
|
import { USelect } from '../../select'
|
|
6
7
|
import { UFormItem } from '../index'
|
|
7
8
|
|
|
@@ -36,18 +37,12 @@ describe('UFormItem change', () => {
|
|
|
36
37
|
|
|
37
38
|
const model = reactive({ behavior: 'event' })
|
|
38
39
|
const itemChanges: unknown[][] = []
|
|
39
|
-
const formChanges: { field: string; args: unknown[] }[] = []
|
|
40
40
|
|
|
41
41
|
const app = createApp({
|
|
42
42
|
render() {
|
|
43
43
|
return h(
|
|
44
44
|
UForm,
|
|
45
|
-
{
|
|
46
|
-
model,
|
|
47
|
-
'onField:change': (field: string, ...args: unknown[]) => {
|
|
48
|
-
formChanges.push({ field, args })
|
|
49
|
-
}
|
|
50
|
-
},
|
|
45
|
+
{ model },
|
|
51
46
|
{
|
|
52
47
|
default: () =>
|
|
53
48
|
h(
|
|
@@ -81,8 +76,43 @@ describe('UFormItem change', () => {
|
|
|
81
76
|
expect(itemChanges).toHaveLength(1)
|
|
82
77
|
expect(itemChanges[0]![0]).toEqual({ label: '接口', value: 'api' })
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
app.unmount()
|
|
80
|
+
host.remove()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('显式 UFormItem 内控件的动态 props 随父级更新', async () => {
|
|
84
|
+
const host = document.createElement('div')
|
|
85
|
+
document.body.appendChild(host)
|
|
86
|
+
|
|
87
|
+
const model = reactive({ name: '' })
|
|
88
|
+
const disabled = ref(false)
|
|
89
|
+
|
|
90
|
+
const app = createApp({
|
|
91
|
+
render() {
|
|
92
|
+
return h(
|
|
93
|
+
UForm,
|
|
94
|
+
{ model },
|
|
95
|
+
{
|
|
96
|
+
default: () =>
|
|
97
|
+
h(
|
|
98
|
+
UFormItem,
|
|
99
|
+
{ field: 'name', label: '姓名' },
|
|
100
|
+
{ default: () => h(UInput, { modelValue: model.name, disabled: disabled.value }) }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
app.mount(host)
|
|
108
|
+
|
|
109
|
+
const input = () => host.querySelector('input')!
|
|
110
|
+
expect(input().disabled).toBe(false)
|
|
111
|
+
|
|
112
|
+
disabled.value = true
|
|
113
|
+
await nextTick()
|
|
114
|
+
|
|
115
|
+
expect(input().disabled).toBe(true)
|
|
86
116
|
|
|
87
117
|
app.unmount()
|
|
88
118
|
host.remove()
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
<section :class="cls.e('wrapper')">
|
|
19
19
|
<div :class="cls.e('content')">
|
|
20
|
-
<ContentSlot
|
|
20
|
+
<ContentSlot>
|
|
21
|
+
<slot></slot>
|
|
22
|
+
</ContentSlot>
|
|
21
23
|
</div>
|
|
22
24
|
|
|
23
25
|
<!-- 只有表单控件处于非只读状态时,才显示错误提示 -->
|
|
@@ -35,16 +37,16 @@
|
|
|
35
37
|
<script lang="tsx" setup>
|
|
36
38
|
import { o } from '@cat-kit/core'
|
|
37
39
|
import { useConfig, useFallbackProps } from '@veltra/compositions'
|
|
38
|
-
import { bem, withUnit } from '@veltra/utils'
|
|
40
|
+
import { bem, extractNormalVNodes, withUnit } from '@veltra/utils'
|
|
39
41
|
import { injectFormContext } from '@veltra/utils'
|
|
40
42
|
import {
|
|
41
43
|
type CSSProperties,
|
|
44
|
+
type SetupContext,
|
|
42
45
|
type VNode,
|
|
43
46
|
cloneVNode,
|
|
44
47
|
computed,
|
|
45
48
|
onBeforeUnmount,
|
|
46
49
|
shallowRef,
|
|
47
|
-
useSlots,
|
|
48
50
|
watch
|
|
49
51
|
} from 'vue'
|
|
50
52
|
|
|
@@ -60,8 +62,6 @@ const props = withDefaults(defineProps<FormItemProps>(), { readonly: undefined }
|
|
|
60
62
|
|
|
61
63
|
const emit = defineEmits<FormItemEmits>()
|
|
62
64
|
|
|
63
|
-
const slots = useSlots()
|
|
64
|
-
|
|
65
65
|
defineSlots<{
|
|
66
66
|
/** 标签插槽 */
|
|
67
67
|
label?: () => any
|
|
@@ -69,14 +69,8 @@ defineSlots<{
|
|
|
69
69
|
}>()
|
|
70
70
|
|
|
71
71
|
/** 表单组件上下文 */
|
|
72
|
-
const {
|
|
73
|
-
|
|
74
|
-
registerField,
|
|
75
|
-
unregisterField,
|
|
76
|
-
shouldValidate,
|
|
77
|
-
handleFieldUpdate,
|
|
78
|
-
handleFieldChange
|
|
79
|
-
} = injectFormContext()
|
|
72
|
+
const { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } =
|
|
73
|
+
injectFormContext()
|
|
80
74
|
|
|
81
75
|
function wrapControlChange(node: VNode) {
|
|
82
76
|
if (!node || typeof node.type === 'symbol') return node
|
|
@@ -84,14 +78,17 @@ function wrapControlChange(node: VNode) {
|
|
|
84
78
|
return cloneVNode(node, {
|
|
85
79
|
onChange: (...args: any[]) => {
|
|
86
80
|
emit('change', ...args)
|
|
87
|
-
if (props.field) handleFieldChange?.(props.field, ...args)
|
|
88
81
|
}
|
|
89
82
|
})
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
/**
|
|
86
|
+
* 承载默认插槽并拦截控件 change。
|
|
87
|
+
* 必须经填充插槽接收内容(而非直接读 UFormItem 的 $slots):
|
|
88
|
+
* 读父级 $slots 时组件自身没有更新路径,父级重渲染后动态 props(disabled、data 等)会冻结在首帧。
|
|
89
|
+
*/
|
|
90
|
+
function ContentSlot(_: unknown, { slots }: SetupContext) {
|
|
91
|
+
return extractNormalVNodes(slots.default?.() ?? []).map(wrapControlChange)
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
const { config } = useConfig()
|
package/src/types/action.ts
CHANGED
|
@@ -4,7 +4,12 @@ import type { ButtonProps } from './button'
|
|
|
4
4
|
|
|
5
5
|
/** 操作组件属性 */
|
|
6
6
|
export interface ActionProps extends ButtonProps {
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 是否需要确认:true 时点击只弹确认气泡(UPopConfirm),点气泡里的「确认」才触发 `run`
|
|
9
|
+
* @default false
|
|
10
|
+
* @description 不要在本项的 `run` 回调里再调 `messageConfirm`:`run` 触发时气泡已经确认过一次,
|
|
11
|
+
* 会变成两次确认。需要弹窗级确认时去掉 `needConfirm`,只在 `run` 里用 `messageConfirm`
|
|
12
|
+
*/
|
|
8
13
|
needConfirm?: boolean
|
|
9
14
|
|
|
10
15
|
/**
|
package/src/types/batch-edit.ts
CHANGED
|
@@ -115,8 +115,6 @@ export interface BatchEditProps extends TableProps {
|
|
|
115
115
|
|
|
116
116
|
/** 批量编辑组件定义的事件 */
|
|
117
117
|
export interface BatchEditEmits extends TableEmits {
|
|
118
|
-
/** 表单控件 change,透传自 UForm */
|
|
119
|
-
(e: 'field:change', field: string, ...args: any[]): void
|
|
120
118
|
/** 更新数据 */
|
|
121
119
|
(e: 'update:data', value: Record<string, any>[]): void
|
|
122
120
|
/** 点击底部「新增一行」 */
|
package/src/types/button.ts
CHANGED
|
@@ -4,7 +4,11 @@ import type { Component, ShallowRef } from 'vue'
|
|
|
4
4
|
/** 按钮类型 */
|
|
5
5
|
export type ButtonType = ColorType
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 按钮属性类型
|
|
9
|
+
* @description 组件不注入默认 `aria-label`:可访问名来自默认插槽文本,
|
|
10
|
+
* 或你自己传入的 `aria-label`(透传到根元素);只有图标的按钮必须自行传 `aria-label`
|
|
11
|
+
*/
|
|
8
12
|
export interface ButtonProps extends ComponentProps {
|
|
9
13
|
/** 按钮类型 */
|
|
10
14
|
type?: ButtonType
|
package/src/types/drawer.ts
CHANGED
|
@@ -15,16 +15,19 @@ export interface DrawerProps {
|
|
|
15
15
|
|
|
16
16
|
/** 是否显示关闭按钮 */
|
|
17
17
|
showClose?: boolean
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* 抽屉标题;传入时在内容区上方渲染标题栏
|
|
20
|
+
* @description 不传则不渲染标题栏,内容仍全部来自默认插槽
|
|
21
|
+
*/
|
|
19
22
|
title?: string
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** 抽屉组件定义的事件 */
|
|
23
26
|
export interface DrawerEmits {
|
|
24
27
|
(e: 'update:modelValue', value: boolean): void
|
|
25
|
-
/**
|
|
28
|
+
/** 开始关闭时触发(点遮罩、点关闭按钮) */
|
|
26
29
|
(e: 'close'): void
|
|
27
|
-
/**
|
|
30
|
+
/** 完全关闭后触发:抽屉与遮罩的退出动画结束、节点已移除时 */
|
|
28
31
|
(e: 'closed'): void
|
|
29
32
|
}
|
|
30
33
|
|
package/src/types/form.ts
CHANGED
|
@@ -29,8 +29,6 @@ export interface FormProps extends ComponentProps {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface FormEmits {
|
|
32
|
-
/** 控件 change 事件,仅用户操作触发 */
|
|
33
|
-
(e: 'field:change', field: string, ...args: any[]): void
|
|
34
32
|
/** model 字段值更新,含编程写入 */
|
|
35
33
|
(e: 'field:update', field: string, value: any): void
|
|
36
34
|
}
|
package/src/types/layout.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface LayoutProps {
|
|
|
18
18
|
* const cols = ['200px', '1fr']
|
|
19
19
|
* ```
|
|
20
20
|
* [fr是什么?](https://developer.mozilla.org/zh-CN/docs/Web/CSS/flex_value)
|
|
21
|
+
* @description 默认插槽的每个直接子元素按顺序占一列,用自定义组件包一层时该组件必须是单根元素;
|
|
22
|
+
* 子元素渲染出 0 个(`v-if` 为假)或多个根节点时会挤动后面的列,此时自行给子元素写 `grid-column`
|
|
21
23
|
*/
|
|
22
24
|
cols?: string[] | string
|
|
23
25
|
|