@veltra/desktop 1.1.9 → 1.1.11

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 (32) hide show
  1. package/dist/components/action/action-group.vue.d.ts +1 -1
  2. package/dist/components/button/style2.css +75 -11
  3. package/dist/components/card/style2.css +4 -6
  4. package/dist/components/cascade/cascade.vue.d.ts +2 -2
  5. package/dist/components/collapse/collapse-item.js +10 -24
  6. package/dist/components/collapse/collapse-item.js.map +1 -1
  7. package/dist/components/collapse/collapse-item.vue.d.ts +5 -9
  8. package/dist/components/collapse/collapse.js +27 -39
  9. package/dist/components/collapse/collapse.js.map +1 -1
  10. package/dist/components/collapse/collapse.vue.d.ts +4 -4
  11. package/dist/components/collapse/di.js.map +1 -1
  12. package/dist/components/collapse/style2.css +45 -34
  13. package/dist/components/tabs/style.js +1 -0
  14. package/dist/components/tabs/tabs-horizontal.vue.d.ts +1 -1
  15. package/dist/components/tabs/tabs.js +6 -11
  16. package/dist/components/tabs/tabs.js.map +1 -1
  17. package/dist/components/tabs/tabs.vue.d.ts +1 -1
  18. package/dist/components/tree/tree.vue.d.ts +1 -1
  19. package/dist/index.d.ts +3 -3
  20. package/dist/types/collapse.d.ts +8 -26
  21. package/dist/types/index.d.ts +2 -2
  22. package/package.json +11 -11
  23. package/src/components/button/style.scss +26 -7
  24. package/src/components/card/style.scss +4 -18
  25. package/src/components/collapse/__test__/collapse.test.ts +178 -0
  26. package/src/components/collapse/collapse-item.vue +5 -13
  27. package/src/components/collapse/collapse.vue +46 -52
  28. package/src/components/collapse/di.ts +4 -2
  29. package/src/components/collapse/style.scss +46 -73
  30. package/src/components/tabs/style.ts +1 -0
  31. package/src/components/tabs/tabs.vue +8 -13
  32. package/src/types/collapse.ts +7 -29
@@ -14,15 +14,12 @@ $root-name: card;
14
14
 
15
15
  @include m.is(integrate) {
16
16
  box-shadow: none;
17
+ --u-card-header-bg: transparent;
18
+ --u-card-action-bg: transparent;
17
19
  & > {
18
20
  @include m.bem($root-name, header) {
19
- border-bottom: none;
20
21
  padding-bottom: 0;
21
22
  }
22
-
23
- @include m.bem($root-name, action) {
24
- border-top: none;
25
- }
26
23
  }
27
24
  }
28
25
 
@@ -40,7 +37,7 @@ $root-name: card;
40
37
  }
41
38
 
42
39
  @include m.e(header) {
43
- border-bottom: fn.use-var(border);
40
+ background-color: fn.use-var(header-bg, transparent);
44
41
  color: fn.use-var(text-color, title);
45
42
  }
46
43
 
@@ -66,21 +63,10 @@ $root-name: card;
66
63
  }
67
64
 
68
65
  @include m.e(action) {
69
- border-top: fn.use-var(border);
66
+ background-color: fn.use-var(action-bg, transparent);
70
67
 
71
68
  #{fn.bem(button)} + #{fn.bem(button)} {
72
69
  margin-left: fn.use-var(gap, default);
73
70
  }
74
71
  }
75
-
76
- // @include m.is(always-shadow) {
77
- // box-shadow: fn.use-var(shadow);
78
- // }
79
-
80
- // @include m.is(hover-shadow) {
81
- // transition: box-shadow 0.25s ease;
82
- // &:hover {
83
- // box-shadow: fn.use-var(shadow);
84
- // }
85
- // }
86
72
  }
