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,180 @@
1
+ /**
2
+ * RsTable 横向列虚拟:冻结列始终渲染,中间 fluid 列按 scrollLeft 切片。
3
+ *
4
+ * 列宽拖拽 DOM 直写仍在 RsTable;本 composable 只消费已算好的列宽 getter。
5
+ */
6
+
7
+ import { computed, type ComputedRef, type Ref } from 'vue'
8
+ import {
9
+ resolveColumnPixelWidth,
10
+ sliceVirtualColumns,
11
+ type RsTableColumn,
12
+ type RsTableRowData,
13
+ } from '../components/table-utils'
14
+
15
+ /** ComputedRef / Ref / getter 均可(抽离后 RsTable 常直接传入 ref) */
16
+ type Source<T> = ComputedRef<T> | Ref<T> | (() => T)
17
+
18
+ /** 统一读取 ComputedRef / Ref / getter */
19
+ function read<T>(source: Source<T>): T {
20
+ if (typeof source === 'function') return (source as () => T)()
21
+ return (source as Ref<T> | ComputedRef<T>).value
22
+ }
23
+
24
+ /** 前缀列固定宽度(与 CSS / resizable 合计一致) */
25
+ export const RS_TABLE_PREFIX_COL_WIDTH = {
26
+ drag: 40,
27
+ expand: 40,
28
+ select: 40,
29
+ status: 52,
30
+ } as const
31
+
32
+ /**
33
+ * 计算前缀列总宽(拖拽/展开/选择/序号或 gutter)。
34
+ * 供列布局合计与列宽拖拽 DOM 直写共用,避免两处公式漂移。
35
+ */
36
+ export function measureRsTablePrefixWidth(flags: {
37
+ showRowDragHandle?: boolean
38
+ detailExpandable?: boolean
39
+ showSelectColumn?: boolean
40
+ showEditGutterColumn?: boolean
41
+ showIndexColumn?: boolean
42
+ gutterWidth?: number
43
+ indexWidth?: number
44
+ }): number {
45
+ let sum = 0
46
+ if (flags.showRowDragHandle) sum += RS_TABLE_PREFIX_COL_WIDTH.drag
47
+ if (flags.detailExpandable) sum += RS_TABLE_PREFIX_COL_WIDTH.expand
48
+ if (flags.showSelectColumn) sum += RS_TABLE_PREFIX_COL_WIDTH.select
49
+ if (flags.showEditGutterColumn) sum += flags.gutterWidth ?? 0
50
+ else if (flags.showIndexColumn) sum += flags.indexWidth ?? 0
51
+ return sum
52
+ }
53
+
54
+ export interface UseRsTableColumnVirtualOptions<T extends RsTableRowData> {
55
+ displayColumns: Source<RsTableColumn<T>[]>
56
+ /** virtualColumns prop:true / false / 'auto' */
57
+ virtualColumns: Source<boolean | 'auto' | undefined>
58
+ virtualColumnsAutoThreshold: Source<number | undefined>
59
+ virtualColumnOverscan: Source<number | undefined>
60
+ /** 列宽拖拽中强制关闭列虚拟,避免切片与 DOM 直写打架 */
61
+ isColumnResizing: Source<boolean>
62
+ scrollLeft: Source<number>
63
+ measuredViewportWidth: Source<number>
64
+ columnWidths: Source<Record<string, number>>
65
+ showRowDragHandle: Source<boolean>
66
+ detailExpandable: Source<boolean>
67
+ showSelectColumn: Source<boolean>
68
+ showEditGutterColumn: Source<boolean>
69
+ showIndexColumn: Source<boolean>
70
+ showRowStatusColumn: Source<boolean>
71
+ resolvedGutterWidth: Source<number>
72
+ resolvedIndexWidth: Source<number>
73
+ }
74
+
75
+ /**
76
+ * 横向列虚拟切片状态。
77
+ *
78
+ * @returns left/right/fluid 列分组、virtualColumnsEnabled、visibleDataColumns、左右 pad
79
+ */
80
+ export function useRsTableColumnVirtual<T extends RsTableRowData>(
81
+ options: UseRsTableColumnVirtualOptions<T>,
82
+ ) {
83
+ const leftFixedDataColumns = computed(() =>
84
+ read(options.displayColumns).filter((c) => c.fixed === 'left'),
85
+ )
86
+ const rightFixedDataColumns = computed(() =>
87
+ read(options.displayColumns).filter((c) => c.fixed === 'right'),
88
+ )
89
+ const fluidDataColumns = computed(() =>
90
+ read(options.displayColumns).filter((c) => !c.fixed),
91
+ )
92
+
93
+ const dataColumnWidth = (column: RsTableColumn<T>): number =>
94
+ resolveColumnPixelWidth(column, read(options.columnWidths))
95
+
96
+ /**
97
+ * 启用规则:
98
+ * - false → 关
99
+ * - 列宽拖拽中 → 关
100
+ * - true → fluid 列非空即开
101
+ * - auto → fluid 列数 >= threshold
102
+ */
103
+ const virtualColumnsEnabled = computed(() => {
104
+ if (read(options.virtualColumns) === false) return false
105
+ if (read(options.isColumnResizing)) return false
106
+ if (read(options.virtualColumns) === true) return fluidDataColumns.value.length > 0
107
+ const threshold = read(options.virtualColumnsAutoThreshold) ?? 0
108
+ return threshold > 0 && fluidDataColumns.value.length >= threshold
109
+ })
110
+
111
+ /** 左侧前缀 + 左冻结列总宽,用于把 scrollLeft 映射到 fluid 区 */
112
+ const fluidLeadingOffset = computed(() => {
113
+ let sum = 0
114
+ if (read(options.showRowDragHandle)) sum += RS_TABLE_PREFIX_COL_WIDTH.drag
115
+ if (read(options.detailExpandable)) sum += RS_TABLE_PREFIX_COL_WIDTH.expand
116
+ if (read(options.showSelectColumn)) sum += RS_TABLE_PREFIX_COL_WIDTH.select
117
+ if (read(options.showEditGutterColumn)) sum += read(options.resolvedGutterWidth)
118
+ else if (read(options.showIndexColumn)) sum += read(options.resolvedIndexWidth)
119
+ if (read(options.showRowStatusColumn)) sum += RS_TABLE_PREFIX_COL_WIDTH.status
120
+ for (const col of leftFixedDataColumns.value) sum += dataColumnWidth(col)
121
+ return sum
122
+ })
123
+
124
+ const columnVirtualSlice = computed(() => {
125
+ const fluidCount = fluidDataColumns.value.length
126
+ if (!virtualColumnsEnabled.value) {
127
+ return {
128
+ columns: read(options.displayColumns),
129
+ paddingLeft: 0,
130
+ paddingRight: 0,
131
+ startIndex: 0,
132
+ endIndex: fluidCount,
133
+ fluidCount,
134
+ }
135
+ }
136
+ const viewport = Math.max(read(options.measuredViewportWidth) || 320, 120)
137
+ const midScroll = Math.max(0, read(options.scrollLeft) - fluidLeadingOffset.value)
138
+ const midViewport = Math.max(120, viewport - Math.min(viewport - 40, fluidLeadingOffset.value))
139
+ const sliced = sliceVirtualColumns(fluidDataColumns.value, {
140
+ scrollLeft: midScroll,
141
+ viewportWidth: midViewport,
142
+ getWidth: dataColumnWidth,
143
+ overscan: read(options.virtualColumnOverscan) ?? 2,
144
+ })
145
+ return {
146
+ columns: [
147
+ ...leftFixedDataColumns.value,
148
+ ...sliced.columns,
149
+ ...rightFixedDataColumns.value,
150
+ ],
151
+ paddingLeft: sliced.paddingLeft,
152
+ paddingRight: sliced.paddingRight,
153
+ startIndex: sliced.startIndex,
154
+ endIndex: sliced.endIndex,
155
+ fluidCount,
156
+ }
157
+ })
158
+
159
+ const visibleDataColumns = computed(() => columnVirtualSlice.value.columns)
160
+ const columnPadLeft = computed(() => columnVirtualSlice.value.paddingLeft)
161
+ const columnPadRight = computed(() => columnVirtualSlice.value.paddingRight)
162
+
163
+ return {
164
+ leftFixedDataColumns,
165
+ rightFixedDataColumns,
166
+ fluidDataColumns,
167
+ dataColumnWidth,
168
+ virtualColumnsEnabled,
169
+ fluidLeadingOffset,
170
+ columnVirtualSlice,
171
+ visibleDataColumns,
172
+ columnPadLeft,
173
+ columnPadRight,
174
+ }
175
+ }
176
+
177
+ /** useRsTableColumnVirtual 返回值类型 */
178
+ export type RsTableColumnVirtualApi<T extends RsTableRowData> = ReturnType<
179
+ typeof useRsTableColumnVirtual<T>
180
+ >
@@ -0,0 +1,200 @@
1
+ /**
2
+ * RsTable 列模型编排:顺序、列筛选、宽度与 displayColumns。
3
+ *
4
+ * 从 RsTable.vue 抽离,保持受控 / 非受控 API 与原先行为一致。
5
+ * 不负责虚拟列切片、列拖拽排序的 DOM 交互(仍由 RsTable 编排)。
6
+ */
7
+
8
+ import { computed, ref, watch, type ComputedRef, type Ref } from 'vue'
9
+ import {
10
+ createInitialColumnWidths,
11
+ resolveColumnOrder,
12
+ resolveOrderedColumns,
13
+ type RsTableColumn,
14
+ type RsTableRowData,
15
+ } from '../components/table-utils'
16
+
17
+ /** 列相关事件(与 RsTable emit 子集对齐) */
18
+ export interface UseRsTableColumnsEmit {
19
+ (e: 'update:columnOrder', value: string[]): void
20
+ (e: 'columnOrderChange', value: string[]): void
21
+ (e: 'update:columnFilters', value: Record<string, string>): void
22
+ (e: 'columnFiltersChange', value: Record<string, string>): void
23
+ }
24
+
25
+ /**
26
+ * useRsTableColumns 入参。
27
+ * 多数字段支持 ComputedRef 或 getter,便于在 RsTable 内直接绑定 props。
28
+ */
29
+ export interface UseRsTableColumnsOptions<T extends RsTableRowData> {
30
+ /** 原始列定义(未排序) */
31
+ columns: ComputedRef<RsTableColumn<T>[]> | (() => RsTableColumn<T>[])
32
+ /** 受控列顺序;undefined 表示非受控 */
33
+ columnOrder: ComputedRef<string[] | undefined> | (() => string[] | undefined)
34
+ /** 非受控初始列顺序 */
35
+ defaultColumnOrder: string[]
36
+ /** 受控列筛选;undefined 表示非受控 */
37
+ columnFilters: ComputedRef<Record<string, string> | undefined> | (() => Record<string, string> | undefined)
38
+ /** 非受控初始列筛选 */
39
+ defaultColumnFilters: Record<string, string>
40
+ /** 初始列宽(像素) */
41
+ initialColumnWidths: ComputedRef<Record<string, number> | undefined> | (() => Record<string, number> | undefined)
42
+ /** 是否允许拖拽改列宽 */
43
+ resizable: ComputedRef<boolean> | (() => boolean)
44
+ /** 列布局:auto 自适应;fixed 固定表布局 */
45
+ columnLayout: ComputedRef<'auto' | 'fixed'> | (() => 'auto' | 'fixed')
46
+ /** 是否树表(影响展开列固定) */
47
+ treeMode: ComputedRef<boolean> | (() => boolean)
48
+ /** 树表是否固定展开列到左侧 */
49
+ treeFixExpandColumn: ComputedRef<boolean> | (() => boolean)
50
+ /** 树表展开列 key */
51
+ treeExpandColumnKey: ComputedRef<string | null> | (() => string | null)
52
+ emit: UseRsTableColumnsEmit
53
+ }
54
+
55
+ /** 统一读取 ComputedRef / getter */
56
+ function read<T>(source: ComputedRef<T> | (() => T)): T {
57
+ return typeof source === 'function' ? (source as () => T)() : source.value
58
+ }
59
+
60
+ /**
61
+ * 列顺序 / 列筛选 / 列宽 / displayColumns。
62
+ *
63
+ * @returns
64
+ * - `columnOrderState` / `columnFiltersState`:受控或非受控读写
65
+ * - `displayColumns`:按顺序排列;树表可将展开列钉为 fixed-left
66
+ * - `resolvedColumnWidths` / `useStableColumnWidths`:拖拽列宽与稳定布局判定
67
+ * - `updateColumnFilter` / `columnFilterValue`:表头筛选写入与读取
68
+ */
69
+ export function useRsTableColumns<T extends RsTableRowData>(options: UseRsTableColumnsOptions<T>) {
70
+ const internalColumnOrder = ref<string[]>([...options.defaultColumnOrder])
71
+ const internalColumnFilters = ref<Record<string, string>>({ ...options.defaultColumnFilters })
72
+ const internalColumnWidths = ref<Record<string, number>>({})
73
+
74
+ const isColumnOrderControlled = computed(() => read(options.columnOrder) !== undefined)
75
+
76
+ /** 当前列 key 顺序;受控时写回 update:columnOrder */
77
+ const columnOrderState = computed({
78
+ get: () => {
79
+ if (isColumnOrderControlled.value) {
80
+ return read(options.columnOrder) ?? []
81
+ }
82
+ if (internalColumnOrder.value.length) {
83
+ return internalColumnOrder.value
84
+ }
85
+ return resolveColumnOrder(read(options.columns))
86
+ },
87
+ set: (value: string[]) => {
88
+ if (isColumnOrderControlled.value) options.emit('update:columnOrder', value)
89
+ else internalColumnOrder.value = value
90
+ options.emit('columnOrderChange', value)
91
+ },
92
+ })
93
+
94
+ const isColumnFiltersControlled = computed(() => read(options.columnFilters) !== undefined)
95
+
96
+ /** 当前列筛选 map(列 key → 查询串) */
97
+ const columnFiltersState = computed(() => read(options.columnFilters) ?? internalColumnFilters.value)
98
+
99
+ /**
100
+ * 更新单列筛选值。
101
+ * 空串会删除该 key;同时发出 update:columnFilters / columnFiltersChange。
102
+ */
103
+ function updateColumnFilter(key: string, value: string) {
104
+ const next = { ...columnFiltersState.value }
105
+ if (value.trim()) next[key] = value
106
+ else delete next[key]
107
+ if (isColumnFiltersControlled.value) options.emit('update:columnFilters', next)
108
+ else internalColumnFilters.value = next
109
+ options.emit('columnFiltersChange', next)
110
+ }
111
+
112
+ /** 读取单列当前筛选文案(无则空串) */
113
+ function columnFilterValue(key: string): string {
114
+ return columnFiltersState.value[key] ?? ''
115
+ }
116
+
117
+ /**
118
+ * 渲染用列列表:先按 columnOrder 排序;
119
+ * 树表且 fixExpandColumn 时,将展开列强制 fixed=left(已有 fixed 则不动)。
120
+ */
121
+ const displayColumns = computed(() => {
122
+ const ordered = resolveOrderedColumns(read(options.columns), columnOrderState.value)
123
+ if (!read(options.treeMode) || !read(options.treeFixExpandColumn)) return ordered
124
+ const expandKey = read(options.treeExpandColumnKey)
125
+ if (!expandKey) return ordered
126
+ return ordered.map((column) => {
127
+ if (column.key !== expandKey || column.fixed) return column
128
+ return { ...column, fixed: 'left' as const }
129
+ })
130
+ })
131
+
132
+ /** 持久化列宽(像素);拖拽过程中由 RsTable 另用 DOM 直写避免整表重渲 */
133
+ const resolvedColumnWidths = computed({
134
+ get: () => internalColumnWidths.value,
135
+ set: (value: Record<string, number>) => {
136
+ internalColumnWidths.value = value
137
+ },
138
+ })
139
+
140
+ /**
141
+ * 是否启用「稳定列宽」布局(resizable 下避免松手回弹)。
142
+ * fixed 布局,或每一列都有显式 width / 已测宽时为 true。
143
+ */
144
+ const useStableColumnWidths = computed(() => {
145
+ if (!read(options.resizable)) return false
146
+ if (read(options.columnLayout) === 'fixed') return true
147
+ const widths = internalColumnWidths.value
148
+ return displayColumns.value.every((col) => {
149
+ return typeof widths[col.key] === 'number' || col.width !== undefined
150
+ })
151
+ })
152
+
153
+ /**
154
+ * 按当前 columns 重建内部列宽:保留已有测量值,合并 initialColumnWidths。
155
+ * columns 变化时由 watch 调用。
156
+ */
157
+ function syncInternalColumnWidths(columns: RsTableColumn<T>[]): void {
158
+ const preserved: Record<string, number> = {}
159
+ for (const column of columns) {
160
+ const width = internalColumnWidths.value[column.key]
161
+ if (typeof width === 'number') preserved[column.key] = width
162
+ }
163
+ internalColumnWidths.value = createInitialColumnWidths(
164
+ columns,
165
+ { ...(read(options.initialColumnWidths) ?? {}), ...preserved },
166
+ { forceAll: read(options.resizable) && useStableColumnWidths.value },
167
+ )
168
+ }
169
+
170
+ watch(
171
+ () => read(options.columns),
172
+ (columns) => {
173
+ syncInternalColumnWidths(columns)
174
+ // 非受控:在保留已有顺序的前提下与新 columns 对齐
175
+ if (!isColumnOrderControlled.value) {
176
+ internalColumnOrder.value = resolveColumnOrder(columns, internalColumnOrder.value)
177
+ }
178
+ },
179
+ { immediate: true },
180
+ )
181
+
182
+ return {
183
+ internalColumnOrder,
184
+ internalColumnFilters,
185
+ internalColumnWidths,
186
+ isColumnOrderControlled,
187
+ columnOrderState,
188
+ isColumnFiltersControlled,
189
+ columnFiltersState,
190
+ displayColumns,
191
+ resolvedColumnWidths,
192
+ useStableColumnWidths,
193
+ syncInternalColumnWidths,
194
+ updateColumnFilter,
195
+ columnFilterValue,
196
+ }
197
+ }
198
+
199
+ /** useRsTableColumns 返回值类型 */
200
+ export type RsTableColumnsApi<T extends RsTableRowData> = ReturnType<typeof useRsTableColumns<T>>
@@ -0,0 +1,257 @@
1
+ /**
2
+ * RsTable 右键菜单:上下文状态、默认复制项、捕获阶段预填。
3
+ *
4
+ * 选择联动(右键选中行)通过回调注入,避免 composable 直接持有 Engine。
5
+ */
6
+
7
+ import { ref, watch, type ComputedRef, type Ref } from 'vue'
8
+ import type { RsContextMenuItem } from '../components/context-menu-utils'
9
+ import {
10
+ buildDefaultTableContextMenuItems,
11
+ resolveTableCellCopyText,
12
+ resolveTableRowCopyText,
13
+ TABLE_CTX_COPY_CELL,
14
+ TABLE_CTX_COPY_ROW,
15
+ } from '../components/table/table-context-menu'
16
+ import { copyTextToClipboard } from '../utils/rs-clipboard'
17
+ import {
18
+ resolveRowKey,
19
+ type RsTableColumn,
20
+ type RsTableRowData,
21
+ type RsTableRowEntry,
22
+ type RsTableRowKey,
23
+ type RsTableSelectionType,
24
+ } from '../components/table-utils'
25
+
26
+ function read<T>(source: ComputedRef<T> | (() => T) | Ref<T>): T {
27
+ if (typeof source === 'function') return (source as () => T)()
28
+ return (source as Ref<T> | ComputedRef<T>).value
29
+ }
30
+
31
+ export interface UseRsTableContextMenuOptions<T extends RsTableRowData> {
32
+ enabled: ComputedRef<boolean> | (() => boolean)
33
+ dataRows: ComputedRef<Array<Extract<RsTableRowEntry<T>, { type: 'row' }>>> | (() => Array<Extract<RsTableRowEntry<T>, { type: 'row' }>>)
34
+ displayColumns: ComputedRef<RsTableColumn<T>[]> | (() => RsTableColumn<T>[])
35
+ selectedRows: ComputedRef<T[]> | (() => T[])
36
+ rowKey: ComputedRef<RsTableRowKey<T> | undefined> | (() => RsTableRowKey<T> | undefined)
37
+ rowKeyByIndex: ComputedRef<Map<number, string>> | (() => Map<number, string>)
38
+ copyCellLabel: ComputedRef<string> | (() => string)
39
+ copyRowLabel: ComputedRef<string> | (() => string)
40
+ contextMenuItems?: (
41
+ row: T | null,
42
+ selectedRows: T[],
43
+ ) => RsContextMenuItem[]
44
+ /**
45
+ * Feature 贡献合并:在 props 自定义项之后调用。
46
+ * 入参为已含默认复制 + 业务自定义的列表,应返回最终列表。
47
+ */
48
+ mergeFeatureMenuItems?: (
49
+ items: RsContextMenuItem[],
50
+ ctx: { row: T | null; selectedRows: T[] },
51
+ ) => RsContextMenuItem[]
52
+ /** 右键时是否选中该行 */
53
+ shouldSelectOnContextmenu: () => boolean
54
+ canSelectRow: (entry: Extract<RsTableRowEntry<T>, { type: 'row' }>) => boolean
55
+ isRowSelected: (entry: Extract<RsTableRowEntry<T>, { type: 'row' }>) => boolean
56
+ selectionType: () => RsTableSelectionType
57
+ getSelectedRowKeys: () => string[]
58
+ setSelectedRowKeys: (keys: string[]) => void
59
+ selectRowKeys: (
60
+ current: string[],
61
+ key: string,
62
+ type: RsTableSelectionType,
63
+ ) => string[]
64
+ getDraft: (rowKey: string, colKey: string) => string | undefined
65
+ onSelect: (key: string, row: T | null, selected: T[]) => void
66
+ onRowContextmenu: (row: T, rowIndex: number, event: MouseEvent) => void
67
+ }
68
+
69
+ /**
70
+ * 表格右键菜单状态与交互。
71
+ */
72
+ export function useRsTableContextMenu<T extends RsTableRowData>(
73
+ options: UseRsTableContextMenuOptions<T>,
74
+ ) {
75
+ /** 当前右键行(null = 表体空白或尚未命中) */
76
+ const ctxRow = ref<T | null>(null) as Ref<T | null>
77
+ const ctxRowIndex = ref<number | null>(null)
78
+ /** 右键命中的数据列 key */
79
+ const ctxColKey = ref<string | null>(null)
80
+ const ctxMenuItems = ref<RsContextMenuItem[]>([])
81
+
82
+ function rowKeyFor(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): string {
83
+ return (
84
+ read(options.rowKeyByIndex).get(entry.rowIndex) ??
85
+ entry.treeKey ??
86
+ resolveRowKey(entry.row, entry.rowIndex, read(options.rowKey))
87
+ )
88
+ }
89
+
90
+ /**
91
+ * 重建菜单项:默认复制 + 业务自定义。
92
+ * 有数据时预置复制项,避免 Trigger 打开瞬间 items 为空拒开。
93
+ */
94
+ function refreshCtxMenuItems(): void {
95
+ if (!read(options.enabled)) {
96
+ ctxMenuItems.value = []
97
+ return
98
+ }
99
+ const hasDataRows = read(options.dataRows).length > 0
100
+ const items = buildDefaultTableContextMenuItems({
101
+ copyCellLabel: read(options.copyCellLabel),
102
+ copyRowLabel: read(options.copyRowLabel),
103
+ hasRow: ctxRow.value != null || hasDataRows,
104
+ hasCell: ctxColKey.value != null || hasDataRows,
105
+ })
106
+ const selected = read(options.selectedRows)
107
+ const custom =
108
+ options.contextMenuItems?.(ctxRow.value as T | null, selected) ?? []
109
+ if (custom.length > 0) {
110
+ if (items.length > 0) {
111
+ items.push({ key: '__ctx-sep', label: '', separator: true })
112
+ }
113
+ items.push(...custom)
114
+ }
115
+ const menuCtx = { row: ctxRow.value as T | null, selectedRows: selected }
116
+ ctxMenuItems.value = options.mergeFeatureMenuItems
117
+ ? options.mergeFeatureMenuItems(items, menuCtx)
118
+ : items
119
+ }
120
+
121
+ watch(
122
+ () => [read(options.enabled), read(options.dataRows).length] as const,
123
+ () => {
124
+ refreshCtxMenuItems()
125
+ },
126
+ { immediate: true },
127
+ )
128
+
129
+ async function copyCtxCell(): Promise<void> {
130
+ if (!ctxRow.value || ctxRowIndex.value == null || !ctxColKey.value) return
131
+ const column = read(options.displayColumns).find((item) => item.key === ctxColKey.value)
132
+ if (!column) return
133
+ const key = resolveRowKey(ctxRow.value, ctxRowIndex.value, read(options.rowKey))
134
+ const text = resolveTableCellCopyText(
135
+ ctxRow.value,
136
+ column,
137
+ ctxRowIndex.value,
138
+ options.getDraft(key, column.key),
139
+ )
140
+ await copyTextToClipboard(text)
141
+ }
142
+
143
+ async function copyCtxRow(): Promise<void> {
144
+ if (!ctxRow.value || ctxRowIndex.value == null) return
145
+ const key = resolveRowKey(ctxRow.value, ctxRowIndex.value, read(options.rowKey))
146
+ const text = resolveTableRowCopyText(
147
+ ctxRow.value,
148
+ ctxRowIndex.value,
149
+ read(options.displayColumns),
150
+ options.getDraft,
151
+ key,
152
+ )
153
+ await copyTextToClipboard(text)
154
+ }
155
+
156
+ async function onCtxMenuSelect(key: string): Promise<void> {
157
+ if (key === TABLE_CTX_COPY_CELL) {
158
+ await copyCtxCell()
159
+ options.onSelect(key, ctxRow.value as T | null, read(options.selectedRows))
160
+ return
161
+ }
162
+ if (key === TABLE_CTX_COPY_ROW) {
163
+ await copyCtxRow()
164
+ options.onSelect(key, ctxRow.value as T | null, read(options.selectedRows))
165
+ return
166
+ }
167
+ options.onSelect(key, ctxRow.value as T | null, read(options.selectedRows))
168
+ }
169
+
170
+ function prepareRowContextmenu(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
171
+ if (
172
+ options.shouldSelectOnContextmenu() &&
173
+ options.canSelectRow(entry) &&
174
+ !options.isRowSelected(entry)
175
+ ) {
176
+ options.setSelectedRowKeys(
177
+ options.selectRowKeys(
178
+ options.getSelectedRowKeys(),
179
+ rowKeyFor(entry),
180
+ options.selectionType(),
181
+ ),
182
+ )
183
+ }
184
+ if (read(options.enabled)) {
185
+ ctxRow.value = entry.row
186
+ ctxRowIndex.value = entry.rowIndex
187
+ }
188
+ }
189
+
190
+ /**
191
+ * 捕获阶段预填:须早于 ContextMenuTrigger 冒泡打开。
192
+ */
193
+ function onContextmenuCapture(event: MouseEvent): void {
194
+ if (!read(options.enabled)) return
195
+ const target = event.target as HTMLElement | null
196
+ if (!target) return
197
+ const tr = target.closest('tr.rs-table__row') as HTMLElement | null
198
+ if (!tr) return
199
+ const rowKeyAttr = tr.getAttribute('data-row-key')
200
+ if (!rowKeyAttr) return
201
+ const entry = read(options.dataRows).find(
202
+ (item) => (item.treeKey ?? read(options.rowKeyByIndex).get(item.rowIndex) ?? '') === rowKeyAttr,
203
+ )
204
+ if (!entry) return
205
+ prepareRowContextmenu(entry)
206
+ const td = target.closest('td.rs-table__td--data') as HTMLElement | null
207
+ const colKey = td?.getAttribute('data-col-key')
208
+ ctxColKey.value = colKey || null
209
+ refreshCtxMenuItems()
210
+ }
211
+
212
+ function onTableContextmenu(event: MouseEvent): void {
213
+ if (!read(options.enabled)) return
214
+ const tr = (event.target as HTMLElement).closest('tr.rs-table__row')
215
+ if (!tr) {
216
+ ctxRow.value = null
217
+ ctxRowIndex.value = null
218
+ ctxColKey.value = null
219
+ refreshCtxMenuItems()
220
+ }
221
+ }
222
+
223
+ function onRowContextmenu(entry: RsTableRowEntry<T>, event: MouseEvent): void {
224
+ if (entry.type !== 'row') return
225
+ if ((event.target as HTMLElement).closest('.rs-table__td--data')) return
226
+ prepareRowContextmenu(entry)
227
+ ctxColKey.value = null
228
+ refreshCtxMenuItems()
229
+ options.onRowContextmenu(entry.row, entry.rowIndex, event)
230
+ }
231
+
232
+ function onCellContextmenu(entry: RsTableRowEntry<T>, colKey: string, event: MouseEvent): void {
233
+ if (entry.type !== 'row') return
234
+ prepareRowContextmenu(entry)
235
+ ctxColKey.value = colKey
236
+ refreshCtxMenuItems()
237
+ options.onRowContextmenu(entry.row, entry.rowIndex, event)
238
+ }
239
+
240
+ return {
241
+ ctxRow,
242
+ ctxRowIndex,
243
+ ctxColKey,
244
+ ctxMenuItems,
245
+ refreshCtxMenuItems,
246
+ onCtxMenuSelect,
247
+ onContextmenuCapture,
248
+ onTableContextmenu,
249
+ onRowContextmenu,
250
+ onCellContextmenu,
251
+ rowKeyFor,
252
+ }
253
+ }
254
+
255
+ export type RsTableContextMenuApi<T extends RsTableRowData> = ReturnType<
256
+ typeof useRsTableContextMenu<T>
257
+ >