@veltra/desktop 1.0.8 → 1.0.10

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 (220) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.vue.d.ts +3 -3
  3. package/dist/components/auto-complete/auto-complete.vue.d.ts +1 -1
  4. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit.vue.d.ts +2 -2
  6. package/dist/components/batch-edit/use-edit.js.map +1 -1
  7. package/dist/components/button/button.js.map +1 -1
  8. package/dist/components/button/button.vue.d.ts +1 -1
  9. package/dist/components/cascade/cascade.js +15 -13
  10. package/dist/components/cascade/cascade.js.map +1 -1
  11. package/dist/components/cascade/cascade.vue.d.ts +7 -7
  12. package/dist/components/cascade/node.js.map +1 -1
  13. package/dist/components/cascade/use-check.js +3 -2
  14. package/dist/components/cascade/use-check.js.map +1 -1
  15. package/dist/components/cascade/use-select.js +3 -3
  16. package/dist/components/cascade/use-select.js.map +1 -1
  17. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  18. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  19. package/dist/components/date-panel/date-panel.js.map +1 -1
  20. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  21. package/dist/components/date-picker/date-picker.js +9 -9
  22. package/dist/components/date-picker/date-picker.js.map +1 -1
  23. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  24. package/dist/components/date-range-picker/date-range-picker.js +9 -9
  25. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  26. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  27. package/dist/components/drawer/drawer.js.map +1 -1
  28. package/dist/components/empty/empty.vue.d.ts +1 -1
  29. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  30. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +1 -1
  31. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +1 -1
  32. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +1 -1
  33. package/dist/components/expression-editor/use-decorators.js.map +1 -1
  34. package/dist/components/expression-editor/use-expression-drag-drop.js.map +1 -1
  35. package/dist/components/file-picker/file-picker.js.map +1 -1
  36. package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
  37. package/dist/components/file-picker/helper.js +19 -7
  38. package/dist/components/file-picker/helper.js.map +1 -1
  39. package/dist/components/file-viewer/file-viewer.js +294 -0
  40. package/dist/components/file-viewer/file-viewer.js.map +1 -0
  41. package/dist/components/file-viewer/file-viewer.vue.d.ts +42 -0
  42. package/dist/components/file-viewer/helper.js +185 -0
  43. package/dist/components/file-viewer/helper.js.map +1 -0
  44. package/dist/components/file-viewer/index.d.ts +1 -0
  45. package/dist/components/file-viewer/previewers/docx-previewer.js +76 -0
  46. package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -0
  47. package/dist/components/file-viewer/previewers/image-previewer.js +64 -0
  48. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -0
  49. package/dist/components/file-viewer/previewers/pdf-previewer.js +90 -0
  50. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -0
  51. package/dist/components/file-viewer/previewers/sheet-previewer.js +175 -0
  52. package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -0
  53. package/dist/components/file-viewer/previewers/text-previewer.js +74 -0
  54. package/dist/components/file-viewer/previewers/text-previewer.js.map +1 -0
  55. package/dist/components/file-viewer/previewers/video-previewer.js +62 -0
  56. package/dist/components/file-viewer/previewers/video-previewer.js.map +1 -0
  57. package/dist/components/file-viewer/style.d.ts +1 -0
  58. package/dist/components/file-viewer/style.js +4 -0
  59. package/dist/components/file-viewer/style2.css +445 -0
  60. package/dist/components/form/form-model.js +2 -1
  61. package/dist/components/form/form-model.js.map +1 -1
  62. package/dist/components/grid/grid.js.map +1 -1
  63. package/dist/components/grid/use-responsive.js.map +1 -1
  64. package/dist/components/grid-input/grid-input.vue.d.ts +1 -1
  65. package/dist/components/index.d.ts +12 -9
  66. package/dist/components/input/input.vue.d.ts +3 -3
  67. package/dist/components/list/list.js.map +1 -1
  68. package/dist/components/loading/directive.js.map +1 -1
  69. package/dist/components/menu/menu-item.js.map +1 -1
  70. package/dist/components/message/message2.js.map +1 -1
  71. package/dist/components/multi-select/multi-select-option.js +14 -10
  72. package/dist/components/multi-select/multi-select-option.js.map +1 -1
  73. package/dist/components/multi-select/multi-select.js +16 -19
  74. package/dist/components/multi-select/multi-select.js.map +1 -1
  75. package/dist/components/multi-select/multi-select.vue.d.ts +4 -4
  76. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +7 -7
  77. package/dist/components/notification/index.js.map +1 -1
  78. package/dist/components/notification/notification.vue.d.ts +1 -1
  79. package/dist/components/number-input/number-input.js.map +1 -1
  80. package/dist/components/number-input/number-input.vue.d.ts +1 -1
  81. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  82. package/dist/components/palette/color-transform.js.map +1 -1
  83. package/dist/components/palette/di.js.map +1 -1
  84. package/dist/components/palette/palette-alpha.js +7 -10
  85. package/dist/components/palette/palette-alpha.js.map +1 -1
  86. package/dist/components/palette/palette-hue.js +5 -7
  87. package/dist/components/palette/palette-hue.js.map +1 -1
  88. package/dist/components/palette/palette-sv.js +8 -10
  89. package/dist/components/palette/palette-sv.js.map +1 -1
  90. package/dist/components/palette/palette.js +22 -24
  91. package/dist/components/palette/palette.js.map +1 -1
  92. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  93. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +2 -2
  94. package/dist/components/progress/progress.js +1 -1
  95. package/dist/components/progress/progress.js.map +1 -1
  96. package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
  97. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  98. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  99. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  100. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  101. package/dist/components/scroll/scroll.js.map +1 -1
  102. package/dist/components/select/select.js +14 -12
  103. package/dist/components/select/select.js.map +1 -1
  104. package/dist/components/select/select.vue.d.ts +3 -3
  105. package/dist/components/slider/slider-thumb.js +10 -13
  106. package/dist/components/slider/slider-thumb.js.map +1 -1
  107. package/dist/components/slider/slider.js +12 -15
  108. package/dist/components/slider/slider.js.map +1 -1
  109. package/dist/components/table/di.js.map +1 -1
  110. package/dist/components/table/node/row.d.ts +10 -1
  111. package/dist/components/table/node/row.js +9 -0
  112. package/dist/components/table/node/row.js.map +1 -1
  113. package/dist/components/table/table-body.js +25 -26
  114. package/dist/components/table/table-body.js.map +1 -1
  115. package/dist/components/table/table-cell.js +16 -9
  116. package/dist/components/table/table-cell.js.map +1 -1
  117. package/dist/components/table/table-row.js +77 -19
  118. package/dist/components/table/table-row.js.map +1 -1
  119. package/dist/components/table/table.js +75 -34
  120. package/dist/components/table/table.js.map +1 -1
  121. package/dist/components/table/table.vue.d.ts +2 -2
  122. package/dist/components/table/use-columns.js +4 -0
  123. package/dist/components/table/use-columns.js.map +1 -1
  124. package/dist/components/table/use-fixed-columns.js +27 -8
  125. package/dist/components/table/use-fixed-columns.js.map +1 -1
  126. package/dist/components/table/use-rows.js +2 -2
  127. package/dist/components/table/use-rows.js.map +1 -1
  128. package/dist/components/table/use-table.js +50 -3
  129. package/dist/components/table/use-table.js.map +1 -1
  130. package/dist/components/tabs/index.d.ts +3 -1
  131. package/dist/components/tabs/style2.css +170 -107
  132. package/dist/components/tabs/tabs-horizontal.js +133 -0
  133. package/dist/components/tabs/tabs-horizontal.js.map +1 -0
  134. package/dist/components/tabs/tabs-horizontal.vue.d.ts +40 -0
  135. package/dist/components/tabs/tabs-vertical.js +89 -0
  136. package/dist/components/tabs/tabs-vertical.js.map +1 -0
  137. package/dist/components/tabs/tabs-vertical.vue.d.ts +38 -0
  138. package/dist/components/tabs/tabs.js +75 -105
  139. package/dist/components/tabs/tabs.js.map +1 -1
  140. package/dist/components/tabs/tabs.vue.d.ts +9 -23
  141. package/dist/components/tabs/use-tabs-bar.js +105 -0
  142. package/dist/components/tabs/use-tabs-bar.js.map +1 -0
  143. package/dist/components/text/text.js.map +1 -1
  144. package/dist/components/textarea/textarea.js.map +1 -1
  145. package/dist/components/textarea/textarea.vue.d.ts +4 -4
  146. package/dist/components/theme/theme.js.map +1 -1
  147. package/dist/components/tip/tip.vue.d.ts +1 -1
  148. package/dist/components/tree/style2.css +45 -23
  149. package/dist/components/tree/tree-node.js +50 -35
  150. package/dist/components/tree/tree-node.js.map +1 -1
  151. package/dist/components/tree/tree-node2.js.map +1 -1
  152. package/dist/components/tree/tree.js +27 -20
  153. package/dist/components/tree/tree.js.map +1 -1
  154. package/dist/components/tree/tree.vue.d.ts +5 -5
  155. package/dist/components/tree/use-check.js +52 -14
  156. package/dist/components/tree/use-check.js.map +1 -1
  157. package/dist/components/tree/use-tree-nodes.js +51 -24
  158. package/dist/components/tree/use-tree-nodes.js.map +1 -1
  159. package/dist/components/tree-select/tree-select.vue.d.ts +7 -7
  160. package/dist/index.d.ts +57 -53
  161. package/dist/index.js +51 -48
  162. package/dist/types/file-viewer.d.ts +71 -0
  163. package/dist/types/index.d.ts +3 -2
  164. package/dist/types/tabs.d.ts +106 -14
  165. package/dist/types/tree.d.ts +6 -1
  166. package/package.json +32 -31
  167. package/src/components/cascade/cascade.vue +18 -18
  168. package/src/components/cascade/node.ts +4 -1
  169. package/src/components/cascade/use-check.ts +5 -4
  170. package/src/components/cascade/use-select.ts +4 -5
  171. package/src/components/date-picker/date-picker.vue +10 -9
  172. package/src/components/date-range-picker/date-range-picker.vue +12 -12
  173. package/src/components/file-picker/helper.ts +21 -10
  174. package/src/components/file-viewer/file-viewer.vue +311 -0
  175. package/src/components/file-viewer/helper.ts +193 -0
  176. package/src/components/file-viewer/index.ts +1 -0
  177. package/src/components/file-viewer/previewers/docx-previewer.vue +78 -0
  178. package/src/components/file-viewer/previewers/image-previewer.vue +64 -0
  179. package/src/components/file-viewer/previewers/pdf-previewer.vue +90 -0
  180. package/src/components/file-viewer/previewers/sheet-previewer.vue +210 -0
  181. package/src/components/file-viewer/previewers/text-previewer.vue +76 -0
  182. package/src/components/file-viewer/previewers/video-previewer.vue +72 -0
  183. package/src/components/file-viewer/style.scss +504 -0
  184. package/src/components/file-viewer/style.ts +4 -0
  185. package/src/components/index.ts +2 -0
  186. package/src/components/multi-select/multi-select-option.vue +16 -3
  187. package/src/components/multi-select/multi-select.vue +17 -9
  188. package/src/components/palette/di.ts +5 -3
  189. package/src/components/palette/palette-alpha.vue +7 -10
  190. package/src/components/palette/palette-hue.vue +5 -7
  191. package/src/components/palette/palette-sv.vue +12 -14
  192. package/src/components/palette/palette.vue +19 -21
  193. package/src/components/progress/progress.vue +1 -1
  194. package/src/components/select/select.vue +21 -19
  195. package/src/components/slider/slider-thumb.vue +11 -14
  196. package/src/components/slider/slider.vue +15 -15
  197. package/src/components/table/di.ts +58 -43
  198. package/src/components/table/node/row.ts +10 -1
  199. package/src/components/table/table-body.vue +28 -33
  200. package/src/components/table/table-cell.vue +17 -9
  201. package/src/components/table/table-row.tsx +83 -17
  202. package/src/components/table/table.vue +72 -45
  203. package/src/components/table/use-fixed-columns.ts +46 -12
  204. package/src/components/table/use-rows.ts +14 -3
  205. package/src/components/table/use-table.ts +27 -3
  206. package/src/components/tabs/index.ts +2 -0
  207. package/src/components/tabs/style.scss +219 -133
  208. package/src/components/tabs/tabs-horizontal.vue +128 -0
  209. package/src/components/tabs/tabs-vertical.vue +77 -0
  210. package/src/components/tabs/tabs.vue +56 -127
  211. package/src/components/tabs/use-tabs-bar.ts +119 -0
  212. package/src/components/tree/style.scss +74 -29
  213. package/src/components/tree/tree-node.vue +10 -4
  214. package/src/components/tree/tree.vue +39 -12
  215. package/src/components/tree/use-check.ts +79 -23
  216. package/src/components/tree/use-tree-nodes.ts +85 -34
  217. package/src/types/file-viewer.ts +69 -0
  218. package/src/types/index.ts +1 -0
  219. package/src/types/tabs.ts +113 -19
  220. package/src/types/tree.ts +6 -1
