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,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
|
+
>
|