niuma-ui 1.0.0 → 1.0.2

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 (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -0,0 +1,320 @@
1
+ /**
2
+ * RsTable 列布局:拉满补宽、渲染列宽、fixed/overflow/前缀 lead 样式。
3
+ *
4
+ * 性能约定:
5
+ * - `emptyFillExtras` 使用冻结空对象恒等,避免无补宽时 renderColumnWidths 复制整表
6
+ * - 列 style Map 一次遍历构建,模板热路径按 key O(1) 取用
7
+ */
8
+
9
+ import { computed, type ComputedRef, type Ref } from 'vue'
10
+ import { measureRsTablePrefixWidth, RS_TABLE_PREFIX_COL_WIDTH } from './useRsTableColumnVirtual'
11
+ import {
12
+ fixedCellStyle,
13
+ parseColumnWidth,
14
+ resolveColumnStyle,
15
+ resolveFixedColumnStyles,
16
+ type RsTableColumn,
17
+ type RsTableRowData,
18
+ } from '../components/table-utils'
19
+
20
+ /** ComputedRef / Ref / getter 均可 */
21
+ type Source<T> = ComputedRef<T> | Ref<T> | (() => T)
22
+
23
+ function read<T>(source: Source<T>): T {
24
+ if (typeof source === 'function') return (source as () => T)()
25
+ return (source as Ref<T> | ComputedRef<T>).value
26
+ }
27
+
28
+ /** 无补宽时的共享空对象(恒等比较,避免无意义拷贝) */
29
+ const EMPTY_FILL_EXTRAS: Record<string, number> = Object.freeze({})
30
+
31
+ export interface UseRsTableColumnLayoutOptions<T extends RsTableRowData> {
32
+ displayColumns: Source<RsTableColumn<T>[]>
33
+ effectiveColumnWidths: Source<Record<string, number>>
34
+ measuredViewportWidth: Source<number>
35
+ tableMinWidth: Source<string | undefined>
36
+ resizable: Source<boolean>
37
+ columnLayout: Source<'auto' | 'fixed'>
38
+ useStableColumnWidths: Source<boolean>
39
+ isColumnResizing: Source<boolean>
40
+ virtualColumnsEnabled: Source<boolean>
41
+ scrollX: Source<number | string | undefined>
42
+ showRowDragHandle: Source<boolean>
43
+ detailExpandable: Source<boolean>
44
+ showSelectColumn: Source<boolean>
45
+ showEditGutterColumn: Source<boolean>
46
+ showIndexColumn: Source<boolean>
47
+ showRowStatusColumn: Source<boolean>
48
+ resolvedGutterWidth: Source<number>
49
+ resolvedIndexWidth: Source<number>
50
+ dragColumnOffset: Source<number>
51
+ expandColumnOffset: Source<number>
52
+ selectColumnOffset: Source<number>
53
+ /** 拖拽会话快照列宽;非空表示正在/刚进入 resize paint */
54
+ resizePaintWidths: Source<Record<string, number> | null>
55
+ }
56
+
57
+ /**
58
+ * 列布局与样式缓存。
59
+ */
60
+ export function useRsTableColumnLayout<T extends RsTableRowData>(
61
+ options: UseRsTableColumnLayoutOptions<T>,
62
+ ) {
63
+ const PREFIX = RS_TABLE_PREFIX_COL_WIDTH
64
+
65
+ /** 前缀列总宽(不含 status;status 在部分合计里另加) */
66
+ function measurePrefixColumnWidth(): number {
67
+ return measureRsTablePrefixWidth({
68
+ showRowDragHandle: read(options.showRowDragHandle),
69
+ detailExpandable: read(options.detailExpandable),
70
+ showSelectColumn: read(options.showSelectColumn),
71
+ showEditGutterColumn: read(options.showEditGutterColumn),
72
+ showIndexColumn: read(options.showIndexColumn),
73
+ gutterWidth: read(options.resolvedGutterWidth),
74
+ indexWidth: read(options.resolvedIndexWidth),
75
+ })
76
+ }
77
+
78
+ /** resizable 且列宽稳定时,或拖拽会话中:表格内容最小宽度 = 各列宽度之和 */
79
+ const resizableTableWidth = computed(() => {
80
+ if (!read(options.resizable)) return undefined
81
+ if (
82
+ !read(options.resizePaintWidths) &&
83
+ read(options.columnLayout) === 'auto' &&
84
+ !read(options.useStableColumnWidths)
85
+ ) {
86
+ return undefined
87
+ }
88
+ let sum = measurePrefixColumnWidth()
89
+ const widths = read(options.effectiveColumnWidths)
90
+ for (const col of read(options.displayColumns)) {
91
+ sum += widths[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
92
+ }
93
+ return `${sum}px`
94
+ })
95
+
96
+ const useFixedColumnLayout = computed(
97
+ () => read(options.useStableColumnWidths) || read(options.isColumnResizing),
98
+ )
99
+
100
+ /**
101
+ * 列总宽不足视口时,把剩余像素均分到数据列(前缀列不动)。
102
+ * 列虚拟开启时不做拉满,避免与横向切片打架。
103
+ */
104
+ const columnFillExtras = computed((): Record<string, number> => {
105
+ if (read(options.isColumnResizing)) return EMPTY_FILL_EXTRAS
106
+ if (!useFixedColumnLayout.value) return EMPTY_FILL_EXTRAS
107
+ if (read(options.virtualColumnsEnabled)) return EMPTY_FILL_EXTRAS
108
+ const viewportW = read(options.measuredViewportWidth)
109
+ if (viewportW <= 0) return EMPTY_FILL_EXTRAS
110
+ const cols = read(options.displayColumns)
111
+ if (cols.length === 0) return EMPTY_FILL_EXTRAS
112
+
113
+ let sum = measurePrefixColumnWidth()
114
+ if (read(options.showRowStatusColumn)) sum += PREFIX.status
115
+ const widths = read(options.effectiveColumnWidths)
116
+ for (const col of cols) {
117
+ sum += widths[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
118
+ }
119
+ const excess = Math.floor(viewportW - sum) - 1
120
+ if (excess <= 0) return EMPTY_FILL_EXTRAS
121
+
122
+ const extras: Record<string, number> = {}
123
+ const base = Math.floor(excess / cols.length)
124
+ let rem = excess - base * cols.length
125
+ for (const col of cols) {
126
+ extras[col.key] = base + (rem > 0 ? 1 : 0)
127
+ if (rem > 0) rem -= 1
128
+ }
129
+ return extras
130
+ })
131
+
132
+ /** 渲染用列宽 = 持久化/拖拽列宽 + 拉满补宽;无补宽时直接返回 base 引用 */
133
+ const renderColumnWidths = computed(() => {
134
+ const extras = columnFillExtras.value
135
+ const base = read(options.effectiveColumnWidths)
136
+ if (extras === EMPTY_FILL_EXTRAS) return base
137
+ const next: Record<string, number> = { ...base }
138
+ for (const col of read(options.displayColumns)) {
139
+ const extra = extras[col.key]
140
+ if (!extra) continue
141
+ const cur = next[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
142
+ next[col.key] = cur + extra
143
+ }
144
+ return next
145
+ })
146
+
147
+ const tableInlineStyle = computed(() => {
148
+ if (read(options.isColumnResizing)) return undefined
149
+ if (read(options.resizable)) {
150
+ if (read(options.columnLayout) === 'auto' && !read(options.useStableColumnWidths)) {
151
+ const minW = read(options.tableMinWidth)
152
+ return minW ? { width: '100%', minWidth: minW } : { width: '100%' }
153
+ }
154
+ const width = resizableTableWidth.value
155
+ if (!width) return undefined
156
+ return { width: '100%', minWidth: read(options.tableMinWidth) ?? width }
157
+ }
158
+ const minW = read(options.tableMinWidth)
159
+ return minW ? { minWidth: minW } : undefined
160
+ })
161
+
162
+ function estimateRequiredTableWidth(): number {
163
+ let sum = measurePrefixColumnWidth()
164
+ if (read(options.showRowStatusColumn)) sum += PREFIX.status
165
+ const widths = read(options.effectiveColumnWidths)
166
+ for (const col of read(options.displayColumns)) {
167
+ const stored = widths[col.key]
168
+ if (typeof stored === 'number') {
169
+ sum += stored
170
+ continue
171
+ }
172
+ if (typeof col.width === 'number') {
173
+ sum += col.width
174
+ continue
175
+ }
176
+ if (col.minWidth !== undefined) {
177
+ sum += typeof col.minWidth === 'number' ? col.minWidth : parseColumnWidth(col.minWidth, 0)
178
+ }
179
+ }
180
+ return sum
181
+ }
182
+
183
+ /** 仅当列宽确需超过容器时开启横向滚动 */
184
+ const overflowXEnabled = computed(() => {
185
+ if (read(options.virtualColumnsEnabled)) return true
186
+ const scrollX = read(options.scrollX)
187
+ if (scrollX != null && scrollX !== '') return true
188
+ const viewportW = read(options.measuredViewportWidth)
189
+ if (viewportW <= 0) return false
190
+ if (useFixedColumnLayout.value) {
191
+ const raw = resizableTableWidth.value
192
+ const sum = raw ? Number.parseInt(raw, 10) : estimateRequiredTableWidth()
193
+ return Number.isFinite(sum) && sum > viewportW
194
+ }
195
+ return estimateRequiredTableWidth() > viewportW
196
+ })
197
+
198
+ function resolvedDataColumnWidth(key: string, fallback?: number | string): number {
199
+ const stored = renderColumnWidths.value[key]
200
+ if (typeof stored === 'number') return stored
201
+ return parseColumnWidth(fallback)
202
+ }
203
+
204
+ const fixedColumnStyles = computed(() =>
205
+ resolveFixedColumnStyles(read(options.displayColumns), renderColumnWidths.value, {
206
+ selectable: read(options.showSelectColumn),
207
+ showIndex: read(options.showIndexColumn),
208
+ showEditGutter: read(options.showEditGutterColumn),
209
+ gutterWidth: read(options.resolvedGutterWidth),
210
+ indexWidth: read(options.resolvedIndexWidth),
211
+ expandable: read(options.detailExpandable),
212
+ rowDraggable: read(options.showRowDragHandle),
213
+ }),
214
+ )
215
+
216
+ /**
217
+ * 一次遍历同时构建 body/header 列样式 Map,避免两遍 resolveColumnStyle。
218
+ */
219
+ const columnStyleMaps = computed(() => {
220
+ const body = new Map<string, Record<string, string> | undefined>()
221
+ const header = new Map<string, Record<string, string> | undefined>()
222
+ const widths = renderColumnWidths.value
223
+ const fixed = fixedColumnStyles.value
224
+ for (const col of read(options.displayColumns)) {
225
+ const base = resolveColumnStyle(col, widths) ?? {}
226
+ const bodyFixed = fixedCellStyle(fixed.get(col.key))
227
+ const headerFixed = fixedCellStyle(fixed.get(col.key), { header: true })
228
+ let bodyMerged: Record<string, string> | undefined
229
+ let headerMerged: Record<string, string> | undefined
230
+ if (bodyFixed) bodyMerged = { ...base, ...bodyFixed }
231
+ else if (Object.keys(base).length) bodyMerged = base
232
+ if (headerFixed) headerMerged = { ...base, ...headerFixed }
233
+ else if (Object.keys(base).length) headerMerged = base
234
+ body.set(col.key, bodyMerged)
235
+ header.set(col.key, headerMerged)
236
+ }
237
+ return { body, header }
238
+ })
239
+
240
+ const columnStyleMap = computed(() => columnStyleMaps.value.body)
241
+ const columnHeaderStyleMap = computed(() => columnStyleMaps.value.header)
242
+
243
+ /** td 静态 class Map:列配置不变时复用 */
244
+ const columnTdClassMap = computed<Map<string, string[]>>(() => {
245
+ const map = new Map<string, string[]>()
246
+ for (const col of read(options.displayColumns)) {
247
+ const classes: string[] = [`rs-table__cell--${col.align ?? 'left'}`]
248
+ if (col.ellipsis) classes.push('rs-table__td--ellipsis')
249
+ if (col.fixed) classes.push('rs-table__cell--fixed')
250
+ map.set(col.key, classes)
251
+ }
252
+ return map
253
+ })
254
+
255
+ const dragLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: 0 }) ?? {})
256
+ const expandLeadStyle = computed(
257
+ () => fixedCellStyle({ fixed: 'left', left: read(options.dragColumnOffset) }) ?? {},
258
+ )
259
+ const selectLeadStyle = computed(
260
+ () => fixedCellStyle({ fixed: 'left', left: read(options.expandColumnOffset) }) ?? {},
261
+ )
262
+ const indexLeadStyle = computed(
263
+ () => fixedCellStyle({ fixed: 'left', left: read(options.selectColumnOffset) }) ?? {},
264
+ )
265
+ const gutterLeadStyle = computed(
266
+ () => fixedCellStyle({ fixed: 'left', left: read(options.selectColumnOffset) }) ?? {},
267
+ )
268
+ const dragLeadHeaderStyle = computed(
269
+ () => fixedCellStyle({ fixed: 'left', left: 0 }, { header: true }) ?? {},
270
+ )
271
+ const expandLeadHeaderStyle = computed(
272
+ () =>
273
+ fixedCellStyle({ fixed: 'left', left: read(options.dragColumnOffset) }, { header: true }) ?? {},
274
+ )
275
+ const selectLeadHeaderStyle = computed(
276
+ () =>
277
+ fixedCellStyle({ fixed: 'left', left: read(options.expandColumnOffset) }, { header: true }) ??
278
+ {},
279
+ )
280
+ const indexLeadHeaderStyle = computed(
281
+ () =>
282
+ fixedCellStyle({ fixed: 'left', left: read(options.selectColumnOffset) }, { header: true }) ??
283
+ {},
284
+ )
285
+ const gutterLeadHeaderStyle = computed(
286
+ () =>
287
+ fixedCellStyle({ fixed: 'left', left: read(options.selectColumnOffset) }, { header: true }) ??
288
+ {},
289
+ )
290
+
291
+ return {
292
+ EMPTY_FILL_EXTRAS,
293
+ measurePrefixColumnWidth,
294
+ resizableTableWidth,
295
+ useFixedColumnLayout,
296
+ columnFillExtras,
297
+ renderColumnWidths,
298
+ tableInlineStyle,
299
+ overflowXEnabled,
300
+ resolvedDataColumnWidth,
301
+ fixedColumnStyles,
302
+ columnStyleMap,
303
+ columnHeaderStyleMap,
304
+ columnTdClassMap,
305
+ dragLeadStyle,
306
+ expandLeadStyle,
307
+ selectLeadStyle,
308
+ indexLeadStyle,
309
+ gutterLeadStyle,
310
+ dragLeadHeaderStyle,
311
+ expandLeadHeaderStyle,
312
+ selectLeadHeaderStyle,
313
+ indexLeadHeaderStyle,
314
+ gutterLeadHeaderStyle,
315
+ }
316
+ }
317
+
318
+ export type RsTableColumnLayoutApi<T extends RsTableRowData> = ReturnType<
319
+ typeof useRsTableColumnLayout<T>
320
+ >
@@ -0,0 +1,244 @@
1
+ /**
2
+ * RsTable 列宽拖拽:mousemove 走 DOM 直写,避免整表 Vue 重渲。
3
+ *
4
+ * 性能关键:
5
+ * - 会话开始时缓存 col/th 元素与基准总宽,RAF 内不再 querySelector 全表
6
+ * - 拖拽中只更新变化列宽度 + table 总宽(O(1)),不重写其它列
7
+ * - activeDragWidths 为非响应式 plain object,禁止写入 reactive
8
+ */
9
+
10
+ import { nextTick, onUnmounted, ref, type ComputedRef, type Ref } from 'vue'
11
+ import { clampColumnWidth, parseColumnWidth, type RsTableColumn, type RsTableRowData } from '../components/table-utils'
12
+
13
+ function read<T>(source: ComputedRef<T> | (() => T) | Ref<T>): T {
14
+ if (typeof source === 'function') return (source as () => T)()
15
+ return (source as Ref<T> | ComputedRef<T>).value
16
+ }
17
+
18
+ export interface UseRsTableColumnResizeOptions<T extends RsTableRowData> {
19
+ resizable: ComputedRef<boolean> | (() => boolean)
20
+ minColumnWidth: ComputedRef<number | undefined> | (() => number | undefined)
21
+ maxColumnWidth: ComputedRef<number | undefined> | (() => number | undefined)
22
+ displayColumns: ComputedRef<RsTableColumn<T>[]> | (() => RsTableColumn<T>[])
23
+ resolvedColumnWidths: {
24
+ get: () => Record<string, number>
25
+ set: (value: Record<string, number>) => void
26
+ }
27
+ /** 前缀列总宽(px),用于 DOM 直写时的 table 总宽 */
28
+ measurePrefixColumnWidth: () => number
29
+ tableMinWidth: ComputedRef<string | undefined> | (() => string | undefined)
30
+ onColumnResize: (key: string, width: number) => void
31
+ }
32
+
33
+ interface ResizeSession {
34
+ key: string
35
+ startX: number
36
+ startWidth: number
37
+ currentWidth: number
38
+ domSnapshot: Record<string, number>
39
+ /** 会话基准总宽(前缀 + 各列快照),便于 O(1) 更新 table.width */
40
+ baseTableWidth: number
41
+ colEls: Map<string, HTMLTableColElement>
42
+ thEls: Map<string, HTMLTableCellElement>
43
+ }
44
+
45
+ /**
46
+ * 列宽拖拽会话。
47
+ */
48
+ export function useRsTableColumnResize<T extends RsTableRowData>(
49
+ options: UseRsTableColumnResizeOptions<T>,
50
+ ) {
51
+ const tableRef = ref<HTMLTableElement | null>(null)
52
+ const isColumnResizing = ref(false)
53
+ /** 拖拽会话中的列宽快照(仅首帧写入一次,供 colgroup 初始渲染) */
54
+ const resizePaintWidths = ref<Record<string, number> | null>(null)
55
+
56
+ let session: ResizeSession | null = null
57
+ /** 非响应式:mousemove 高频更新不得触发 Vue */
58
+ let activeDragWidths: Record<string, number> | null = null
59
+ let resizeRafId = 0
60
+ let latestResizeClientX = 0
61
+ let resizeMoved = false
62
+
63
+ function cacheColumnElements(table: HTMLTableElement): {
64
+ colEls: Map<string, HTMLTableColElement>
65
+ thEls: Map<string, HTMLTableCellElement>
66
+ } {
67
+ const colEls = new Map<string, HTMLTableColElement>()
68
+ const thEls = new Map<string, HTMLTableCellElement>()
69
+ for (const col of table.querySelectorAll<HTMLTableColElement>('colgroup col[data-col-key]')) {
70
+ const key = col.dataset.colKey
71
+ if (key) colEls.set(key, col)
72
+ }
73
+ for (const th of table.querySelectorAll<HTMLTableCellElement>('thead th[data-col-key]')) {
74
+ const key = th.dataset.colKey
75
+ if (key) thEls.set(key, th)
76
+ }
77
+ return { colEls, thEls }
78
+ }
79
+
80
+ /** 会话开始:一次性写满所有列宽到 DOM */
81
+ function paintAllColumnWidths(table: HTMLTableElement, widths: Record<string, number>, cache: {
82
+ colEls: Map<string, HTMLTableColElement>
83
+ thEls: Map<string, HTMLTableCellElement>
84
+ }): number {
85
+ let sum = options.measurePrefixColumnWidth()
86
+ for (const column of read(options.displayColumns)) {
87
+ const width = widths[column.key] ?? parseColumnWidth(column.width ?? column.minWidth)
88
+ const col = cache.colEls.get(column.key)
89
+ if (col) col.style.width = `${width}px`
90
+ const th = cache.thEls.get(column.key)
91
+ if (th) th.style.width = `${width}px`
92
+ sum += width
93
+ }
94
+ table.style.width = `${sum}px`
95
+ const minWidth = read(options.tableMinWidth)
96
+ if (minWidth) table.style.minWidth = minWidth
97
+ return sum
98
+ }
99
+
100
+ /** 确保 colgroup 元素已缓存;fixed 布局首帧可能尚未挂上,失败则返回 false */
101
+ function ensureSessionDomCache(): boolean {
102
+ if (!session || !tableRef.value || !activeDragWidths) return false
103
+ if (session.colEls.size > 0) return true
104
+ const cache = cacheColumnElements(tableRef.value)
105
+ if (cache.colEls.size === 0) return false
106
+ session.colEls = cache.colEls
107
+ session.thEls = cache.thEls
108
+ session.baseTableWidth = paintAllColumnWidths(tableRef.value, activeDragWidths, cache)
109
+ return true
110
+ }
111
+
112
+ /** RAF 热路径:只改变化列 + 总宽;缓存未就绪时先全量写一次 */
113
+ function paintChangedColumnWidth(width: number): void {
114
+ if (!session || !tableRef.value) return
115
+ if (!ensureSessionDomCache()) return
116
+ const { key, colEls, thEls, baseTableWidth, startWidth } = session
117
+ const col = colEls.get(key)
118
+ if (col) col.style.width = `${width}px`
119
+ const th = thEls.get(key)
120
+ if (th) th.style.width = `${width}px`
121
+ tableRef.value.style.width = `${baseTableWidth - startWidth + width}px`
122
+ }
123
+
124
+ function beginResizeSession(): void {
125
+ if (!session || isColumnResizing.value || !tableRef.value) return
126
+ isColumnResizing.value = true
127
+ activeDragWidths = { ...session.domSnapshot }
128
+ resizePaintWidths.value = { ...activeDragWidths }
129
+ // colgroup 依赖 fixed 布局重渲;先同步尝试,再 nextTick 兜底(避免 RAF 抢跑打空)
130
+ if (!ensureSessionDomCache()) {
131
+ void nextTick(() => {
132
+ ensureSessionDomCache()
133
+ })
134
+ }
135
+ }
136
+
137
+ function onResizeMove(event: MouseEvent): void {
138
+ if (!session) return
139
+ latestResizeClientX = event.clientX
140
+ if (!resizeMoved && Math.abs(latestResizeClientX - session.startX) < 1) return
141
+ if (!resizeMoved) {
142
+ resizeMoved = true
143
+ beginResizeSession()
144
+ }
145
+ if (resizeRafId !== 0) return
146
+ resizeRafId = requestAnimationFrame(() => {
147
+ resizeRafId = 0
148
+ if (!session || !activeDragWidths) return
149
+ const nextWidth = clampColumnWidth(
150
+ session.startWidth + latestResizeClientX - session.startX,
151
+ read(options.minColumnWidth),
152
+ read(options.maxColumnWidth),
153
+ )
154
+ if (nextWidth === session.currentWidth) return
155
+ session.currentWidth = nextWidth
156
+ activeDragWidths[session.key] = nextWidth
157
+ paintChangedColumnWidth(nextWidth)
158
+ })
159
+ }
160
+
161
+ function stopResize(): void {
162
+ if (session && resizeMoved) {
163
+ const finalWidth = clampColumnWidth(
164
+ session.startWidth + latestResizeClientX - session.startX,
165
+ read(options.minColumnWidth),
166
+ read(options.maxColumnWidth),
167
+ )
168
+ if (finalWidth !== session.startWidth) {
169
+ const merged = { ...session.domSnapshot, [session.key]: finalWidth }
170
+ options.resolvedColumnWidths.set(merged)
171
+ options.onColumnResize(session.key, finalWidth)
172
+ }
173
+ }
174
+ if (tableRef.value) {
175
+ tableRef.value.style.width = ''
176
+ tableRef.value.style.minWidth = ''
177
+ for (const th of tableRef.value.querySelectorAll<HTMLTableCellElement>('thead th[data-col-key]')) {
178
+ th.style.width = ''
179
+ }
180
+ }
181
+ isColumnResizing.value = false
182
+ resizePaintWidths.value = null
183
+ activeDragWidths = null
184
+ session = null
185
+ resizeMoved = false
186
+ if (resizeRafId !== 0) {
187
+ cancelAnimationFrame(resizeRafId)
188
+ resizeRafId = 0
189
+ }
190
+ document.body.style.cursor = ''
191
+ document.body.style.userSelect = ''
192
+ document.removeEventListener('mousemove', onResizeMove)
193
+ document.removeEventListener('mouseup', stopResize, { capture: true })
194
+ }
195
+
196
+ function onResizeStart(key: string, event: MouseEvent): void {
197
+ if (!read(options.resizable)) return
198
+
199
+ const thEl = (event.currentTarget as HTMLElement).closest('th') as HTMLTableCellElement | null
200
+ if (!thEl) return
201
+ const tableEl = thEl.closest('table') as HTMLTableElement | null
202
+ if (!tableEl) return
203
+
204
+ const dataThs = Array.from(
205
+ tableEl.querySelectorAll<HTMLTableCellElement>('thead th[data-col-key]'),
206
+ )
207
+ const domSnapshot: Record<string, number> = {}
208
+ for (const th of dataThs) {
209
+ domSnapshot[th.dataset.colKey!] = th.offsetWidth
210
+ }
211
+
212
+ const startWidth = domSnapshot[key] ?? thEl.offsetWidth
213
+ latestResizeClientX = event.clientX
214
+ resizeMoved = false
215
+ session = {
216
+ key,
217
+ startX: event.clientX,
218
+ startWidth,
219
+ currentWidth: startWidth,
220
+ domSnapshot,
221
+ baseTableWidth: 0,
222
+ colEls: new Map(),
223
+ thEls: new Map(),
224
+ }
225
+ document.body.style.cursor = 'col-resize'
226
+ document.body.style.userSelect = 'none'
227
+ document.addEventListener('mousemove', onResizeMove)
228
+ document.addEventListener('mouseup', stopResize, { capture: true })
229
+ }
230
+
231
+ onUnmounted(() => {
232
+ stopResize()
233
+ })
234
+
235
+ return {
236
+ tableRef,
237
+ isColumnResizing,
238
+ resizePaintWidths,
239
+ onResizeStart,
240
+ stopResize,
241
+ }
242
+ }
243
+
244
+ export type RsTableColumnResizeApi = ReturnType<typeof useRsTableColumnResize>