@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.
Files changed (42) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +2 -6
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit.js +0 -1
  4. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  5. package/dist/components/button/button.js +0 -1
  6. package/dist/components/button/button.js.map +1 -1
  7. package/dist/components/drawer/drawer.js +18 -10
  8. package/dist/components/drawer/drawer.js.map +1 -1
  9. package/dist/components/drawer/style2.css +15 -0
  10. package/dist/components/form/form.js.map +1 -1
  11. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +12 -13
  12. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/form/use-node-interceptor.js +31 -3
  14. package/dist/components/form/use-node-interceptor.js.map +1 -1
  15. package/dist/components/form-item/form-item.js.map +1 -1
  16. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +14 -8
  17. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  18. package/dist/types/action.d.ts +6 -1
  19. package/dist/types/batch-edit.d.ts +0 -2
  20. package/dist/types/button.d.ts +5 -1
  21. package/dist/types/drawer.d.ts +6 -3
  22. package/dist/types/form.d.ts +0 -2
  23. package/dist/types/layout.d.ts +2 -0
  24. package/package.json +5 -5
  25. package/src/components/batch-edit/__test__/batch-edit.test.ts +0 -138
  26. package/src/components/batch-edit/batch-edit-form.vue +2 -16
  27. package/src/components/button/__test__/button.test.ts +48 -0
  28. package/src/components/button/button.vue +1 -8
  29. package/src/components/drawer/__test__/drawer.test.ts +118 -0
  30. package/src/components/drawer/drawer.vue +5 -2
  31. package/src/components/drawer/style.scss +15 -0
  32. package/src/components/form/__test__/form.test.ts +167 -31
  33. package/src/components/form/form.vue +10 -11
  34. package/src/components/form/use-node-interceptor.ts +36 -2
  35. package/src/components/form-item/__test__/form-item.test.ts +40 -10
  36. package/src/components/form-item/form-item.vue +14 -17
  37. package/src/types/action.ts +6 -1
  38. package/src/types/batch-edit.ts +0 -2
  39. package/src/types/button.ts +5 -1
  40. package/src/types/drawer.ts +6 -3
  41. package/src/types/form.ts +0 -2
  42. package/src/types/layout.ts +2 -0
@@ -99,8 +99,6 @@ interface BatchEditProps extends TableProps {
99
99
  }
100
100
  /** 批量编辑组件定义的事件 */