@@ -0,0 +1,178 @@
1
+ // @vitest-environment happy-dom
2
+ import { createApp, h, nextTick, ref } from 'vue'
3
+
4
+ import UCollapseItem from '../collapse-item.vue'
5
+ import UCollapse from '../collapse.vue'
6
+
7
+ function mountCollapse(
8
+ props: Record<string, unknown> = {},
9
+ items: Array<{ value: string; title: string }> = [
10
+ { value: 'a', title: 'Title A' },
11
+ { value: 'b', title: 'Title B' }
12
+ ]
13
+ ) {
14
+ const host = document.createElement('div')
15
+ document.body.appendChild(host)
16
+
17
+ const model = ref(props.modelValue)
18
+ const app = createApp({
19
+ render() {
20
+ return h(
21
+ UCollapse,
22
+ {
23
+ ...props,
24
+ modelValue: model.value,
25
+ 'onUpdate:modelValue': (value: any) => {
26
+ model.value = value
27
+ }
28
+ },
29
+ () =>
30
+ items.map((item) =>
31
+ h(
32
+ UCollapseItem,
33
+ { value: item.value, title: item.title },
34
+ () => `Content ${item.value}`
35
+ )
36
+ )
37
+ )
38
+ }
39
+ })
40
+
41
+ app.mount(host)
42
+
43
+ return {
44
+ host,
45
+ model,
46
+ unmount() {
47
+ app.unmount()
48
+ host.remove()
49
+ }
50
+ }
51
+ }
52
+
53
+ describe('Collapse', () => {
54
+ it('defaultCollapseAll defaults to false, which expands all items when no initial modelValue is provided', async () => {
55
+ const { host, model, unmount } = mountCollapse()
56
+
57
+ try {
58
+ await nextTick()
59
+ // 默认展开全部:modelValue 应该包含所有项的 value
60
+ expect(model.value).toEqual(['a', 'b'])
61
+
62
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
63
+ expect(activeItems.length).toBe(2)
64
+ } finally {
65
+ unmount()
66
+ }
67
+ })
68
+
69
+ it('defaultCollapseAll sets to true collapses all items', async () => {
70
+ const { host, model, unmount } = mountCollapse({ defaultCollapseAll: true })
71
+
72
+ try {
73
+ await nextTick()
74
+ // 全部折叠:modelValue 应该为空或未定义,没有任何项具有 is-active 样式
75
+ expect(model.value).toBeUndefined()
76
+
77
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
78
+ expect(activeItems.length).toBe(0)
79
+ } finally {
80
+ unmount()
81
+ }
82
+ })
83
+
84
+ it('defaultCollapseAll sets to empty string collapses all items', async () => {
85
+ const { host, model, unmount } = mountCollapse({ defaultCollapseAll: '' })
86
+
87
+ try {
88
+ await nextTick()
89
+ expect(model.value).toBeUndefined()
90
+
91
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
92
+ expect(activeItems.length).toBe(0)
93
+ } finally {
94
+ unmount()
95
+ }
96
+ })
97
+
98
+ it('defaultCollapseAll sets to "true" collapses all items', async () => {
99
+ const { host, model, unmount } = mountCollapse({ defaultCollapseAll: 'true' })
100
+
101
+ try {
102
+ await nextTick()
103
+ expect(model.value).toBeUndefined()
104
+
105
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
106
+ expect(activeItems.length).toBe(0)
107
+ } finally {
108
+ unmount()
109
+ }
110
+ })
111
+
112
+ it('respects initial modelValue even if defaultCollapseAll is false', async () => {
113
+ const { host, model, unmount } = mountCollapse({ modelValue: ['a'] })
114
+
115
+ try {
116
+ await nextTick()
117
+ // 外部传入初始值:即使 defaultCollapseAll 为 false,也仅激活外部指定的项
118
+ expect(model.value).toEqual(['a'])
119
+
120
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
121
+ expect(activeItems.length).toBe(1)
122
+ expect(activeItems[0].querySelector('.u-collapse__title')?.textContent).toBe('Title A')
123
+ } finally {
124
+ unmount()
125
+ }
126
+ })
127
+
128
+ it('accordion mode with defaultCollapseAll=false only expands the first item', async () => {
129
+ const { host, model, unmount } = mountCollapse({ accordion: true })
130
+
131
+ try {
132
+ await nextTick()
133
+ // 手风琴模式且没有初始值,默认只展开第一个
134
+ expect(model.value).toBe('a')
135
+
136
+ const activeItems = host.querySelectorAll('.u-collapse__item.is-active')
137
+ expect(activeItems.length).toBe(1)
138
+ expect(activeItems[0].querySelector('.u-collapse__title')?.textContent).toBe('Title A')
139
+ } finally {
140
+ unmount()
141
+ }
142
+ })
143
+
144
+ it('toggles collapse items on header click', async () => {
145
+ const { host, model, unmount } = mountCollapse({ defaultCollapseAll: true })
146
+
147
+ try {
148
+ await nextTick()
149
+ expect(model.value).toBeUndefined()
150
+
151
+ const headers = host.querySelectorAll('.u-collapse__header')
152
+ expect(headers.length).toBe(2)
153
+
154
+ // 点击第一个 header 展开它
155
+ ;(headers[0] as HTMLElement).click()
156
+ await nextTick()
157
+
158
+ expect(model.value).toEqual(['a'])
159
+ expect(host.querySelectorAll('.u-collapse__item.is-active').length).toBe(1)
160
+
161
+ // 点击第二个 header 展开它
162
+ ;(headers[1] as HTMLElement).click()
163
+ await nextTick()
164
+
165
+ expect(model.value).toEqual(['a', 'b'])
166
+ expect(host.querySelectorAll('.u-collapse__item.is-active').length).toBe(2)
167
+
168
+ // 再次点击第一个 header 收起它
169
+ ;(headers[0] as HTMLElement).click()
170
+ await nextTick()
171
+
172
+ expect(model.value).toEqual(['b'])
173
+ expect(host.querySelectorAll('.u-collapse__item.is-active').length).toBe(1)
174
+ } finally {
175
+ unmount()
176
+ }
177
+ })
178
+ })
@@ -10,17 +10,11 @@
10
10
  @keydown.enter.prevent="handleClick"