@@ -1,167 +1,96 @@
1
1
  <template>
2
- <div :class="[cls.b, cls.m(position!), cls.m(size)]">
3
- <ul :class="[cls.e('header'), cls.em('header', position!)]" ref="headerRef">
4
- <li
5
- v-for="(item, index) in tabItems"
6
- :key="item.key"
7
- :class="[
8
- cls.e('header-item'),
9
- bem.is('active', model === item.key),
10
- bem.is('disabled', item.disabled === true)
11
- ]"
12
- v-ripple="item.disabled ? false : cls.e('ripple')"
13
- @click.stop="handleClick(item, index)"
14
- >
15
- <slot :name="`name:${item.key}`">
16
- {{ item.name }}
17
- </slot>
18
-
19
- <template v-if="editable">
20
- <u-icon
21
- v-if="!item.disabled"
22
- :class="cls.e('close')"
23
- @click.stop="handleClose(item, index)"
24
- @mousedown.stop
25
- >
26
- <Close />
27
- </u-icon>
28
- <i v-else :class="cls.e('close-placeholder')"></i>
29
- </template>
30
- </li>
31
-
32
- <li v-if="editable" :class="[cls.e('header-item'), cls.e('add')]" @click="handleAdd">
33
- <u-icon>
34
- <Plus />
35
- </u-icon>
36
- </li>
37
- </ul>
38
-
39
- <transition name="fade" mode="out-in">
40
- <KeepAlive v-if="keepAlive">
41
- <component :key="model" :is="renderSlots()" />
42
- </KeepAlive>
43
- <component v-else :key="model" :is="renderSlots()" />
44
- </transition>
2
+ <div :class="[cls.b, cls.m(position!)]">
3
+ <u-tabs-horizontal
4
+ v-if="isHorizontal"
5
+ v-model="model"
6
+ :items="items"
7
+ :position="horizontalPosition"
8
+ :size="size"
9
+ :closable="closable"
10
+ :block="block"
11
+ :rounded="rounded"
12
+ @click="onClick"
13
+ @close="onClose"
14
+ />
15
+ <u-tabs-vertical
16
+ v-else
17
+ v-model="model"
18
+ :items="items"
19
+ :position="verticalPosition"
20
+ :size="size"
21
+ :closable="closable"
22
+ :rounded="rounded"
23
+ @click="onClick"
24
+ @close="onClose"
25
+ />
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>
45
35
  </div>
