@veltra/desktop 1.0.8 → 1.0.9

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 (183) 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/style.js +2 -2
  5. package/dist/components/button/button.vue.d.ts +1 -1
  6. package/dist/components/cascade/cascade.js +15 -13
  7. package/dist/components/cascade/cascade.js.map +1 -1
  8. package/dist/components/cascade/cascade.vue.d.ts +4 -4
  9. package/dist/components/cascade/node.js.map +1 -1
  10. package/dist/components/cascade/use-check.js +3 -2
  11. package/dist/components/cascade/use-check.js.map +1 -1
  12. package/dist/components/cascade/use-select.js +3 -3
  13. package/dist/components/cascade/use-select.js.map +1 -1
  14. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  15. package/dist/components/date-picker/date-picker.js +9 -9
  16. package/dist/components/date-picker/date-picker.js.map +1 -1
  17. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  18. package/dist/components/date-range-picker/date-range-picker.js +9 -9
  19. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  20. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  21. package/dist/components/empty/empty.vue.d.ts +1 -1
  22. package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
  23. package/dist/components/file-picker/helper.js +19 -7
  24. package/dist/components/file-picker/helper.js.map +1 -1
  25. package/dist/components/file-viewer/file-viewer.js +294 -0
  26. package/dist/components/file-viewer/file-viewer.js.map +1 -0
  27. package/dist/components/file-viewer/file-viewer.vue.d.ts +42 -0
  28. package/dist/components/file-viewer/helper.js +185 -0
  29. package/dist/components/file-viewer/helper.js.map +1 -0
  30. package/dist/components/file-viewer/index.d.ts +1 -0
  31. package/dist/components/file-viewer/previewers/docx-previewer.js +76 -0
  32. package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -0
  33. package/dist/components/file-viewer/previewers/image-previewer.js +64 -0
  34. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -0
  35. package/dist/components/file-viewer/previewers/pdf-previewer.js +90 -0
  36. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -0
  37. package/dist/components/file-viewer/previewers/sheet-previewer.js +175 -0
  38. package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -0
  39. package/dist/components/file-viewer/previewers/text-previewer.js +74 -0
  40. package/dist/components/file-viewer/previewers/text-previewer.js.map +1 -0
  41. package/dist/components/file-viewer/previewers/video-previewer.js +62 -0
  42. package/dist/components/file-viewer/previewers/video-previewer.js.map +1 -0
  43. package/dist/components/file-viewer/style.d.ts +1 -0
  44. package/dist/components/file-viewer/style.js +4 -0
  45. package/dist/components/file-viewer/style2.css +445 -0
  46. package/dist/components/grid-input/grid-input.vue.d.ts +1 -1
  47. package/dist/components/index.d.ts +12 -9
  48. package/dist/components/input/input.vue.d.ts +1 -1
  49. package/dist/components/multi-select/multi-select-option.js +14 -10
  50. package/dist/components/multi-select/multi-select-option.js.map +1 -1
  51. package/dist/components/multi-select/multi-select.js +16 -19
  52. package/dist/components/multi-select/multi-select.js.map +1 -1
  53. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  54. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +4 -4
  55. package/dist/components/notification/notification.vue.d.ts +1 -1
  56. package/dist/components/number-input/number-input.vue.d.ts +1 -1
  57. package/dist/components/number-range-input/number-range-input.vue.d.ts +2 -2
  58. package/dist/components/palette/di.js.map +1 -1
  59. package/dist/components/palette/palette-alpha.js +7 -10
  60. package/dist/components/palette/palette-alpha.js.map +1 -1
  61. package/dist/components/palette/palette-hue.js +5 -7
  62. package/dist/components/palette/palette-hue.js.map +1 -1
  63. package/dist/components/palette/palette-sv.js +8 -10
  64. package/dist/components/palette/palette-sv.js.map +1 -1
  65. package/dist/components/palette/palette.js +22 -24
  66. package/dist/components/palette/palette.js.map +1 -1
  67. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  68. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  69. package/dist/components/progress-nodes/progress-nodes.vue.d.ts +3 -3
  70. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +2 -2
  71. package/dist/components/select/select.js +14 -12
  72. package/dist/components/select/select.js.map +1 -1
  73. package/dist/components/select/select.vue.d.ts +2 -2
  74. package/dist/components/slider/slider-thumb.js +10 -13
  75. package/dist/components/slider/slider-thumb.js.map +1 -1
  76. package/dist/components/slider/slider.js +12 -15
  77. package/dist/components/slider/slider.js.map +1 -1
  78. package/dist/components/table/di.js.map +1 -1
  79. package/dist/components/table/node/row.d.ts +10 -1
  80. package/dist/components/table/node/row.js +9 -0
  81. package/dist/components/table/node/row.js.map +1 -1
  82. package/dist/components/table/style.js +1 -1
  83. package/dist/components/table/table-body.js +25 -26
  84. package/dist/components/table/table-body.js.map +1 -1
  85. package/dist/components/table/table-cell.js +16 -9
  86. package/dist/components/table/table-cell.js.map +1 -1
  87. package/dist/components/table/table-row.js +78 -19
  88. package/dist/components/table/table-row.js.map +1 -1
  89. package/dist/components/table/table.js +76 -34
  90. package/dist/components/table/table.js.map +1 -1
  91. package/dist/components/table/use-fixed-columns.js +27 -8
  92. package/dist/components/table/use-fixed-columns.js.map +1 -1
  93. package/dist/components/table/use-rows.js +2 -2
  94. package/dist/components/table/use-rows.js.map +1 -1
  95. package/dist/components/table/use-table.js +25 -3
  96. package/dist/components/table/use-table.js.map +1 -1
  97. package/dist/components/tabs/index.d.ts +3 -1
  98. package/dist/components/tabs/style2.css +169 -107
  99. package/dist/components/tabs/tabs-horizontal.js +133 -0
  100. package/dist/components/tabs/tabs-horizontal.js.map +1 -0
  101. package/dist/components/tabs/tabs-horizontal.vue.d.ts +29 -0
  102. package/dist/components/tabs/tabs-vertical.js +89 -0
  103. package/dist/components/tabs/tabs-vertical.js.map +1 -0
  104. package/dist/components/tabs/tabs-vertical.vue.d.ts +28 -0
  105. package/dist/components/tabs/tabs.js +75 -105
  106. package/dist/components/tabs/tabs.js.map +1 -1
  107. package/dist/components/tabs/tabs.vue.d.ts +9 -23
  108. package/dist/components/tabs/use-tabs-bar.js +105 -0
  109. package/dist/components/tabs/use-tabs-bar.js.map +1 -0
  110. package/dist/components/textarea/textarea.vue.d.ts +1 -1
  111. package/dist/components/tip/tip.vue.d.ts +2 -2
  112. package/dist/components/tree/style2.css +45 -23
  113. package/dist/components/tree/tree-node.js +50 -35
  114. package/dist/components/tree/tree-node.js.map +1 -1
  115. package/dist/components/tree/tree.js +27 -20
  116. package/dist/components/tree/tree.js.map +1 -1
  117. package/dist/components/tree/tree.vue.d.ts +1 -1
  118. package/dist/components/tree/use-check.js +52 -14
  119. package/dist/components/tree/use-check.js.map +1 -1
  120. package/dist/components/tree/use-tree-nodes.js +51 -24
  121. package/dist/components/tree/use-tree-nodes.js.map +1 -1
  122. package/dist/components/tree-select/tree-select.vue.d.ts +3 -3
  123. package/dist/components/watermark/watermark.vue.d.ts +1 -1
  124. package/dist/index.d.ts +57 -53
  125. package/dist/index.js +51 -48
  126. package/dist/types/file-viewer.d.ts +71 -0
  127. package/dist/types/index.d.ts +3 -2
  128. package/dist/types/tabs.d.ts +106 -14
  129. package/dist/types/tree.d.ts +6 -1
  130. package/package.json +32 -31
  131. package/src/components/cascade/cascade.vue +18 -18
  132. package/src/components/cascade/node.ts +4 -1
  133. package/src/components/cascade/use-check.ts +5 -4
  134. package/src/components/cascade/use-select.ts +4 -5
  135. package/src/components/date-picker/date-picker.vue +10 -9
  136. package/src/components/date-range-picker/date-range-picker.vue +12 -12
  137. package/src/components/file-picker/helper.ts +21 -10
  138. package/src/components/file-viewer/file-viewer.vue +311 -0
  139. package/src/components/file-viewer/helper.ts +193 -0
  140. package/src/components/file-viewer/index.ts +1 -0
  141. package/src/components/file-viewer/previewers/docx-previewer.vue +78 -0
  142. package/src/components/file-viewer/previewers/image-previewer.vue +64 -0
  143. package/src/components/file-viewer/previewers/pdf-previewer.vue +90 -0
  144. package/src/components/file-viewer/previewers/sheet-previewer.vue +210 -0
  145. package/src/components/file-viewer/previewers/text-previewer.vue +76 -0
  146. package/src/components/file-viewer/previewers/video-previewer.vue +72 -0
  147. package/src/components/file-viewer/style.scss +504 -0
  148. package/src/components/file-viewer/style.ts +4 -0
  149. package/src/components/index.ts +2 -0
  150. package/src/components/multi-select/multi-select-option.vue +16 -3
  151. package/src/components/multi-select/multi-select.vue +17 -9
  152. package/src/components/palette/di.ts +5 -3
  153. package/src/components/palette/palette-alpha.vue +7 -10
  154. package/src/components/palette/palette-hue.vue +5 -7
  155. package/src/components/palette/palette-sv.vue +12 -14
  156. package/src/components/palette/palette.vue +19 -21
  157. package/src/components/select/select.vue +21 -19
  158. package/src/components/slider/slider-thumb.vue +11 -14
  159. package/src/components/slider/slider.vue +15 -15
  160. package/src/components/table/di.ts +20 -3
  161. package/src/components/table/node/row.ts +10 -1
  162. package/src/components/table/table-body.vue +28 -33
  163. package/src/components/table/table-cell.vue +17 -9
  164. package/src/components/table/table-row.tsx +83 -17
  165. package/src/components/table/table.vue +73 -45
  166. package/src/components/table/use-fixed-columns.ts +46 -12
  167. package/src/components/table/use-rows.ts +14 -3
  168. package/src/components/table/use-table.ts +27 -3
  169. package/src/components/tabs/index.ts +2 -0
  170. package/src/components/tabs/style.scss +217 -134
  171. package/src/components/tabs/tabs-horizontal.vue +125 -0
  172. package/src/components/tabs/tabs-vertical.vue +75 -0
  173. package/src/components/tabs/tabs.vue +56 -127
  174. package/src/components/tabs/use-tabs-bar.ts +119 -0
  175. package/src/components/tree/style.scss +74 -29
  176. package/src/components/tree/tree-node.vue +10 -4
  177. package/src/components/tree/tree.vue +39 -12
  178. package/src/components/tree/use-check.ts +79 -23
  179. package/src/components/tree/use-tree-nodes.ts +85 -34
  180. package/src/types/file-viewer.ts +69 -0
  181. package/src/types/index.ts +1 -0
  182. package/src/types/tabs.ts +113 -19
  183. package/src/types/tree.ts +6 -1