11
11
  @keydown.space.prevent="handleClick"
12
12
  >
13
- <span v-if="showLeftIcon" :class="cls.e('icon')">
14
- <slot name="icon" :is-active="isActive">
15
- <UIcon><component :is="iconComponent" /></UIcon>
16
- </slot>
17
- </span>
18
-
19
13
  <span :class="cls.e('title')">
20
14
  <slot name="title">{{ title }}</slot>
21
15
  </span>
22
16
 
23
- <span v-if="showRightIcon" :class="cls.e('icon')">
17
+ <span :class="cls.e('icon')">
24
18
  <slot name="icon" :is-active="isActive">
25
19
  <UIcon><component :is="iconComponent" /></UIcon>
26
20
  </slot>
@@ -55,17 +49,13 @@ const cls = context?.cls ?? bem('collapse')
55
49
 
56
50
  const isActive = computed(() => !!context?.activeValues.value.includes(props.value))
57
51
 
58
- const iconPosition = computed(() => context?.iconPosition.value ?? 'right')
59
-
60
52
  const iconComponent = computed(() => context?.expandIcon.value ?? ArrowRight)
61
53
 
62
- const showLeftIcon = computed(() => !props.hideIcon && iconPosition.value === 'left')
63
- const showRightIcon = computed(() => !props.hideIcon && iconPosition.value === 'right')
64
-
65
54
  const classList = computed(() => [
66
55
  cls.e('item'),
67
56
  bem.is('active', isActive.value),
68
- bem.is('disabled', props.disabled)
57
+ bem.is('disabled', props.disabled),
58
+ bem.is('bordered', context?.bordered.value)
69
59
  ])
70
60
 