46
36
  </template>
47
37
 
48
38
  <script lang="ts" setup>
49
39
  import { useFallbackProps } from '@veltra/compositions'
50
- import { vRipple } from '@veltra/directives'
51
- import { Close, Plus } from '@veltra/icons/normal'
52
40
  import { bem } from '@veltra/utils'
53
- import { computed, createVNode, nextTick, shallowRef, watch, type CSSProperties } from 'vue'
41
+ import { computed, createVNode, useSlots } from 'vue'
54
42
 
55
- import type { TabItem, TabsProps, TabsEmits, ComponentSize } from '../../types'
56
- import { UIcon } from '../icon'
43
+ import type { ComponentSize, TabItem, TabsEmits, TabsProps } from '../../types'
57
44
  import { UScroll } from '../scroll'
45
+ import UTabsHorizontal from './tabs-horizontal.vue'
46
+ import UTabsVertical from './tabs-vertical.vue'
58
47
 
59
- defineOptions({
60
- name: 'Tabs'
61
- })
48
+ defineOptions({ name: 'Tabs' })
62
49
 
63
50
  const props = withDefaults(defineProps<TabsProps>(), {
64
- position: 'top'
51
+ position: 'top',
52
+ closable: false,
53
+ block: false,
54
+ rounded: false,
55
+ keepAlive: false
65
56
  })