@@ -1,7 +1,6 @@
1
1
  import { ArrowRight } from '@veltra/icons/normal'
2
- import { bem } from '@veltra/utils'
3
- import { defineComponent, type DefineComponent, type VNodeRef } from 'vue'
4
- import { inject } from 'vue'
2
+ import { defineComponent, type DefineComponent } from 'vue'
3
+ import { inject, onUnmounted } from 'vue'
5
4
  import type { JSX } from 'vue/jsx-runtime'
6
5
 
7
6
  import type { TableRow as ITableRow } from '../../types'
@@ -10,14 +9,40 @@ import { UIcon } from '../icon'
10
9
  import { TableDIKey } from './di'
11
10
  import UTabelCell from './table-cell.vue'
12
11
 
13
- export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
12
+ /**
13
+ * A3:行修饰类组合静态查找表。
14
+ *
15
+ * 位模式:bit2 = isCurrent, bit1 = checked, bit0 = expanded。
16
+ * 与 `bem.is('...')` 对于 `'current' | 'checked' | 'expanded'` 的输出完全等价,
17
+ * 直接从 index 读字符串,消除每行渲染期间的字符串拼接和 3 次 bem.is 调用。
18
+ *
19
+ * 放在模块顶层使所有 TableRow 实例共享一份引用(不会随 setup 重建)。
20
+ */
21
+ const ROW_MOD_CLASS: readonly string[] = [
22
+ '',
23
+ 'is-expanded',
24
+ 'is-checked',
25
+ 'is-checked is-expanded',
26
+ 'is-current',
27
+ 'is-current is-expanded',
28
+ 'is-current is-checked',
29
+ 'is-current is-checked is-expanded'
30
+ ] as const
31
+
32
+ function rowModCls(row: ITableRow): string {
33
+ const i = (row.isCurrent ? 4 : 0) | (row.checked ? 2 : 0) | (row.expanded ? 1 : 0)
34
+ return ROW_MOD_CLASS[i]!
35
+ }
36
+
37
+ export const UTableRow: DefineComponent<{ row: ITableRow; index: number }> = defineComponent({
14
38
  name: 'TableRow',
15
39
 
16
- props: ['row'],
40
+ props: {
41
+ row: { type: Object as () => ITableRow, required: true },
42
+ index: { type: Number, required: true }
43
+ },
17
44
 
18
45
  setup(props) {
19
- const { row } = props
20
-
21
46
  const {
22
47
  cls,
23
48
  columnConfig,
@@ -27,12 +52,39 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
27
52
  handleRowClick,
28
53
  handleCellClick,
29
54
  tableProps,
30
- measureElement
55
+ measureElement,
56
+ getCellClass
31
57
  } = inject(TableDIKey)!
32
58
 
59
+ // A1:把 cls.e('row') / cls.e('expand-toggle') 等静态字符串提升到 setup 一次性计算,
60
+ // 避免每次 render 重新走 `cls.e` 拼接。
61
+ const rowBaseCls = cls.e('row')
62
+ const expandToggleCls = cls.e('expand-toggle')
63
+ const expandSpaceCls = cls.e('expand-space')
64
+
65
+ // A2:measureRef 按 index 缓存闭包。
66
+ // 行虚拟化下,同一 TableRow 实例的 props.index 大多稳定,但在虚拟化重排或展开行切换时
67
+ // 仍会发生变化;把 (index) → refFn 缓存起来,既保证引用稳定(利于 Vue 的 ref diff),
68
+ // 又避免在未来可能的"同实例跨 index 复用"情形下重复分配闭包。
69
+ const measureRefCache = new Map<number, (el: unknown) => void>()
70
+ const getMeasureRef = (index: number): ((el: unknown) => void) => {
71
+ let ref = measureRefCache.get(index)
72
+ if (ref === undefined) {
73
+ ref = (el: unknown) => {
74
+ measureElement(index, el as Element | null)
75
+ }
76
+ measureRefCache.set(index, ref)
77
+ }
78
+ return ref
79
+ }
80
+ onUnmounted(() => {
81
+ measureRefCache.clear()
82
+ })
83
+
33
84
  const { columns, expandColumn } = columnConfig
34
85
 
35
86
  return () => {
87
+ const { row } = props
36
88
  const _expandColumn = expandColumn.value
37
89
 
38
90
  let expandCell: JSX.Element | null = null
@@ -44,7 +96,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
44
96
  const marginLeft = tableProps.tree ? (row.depth - 1) * 14 : 0
45
97
  expandCell = (
46
98
  <UTabelCell
47
- column={_expandColumn}
99
+ cellClass={getCellClass(_expandColumn)}
48
100
  left={_expandColumn.style.left}
49
101
  right={_expandColumn.style.right}
50
102
  key={row.uid + _expandColumn.key}
@@ -53,7 +105,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
53
105
  {!row.isLeaf || (tableProps.expandable && !row.isExpandRow) ? (
54
106
  <UButton
55
107
  text
56
- class={cls.e('expand-toggle')}
108
+ class={expandToggleCls}
57
109
  type='primary'
58
110
  size='small'
59
111
  circle
@@ -68,7 +120,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
68
120
  </UIcon>
69
121
  </UButton>
70
122
  ) : (
71
- <i class={cls.e('expand-space')} style={`margin-left: ${marginLeft}px`}></i>
123
+ <i class={expandSpaceCls} style={`margin-left: ${marginLeft}px`}></i>
72
124
  )}
73
125
 
74
126
  {getColumnSlotsNode(expandCtx)}
@@ -77,11 +129,15 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
77
129
  }
78
130
  }
79
131
 
132
+ // A3:行 class 由 base + 3-bit 组合查找产出,避免 Vue 的 class 数组归一化开销。
133
+ const mod = rowModCls(row)
134
+ const rowCls = mod ? rowBaseCls + ' ' + mod : rowBaseCls
135
+
80
136
  return (
81
137
  <tr
82
- class={[cls.e('row'), bem.is('expanded', row.expanded), bem.is('checked', row.checked)]}
138
+ class={rowCls}
83
139
  onClick={(e) => handleRowClick(row, e)}
84
- ref={measureElement as VNodeRef}
140
+ ref={getMeasureRef(props.index)}
85
141
  key={row.uid}
86
142
  >
87
143
  {expandCell}
@@ -97,7 +153,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
97
153
 
98
154
  return (
99
155
  <UTabelCell
100
- column={column}
156
+ cellClass={getCellClass(column)}
101
157
  left={column.style.left}
102
158
  right={column.style.right}
103
159
  key={row.uid + column.key}
@@ -117,11 +173,21 @@ export const UTableRow: DefineComponent<{ row: ITableRow }> = defineComponent({
117
173
 
118
174
  export const UExpandTableRow = defineComponent({
119
175
  name: 'ExpandTableRow',
120
- props: ['row'],
176
+ props: {
177
+ row: { type: Object as () => ITableRow, required: true },
178
+ index: { type: Number, required: true }
179
+ },
121
180
  setup(props) {
122
181
  const { getExpandRowSlotsNode, cls, measureElement, columnConfig } = inject(TableDIKey)!
123
182
  const { leafColumns } = columnConfig
124
183
 
184
+ const rowBaseCls = cls.e('row')
185
+ const cellCls = cls.e('cell')
186
+
187
+ const measureRef = (el: unknown) => {
188
+ measureElement(props.index, el as Element | null)
189
+ }
190
+
125
191
  return () => {
126
192
  const { row } = props
127
193
  const node = getExpandRowSlotsNode({
@@ -131,8 +197,8 @@ export const UExpandTableRow = defineComponent({
131
197
  rowData: row.data
132
198
  })
133
199
  return (
134
- <tr class={cls.e('row')} ref={measureElement as VNodeRef}>
135
- <td colspan={leafColumns.value.length} class={cls.e('cell')}>
200
+ <tr class={rowBaseCls} ref={measureRef}>
201
+ <td colspan={leafColumns.value.length} class={cellCls}>
136
202
  {node}
137
203
  </td>
138
204
  </tr>
@@ -11,6 +11,7 @@
11
11
  :content-class="cls.e('content')"
12
12
  @scroll="handleScroll"
13
13
  >
14
+ {{ console.log(1) }}
14
15
  <table :class="cls.e('wrap')">
15
16
  <colgroup ref="colgroupRef">
16
17
  <col
@@ -25,7 +26,18 @@
25
26
  </colgroup>
26
27
  <UTableHead />
27
28
 
28
- <u-table-body ref="tableBody">
29
+ <!--
30
+ 虚拟滚动上占位:替代 tbody transform,避免 table-layout: fixed 下列宽抖动。
31
+ 高度由 useVirtualizer 命令式写入 `style.height`,不经过模板响应式绑定,
32
+ 因此滚动窗口变化不会引起表格模板重渲染。
33
+ -->
34
+ <tbody v-if="virtualEnabled" aria-hidden="true">
35
+ <tr ref="beforeSpacerRef">
36
+ <td :colspan="leafColumns.length" style="padding: 0; border: none"></td>
37
+ </tr>
38
+ </tbody>
39
+
40
+ <u-table-body>
29
41
  <slot name="body" :columns="leafColumns" :rows="rows" />
30
42
 
31
43
  <template #empty v-if="slots.empty">
@@ -33,8 +45,12 @@
33
45
  </template>
34
46
  </u-table-body>
35
47
 
36
- <!-- 占用空间,用来撑开表格高度 -->
37
- <tbody v-if="virtualEnabled" ref="spaceRef" :style="{ width: '1px' }"></tbody>
48
+ <!-- 虚拟滚动下占位(高度由 useVirtualizer 命令式写入) -->
49
+ <tbody v-if="virtualEnabled" aria-hidden="true">
50
+ <tr ref="afterSpacerRef">
51
+ <td :colspan="leafColumns.length" style="padding: 0; border: none"></td>
52
+ </tr>
53
+ </tbody>
38
54
 
39
55
  <u-table-foot ref="tableFoot">
40
56
  <slot name="foot" :columns="leafColumns" :rows="rows" />
@@ -50,9 +66,9 @@
50
66
  </template>
51
67
 
52
68
  <script lang="ts" setup>
53
- import { useFallbackProps, useVirtual } from '@veltra/compositions'
54
- import { bem, setStyles, withUnit } from '@veltra/utils'
55
- import { computed, nextTick, provide, shallowRef, toRef, useTemplateRef, watch } from 'vue'
69
+ import { useFallbackProps, useVirtualizer } from '@veltra/compositions'
70
+ import { bem, withUnit } from '@veltra/utils'
71
+ import { computed, provide, shallowRef, toRef, useTemplateRef, watch } from 'vue'
56
72
 
57
73
  import type {
58
74
  TableProps,
@@ -102,7 +118,16 @@ const cls = bem('table')
102
118
 
103
119
  const { size } = useFallbackProps([props], { size: 'default' as ComponentSize })
104
120
 
105
- // 行
121
+ const scrollRef = shallowRef<ScrollExposed>()
122
+
123
+ // 占位 tr 的 DOM 句柄;交给 useVirtualizer 直接命令式写 style.height。
124
+ const beforeSpacerRef = useTemplateRef<HTMLElement>('beforeSpacerRef')
125
+ const afterSpacerRef = useTemplateRef<HTMLElement>('afterSpacerRef')
126
+
127
+ // 行(useRows / useFixedColumns 需要 isScrolling,而虚拟化 hook 又依赖 rows.length;
128
+ // 先用中继占位,下方 isScrolling 到位后 watch 回灌)。
129
+ const isScrollingRelay = shallowRef(false)
130
+
106
131
  const {
107
132
  rows,
108
133
  toggleTreeRowExpand,
@@ -112,13 +137,11 @@ const {
112
137
  handleRowClick,
113
138
  handleCellClick,
114
139
  getRowByData
115
- } = useRows({ props, emit })
140
+ } = useRows({ props, emit, isScrolling: isScrollingRelay })
116
141
 
117
- // 选中
118
142
  const { createCheckColumn, createSelectColumn, clearChecked, clearSelected, checkedRows } =
119
143
  useCheck({ size, props, rows, rowForest, emit, cls })
120
144
 
121
- // 列
122
145
  const columnConfig = useColumns({
123
146
  props,
124
147
  createCheckColumn,
@@ -129,9 +152,10 @@ const columnConfig = useColumns({
129
152
 
130
153
  const { leafColumns } = columnConfig
131
154
 
132
- const { handleScroll, leftFixed, rightFixed } = useFixedColumns()
155
+ const { handleScroll, leftFixed, rightFixed } = useFixedColumns({
156
+ isScrolling: isScrollingRelay
157
+ })
133
158
 
134
- // 在表格中提供的通用方法和属性
135
159
  const {
136
160
  getColumnSlotsNode,
137
161
  getExpandRowSlotsNode,
@@ -141,49 +165,51 @@ const {
141
165
  getHeaderCellClass
142
166
  } = useTable({ props, cls, leftFixed, rightFixed })
143
167
 
144
- const scrollRef = shallowRef<ScrollExposed>()
145
-
146
168
  const { showResizeLine, resizeLineRef, colgroupRef } = useColResize({ scrollRef, leafColumns })
147
169
 
148
- const virtualCtx = useVirtual({
170
+ const { virtualizer, items, isScrolling } = useVirtualizer({
149
171
  count: computed(() => rows.value.length),
150
- scrollEl: computed(() => scrollRef.value?.containerRef ?? null),
151
- estimateSize: () => 52,
152
- virtualThreshold: toRef(props, 'virtualThreshold')
172
+ scrollEl: () => scrollRef.value?.containerRef ?? null,
173
+ beforeEl: beforeSpacerRef,
174
+ afterEl: afterSpacerRef,
175
+ estimateSize: () => 41,
176
+ // 注意:getItemKey 在构造期一次性写入底层 Virtualizer;闭包每次读的是当前 rows.value,
177
+ // rows 引用整体替换后 key 仍有效(无需再 setOptions)。
178
+ getItemKey: (i) => rows.value[i]?.uid ?? i
153
179
  })
154
180
 
155
- const { virtualList, totalHeight, virtualEnabled } = virtualCtx
156
-
157
- const spaceHeight = computed(() => {
158
- if (virtualList.value.length) {
159
- return (
160
- totalHeight.value -
161
- virtualList.value[virtualList.value.length - 1]!.end +
162
- virtualList.value[0]!.start
163
- )
164
- }
165
- return totalHeight.value
181
+ const virtualEnabled = computed(() => {
182
+ const t = props.virtualThreshold
183
+ return t ? rows.value.length > t : true
166
184
  })
167
185
 
168
- const spaceRef = shallowRef<HTMLElement>()
186
+ // 把虚拟化 isScrolling 回灌到中继,供 useRows / useFixedColumns 读取。
187
+ watch(isScrolling, (v) => {
188
+ isScrollingRelay.value = v
189
+ })
169
190
 
170
- watch(
171
- spaceHeight,
172
- (spaceHeight) => {
173
- nextTick(() => {
174
- spaceRef.value &&
175
- setStyles(spaceRef.value, { height: spaceHeight ? `${spaceHeight}px` : undefined })
176
- })
177
- },
178
- { immediate: true }
179
- )
191
+ /**
192
+ * E1:数据替换时的条件置顶。
193
+ *
194
+ * 旧实现无差别地 `scrollTo({ y: 0 })`,在「行内增删 / 排序 / 单行更新」等
195
+ * 场景会把用户当前阅读位置跳走;仅在「翻页 / 显著数据集替换」时才需要复位。
196
+ *
197
+ * 判定阈值:|Δlen| / max(newLen, oldLen) ≥ 0.5,即行数变化量超过更大侧的一半时视为显著变化。
198
+ * 选择 0.5 是因为典型的分页/搜索场景会带来 50% 以上的行数差(通常完全替换数据集),
199
+ * 而行内追加 / 删除个位数通常只带来 <5% 的变化;两者区分度足够宽。
200
+ */
201
+ const LENGTH_DELTA_RATIO_TO_RESET_SCROLL = 0.5
180
202
 
181
- const tableBodyRef = useTemplateRef('tableBody')
182
203
  watch(
183
204
  () => props.data,
184
- () => {
185
- tableBodyRef.value?.setBodyTransform(0)
186
- scrollRef.value?.scrollTo({ y: 0 })
205
+ (next, prev) => {
206
+ const nextLen = next?.length ?? 0
207
+ const prevLen = prev?.length ?? 0
208
+ const denom = Math.max(nextLen, prevLen, 1)
209
+ const ratio = Math.abs(nextLen - prevLen) / denom
210
+ if (ratio >= LENGTH_DELTA_RATIO_TO_RESET_SCROLL) {
211
+ scrollRef.value?.scrollTo({ y: 0 })
212
+ }
187
213
  }
188
214
  )
189
215
 
@@ -206,7 +232,9 @@ provide(TableDIKey, {
206
232
  getHeaderCellClass,
207
233
  // tipRef,
208
234
  toggleTreeRowExpand,
209
- ...virtualCtx
235
+ virtualList: items,
236
+ virtualEnabled,
237
+ measureElement: (index, el) => virtualizer.measureElement(index, el)
210
238
  })
211
239
 
212
240
  const tableFootRef = useTemplateRef('tableFoot')
@@ -1,5 +1,5 @@
1
1
  import { debounce } from '@cat-kit/core'
2
- import { type ShallowRef, ref } from 'vue'
2
+ import { type ShallowRef, type Ref, ref, watch } from 'vue'
3
3
 
4
4
  import type { ScrollPosition } from '../../types'
5
5
 
@@ -9,28 +9,62 @@ interface UseColumnFixedReturned {
9
9
  handleScroll: (e: Required<ScrollPosition>) => void
10
10
  }
11
11
 
12
+ interface Options {
13
+ /**
14
+ * D1:滚动中 (`true`) 暂缓 `leftFixed` / `rightFixed` 的切换,滚动结束后
15
+ * 一次性补算。
16
+ *
17
+ * 背景:固定列的阴影是通过在 cell 上切换 `is-last-fixed` / `is-first-fixed`
18
+ * 等 class 实现的,这些 class 会穿透到 `useTable.getCellClass` 的缓存键,
19
+ * 任何一次切换都会让全表的单元格类名失效重算。滚动本身并不需要实时更新阴影,
20
+ * 因此把切换推迟到 `isScrolling` 归零时做一次即可。
21
+ *
22
+ * 未传入时退化到旧行为(实时切换)。
23
+ */
24
+ isScrolling?: Ref<boolean>
25
+ }
26
+
12
27
  /**
13
28
  * 固定列
14
29
  * @returns
15
30
  */
16
- export function useFixedColumns(): UseColumnFixedReturned {
31
+ export function useFixedColumns(options: Options = {}): UseColumnFixedReturned {
32
+ const { isScrolling } = options
33
+
17
34
  const leftFixed = ref(false)
18
35
  const rightFixed = ref(false)
19
36
 
37
+ let pendingLeft: boolean | null = null
38
+ let pendingRight: boolean | null = null
39
+
40
+ const applyImmediate = (nextLeft: boolean, nextRight: boolean): void => {
41
+ if (nextLeft !== leftFixed.value) leftFixed.value = nextLeft
42
+ if (nextRight !== rightFixed.value) rightFixed.value = nextRight
43
+ pendingLeft = null
44
+ pendingRight = null
45
+ }
46
+
20
47
  const handleScroll = debounce((e: Required<ScrollPosition>) => {
21
- if (e.x > 0) {
22
- leftFixed.value = true
23
- } else {
24
- leftFixed.value = false
25
- }
48
+ const nextLeft = e.x > 0
49
+ // 精度修正:浏览器滚动条最后 1px 的误差
50
+ const nextRight = !(e.cw + e.x + 1 >= e.sw)
26
51
 
27
- // 精度修正
28
- if (e.cw + e.x + 1 >= e.sw) {
29
- rightFixed.value = false
30
- } else {
31
- rightFixed.value = true
52
+ if (isScrolling?.value) {
53
+ pendingLeft = nextLeft
54
+ pendingRight = nextRight
55
+ return
32
56
  }
57
+
58
+ applyImmediate(nextLeft, nextRight)
33
59
  }, 50)
34
60
 
61
+ if (isScrolling) {
62
+ watch(isScrolling, (v) => {
63
+ if (v) return
64
+ if (pendingLeft === null && pendingRight === null) return
65
+ applyImmediate(pendingLeft ?? leftFixed.value, pendingRight ?? rightFixed.value)
66
+ })
67
+ }
68
+
35
69
  return { leftFixed, rightFixed, handleScroll }
36
70
  }
@@ -1,6 +1,6 @@
1
1
  import { Forest, o } from '@cat-kit/core'
2
2
  import { useModel } from '@veltra/compositions'
3
- import { shallowRef, watch, type ShallowRef } from 'vue'
3
+ import { shallowRef, watch, type Ref, type ShallowRef } from 'vue'
4
4
 
5
5
  import type { TableColumn, TableEmits, TableProps, TableRow } from '../../types'
6
6
  import { TableRowNode } from './node/row'
@@ -8,6 +8,14 @@ import { TableRowNode } from './node/row'
8
8
  interface Options {
9
9
  props: TableProps
10
10
  emit: TableEmits
11
+ /**
12
+ * D2:滚动中 (`true`) 跳过 `currentRow` 高亮写入。
13
+ *
14
+ * 行点击处理会通过 `currentRow` watcher 把 `isCurrent` 切换到新行 / 旧行上,
15
+ * 触发对应 `<tr>` 的 class 重绘。在滚动期间 (移动端/触控板惯性滚动常见)
16
+ * 意外点击不应改变高亮,因此直接跳过;滚动结束后的正常点击仍会生效。
17
+ */
18
+ isScrolling?: Ref<boolean>
11
19
  }
12
20
 
13
21
  interface UseRowsReturned {
@@ -30,7 +38,7 @@ interface UseRowsReturned {
30
38
  }
31
39
 
32
40
  export function useRows(options: Options): UseRowsReturned {
33
- const { props, emit } = options
41
+ const { props, emit, isScrolling } = options
34
42
 
35
43
  // --- 状态定义 ---
36
44
 
@@ -217,7 +225,10 @@ export function useRows(options: Options): UseRowsReturned {
217
225
  }
218
226
 
219
227
  function handleRowClick(row: TableRow, e: MouseEvent): void {
220
- currentRow.value = currentRow.value === row ? undefined : row
228
+ // D2:滚动中的点击(多为惯性滚动误触)不应改变 currentRow 高亮。
229
+ if (!isScrolling?.value) {
230
+ currentRow.value = currentRow.value === row ? undefined : row
231
+ }
221
232
  emit('row-click', row, e)
222
233
  }
223
234
 
@@ -1,6 +1,6 @@
1
1
  import { o } from '@cat-kit/core'
2
2
  import { bem, type BEM } from '@veltra/utils'
3
- import { useSlots, type Ref } from 'vue'
3
+ import { useSlots, watch, type Ref } from 'vue'
4
4
 
5
5
  import type {
6
6
  TableColumnRenderContext,
@@ -87,12 +87,36 @@ export function useTable(options: Options): UseTableReturn {
87
87
  return classList.join(' ')
88
88
  }
89
89
 
90
+ /**
91
+ * B1: 单元格类名按 ColumnNode 身份缓存。
92
+ *
93
+ * 对同一列,类名只取决于 (column.fixed, column.isLastFixed, column.isFirstFixed,
94
+ * column.align, leftFixed, rightFixed)。其中前四个绑定在 ColumnNode 实例上,
95
+ * 后两个在 leftFixed/rightFixed 变化时失效缓存即可。用 WeakMap 以 ColumnNode
96
+ * 为键可避免字符串 key 的拼接/哈希开销,且列树重建时节点随 GC 自动释放。
97
+ */
98
+ let cellClassCache = new WeakMap<ColumnNode, string>()
99
+ let headerCellClassCache = new WeakMap<ColumnNode, string>()
100
+
101
+ watch([leftFixed, rightFixed], () => {
102
+ cellClassCache = new WeakMap()
103
+ headerCellClassCache = new WeakMap()
104
+ })
105
+
90
106
  const getCellClass = (column: ColumnNode): string => {
91
- return getCommonClassName(column) + ` ${bem.is(column.align)}`
107
+ const cached = cellClassCache.get(column)
108
+ if (cached !== undefined) return cached
109
+ const result = getCommonClassName(column) + ` ${bem.is(column.align)}`
110
+ cellClassCache.set(column, result)
111
+ return result
92
112
  }
93
113
 
94
114
  const getHeaderCellClass = (column: ColumnNode): string => {
95
- return getCommonClassName(column) + ` ${bem.is(column.headerAlign)}`
115
+ const cached = headerCellClassCache.get(column)
116
+ if (cached !== undefined) return cached
117
+ const result = getCommonClassName(column) + ` ${bem.is(column.headerAlign)}`
118
+ headerCellClassCache.set(column, result)
119
+ return result
96
120
  }
97
121
 
98
122
  const getCellCtx = (
@@ -1 +1,3 @@
1
1
  export { default as UTabs } from './tabs.vue'
2
+ export { default as UTabsHorizontal } from './tabs-horizontal.vue'
3
+ export { default as UTabsVertical } from './tabs-vertical.vue'