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.
- package/CHANGELOG.md +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- 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>
|