66
57
 
67
58
  const emit = defineEmits<TabsEmits>()
68
59
 
69
- const slots = defineSlots<{
70
- [key: string]: (props: { key: string }) => any
71
- [key: `name:${string}`]: () => any
72
- }>()
60
+ const slots = useSlots() as Record<string, ((props?: any) => any) | undefined>
73
61
 
74
62
  const { size } = useFallbackProps([props], {
75
63
  size: 'default' as ComponentSize
76
64
  })
77
65
 
78
- const tabItems = computed(() => {
79
- return props.items
80
- })
81
-
82
66
  const cls = bem('tabs')
83
67
 
84
- const headerRef = shallowRef<HTMLElement>()
85
-
86
- /** 当前活动标签 */
87
- const index = shallowRef(-1)
88
68
  const model = defineModel<string>()
89
69
 
90
- const markStyle = shallowRef<CSSProperties>({})
70
+ const isHorizontal = computed(() => props.position === 'top' || props.position === 'bottom')
91
71
 
92
- let changedByEvent = false
93
- watch(
94
- [model, () => props.items],
95
- ([model, items]) => {
96
- if (changedByEvent) return
97
- index.value = items.findIndex((item) => item.key === model)
98
- },
99
- { immediate: true }
72
+ /** 避免模板中使用类型断言:收敛 position 到水平 / 垂直子组件所需的子集。 */
73
+ const horizontalPosition = computed<'top' | 'bottom'>(() =>
74
+ props.position === 'bottom' ? 'bottom' : 'top'
100
75
  )
101
76
 
102
- watch(
103
- [index, () => props.position, () => props.editable],
104
- async ([index, position, editable]) => {
105
- if (index === -1 || editable) return
106
- await nextTick()
107
-
108
- const headerEl = headerRef.value!
109
-
110
- const headerRect = headerEl.getBoundingClientRect()
111
- const el = headerEl.children[index]! as HTMLLIElement
112
- const { offsetWidth, offsetHeight } = el
113
-
114
- const rect = el.getBoundingClientRect()
115
-
116
- if (position === 'top' || position === 'bottom') {
117
- markStyle.value = {
118
- width: offsetWidth + 'px',
119
- transform: `translate3d(${rect.left - headerRect.left}px, 0, 0)`
120
- }
121
- } else {
122
- markStyle.value = {
123
- height: offsetHeight + 'px',
124
- transform: `translate3d(0, ${rect.top - headerRect.top}px, 0)`
125
- }
126
- }
127
- },
128
- { immediate: true }
77
+ const verticalPosition = computed<'left' | 'right'>(() =>
78
+ props.position === 'right' ? 'right' : 'left'
129
79
  )
130
80
 
131
- const handleClick = (item: TabItem, _index: number) => {
132
- if (item.disabled) return
81
+ /** 是否存在任一内容面板插槽;没有则不渲染内容区域。 */
82
+ const hasContentSlots = computed(() => Object.keys(slots).length > 0)
133
83
 
134
- changedByEvent = true
135
- index.value = _index
136
- model.value = item.key
137
- nextTick(() => {
138
- changedByEvent = false
139
- })
140
- }
84
+ const onClick = (item: TabItem, index: number) => emit('click', item, index)
85
+ const onClose = (item: TabItem, index: number) => emit('close', item, index)
141
86
 
142
- const renderSlots = () => {
87
+ const renderContent = () => {
143
88
  const key = model.value
144
89
  if (!key) return null
145
90
  const nodes = slots[key]?.({ key })
146
91
  if (Array.isArray(nodes)) {
147
- return createVNode(UScroll, { class: cls.e('content') }, { default: () => nodes })
92
+ return createVNode(UScroll, null, { default: () => nodes })
148
93
  }
149
94
  return nodes
150
95
  }
151
-
152
- /** 关闭标签 */
153
- const handleClose = (item: TabItem, index: number) => {
154
- const { items } = props
155
- if (model.value === item.key) {
156
- model.value = (items[index + 1] ?? items[index - 1])!.key
157
- }
158
-
159
- emit('update:items', [...items.slice(0, index), ...items.slice(index + 1)])
160
-
161
- emit('delete', item, index)
162
- }
163
-
164
- const handleAdd = () => {
165
- emit('create')
166
- }
167
96
  </script>
@@ -0,0 +1,119 @@
1
+ import { useResizeObserver } from '@veltra/compositions'
2
+ import {
3
+ type ModelRef,
4
+ type Ref,
5
+ type ShallowRef,
6
+ nextTick,
7
+ onBeforeUnmount,
8
+ onMounted,
9
+ shallowRef,
10
+ watch
11
+ } from 'vue'
12
+
13
+ import type { TabItem } from '../../types'
14
+
15
+ interface UseTabsBarOptions {
16
+ viewportRef: ShallowRef<HTMLElement | undefined>
17
+ listRef: ShallowRef<HTMLElement | undefined>
18
+ items: Ref<TabItem[]>
19
+ model: ModelRef<string | undefined>
20
+ }
21
+
22
+ /**
23
+ * 水平标签栏的溢出滚动逻辑:
24
+ * - 计算 nav 按钮显隐与可用状态
25
+ * - 提供按步滚动与活动标签自动滚入视野的方法
26
+ * - 绑定 wheel / scroll / resize 事件
27
+ */
28
+ export function useTabsBar(options: UseTabsBarOptions) {
29
+ const { viewportRef, listRef, items, model } = options
30
+
31
+ const showNav = shallowRef(false)
32
+ const canPrev = shallowRef(false)
33
+ const canNext = shallowRef(false)
34
+
35
+ /** 依据 viewport 与内容尺寸推导滚动按钮可用状态 */
36
+ const updateNavState = () => {
37
+ const vp = viewportRef.value
38
+ if (!vp) return
39
+ const { scrollLeft, scrollWidth, clientWidth } = vp
40
+ const overflowing = scrollWidth - clientWidth > 1
41
+ showNav.value = overflowing
42
+ canPrev.value = overflowing && scrollLeft > 0
43
+ canNext.value = overflowing && scrollLeft + clientWidth < scrollWidth - 1
44
+ }
45
+
46
+ /** 箭头按钮:按视口宽度 80% 的步长平滑滚动 */
47
+ const scrollByStep = (dir: 1 | -1) => {
48
+ const vp = viewportRef.value
49
+ if (!vp) return
50
+ vp.scrollTo({ left: vp.scrollLeft + dir * vp.clientWidth * 0.8, behavior: 'smooth' })
51
+ }
52
+
53
+ /** 活动标签自动滚入视野 */
54
+ const ensureActiveVisible = () => {
55
+ const vp = viewportRef.value
56
+ const list = listRef.value
57
+ if (!vp || !list) return
58
+ const activeKey = model.value
59
+ if (!activeKey) return
60
+ const activeIndex = items.value.findIndex((i) => i.key === activeKey)
61
+ if (activeIndex < 0) return
62
+ const el = list.children[activeIndex] as HTMLElement | undefined
63
+ if (!el) return
64
+
65
+ const vpRect = vp.getBoundingClientRect()
66
+ const elRect = el.getBoundingClientRect()
67
+ if (elRect.left < vpRect.left) {
68
+ vp.scrollTo({ left: vp.scrollLeft + (elRect.left - vpRect.left) - 8, behavior: 'smooth' })
69
+ } else if (elRect.right > vpRect.right) {
70
+ vp.scrollTo({ left: vp.scrollLeft + (elRect.right - vpRect.right) + 8, behavior: 'smooth' })
71
+ }
72
+ }
73
+
74
+ /** 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截 */
75
+ const handleWheel = (e: WheelEvent) => {
76
+ if (!showNav.value) return
77
+ const vp = viewportRef.value
78
+ if (!vp) return
79
+ if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return
80
+ if (e.deltaY === 0) return
81
+ e.preventDefault()
82
+ vp.scrollLeft += e.deltaY
83
+ }
84
+
85
+ useResizeObserver({ targets: [viewportRef, listRef], onResize: () => updateNavState() })
86
+
87
+ watch(model, async () => {
88
+ await nextTick()
89
+ ensureActiveVisible()
90
+ })
91
+
92
+ watch(
93
+ items,
94
+ async () => {
95
+ await nextTick()
96
+ updateNavState()
97
+ ensureActiveVisible()
98
+ },
99
+ { deep: false }
100
+ )
101
+
102
+ onMounted(() => {
103
+ const vp = viewportRef.value
104
+ if (!vp) return
105
+ vp.addEventListener('wheel', handleWheel, { passive: false })
106
+ vp.addEventListener('scroll', updateNavState, { passive: true })
107
+ updateNavState()
108
+ nextTick(ensureActiveVisible)
109
+ })
110
+
111
+ onBeforeUnmount(() => {
112
+ const vp = viewportRef.value
113
+ if (!vp) return
114
+ vp.removeEventListener('wheel', handleWheel)
115
+ vp.removeEventListener('scroll', updateNavState)
116
+ })
117
+
118
+ return { showNav, canPrev, canNext, updateNavState, scrollByStep, ensureActiveVisible }
119
+ }
@@ -1,19 +1,40 @@
1
1
  @use 'pkg:@veltra/styles/mixins' as m;
2
2
  @use 'pkg:@veltra/styles/functions' as fn;
3
+ @use 'sass:map';
3
4
 
4
5
  $root-name: tree;
5
6
 
7
+ // 每一级缩进与展开图标 / 叶子占位保持一致的宽度,
8
+ // 保证节点内容在各层之间横向对齐。
9
+ $indent-width: 20px;
10
+ $indent-leaf-width: 20px;
11
+ $expand-icon-size: 20px;
12
+ // 节点稳态高度,与 `tree.vue` 中 `estimateSize` 协同,用于虚拟滚动初始估算。
13
+ $node-min-height-small: 28px;
14
+ $node-min-height-default: 32px;
15
+ $node-min-height-large: 40px;
16
+ $node-min-heights: (
17
+ small: $node-min-height-small,
18
+ default: $node-min-height-default,
19
+ large: $node-min-height-large
20
+ );
21
+
6
22
  @include m.b($root-name) {
7
23
  @include m.size using ($size) {
8
24
  @include m.bem($root-name, node-content) {
9
25
  border-radius: fn.use-var(radius, $size);
10
26
  padding: fn.use-var(gap, $size);
11
27
  line-height: 1;
28
+ gap: fn.use-var(gap, $size);
12
29
  }
13
30
 
14
31
  @include m.bem($root-name, checkbox) {
15
32
  margin-right: fn.use-var(gap, $size);
16
33
  }
34
+
35
+ @include m.bem($root-name, node) {
36
+ min-height: map.get($node-min-heights, $size);
37
+ }
17
38
  }
18
39
 
19
40
  @include m.e(checkbox) {
@@ -32,8 +53,20 @@ $root-name: tree;
32
53
  width: 100%;
33
54
  }
34
55
  }
56
+
57
+ // 非虚拟模式通过相邻兄弟间距提供呼吸;
58
+ // 虚拟模式下改由 virtualizer 的 gap 参数统一负责。
59
+ &:not(.is-virtual) {
60
+ @include m.bem($root-name, node) {
61
+ & + & {
62
+ margin-top: 2px;
63
+ }
64
+ }
65
+ }
35
66
  }
36
67
 
68
+ // 选中 / 勾选态下,对节点内容提供 hover 反馈;
69
+ // 禁用节点不参与 hover 反馈,避免假性交互。
37
70
  @include m.is(selectable, checkable) {
38
71
  @include m.bem($root-name, node) {
39
72
  &:not(.is-disabled) {
@@ -47,15 +80,27 @@ $root-name: tree;
47
80
  }
48
81
 
49
82
  @include m.e(node) {
83
+ position: relative;
50
84
  cursor: pointer;
51
- margin-bottom: 0.2rem;
52
85
  @include m.flex($align: center);
53
86
 
54
87
  @include m.is(selected) {
88
+ // 左侧 3px 色带,配合淡色背景共同构成选中态。
89
+ &::before {
90
+ content: '';
91
+ position: absolute;
92
+ left: 0;
93
+ top: 4px;
94
+ bottom: 4px;
95
+ width: 3px;
96
+ border-radius: 3px;
97
+ background-color: fn.use-var(color, primary);
98
+ }
99
+
55
100
  @include m.bem($root-name, node-content) {
56
- background-color: fn.use-var(color, primary, light-9) !important;
101
+ background-color: fn.use-var(color, primary, light-9);
57
102
  color: fn.use-var(color, primary);
58
- font-weight: bold;
103
+ font-weight: 500;
59
104
  }
60
105
  }
61
106
 
@@ -67,43 +112,41 @@ $root-name: tree;
67
112
 
68
113
  @include m.is(disabled) {
69
114
  cursor: not-allowed;
115
+
70
116
  @include m.bem($root-name, node-content) {
71
117
  color: fn.use-var(text-color, disabled);
118
+
119
+ &:hover {
120
+ background-color: transparent;
121
+ }
72
122
  }
73
123
  }
74
124
  }
75
125
 
76
- @include m.e(expand-icon, icon-placeholder) {
126
+ // 纯缩进占位:每级 20px 空白,不绘制连接线,
127
+ // 视觉层级由缩进 + 展开图标 + 选中态共同表达。
128
+ @include m.e(indent) {
77
129
  flex-shrink: 0;
78
- width: 20px;
79
- height: 20px;
130
+ width: $indent-width;
131
+
132
+ @include m.em(indent, leaf) {
133
+ width: $indent-leaf-width;
134
+ }
80
135
  }
81
136
 
82
137
  @include m.e(expand-icon) {
83
138
  cursor: pointer;
84
- transition:
85
- transform 0.2s,
86
- background-color 0.2s;
87
139
  flex-shrink: 0;
88
- font-size: 20px;
140
+ width: $expand-icon-size;
141
+ height: $expand-icon-size;
142
+ font-size: $expand-icon-size;
89
143
  border-radius: 50%;
144
+ transition:
145
+ transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
146
+ background-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
90
147
 
91
148
  &:hover {
92
- background-color: fn.use-var(color, primary, light-9);
93
- }
94
- }
95
-
96
- @include m.e(icon-placeholder) {
97
- padding: 5px;
98
-
99
- &::before {
100
- content: '';
101
- box-sizing: border-box;
102
- width: 100%;
103
- height: 100%;
104
- display: block;
105
- border-radius: 50%;
106
- border: 2px solid fn.use-var(text-color, main);
149
+ background-color: fn.use-var(bg-color, hover);
107
150
  }
108
151
  }
109
152
 
@@ -111,17 +154,19 @@ $root-name: tree;
111
154
  flex-grow: 1;
112
155
  overflow: hidden;
113
156
  user-select: none;
114
- word-break: break-all;
115
- transition: background-color 0.25s;
157
+ word-break: break-word;
158
+ transition: background-color 0.2s ease;
116
159
  @include m.flex($align: center);
117
160
  }
118
161
 
119
162
  @include m.e(ripple) {
120
- background-color: fn.use-var(color, primary, light-7);
121
- opacity: 0.5;
163
+ background-color: fn.use-var(color, primary, light-5);
164
+ opacity: 0.25;
122
165
  }
123
166
 
124
167
  @include m.e(empty) {
125
168
  text-align: center;
169
+ padding: fn.use-var(gap, default) 0;
170
+ color: fn.use-var(text-color, secondary);
126
171
  }
127
172
  }
@@ -1,13 +1,14 @@
1
1
  <template>
2
2
  <div
3
3
  :class="[cls.e('node'), bem.is('expanded', node.expanded), bem.is('disabled', node.disabled)]"
4
- :style="{ paddingLeft: withUnit(node.depth * 20 - 20, 'px') }"
5
- :ref="measureElement"
4
+ :ref="measureRef"
6
5
  >
6
+ <span v-for="i in node.depth" :key="i" :class="cls.e('indent')" aria-hidden="true"></span>
7
+
7
8
  <u-icon v-if="!node.isLeaf" :class="cls.e('expand-icon')" @click.stop="toggleExpand">
8
9
  <ArrowRight />
9
10
  </u-icon>
10
- <i v-else :class="cls.e('icon-placeholder')"> </i>
11
+ <i v-else :class="[cls.e('indent'), cls.em('indent', 'leaf')]" aria-hidden="true"></i>
11
12
 
12
13
  <div
13
14
  :class="cls.e('node-content')"
@@ -35,7 +36,7 @@
35
36
  <script lang="ts" setup>
36
37
  import { vRipple } from '@veltra/directives'
37
38
  import { ArrowRight } from '@veltra/icons/normal'
38
- import { bem, withUnit } from '@veltra/utils'
39
+ import { bem } from '@veltra/utils'
39
40
  import { inject } from 'vue'
40
41
 
41
42
  import type { TreeNodeProps } from '../../types'
@@ -53,6 +54,11 @@ const props = defineProps<TreeNodeProps>()
53
54
  const { treeProps, treeEmit, cls, getTreeSlotsNode, getFlattedNodes, toggleCheck, handleSelect } =
54
55
  inject(TreeDIKey)!
55
56
 
57
+ function measureRef(el: unknown) {
58
+ if (typeof props.index !== 'number' || !props.measureElement) return
59
+ props.measureElement(props.index, el as Element | null)
60
+ }
61
+
56
62
  function toggleExpand() {
57
63
  const { node } = props
58
64
  node.expanded = !node.expanded
@@ -2,7 +2,6 @@
2
2
  <u-scroll
3
3
  :class="className"
4
4
  ref="scrollRef"
5
- :content-style="{ height: virtualEnabled ? withUnit(totalHeight, 'px') : undefined }"
6
5
  :content-class="[cls.e('wrap'), bem.is('virtual', virtualEnabled)]"
7
6
  >
8
7
  <template v-if="virtualEnabled">
@@ -11,7 +10,7 @@
11
10
  :node="node"
12
11
  :key="key"
13
12
  :class="bem.is('selected', node.data === selectedData)"
14
- :data-index="index"
13
+ :index="index"
15
14
  :measure-element="measureElement"
16
15
  :style="{ transform: `translateY(${offset}px)` }"
17
16
  />
@@ -32,9 +31,8 @@
32
31
  </template>
33
32
 
34
33
  <script lang="ts" setup>
35
- import { useFormComponent, useFormFallbackProps } from '@veltra/compositions'
36
- import { useVirtual } from '@veltra/compositions'
37
- import { bem, nextFrame, withUnit, scrollIntoContainerView } from '@veltra/utils'
34
+ import { useFormComponent, useFormFallbackProps, useVirtualizer } from '@veltra/compositions'
35
+ import { bem, nextFrame, scrollIntoContainerView } from '@veltra/utils'
38
36
  import { computed, provide, shallowRef, useSlots, watch, watchEffect, type VNode } from 'vue'
39
37
 
40
38
  import type { TreeProps, TreeEmit, _TreeExposed } from '../../types'
@@ -122,28 +120,57 @@ const { checkedData, toggleCheck } = useCheck({
122
120
  getFlattedNodes
123
121
  })
124
122
 
125
- const { totalHeight, virtualList, scrollTo, virtualEnabled, measureElement } = useVirtual({
123
+ const estimateSize = (): number => {
124
+ // 基于 style.scss 中节点稳态高度推算的默认值;
125
+ // 实际高度仍由 `measureElement` 通过 ResizeObserver 回填覆盖。
126
+ switch (size.value) {
127
+ case 'small':
128
+ return 32
129
+ case 'large':
130
+ return 44
131
+ default:
132
+ return 36
133
+ }
134
+ }
135
+
136
+ // 80 阈值内化为消费者侧语义;低于此值时不启用虚拟化。
137
+ const virtualEnabled = computed(() => nodes.value.length > 80)
138
+
139
+ const { virtualizer, items } = useVirtualizer({
126
140
  count: computed(() => nodes.value.length),
127
- estimateSize: () => 40,
141
+ scrollEl: () => scrollRef.value?.containerRef ?? null,
142
+ // 仅在虚拟化启用时把内容容器的 height 撑开到 totalSize;
143
+ // 关闭时传 null,hook 会清除此前写入的内联 height,回到 CSS 默认。
144
+ contentEl: () => (virtualEnabled.value ? (scrollRef.value?.contentRef ?? null) : null),
145
+ estimateSize,
128
146
  gap: 2,
129
- virtualThreshold: 80,
130
- scrollEl: computed(() => scrollRef.value?.containerRef ?? null)
147
+ // 以节点稳定 key 作为虚拟项身份,展开 / 收起 / 过滤引起的 nodes 数组变更
148
+ // 保留未移动节点的真实测量值,避免再次滚动首屏抖动。
149
+ getItemKey: (i) => nodes.value[i]?.key ?? i
131
150
  })
132
151
 
152
+ // 身份稳定性已由底层 `getItemKey` 保证;直接以 `node.key` 作为 Vue 层 key。
153
+ // `item.index` 仅作为 nodes / snapshot 竞态边缘的 fallback。
133
154
  const virtualNodes = computed(() => {
134
155
  const _nodes = nodes.value
135
-
136
- return virtualList.value.map((item) => {
156
+ return items.value.map((item) => {
137
157
  const node = _nodes[item.index]!
138
158
  return {
139
159
  node,
140
- key: node.key || item.key,
160
+ key: node?.key ?? item.index,
141
161
  offset: item.start,
142
162
  index: item.index
143
163
  }
144
164
  })
145
165
  })
146
166
 
167
+ const measureElement: (index: number, el: Element | null) => void = (index, el) =>
168
+ virtualizer.measureElement(index, el)
169
+
170
+ function scrollTo(index: number): void {
171
+ virtualizer.scrollToIndex(index, { align: 'center' })
172
+ }
173
+
147
174
  function scrollIntoView() {
148
175
  const { selectable, checkable, scrollToView } = props
149
176
  if (!scrollToView || (!selectable && !checkable) || !nodes.value) return