101
101
  interface BatchEditEmits extends TableEmits {
102
- /** 表单控件 change,透传自 UForm */
103
- (e: 'field:change', field: string, ...args: any[]): void;
104
102
  /** 更新数据 */
105
103
  (e: 'update:data', value: Record<string, any>[]): void;
106
104
  /** 点击底部「新增一行」 */
@@ -3,7 +3,11 @@ import { ColorType, ComponentProps, DeconstructValue } from "@veltra/utils";
3
3
  //#region src/types/button.d.ts
4
4
  /** 按钮类型 */
5
5
  type ButtonType = ColorType;
6
- /** 按钮属性类型 */
6
+ /**
7
+ * 按钮属性类型
8
+ * @description 组件不注入默认 `aria-label`:可访问名来自默认插槽文本,
9
+ * 或你自己传入的 `aria-label`(透传到根元素);只有图标的按钮必须自行传 `aria-label`
10
+ */
7
11
  interface ButtonProps extends ComponentProps {
8
12
  /** 按钮类型 */
9
13
  type?: ButtonType;
@@ -12,15 +12,18 @@ interface DrawerProps {
12
12
  direction?: DrawerDirection;
13
13
  /** 是否显示关闭按钮 */
14
14
  showClose?: boolean;
15
- /** 抽屉标题 */
15
+ /**
16
+ * 抽屉标题;传入时在内容区上方渲染标题栏
17
+ * @description 不传则不渲染标题栏,内容仍全部来自默认插槽
18
+ */
16
19
  title?: string;
17
20
  }
18
21
  /** 抽屉组件定义的事件 */
19
22
  interface DrawerEmits {
20
23
  (e: 'update:modelValue', value: boolean): void;
21
- /** 关闭时触发 */
24
+ /** 开始关闭时触发(点遮罩、点关闭按钮) */
22
25
  (e: 'close'): void;
23
- /** 完全关闭后触发 */
26
+ /** 完全关闭后触发:抽屉与遮罩的退出动画结束、节点已移除时 */
24
27
  (e: 'closed'): void;
25
28
  }
26
29
  /** 抽屉组件暴露的属性和方法(组件内部使用) */
@@ -28,8 +28,6 @@ interface FormProps extends ComponentProps {
28
28
  disabled?: boolean;
29
29
  }
30
30
  interface FormEmits {
31
- /** 控件 change 事件,仅用户操作触发 */
32
- (e: 'field:change', field: string, ...args: any[]): void;
33
31
  /** model 字段值更新,含编程写入 */
34
32
  (e: 'field:update', field: string, value: any): void;
35
33
  }
@@ -18,6 +18,8 @@ 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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.7.10",
3
+ "version": "1.7.12",
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"
@@ -80,12 +80,12 @@
80
80
  "peerDependencies": {
81
81
  "@cat-kit/core": ">=1.2.1",
82
82
  "@cat-kit/fe": ">=1.2.1",
83
- "@veltra/compositions": "^1.7.10",
84
- "@veltra/directives": "^1.7.10",
83
+ "@veltra/compositions": "^1.7.12",
84
+ "@veltra/directives": "^1.7.12",
85
85
  "@veltra/icons": "^1.5.0",
86
86
  "@veltra/sheet-core": "^2.5.6",
87
- "@veltra/styles": "^1.7.10",
88
- "@veltra/utils": "^1.7.10",
87
+ "@veltra/styles": "^1.7.12",
88
+ "@veltra/utils": "^1.7.12",
89
89
  "vue": ">=3.5.42"
90
90
  },
91
91
  "peerDependenciesMeta": {
@@ -30,8 +30,6 @@ function mountBatchEdit() {
30
30
  apiMethod: undefined as string | undefined
31
31
  })
32
32
 
33
- const fieldChanges: { field: string; args: unknown[] }[] = []
34
-
35
33
  const app = createApp({
36
34
  render() {
37
35
  return h(
@@ -41,9 +39,6 @@ function mountBatchEdit() {
41
39
  'onUpdate:data': (value: any[]) => {
42
40
  data.value = value
43
41
  },
44
- 'onField:change': (field: string, ...args: unknown[]) => {
45
- fieldChanges.push({ field, args })
46
- },
47
42
  columns,
48
43
  model,
49
44
  rowKey: 'id',
@@ -94,7 +89,6 @@ function mountBatchEdit() {
94
89
  host,
95
90
  data,
96
91
  model,
97
- fieldChanges,
98
92
  unmount() {
99
93
  app.unmount()
100
94
  host.remove()
@@ -202,136 +196,4 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
202
196
 
203
197
  unmount()
204
198
  })
205
-
206
- it('切换行回显不触发 field:change', async () => {
207
- const { host, fieldChanges, unmount } = mountBatchEdit()
208
-
209
- fieldChanges.length = 0
210
- await clickRow(host, 0)
211
- await clickRow(host, 1)
212
-
213
- expect(fieldChanges).toHaveLength(0)
214
-
215
- unmount()
216
- })
217
-
218
- it('用户变更 Select 触发 field:change', async () => {
219
- const { host, fieldChanges, unmount } = mountBatchEdit()
220
-
221
- await clickRow(host, 0)
222
- fieldChanges.length = 0
223
-
224
- const input = host.querySelector<HTMLInputElement>('.u-select input')
225
- expect(input).toBeTruthy()
226
- input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
227
- await nextTick()
228
- await nextTick()
229
-
230
- const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
231
- (el) => el.textContent === '接口'
232
- )
233
- expect(option).toBeTruthy()
234
- option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
235
- await nextTick()
236
- await nextTick()
237
-
238
- expect(fieldChanges).toHaveLength(1)
239
- expect(fieldChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
240
-
241
- unmount()
242
- })
243
-
244
- it('field:change 联动改其他字段不循环触发', async () => {
245
- const host = document.createElement('div')
246
- document.body.appendChild(host)
247
-
248
- const columns = defineTableColumns([
249
- { key: 'label', name: '名称' },
250
- { key: 'behavior', name: '行为' }
251
- ])
252
-
253
- const data = ref([
254
- { id: '1', label: '存草稿', behavior: 'common-resource', submitType: 'DRAFT' },
255
- { id: '2', label: '调接口', behavior: 'api', submitType: 'SUBMIT', apiMethod: 'POST' }
256
- ])
257
-
258
- const model = reactive({
259
- id: '',
260
- label: '',
261
- behavior: 'event',
262
- submitType: undefined as string | undefined,
263
- apiMethod: undefined as string | undefined
264
- })
265
-
266
- const fieldChanges: string[] = []
267
-
268
- const app = createApp({
269
- render() {
270
- return h(
271
- UBatchEdit,
272
- {
273
- data: data.value,
274
- 'onUpdate:data': (value: any[]) => {
275
- data.value = value
276
- },
277
- 'onField:change': (field: string) => {
278
- fieldChanges.push(field)
279
- if (field === 'behavior') {
280
- model.submitType = undefined
281
- }
282
- },
283
- columns,
284
- model,
285
- rowKey: 'id',
286
- quickEdit: true
287
- },
288
- {
289
- form: () => [
290
- h(UInput, { field: 'label', label: '名称' }),
291
- h(USelect, {
292
- field: 'behavior',
293
- label: '行为',
294
- options: [
295
- { label: '事件', value: 'event' },
296
- { label: '通用资源', value: 'common-resource' },
297
- { label: '接口', value: 'api' }
298
- ]
299
- }),
300
- h(USelect, {
301
- field: 'submitType',
302
- label: '提交类型',
303
- options: [
304
- { label: 'DRAFT', value: 'DRAFT' },
305
- { label: 'SUBMIT', value: 'SUBMIT' }
306
- ]
307
- })
308
- ]
309
- }
310
- )
311
- }
312
- })
313
-
314
- app.mount(host)
315
-
316
- await clickRow(host, 0)
317
- fieldChanges.length = 0
318
-
319
- const input = host.querySelector<HTMLInputElement>('.u-select input')
320
- input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
321
- await nextTick()
322
- await nextTick()
323
-
324
- const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
325
- (el) => el.textContent === '接口'
326
- )
327
- option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
328
- await nextTick()
329
- await nextTick()
330
-
331
- expect(fieldChanges).toEqual(['behavior'])
332
- expect(model.submitType).toBeUndefined()
333
-
334
- app.unmount()
335
- host.remove()
336
- })
337
199
  })
@@ -29,7 +29,6 @@
29
29
  :readonly="props.readonly"
30
30
  :label-width="props.labelWidth"
31
31
  @field:update="handleFieldUpdate"
32
- @field:change="handleFormFieldChange"
33
32
  >
34
33
  <slot
35
34
  v-bind="{
@@ -92,17 +91,8 @@ defineOptions({ name: 'UBatchEditForm' })
92
91
 
93
92
  const batchEditCtx = inject(BatchEditDIKey)!
94
93
 
95
- const {
96
- cls,
97
- props,
98
- state,
99
- emit,
100
- handleClose,
101
- handleSave,
102
- staticFeatures,
103
- dynamicFeatures,
104
- syncing
105
- } = batchEditCtx
94
+ const { cls, props, state, handleClose, handleSave, staticFeatures, dynamicFeatures, syncing } =
95
+ batchEditCtx
106
96
 
107
97
  const focused = toRef(batchEditCtx, 'focused')
108
98
 
@@ -124,10 +114,6 @@ function handleFieldUpdate(field: string, value: any) {
124
114
  }
125
115
  }
126
116
 
127
- function handleFormFieldChange(field: string, ...args: any[]) {
128
- emit('field:change', field, ...args)
129
- }
130
-
131
117
  const creatable = computed(() => {
132
118
  if (state.formActionType !== 'create') return false
133
119
  return staticFeatures.value.has('create') || dynamicFeatures.value.create?.(state.row)
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h } from 'vue'
3
+
4
+ import UButton from '../button.vue'
5
+
6
+ function mountButton(props: Record<string, unknown> = {}, slot = '新建用户') {
7
+ const host = document.createElement('div')
8
+ document.body.appendChild(host)
9
+
10
+ const app = createApp({ render: () => h(UButton, props, () => slot) })
11
+ app.mount(host)
12
+
13
+ return {
14
+ host,
15
+ button: host.querySelector('button')!,
16
+ unmount() {
17
+ app.unmount()
18
+ host.remove()
19
+ }
20
+ }
21
+ }
22
+
23
+ describe('UButton 原生属性与可访问名', () => {
24
+ it('不注入默认 aria-label:可访问名来自默认插槽文本', () => {
25
+ const { button, unmount } = mountButton()
26
+
27
+ expect(button.getAttribute('aria-label')).toBeNull()
28
+ expect(button.textContent?.trim()).toBe('新建用户')
29
+
30
+ unmount()
31
+ })
32
+
33
+ it('自己传的 aria-label 透传到根元素,图标按钮仍可命名', () => {
34
+ const { button, unmount } = mountButton({ 'aria-label': '编辑' }, '')
35
+
36
+ expect(button.getAttribute('aria-label')).toBe('编辑')
37
+
38
+ unmount()
39
+ })
40
+
41
+ it('原生 type 固定为 button,放进 form 里不会触发提交', () => {
42
+ const { button, unmount } = mountButton()
43
+
44
+ expect(button.getAttribute('type')).toBe('button')
45
+
46
+ unmount()
47
+ })
48
+ })
@@ -1,12 +1,5 @@
1
1
  <template>
2
- <button
3
- :class="classList"
4
- type="button"
5
- aria-label="button"
6
- ref="buttonRef"
7
- v-ripple="ripple"
8
- @click="handleClick"
9
- >
2
+ <button :class="classList" type="button" ref="buttonRef" v-ripple="ripple" @click="handleClick">
10
3
  <!-- 加载图标 -->
11
4
  <u-icon v-if="loading" :class="[bem.is('loading')]" :size="iconSize">
12
5
  <component :is="loadingIcon" />
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { createApp, h, nextTick, ref } from 'vue'
3
+
4
+ import UDrawer from '../drawer.vue'
5
+
6
+ function mountDrawer(
7
+ props: Record<string, unknown> = {},
8
+ handlers: { onClose?: () => void; onClosed?: () => void } = {}
9
+ ) {
10
+ const host = document.createElement('div')
11
+ document.body.appendChild(host)
12
+
13
+ const model = ref(false)
14
+ const closedCount = ref(0)
15
+ const app = createApp({
16
+ render: () =>
17
+ h(
18
+ UDrawer,
19
+ {
20
+ ...props,
21
+ modelValue: model.value,
22
+ 'onUpdate:modelValue': (value: boolean) => {
23
+ model.value = value
24
+ },
25
+ onClose: () => handlers.onClose?.(),
26
+ onClosed: () => {
27
+ closedCount.value += 1
28
+ handlers.onClosed?.()
29
+ }
30
+ },
31
+ () => '抽屉内容'
32
+ )
33
+ })
34
+ app.mount(host)
35
+
36
+ return {
37
+ host,
38
+ model,
39
+ closedCount,
40
+ panel: () => document.body.querySelector<HTMLElement>('.u-drawer'),
41
+ overlay: () => document.body.querySelector<HTMLElement>('.u-drawer-overlay'),
42
+ unmount() {
43
+ app.unmount()
44
+ host.remove()
45
+ }
46
+ }
47
+ }
48
+
49
+ async function openDrawer(ctx: ReturnType<typeof mountDrawer>) {
50
+ ctx.model.value = true
51
+ await vi.waitFor(() => {
52
+ expect(ctx.panel()).toBeTruthy()
53
+ })
54
+ }
55
+
56
+ describe('UDrawer title', () => {
57
+ it('传入 title 时渲染标题栏', async () => {
58
+ const ctx = mountDrawer({ title: '用户详情' })
59
+ await openDrawer(ctx)
60
+
61
+ expect(ctx.panel()!.querySelector('.u-drawer__title')?.textContent).toBe('用户详情')
62
+
63
+ ctx.unmount()
64
+ })
65
+
66
+ it('不传 title 时不渲染标题栏', async () => {
67
+ const ctx = mountDrawer()
68
+ await openDrawer(ctx)
69
+
70
+ expect(ctx.panel()!.querySelector('.u-drawer__header')).toBeNull()
71
+
72
+ ctx.unmount()
73
+ })
74
+ })
75
+
76
+ describe('UDrawer closed', () => {
77
+ it('关闭动画结束后触发 closed,且晚于 close', async () => {
78
+ const order: string[] = []
79
+ const ctx = mountDrawer(
80
+ { showClose: true },
81
+ { onClose: () => order.push('close'), onClosed: () => order.push('closed') }
82
+ )
83
+ await openDrawer(ctx)
84
+
85
+ ctx.panel()!.querySelector<HTMLElement>('.u-drawer__close')!.click()
86
+ await vi.waitFor(() => {
87
+ expect(order).toContain('closed')
88
+ })
89
+
90
+ expect(order).toEqual(['close', 'closed'])
91
+ // 完全关闭后遮罩与抽屉都已移除
92
+ expect(ctx.overlay()).toBeNull()
93
+
94
+ ctx.unmount()
95
+ })
96
+
97
+ it('点遮罩关闭也触发 closed', async () => {
98
+ const ctx = mountDrawer()
99
+ await openDrawer(ctx)
100
+
101
+ ctx.overlay()!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
102
+ await vi.waitFor(() => {
103
+ expect(ctx.closedCount.value).toBe(1)
104
+ })
105
+
106
+ ctx.unmount()
107
+ })
108
+
109
+ it('从未打开时不触发 closed', async () => {
110
+ const ctx = mountDrawer()
111
+ await nextTick()
112
+ await nextTick()
113
+
114
+ expect(ctx.closedCount.value).toBe(0)
115
+
116
+ ctx.unmount()
117
+ })
118
+ })
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
- <transition name="fade" mode="out-in" @enter="onEnter">
3
+ <transition name="fade" mode="out-in" @enter="onEnter" @after-leave="emit('closed')">
4
4
  <div v-if="overlayVisible" :class="overlayCls.b" :style="{ zIndex: zIndex() }" @click="close">
5
5
  <transition :name="transitionName" appear @after-leave="onAfterDrawerLeave">
6
6
  <div
@@ -10,6 +10,9 @@
10
10
  :style="drawerStyle"
11
11
  @click.stop
12
12
  >
13
+ <div v-if="title" :class="cls.e('header')">
14
+ <span :class="cls.e('title')">{{ title }}</span>
15
+ </div>
13
16
  <span :class="cls.e('close')" @click="close" v-if="showClose">
14
17
  <u-icon><Close /></u-icon>
15
18
  </span>
@@ -31,7 +34,7 @@ import { UIcon } from '../icon'
31
34
 
32
35
  defineOptions({ name: 'UDrawer', inheritAttrs: false })
33
36
 
34
- const props = withDefaults(defineProps<DrawerProps>(), { direction: 'right', closable: true })
37
+ const props = withDefaults(defineProps<DrawerProps>(), { direction: 'right' })
35
38
 
36
39
  const emit = defineEmits<DrawerEmits>()
37
40
 
@@ -40,6 +40,21 @@ $direction-map: (
40
40
  justify-content: center;
41
41
  }
42
42
 
43
+ // 标题栏:传入 title 时渲染在内容区上方,其余行为与把标题写进默认插槽一致
44
+ @include m.e(header) {
45
+ height: $close-size;
46
+ flex-shrink: 0;
47
+ padding: 0 fn.use-var(gap, default);
48
+ font-size: fn.use-var(font-size-title, default);
49
+ border-bottom: fn.use-var(border);
50
+ @include m.flex();
51
+ }
52
+
53
+ @include m.e(title) {
54
+ font-weight: 600;
55
+ line-height: 1em;
56
+ }
57
+
43
58
  @include m.is(right, left) {
44
59
  top: $edge-offset;
45
60
  bottom: $edge-offset;