71
61
  const headerClassList = computed(() => [
@@ -84,6 +74,7 @@ const wrapperEl = ref<HTMLElement>()
84
74
  const { expandTransition } = context
85
75
 
86
76
  onMounted(() => {
77
+ context?.register(props.value)
87
78
  if (!wrapperEl.value) return
88
79
  expandTransition.setExpanded(wrapperEl.value, isActive.value)
89
80
  })
@@ -94,6 +85,7 @@ watch(isActive, (active) => {
94
85
  })
95
86
 
96
87
  onBeforeUnmount(() => {
88
+ context?.unregister(props.value)
97
89
  if (!wrapperEl.value) return
98
90
  expandTransition.cancel(wrapperEl.value)
99
91
  })
@@ -7,15 +7,9 @@
7
7
  <script lang="ts" setup>
8
8
  import { useFormFallbackProps, useModel } from '@veltra/compositions'
9
9
  import { bem, ExpandTransition } from '@veltra/utils'
10
- import { computed, provide } from 'vue'
11
-
12
- import type {
13
- CollapseEmits,
14
- CollapseModelValue,
15
- CollapseProps,
16
- CollapseValue,
17
- _CollapseExposed
18
- } from '../../types'
10
+ import { computed, provide, ref } from 'vue'
11
+
12
+ import type { CollapseEmits, CollapseModelValue, CollapseProps, CollapseValue } from '../../types'
19
13
  import { CollapseDIKey } from './di'
20
14
 
21
15
  defineOptions({
@@ -24,8 +18,8 @@ defineOptions({
24
18
 
25
19
  const props = withDefaults(defineProps<CollapseProps>(), {
26
20
  accordion: false,
27
- bordered: true,
28
- iconPosition: 'right'
21
+ defaultCollapseAll: false,
22
+ bordered: false
29
23
  })
30
24
  const emit = defineEmits<CollapseEmits>()
31
25
 
@@ -37,15 +31,11 @@ const expandTransition = new ExpandTransition({
37
31
 
38
32
  const { size } = useFormFallbackProps([props], { size: 'default' })
39
33
 
40
- const iconPosition = computed(() => props.iconPosition ?? 'right')
41
34
  const expandIcon = computed(() => props.expandIcon)
42
35
 
43
- const classList = computed(() => [
44
- cls.b,
45
- cls.m(size.value),
46
- cls.m(`icon-${iconPosition.value}`),
47
- bem.is('bordered', props.bordered)
48
- ])
36
+ const bordered = computed(() => props.bordered)
37
+
38
+ const classList = computed(() => [cls.b, cls.m(size.value), bem.is('bordered', bordered.value)])
49
39
 
50
40
  const modelValue = useModel<CollapseProps, 'modelValue'>({
51
41
  props,
@@ -77,51 +67,55 @@ const toggle = (value: CollapseValue) => {
77
67
  update(values)
78
68
  }
79
69
 
80
- const expand = (value: CollapseValue) => {
81
- if (activeValues.value.includes(value)) return
82
- if (props.accordion) {
83
- update(value)
84
- return
85
- }
86
- update([...activeValues.value, value])
87
- }
88
-
89
- const collapse = (value: CollapseValue) => {
90
- if (!activeValues.value.includes(value)) return
91
- if (props.accordion) {
92
- update([])
93
- return
70
+ // 收集子组件已注册的 values
71
+ const registeredValues = ref<CollapseValue[]>([])
72
+
73
+ // 初始捕获 modelValue 是否具有有效初值
74
+ const hasInitialValue =
75
+ modelValue.value !== undefined &&
76
+ modelValue.value !== null &&
77
+ (!Array.isArray(modelValue.value) || modelValue.value.length > 0)
78
+
79
+ const register = (value: CollapseValue) => {
80
+ if (!registeredValues.value.includes(value)) {
81
+ registeredValues.value.push(value)
82
+
83
+ // 若默认展开全部(defaultCollapseAll 为 false)且外部未指定初始值,则自动展开该子项
84
+ if (!props.defaultCollapseAll && !hasInitialValue) {
85
+ if (props.accordion) {
86
+ if (registeredValues.value.length === 1) {
87
+ update(value)
88
+ }
89
+ } else {
90
+ const current = Array.isArray(modelValue.value)
91
+ ? modelValue.value
92
+ : modelValue.value !== undefined && modelValue.value !== null
93
+ ? [modelValue.value]
94
+ : []
95
+ if (!current.includes(value)) {
96
+ update([...current, value])
97
+ }
98
+ }
99
+ }
94
100
  }
95
- update(activeValues.value.filter((v) => v !== value))
96
101
  }
97
102
 
98
- const expandAll = (values: CollapseValue[]) => {
99
- if (props.accordion) {
100
- update(values[0] ?? [])
101
- return
103
+ const unregister = (value: CollapseValue) => {
104
+ const idx = registeredValues.value.indexOf(value)
105
+ if (idx > -1) {
106
+ registeredValues.value.splice(idx, 1)
102
107
  }
103
- update([...values])
104
- }
105
-
106
- const collapseAll = () => {
107
- update([])
108
108
  }
109
109
 
110
110
  provide(CollapseDIKey, {
111
111
  cls,
112
112
  size,
113
- iconPosition,
113
+ bordered,
114
114
  expandIcon,
115
115
  activeValues,
116
116
  toggle,
117
- expandTransition
118
- })
119
-
120
- defineExpose<_CollapseExposed>({
121
- toggle,
122
- expand,
123
- collapse,
124
- expandAll,
125
- collapseAll
117
+ expandTransition,
118
+ register,
119
+ unregister
126
120
  })
127
121
  </script>
@@ -1,16 +1,18 @@
1
1
  import { type BEM, type ComponentSize, ExpandTransition } from '@veltra/utils'
2
2
  import type { Component, ComputedRef, InjectionKey } from 'vue'
3
3
 
4
- import type { CollapseIconPosition, CollapseValue } from '../../types'
4
+ import type { CollapseValue } from '../../types'
5
5
 
6
6
  export interface CollapseContext {
7
7
  cls: BEM<'collapse'>
8
8
  size: ComputedRef<ComponentSize>
9
- iconPosition: ComputedRef<CollapseIconPosition>
9
+ bordered: ComputedRef<boolean>
10
10
  expandIcon: ComputedRef<Component | undefined>
11
11
  activeValues: ComputedRef<CollapseValue[]>
12
12
  toggle: (value: CollapseValue) => void
13
13
  expandTransition: ExpandTransition
14
+ register: (value: CollapseValue) => void
15
+ unregister: (value: CollapseValue) => void
14
16
  }
15
17
 
16
18
  export const CollapseDIKey: InjectionKey<CollapseContext> = Symbol('Collapse')
@@ -10,54 +10,48 @@ $ease: cubic-bezier(0.4, 0, 0.2, 1);
10
10
  $transition-fast: 0.18s $ease;
11
11
  $transition-normal: 0.24s $ease;
12
12
 
13
- // 各尺寸的水平 / 垂直内边距:决定 header 和 content 的呼吸感
14
- // 节奏在配置项分组场景下既不过紧、也不过松
15
- $header-padding-x: (
16
- small: 12px,
17
- default: 16px,
18
- large: 20px
19
- );
20
- $header-padding-y: (
21
- small: 10px,
22
- default: 14px,
23
- large: 18px
24
- );
25
-
26
13
  @include m.b($root-name) {
27
14
  width: 100%;
28
15
  font-size: fn.use-var(font-size-main, default);
29
16
  color: fn.use-var(text-color, main);
17
+ background-color: transparent;
30
18
 
31
- // ===== Bordered(默认):整体一张卡片,项之间细线分隔 =====
32
- // 面向"配置项分组"场景:清晰边界 + 克制反馈,不抢内部控件视觉
33
- @include m.is(bordered) {
34
- border: 1px solid fn.use-var(border, color);
35
- border-radius: calc(fn.use-var(radius, large) + 2px);
36
- background-color: fn.use-var(bg-color, top);
37
- box-shadow:
38
- 0 1px 2px rgba(15, 23, 42, 0.04),
39
- 0 1px 3px rgba(15, 23, 42, 0.04);
40
- overflow: hidden;
19
+ // ===== Item =====
20
+ @include m.e(item) {
21
+ border-radius: calc(fn.use-var(radius, default) + 2px);
22
+ background-color: fn.use-var(bg-color, hover);
23
+ transition: background-color $transition-fast;
41
24
 
42
- @include m.e(item) {
43
- &:not(:first-child) {
44
- border-top: 1px solid color-mix(in srgb, #{fn.use-var(border, color)} 60%, transparent);
25
+ & + #{fn.bem($root-name, item)} {
26
+ margin-top: fn.use-var(gap, default);
27
+ }
28
+
29
+ @include m.is-not(disabled) {
30
+ &:hover {
31
+ // hover 状态与展开状态保持一致的微加深背景(混入 6% 的正文文字色)
32
+ background-color: color-mix(
33
+ in srgb,
34
+ #{fn.use-var(text-color, main)} 6%,
35
+ #{fn.use-var(bg-color, hover)}
36
+ );
45
37
  }
46
38
  }
47
- }
48
39
 
49
- // ===== Item 通用 =====
50
- @include m.e(item) {
40
+ @include m.is(active) {
41
+ // 展开/活动状态背景(混入 6% 的正文文字色)
42
+ background-color: color-mix(
43
+ in srgb,
44
+ #{fn.use-var(text-color, main)} 6%,
45
+ #{fn.use-var(bg-color, hover)}
46
+ );
47
+ }
48
+
51
49
  @include m.is(disabled) {
52
50
  opacity: 0.55;
53
51
  }
54
52
  }
55
53
 
56
54
  // ===== Header =====
57
- // 反馈策略:
58
- // - hover:仅背景轻微变化("我可点击"信号)
59
- // - active:仅 font-weight 加重 + 箭头旋转("我已展开"信号由内容显现承担)
60
- // - 颜色不切换到 primary,避免在大量配置块同时展开时产生视觉嘈杂
61
55
  @include m.e(header) {
62
56
  position: relative;
63
57
  display: flex;
@@ -72,12 +66,7 @@ $header-padding-y: (
72
66
  transition:
73
67
  color $transition-fast,
74
68
  background-color $transition-fast;
75
-
76
- @include m.is-not(disabled) {
77
- &:hover {
78
- background-color: fn.use-var(bg-color, hover);
79
- }
80
- }
69
+ border-radius: inherit;
81
70
 
82
71
  &:focus-visible {
83
72
  box-shadow: inset 0 0 0 2px fn.use-var(color, primary);
@@ -86,11 +75,6 @@ $header-padding-y: (
86
75
  @include m.is(disabled) {
87
76
  cursor: not-allowed;
88
77
  color: fn.use-var(text-color, placeholder);
89
-
90
- &:hover {
91
- background-color: transparent;
92
- color: fn.use-var(text-color, placeholder);
93
- }
94
78
  }
95
79
 
96
80
  @include m.is(active) {
@@ -124,12 +108,6 @@ $header-padding-y: (
124
108
  }
125
109
 
126
110
  // ===== Content wrapper =====
127
- // 高度由共享 expand transition hook 在 isActive 切换时驱动:
128
- // 未挂载前 → CSS 兜底 height: 0
129
- // 未激活 → 0px(带 transition)
130
- // 激活中 → 0 → scrollHeight(CSS transition: height)
131
- // 已激活 → auto(动画结束后回归,避免锁死内容动态变化)
132
- // 比 grid-template-rows: 0fr ↔ 1fr 减少每帧 grid 布局重算,嵌套场景显著更流畅。
133
111
  @include m.e(content-wrapper) {
134
112
  overflow: hidden;
135
113
  height: 0;
@@ -142,50 +120,45 @@ $header-padding-y: (
142
120
  }
143
121
 
144
122
  // ===== Sizes =====
145
- // padding-bottom 始终生效;inactive 时由 wrapper height: 0 + overflow: hidden
146
- // 自然裁切,scrollHeight 计算因此始终一致,避免动画首帧抖动。
147
123
  @include m.size using ($size) {
148
124
  font-size: fn.use-var(font-size-main, $size);
149
125
 
150
126
  @include m.bem($root-name, header) {
151
- padding: map.get($header-padding-y, $size) map.get($header-padding-x, $size);
127
+ padding: calc(fn.use-var(gap, $size) - 2px) fn.use-var(gap, $size);
152
128
  font-size: fn.use-var(font-size-title, $size);
153
129
  }
154
130
 
155
131
  @include m.bem($root-name, content) {
156
132
  font-size: fn.use-var(font-size-main, $size);
157
- padding: 0 map.get($header-padding-x, $size) map.get($header-padding-y, $size);
133
+ padding: calc(fn.use-var(gap, $size) - 2px) fn.use-var(gap, $size) fn.use-var(gap, $size);
158
134
  }
159
- }
160
-
161
- // ===== Ghost(无边框):项级胶囊化,反馈完全由 item hover 承担 =====
162
- // 嵌入卡片 / 对话框时使用,不引入额外的容器边界
163
- @include m.is-not(bordered) {
164
- background-color: transparent;
165
135
 
166
136
  #{fn.bem($root-name, item)} {
167
- border-radius: calc(fn.use-var(radius, default) + 2px);
168
- transition: background-color $transition-fast;
137
+ border-radius: calc(fn.use-var(radius, $size) + 2px);
169
138
 
170
139
  & + #{fn.bem($root-name, item)} {
171
- margin-top: 4px;
140
+ margin-top: fn.use-var(gap, $size);
172
141
  }
142
+ }
143
+ }
173
144
 
174
- @include m.is-not(disabled) {
175
- &:hover {
176
- background-color: fn.use-var(bg-color, hover);
177
- }
145
+ // ===== Bordered =====
146
+ @include m.is(bordered) {
147
+ #{fn.bem($root-name, item)} {
148
+ background-color: transparent;
149
+ border: 1px solid vars.$border-color;
150
+
151
+ &:hover,
152
+ &.is-active {
153
+ background-color: transparent;
178
154
  }
179
155
  }
180
156
 
181
- // ghost 下 header 自身不再单独 hover,由 item 整体承载
182
157
  #{fn.bem($root-name, header)} {
183
- border-radius: inherit;
158
+ border-radius: 0;
184
159
 
185
- @include m.is-not(disabled) {
186
- &:hover {
187
- background-color: transparent;
188
- }
160
+ @include m.is(active) {
161
+ border-bottom: 1px solid vars.$border-color;
189
162
  }
190
163
  }
191
164
  }
@@ -1,4 +1,5 @@
1
1
  import '../scroll/style'
2
2
  import '../icon/style'
3
3
  import '@veltra/directives/ripple/style.js'
4
+ import '@veltra/styles/anime/fade.scss'
4
5
  import './style.scss'
@@ -24,14 +24,12 @@
24
24
  @close="onClose"
25
25
  />
26
26
 
27
- <div v-if="hasContentSlots" :class="cls.e('content')">
28
- <transition name="fade" mode="out-in">
29
- <keep-alive v-if="keepAlive">
30
- <component :key="model" :is="renderContent()" />
31
- </keep-alive>
32
- <component v-else :key="model" :is="renderContent()" />
33
- </transition>
34
- </div>
27
+ <transition name="fade" mode="out-in">
28
+ <KeepAlive v-if="keepAlive">
29
+ <component :key="model" :is="renderSlots()" />
30
+ </KeepAlive>
31
+ <component v-else :key="model" :is="renderSlots()" />
32
+ </transition>
35
33
  </div>
36
34
  </template>
37
35
 
@@ -78,18 +76,15 @@ const verticalPosition = computed<'left' | 'right'>(() =>
78
76
  props.position === 'right' ? 'right' : 'left'
79
77
  )
80
78
 
81
- /** 是否存在任一内容面板插槽;没有则不渲染内容区域。 */
82
- const hasContentSlots = computed(() => Object.keys(slots).length > 0)
83
-
84
79
  const onClick = (item: TabItem, index: number) => emit('click', item, index)
85
80
  const onClose = (item: TabItem, index: number) => emit('close', item, index)
86
81
 
87
- const renderContent = () => {
82
+ const renderSlots = () => {
88
83
  const key = model.value
89
84
  if (!key) return null
90
85
  const nodes = slots[key]?.({ key })
91
86
  if (Array.isArray(nodes)) {
92
- return createVNode(UScroll, null, { default: () => nodes })
87
+ return createVNode(UScroll, { class: cls.e('content') }, { default: () => nodes })
93
88
  }
94
89
  return nodes
95
90
  }