niuma-ui 1.0.0
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 +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- package/vite-plugins/silence-antlr-parse-console.ts +29 -0
|
@@ -0,0 +1,2664 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends import('./table-utils').RsTableRowData">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
onActivated,
|
|
6
|
+
onDeactivated,
|
|
7
|
+
onMounted,
|
|
8
|
+
onUnmounted,
|
|
9
|
+
ref,
|
|
10
|
+
useSlots,
|
|
11
|
+
watch,
|
|
12
|
+
} from 'vue'
|
|
13
|
+
import { useRsI18n } from '../composables/useRsI18n'
|
|
14
|
+
import RsIcon from './RsIcon.vue'
|
|
15
|
+
import RsContextMenu from './RsContextMenu.vue'
|
|
16
|
+
import type { RsContextMenuItem } from './context-menu-utils'
|
|
17
|
+
import type {
|
|
18
|
+
RsTableColumn,
|
|
19
|
+
RsTableGroupBy,
|
|
20
|
+
RsTableRowDropPosition,
|
|
21
|
+
RsTableRowEntry,
|
|
22
|
+
RsTableRowKey,
|
|
23
|
+
RsTableSelectionType,
|
|
24
|
+
RsTableSize,
|
|
25
|
+
RsTableSortOrder,
|
|
26
|
+
RsTableSortState,
|
|
27
|
+
} from './table-utils'
|
|
28
|
+
import { useTableCellTooltip } from '../composables/useTableCellTooltip'
|
|
29
|
+
import { useTableEdit } from '../composables/useTableEdit'
|
|
30
|
+
import { useTableRowHighlight } from '../composables/useTableRowHighlight'
|
|
31
|
+
import RsTableBodyRow from './table/RsTableBodyRow.vue'
|
|
32
|
+
import RsTableHeaderFilter from './table/RsTableHeaderFilter.vue'
|
|
33
|
+
import {
|
|
34
|
+
isBooleanToggleColumn,
|
|
35
|
+
isCellValueChanged,
|
|
36
|
+
isColumnEditable,
|
|
37
|
+
isColumnNullable,
|
|
38
|
+
isRowEditPending,
|
|
39
|
+
listBatchColumnTargets,
|
|
40
|
+
navigateEditableCell,
|
|
41
|
+
nullToEditText,
|
|
42
|
+
parseCellEditInput,
|
|
43
|
+
parseClipboardGrid,
|
|
44
|
+
resolveCellEditText,
|
|
45
|
+
resolveColumnEditTrigger,
|
|
46
|
+
resolveColumnEditorOptions,
|
|
47
|
+
resolveColumnRawValue,
|
|
48
|
+
validateCellValueAsync,
|
|
49
|
+
type RsTableCellEditFocusMode,
|
|
50
|
+
type RsTableCellNavigateDirection,
|
|
51
|
+
} from './table/table-edit-utils'
|
|
52
|
+
import {
|
|
53
|
+
buildDefaultTableContextMenuItems,
|
|
54
|
+
resolveTableCellCopyText,
|
|
55
|
+
resolveTableRowCopyText,
|
|
56
|
+
TABLE_CTX_COPY_CELL,
|
|
57
|
+
TABLE_CTX_COPY_ROW,
|
|
58
|
+
} from './table/table-context-menu'
|
|
59
|
+
import { copyTextToClipboard } from '../utils/rs-clipboard'
|
|
60
|
+
import {
|
|
61
|
+
buildTableEntries,
|
|
62
|
+
buildVirtualHeightModel,
|
|
63
|
+
clampColumnWidth,
|
|
64
|
+
columnUsesSharedTooltip,
|
|
65
|
+
createInitialColumnWidths,
|
|
66
|
+
fixedCellStyle,
|
|
67
|
+
getSortOrderForKey,
|
|
68
|
+
getSortPriorityForKey,
|
|
69
|
+
injectExpandRows,
|
|
70
|
+
isColumnFilterActive,
|
|
71
|
+
isNearScrollBottom,
|
|
72
|
+
isTableRowDisabled,
|
|
73
|
+
parseColumnWidth,
|
|
74
|
+
reorderColumnKeys,
|
|
75
|
+
resolveCellTooltipMode,
|
|
76
|
+
resolveCellTooltipText,
|
|
77
|
+
resolveColumnOrder,
|
|
78
|
+
resolveColumnStyle,
|
|
79
|
+
resolveEntryKey,
|
|
80
|
+
resolveFixedColumnStyles,
|
|
81
|
+
resolveOrderedColumns,
|
|
82
|
+
resolveRowKey,
|
|
83
|
+
resolveScrollWidth,
|
|
84
|
+
resolveSelectAllState,
|
|
85
|
+
resolveTableRowHeight,
|
|
86
|
+
resolveTableSize,
|
|
87
|
+
selectRowKeys,
|
|
88
|
+
selectRowKeysByClick,
|
|
89
|
+
sliceVirtualColumns,
|
|
90
|
+
sliceVirtualHeightModel,
|
|
91
|
+
resolveColumnPixelWidth,
|
|
92
|
+
toggleExpandedRowKeys,
|
|
93
|
+
toggleMultiSortState,
|
|
94
|
+
toggleSelectAll,
|
|
95
|
+
toggleSortState,
|
|
96
|
+
} from './table-utils'
|
|
97
|
+
import {
|
|
98
|
+
createTableRowDragHandlers,
|
|
99
|
+
createTableRowDragState,
|
|
100
|
+
type RsTableRowDragTrigger,
|
|
101
|
+
type RsTableRowDropMode,
|
|
102
|
+
} from './table-drag'
|
|
103
|
+
import { resolveVirtualListHeight } from './virtual-list-utils'
|
|
104
|
+
import './table/rs-table.css'
|
|
105
|
+
|
|
106
|
+
defineOptions({ inheritAttrs: false })
|
|
107
|
+
|
|
108
|
+
const props = withDefaults(
|
|
109
|
+
defineProps<{
|
|
110
|
+
columns: RsTableColumn<T>[]
|
|
111
|
+
data: T[]
|
|
112
|
+
rowKey?: RsTableRowKey<T>
|
|
113
|
+
loading?: boolean
|
|
114
|
+
bordered?: boolean
|
|
115
|
+
columnBordered?: boolean
|
|
116
|
+
/** 外容器圆角;嵌套在已有圆角/直角父级时可设为 false */
|
|
117
|
+
rounded?: boolean
|
|
118
|
+
/** 填充父级高度:表格自身成为受限滚动容器,纵向滚动内置、横向滚动条固定在可视底部 */
|
|
119
|
+
fill?: boolean
|
|
120
|
+
compact?: boolean
|
|
121
|
+
size?: RsTableSize
|
|
122
|
+
scrollX?: number | string
|
|
123
|
+
selectable?: boolean
|
|
124
|
+
selectionType?: RsTableSelectionType
|
|
125
|
+
selectedRowKeys?: string[]
|
|
126
|
+
defaultSelectedRowKeys?: string[]
|
|
127
|
+
rowSelectable?: (row: T, index: number) => boolean
|
|
128
|
+
expandable?: boolean
|
|
129
|
+
expandedRowKeys?: string[]
|
|
130
|
+
defaultExpandedRowKeys?: string[]
|
|
131
|
+
rowExpandable?: (row: T, index: number) => boolean
|
|
132
|
+
expandRowHeight?: number
|
|
133
|
+
striped?: boolean
|
|
134
|
+
showIndex?: boolean
|
|
135
|
+
/** 行号列宽度(px,showIndex 且未显示 edit gutter 时生效) */
|
|
136
|
+
indexWidth?: number
|
|
137
|
+
showHeader?: boolean
|
|
138
|
+
remoteSort?: boolean
|
|
139
|
+
filterText?: string
|
|
140
|
+
filterKeys?: string[]
|
|
141
|
+
columnFilters?: Record<string, string>
|
|
142
|
+
defaultColumnFilters?: Record<string, string>
|
|
143
|
+
groupBy?: RsTableGroupBy<T>
|
|
144
|
+
groupLabel?: (key: string) => string
|
|
145
|
+
sort?: RsTableSortState | null
|
|
146
|
+
defaultSort?: RsTableSortState | null
|
|
147
|
+
multiSort?: boolean
|
|
148
|
+
maxSort?: number
|
|
149
|
+
sorts?: RsTableSortState[]
|
|
150
|
+
defaultSorts?: RsTableSortState[]
|
|
151
|
+
columnDraggable?: boolean
|
|
152
|
+
columnOrder?: string[]
|
|
153
|
+
defaultColumnOrder?: string[]
|
|
154
|
+
rowDraggable?: boolean
|
|
155
|
+
/** 行拖拽触发方式:手柄列或整行 */
|
|
156
|
+
rowDragTrigger?: RsTableRowDragTrigger
|
|
157
|
+
/** reorder=上下插入排序;into=拖入目标行 */
|
|
158
|
+
rowDropMode?: RsTableRowDropMode
|
|
159
|
+
rowDraggableWhen?: (row: T, index: number) => boolean
|
|
160
|
+
rowDropTargetWhen?: (row: T, index: number) => boolean
|
|
161
|
+
canRowDrop?: (dragKeys: string[], dropKey: string) => boolean
|
|
162
|
+
virtual?: boolean
|
|
163
|
+
/** fill 模式下数据行数达到该阈值时自动启用虚拟滚动;0 表示禁用自动启用 */
|
|
164
|
+
virtualAutoThreshold?: number
|
|
165
|
+
/**
|
|
166
|
+
* 列虚拟化:仅渲染横向可视区附近的数据列。
|
|
167
|
+
* true 强制开;false 强制关;默认 `'auto'` 由 virtualColumnsAutoThreshold 判定。
|
|
168
|
+
* (勿用可选 boolean:Vue 会把缺省 prop 收成 false,导致自动模式失效。)
|
|
169
|
+
*/
|
|
170
|
+
virtualColumns?: boolean | 'auto'
|
|
171
|
+
/** 数据列数达到该阈值时自动列虚拟化;0 表示禁用自动。默认 40 */
|
|
172
|
+
virtualColumnsAutoThreshold?: number
|
|
173
|
+
/** 列虚拟化左右额外保留列数 */
|
|
174
|
+
virtualColumnOverscan?: number
|
|
175
|
+
height?: number | string
|
|
176
|
+
rowHeight?: number
|
|
177
|
+
overscan?: number
|
|
178
|
+
infinite?: boolean
|
|
179
|
+
hasMore?: boolean
|
|
180
|
+
loadingMore?: boolean
|
|
181
|
+
infiniteDistance?: number
|
|
182
|
+
virtualOnInfinite?: boolean
|
|
183
|
+
resizable?: boolean
|
|
184
|
+
/** auto:列宽随内容/表头自适应;fixed:列宽固定(拖拽后精确锁定) */
|
|
185
|
+
columnLayout?: 'auto' | 'fixed'
|
|
186
|
+
/** 列宽初始值(单向);拖拽后由组件内部维护,不受外部 prop 覆盖 */
|
|
187
|
+
initialColumnWidths?: Record<string, number>
|
|
188
|
+
minColumnWidth?: number
|
|
189
|
+
maxColumnWidth?: number
|
|
190
|
+
/** 右键行时自动选中(selectable 开启时默认 true) */
|
|
191
|
+
selectOnContextmenu?: boolean
|
|
192
|
+
/**
|
|
193
|
+
* 额外右键菜单项(追加在默认「复制单元格 / 复制行」之后)。
|
|
194
|
+
* @param row 右键的行(空白区域为 null)
|
|
195
|
+
* @param selectedRows 当前已选行
|
|
196
|
+
*/
|
|
197
|
+
contextMenuItems?: (row: T | null, selectedRows: T[]) => RsContextMenuItem[]
|
|
198
|
+
/** 启用内置右键菜单(复制单元格 / 复制行),默认 true */
|
|
199
|
+
contextMenu?: boolean
|
|
200
|
+
/** 启用表格级共享 cell tooltip(ellipsis / column.tooltip);整表仅一个浮层 */
|
|
201
|
+
cellTooltip?: boolean
|
|
202
|
+
/** 共享 tooltip 悬停延迟(ms),0 为即时显示 */
|
|
203
|
+
cellTooltipDelay?: number
|
|
204
|
+
/** 列头 headerTip 走同一共享浮层,默认 true */
|
|
205
|
+
headerTooltip?: boolean
|
|
206
|
+
/** 启用行内单元格编辑 */
|
|
207
|
+
editable?: boolean
|
|
208
|
+
/** 默认单元格编辑触发方式 */
|
|
209
|
+
editTrigger?: 'click' | 'dblclick'
|
|
210
|
+
/** 行内编辑时显示 Monaco 式 gutter(行号 / 待提交图标) */
|
|
211
|
+
editGutter?: boolean
|
|
212
|
+
/**
|
|
213
|
+
* 编辑 gutter(行号 / 提交)列宽度(px)。
|
|
214
|
+
* 可与 RsCodeEditor 的 gutterWidth 对齐。
|
|
215
|
+
*/
|
|
216
|
+
editGutterWidth?: number
|
|
217
|
+
/** 启用当前行高亮(与 checkbox 选区独立) */
|
|
218
|
+
highlightRow?: boolean
|
|
219
|
+
highlightedRowKey?: string
|
|
220
|
+
defaultHighlightedRowKey?: string
|
|
221
|
+
/** 点击行时更新高亮行 */
|
|
222
|
+
highlightRowOnClick?: boolean
|
|
223
|
+
/** 单行手动提交:单元格编辑暂存,点击行状态列提交 */
|
|
224
|
+
rowCommit?: boolean
|
|
225
|
+
/**
|
|
226
|
+
* 额外标记行待提交(如业务侧「新建行」草稿)。
|
|
227
|
+
* 为 true 时 gutter 显示提交/撤销;点击提交触发 rowEditCommit(changes 可为空)。
|
|
228
|
+
*/
|
|
229
|
+
rowPending?: (row: T, index: number) => boolean
|
|
230
|
+
/** 显示行状态列(commit / rollback 图标) */
|
|
231
|
+
showRowStatus?: boolean
|
|
232
|
+
/** 允许显式 NULL(Ctrl/Cmd+0);列级 nullable 可覆盖 */
|
|
233
|
+
allowNull?: boolean
|
|
234
|
+
/** NULL 展示文案 */
|
|
235
|
+
nullLabel?: string
|
|
236
|
+
/** 进入编辑时光标策略 */
|
|
237
|
+
editFocusMode?: RsTableCellEditFocusMode
|
|
238
|
+
/** 启用键盘导航(F2 / 方向键 / Tab) */
|
|
239
|
+
editKeyboard?: boolean
|
|
240
|
+
/** 启用 Ctrl+Z / Ctrl+Y 撤销重做 */
|
|
241
|
+
editUndo?: boolean
|
|
242
|
+
/** 撤销栈上限 */
|
|
243
|
+
editUndoLimit?: number
|
|
244
|
+
/** 启用 TSV 粘贴到焦点格 */
|
|
245
|
+
editPaste?: boolean
|
|
246
|
+
/**
|
|
247
|
+
* 多选行时,提交当前列会批量写入所有已选可编辑行。
|
|
248
|
+
* 需同时开启 selectable(checkbox 或 row)。
|
|
249
|
+
*/
|
|
250
|
+
editBatch?: boolean
|
|
251
|
+
/**
|
|
252
|
+
* 视图切换标识:变化时重置滚动位置(不销毁实例)。
|
|
253
|
+
* 用于同一表格实例切换不同数据集(如多结果页签),避免 :key 整表重建。
|
|
254
|
+
*/
|
|
255
|
+
viewKey?: string | number
|
|
256
|
+
/**
|
|
257
|
+
* 外层 keep-alive 可见性;变为 true 时重测视口并把已保存的 scroll 写回 DOM。
|
|
258
|
+
* 脱离文档后浏览器常把 scrollTop 清零,故须用内部状态恢复,而非从 DOM 回读。
|
|
259
|
+
* 父级 Session 经 onActivated/onDeactivated 传入(比单靠本组件 onActivated 更稳)。
|
|
260
|
+
*/
|
|
261
|
+
layoutActive?: boolean
|
|
262
|
+
}>(),
|
|
263
|
+
{
|
|
264
|
+
loading: false,
|
|
265
|
+
bordered: true,
|
|
266
|
+
columnBordered: false,
|
|
267
|
+
rounded: false,
|
|
268
|
+
fill: false,
|
|
269
|
+
compact: false,
|
|
270
|
+
size: 'md',
|
|
271
|
+
selectable: false,
|
|
272
|
+
selectionType: 'checkbox',
|
|
273
|
+
expandable: false,
|
|
274
|
+
defaultExpandedRowKeys: () => [],
|
|
275
|
+
striped: false,
|
|
276
|
+
showIndex: false,
|
|
277
|
+
indexWidth: 56,
|
|
278
|
+
showHeader: true,
|
|
279
|
+
remoteSort: false,
|
|
280
|
+
defaultSelectedRowKeys: () => [],
|
|
281
|
+
defaultSorts: () => [],
|
|
282
|
+
defaultColumnOrder: () => [],
|
|
283
|
+
multiSort: false,
|
|
284
|
+
maxSort: 3,
|
|
285
|
+
columnDraggable: false,
|
|
286
|
+
rowDraggable: false,
|
|
287
|
+
rowDragTrigger: 'handle',
|
|
288
|
+
rowDropMode: 'reorder',
|
|
289
|
+
overscan: 4,
|
|
290
|
+
virtualColumnOverscan: 2,
|
|
291
|
+
infiniteDistance: 80,
|
|
292
|
+
virtualOnInfinite: true,
|
|
293
|
+
virtualAutoThreshold: 50,
|
|
294
|
+
virtualColumns: 'auto',
|
|
295
|
+
virtualColumnsAutoThreshold: 40,
|
|
296
|
+
resizable: false,
|
|
297
|
+
columnLayout: 'auto',
|
|
298
|
+
minColumnWidth: 48,
|
|
299
|
+
maxColumnWidth: 640,
|
|
300
|
+
cellTooltip: true,
|
|
301
|
+
cellTooltipDelay: 300,
|
|
302
|
+
headerTooltip: true,
|
|
303
|
+
editable: false,
|
|
304
|
+
editTrigger: 'dblclick',
|
|
305
|
+
editGutter: true,
|
|
306
|
+
editGutterWidth: 32,
|
|
307
|
+
highlightRow: false,
|
|
308
|
+
highlightRowOnClick: true,
|
|
309
|
+
rowCommit: false,
|
|
310
|
+
showRowStatus: true,
|
|
311
|
+
allowNull: true,
|
|
312
|
+
nullLabel: '(NULL)',
|
|
313
|
+
editFocusMode: 'end',
|
|
314
|
+
editKeyboard: true,
|
|
315
|
+
editUndo: true,
|
|
316
|
+
editUndoLimit: 50,
|
|
317
|
+
editPaste: true,
|
|
318
|
+
editBatch: true,
|
|
319
|
+
contextMenu: true,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const emit = defineEmits<{
|
|
324
|
+
rowClick: [row: T, index: number]
|
|
325
|
+
rowDblclick: [row: T, index: number]
|
|
326
|
+
/**
|
|
327
|
+
* 单元格双击且未进入编辑时发出(只读查看大字段等)。
|
|
328
|
+
* 与 rowDblclick 同时发出,便于结果网格打开查看弹窗而不影响行级双击。
|
|
329
|
+
*/
|
|
330
|
+
cellView: [row: T, column: RsTableColumn<T>, index: number]
|
|
331
|
+
rowContextmenu: [row: T, index: number, event: MouseEvent]
|
|
332
|
+
contextMenuSelect: [key: string, row: T | null, selectedRows: T[]]
|
|
333
|
+
'update:sort': [value: RsTableSortState | null]
|
|
334
|
+
'update:sorts': [value: RsTableSortState[]]
|
|
335
|
+
sortsChange: [value: RsTableSortState[]]
|
|
336
|
+
'update:columnOrder': [value: string[]]
|
|
337
|
+
columnOrderChange: [value: string[]]
|
|
338
|
+
'update:columnFilters': [value: Record<string, string>]
|
|
339
|
+
columnFiltersChange: [value: Record<string, string>]
|
|
340
|
+
rowDrop: [dragKeys: string[], dropKey: string, position: RsTableRowDropPosition]
|
|
341
|
+
rowDragStart: [dragKeys: string[], event: DragEvent]
|
|
342
|
+
'update:selectedRowKeys': [keys: string[]]
|
|
343
|
+
'update:expandedRowKeys': [keys: string[]]
|
|
344
|
+
selectionChange: [keys: string[]]
|
|
345
|
+
expandChange: [keys: string[]]
|
|
346
|
+
loadMore: []
|
|
347
|
+
columnResize: [key: string, width: number]
|
|
348
|
+
'update:highlightedRowKey': [key: string | undefined]
|
|
349
|
+
highlightChange: [key: string | undefined]
|
|
350
|
+
cellEditStart: [row: T, column: RsTableColumn<T>, index: number]
|
|
351
|
+
/** 大字段等:请求外层弹窗编辑(不进入行内编辑态) */
|
|
352
|
+
cellEditDialog: [row: T, column: RsTableColumn<T>, index: number, draft: string]
|
|
353
|
+
cellEditCommit: [row: T, column: RsTableColumn<T>, index: number, value: unknown, previous: unknown]
|
|
354
|
+
cellEditCancel: [row: T, column: RsTableColumn<T>, index: number]
|
|
355
|
+
cellEditInvalid: [row: T, column: RsTableColumn<T>, index: number, message: string, value: unknown]
|
|
356
|
+
cellEditBatchCommit: [
|
|
357
|
+
column: RsTableColumn<T>,
|
|
358
|
+
changes: Array<{ row: T; index: number; value: unknown; previous: unknown }>,
|
|
359
|
+
]
|
|
360
|
+
cellEditUndo: [entry: {
|
|
361
|
+
items: Array<{ rowKey: string; colKey: string; rowIndex: number; previous: unknown; next: unknown }>
|
|
362
|
+
}]
|
|
363
|
+
cellEditRedo: [entry: {
|
|
364
|
+
items: Array<{ rowKey: string; colKey: string; rowIndex: number; previous: unknown; next: unknown }>
|
|
365
|
+
}]
|
|
366
|
+
cellEditReject: [row: T, index: number, reason?: string]
|
|
367
|
+
rowEditCommit: [
|
|
368
|
+
row: T,
|
|
369
|
+
index: number,
|
|
370
|
+
changes: Array<{ colKey: string; value: unknown; previous: unknown }>,
|
|
371
|
+
]
|
|
372
|
+
rowEditRollback: [row: T, index: number]
|
|
373
|
+
}>()
|
|
374
|
+
|
|
375
|
+
const { t } = useRsI18n()
|
|
376
|
+
const slots = useSlots()
|
|
377
|
+
|
|
378
|
+
const sharedTipRef = ref<HTMLElement | null>(null)
|
|
379
|
+
const {
|
|
380
|
+
state: cellTipState,
|
|
381
|
+
onPointerOver: onCellTipOver,
|
|
382
|
+
onPointerOut: onCellTipOut,
|
|
383
|
+
hide: hideCellTooltip,
|
|
384
|
+
} = useTableCellTooltip({
|
|
385
|
+
enabled: () => props.cellTooltip,
|
|
386
|
+
headerEnabled: () => props.headerTooltip,
|
|
387
|
+
delay: () => props.cellTooltipDelay,
|
|
388
|
+
tipRef: sharedTipRef,
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const showEditGutterColumn = computed(() => props.editable && props.editGutter)
|
|
392
|
+
const showIndexColumn = computed(() => props.showIndex && !showEditGutterColumn.value)
|
|
393
|
+
const showRowStatusColumn = computed(() => props.rowCommit && props.showRowStatus && !showEditGutterColumn.value)
|
|
394
|
+
|
|
395
|
+
const resolvedGutterWidth = computed(() => Math.max(24, props.editGutterWidth || 32))
|
|
396
|
+
const resolvedIndexWidth = computed(() => Math.max(24, props.indexWidth || 56))
|
|
397
|
+
|
|
398
|
+
const tableEdit = useTableEdit({
|
|
399
|
+
enabled: () => props.editable,
|
|
400
|
+
undoLimit: () => props.editUndoLimit,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const activeEditCellKey = computed(() => {
|
|
404
|
+
const cell = tableEdit.editingCell.value
|
|
405
|
+
return cell ? `${cell.rowKey}:${cell.colKey}` : ''
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const activeFocusCellKey = computed(() => {
|
|
409
|
+
const cell = tableEdit.focusCell.value
|
|
410
|
+
return cell ? `${cell.rowKey}:${cell.colKey}` : ''
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
const activeErrorMapSize = computed(() => tableEdit.errorMap.value.size)
|
|
414
|
+
const activeValidatingMapSize = computed(() => tableEdit.validatingMap.value.size)
|
|
415
|
+
|
|
416
|
+
const contextMenuEnabled = computed(() => props.contextMenu !== false)
|
|
417
|
+
|
|
418
|
+
const isHighlightControlled = computed(() => props.highlightedRowKey !== undefined)
|
|
419
|
+
const rowHighlight = useTableRowHighlight({
|
|
420
|
+
enabled: () => props.highlightRow,
|
|
421
|
+
highlightRowOnClick: () => props.highlightRowOnClick,
|
|
422
|
+
isControlled: () => isHighlightControlled.value,
|
|
423
|
+
highlightedRowKey: () => props.highlightedRowKey,
|
|
424
|
+
defaultHighlightedRowKey: () => props.defaultHighlightedRowKey,
|
|
425
|
+
onUpdate: (value) => {
|
|
426
|
+
if (isHighlightControlled.value) emit('update:highlightedRowKey', value)
|
|
427
|
+
emit('highlightChange', value)
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const internalSort = ref<RsTableSortState | null>(props.defaultSort ?? null)
|
|
432
|
+
const internalSorts = ref<RsTableSortState[]>([...props.defaultSorts])
|
|
433
|
+
const internalColumnOrder = ref<string[]>([...props.defaultColumnOrder])
|
|
434
|
+
const scrollTop = ref(0)
|
|
435
|
+
const scrollLeft = ref(0)
|
|
436
|
+
const internalColumnWidths = ref<Record<string, number>>({})
|
|
437
|
+
const loadMoreLocked = ref(false)
|
|
438
|
+
const internalSelectedRowKeys = ref<string[]>([...props.defaultSelectedRowKeys])
|
|
439
|
+
const internalExpandedRowKeys = ref<string[]>([...props.defaultExpandedRowKeys])
|
|
440
|
+
const dragColumnKey = ref<string | null>(null)
|
|
441
|
+
const dropColumnKey = ref<string | null>(null)
|
|
442
|
+
const rowDragState = createTableRowDragState()
|
|
443
|
+
const { dragRowKeys, dropRowTargetKey, dropRowPosition } = rowDragState
|
|
444
|
+
const showRowDragHandle = computed(
|
|
445
|
+
() => props.rowDraggable && props.rowDragTrigger === 'handle',
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const resolvedSize = computed(() => resolveTableSize(props.compact, props.size))
|
|
449
|
+
|
|
450
|
+
const isSelectionControlled = computed(() => props.selectedRowKeys !== undefined)
|
|
451
|
+
const selectedRowKeys = computed({
|
|
452
|
+
get: () => (isSelectionControlled.value ? props.selectedRowKeys ?? [] : internalSelectedRowKeys.value),
|
|
453
|
+
set: (value: string[]) => {
|
|
454
|
+
if (isSelectionControlled.value) emit('update:selectedRowKeys', value)
|
|
455
|
+
else internalSelectedRowKeys.value = value
|
|
456
|
+
emit('selectionChange', value)
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
const selectedKeySet = computed(() => new Set(selectedRowKeys.value))
|
|
461
|
+
|
|
462
|
+
const isExpandedControlled = computed(() => props.expandedRowKeys !== undefined)
|
|
463
|
+
const expandedRowKeys = computed({
|
|
464
|
+
get: () => (isExpandedControlled.value ? props.expandedRowKeys ?? [] : internalExpandedRowKeys.value),
|
|
465
|
+
set: (value: string[]) => {
|
|
466
|
+
if (isExpandedControlled.value) emit('update:expandedRowKeys', value)
|
|
467
|
+
else internalExpandedRowKeys.value = value
|
|
468
|
+
emit('expandChange', value)
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
const expandedKeySet = computed(() => new Set(expandedRowKeys.value))
|
|
472
|
+
|
|
473
|
+
const virtualScrollEnabled = computed(() => {
|
|
474
|
+
if (props.virtual === true) return true
|
|
475
|
+
if (props.infinite && props.virtualOnInfinite !== false) return true
|
|
476
|
+
if (props.virtual === false) return false
|
|
477
|
+
const threshold = props.virtualAutoThreshold ?? 0
|
|
478
|
+
return Boolean(props.fill && threshold > 0 && props.data.length >= threshold)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const isSortControlled = computed(() => props.sort !== undefined)
|
|
482
|
+
const sortState = computed({
|
|
483
|
+
get: () => (isSortControlled.value ? props.sort ?? null : internalSort.value),
|
|
484
|
+
set: (value: RsTableSortState | null) => {
|
|
485
|
+
if (isSortControlled.value) emit('update:sort', value)
|
|
486
|
+
else internalSort.value = value
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const isSortsControlled = computed(() => props.sorts !== undefined)
|
|
491
|
+
const sortsState = computed({
|
|
492
|
+
get: () => (isSortsControlled.value ? props.sorts ?? [] : internalSorts.value),
|
|
493
|
+
set: (value: RsTableSortState[]) => {
|
|
494
|
+
if (isSortsControlled.value) emit('update:sorts', value)
|
|
495
|
+
else internalSorts.value = value
|
|
496
|
+
emit('sortsChange', value)
|
|
497
|
+
},
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const isColumnOrderControlled = computed(() => props.columnOrder !== undefined)
|
|
501
|
+
const columnOrderState = computed({
|
|
502
|
+
get: () => {
|
|
503
|
+
if (isColumnOrderControlled.value) {
|
|
504
|
+
return props.columnOrder ?? []
|
|
505
|
+
}
|
|
506
|
+
if (internalColumnOrder.value.length) {
|
|
507
|
+
return internalColumnOrder.value
|
|
508
|
+
}
|
|
509
|
+
return resolveColumnOrder(props.columns)
|
|
510
|
+
},
|
|
511
|
+
set: (value: string[]) => {
|
|
512
|
+
if (isColumnOrderControlled.value) emit('update:columnOrder', value)
|
|
513
|
+
else internalColumnOrder.value = value
|
|
514
|
+
emit('columnOrderChange', value)
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
const isColumnFiltersControlled = computed(() => props.columnFilters !== undefined)
|
|
519
|
+
const internalColumnFilters = ref<Record<string, string>>(props.defaultColumnFilters ?? {})
|
|
520
|
+
const columnFiltersState = computed(() => props.columnFilters ?? internalColumnFilters.value)
|
|
521
|
+
|
|
522
|
+
function updateColumnFilter(key: string, value: string) {
|
|
523
|
+
const next = { ...columnFiltersState.value }
|
|
524
|
+
if (value.trim()) next[key] = value
|
|
525
|
+
else delete next[key]
|
|
526
|
+
if (isColumnFiltersControlled.value) emit('update:columnFilters', next)
|
|
527
|
+
else internalColumnFilters.value = next
|
|
528
|
+
emit('columnFiltersChange', next)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function columnFilterValue(key: string): string {
|
|
532
|
+
return columnFiltersState.value[key] ?? ''
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const displayColumns = computed(() => resolveOrderedColumns(props.columns, columnOrderState.value))
|
|
536
|
+
|
|
537
|
+
const resolvedColumnWidths = computed({
|
|
538
|
+
get: () => internalColumnWidths.value,
|
|
539
|
+
set: (value: Record<string, number>) => {
|
|
540
|
+
internalColumnWidths.value = value
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
/** 列宽已显式定义(fixed / 每列均有 width 或初始值)时需稳定布局,避免松手回弹 */
|
|
545
|
+
const useStableColumnWidths = computed(() => {
|
|
546
|
+
if (!props.resizable) return false
|
|
547
|
+
if (props.columnLayout === 'fixed') return true
|
|
548
|
+
const widths = internalColumnWidths.value
|
|
549
|
+
return displayColumns.value.every((col) => {
|
|
550
|
+
return typeof widths[col.key] === 'number' || col.width !== undefined
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
function syncInternalColumnWidths(columns: typeof props.columns): void {
|
|
555
|
+
const preserved: Record<string, number> = {}
|
|
556
|
+
for (const column of columns) {
|
|
557
|
+
const width = internalColumnWidths.value[column.key]
|
|
558
|
+
if (typeof width === 'number') preserved[column.key] = width
|
|
559
|
+
}
|
|
560
|
+
internalColumnWidths.value = createInitialColumnWidths(
|
|
561
|
+
columns,
|
|
562
|
+
{ ...props.initialColumnWidths, ...preserved },
|
|
563
|
+
{ forceAll: props.resizable && useStableColumnWidths.value },
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
watch(
|
|
568
|
+
() => props.columns,
|
|
569
|
+
(columns) => {
|
|
570
|
+
syncInternalColumnWidths(columns)
|
|
571
|
+
if (!isColumnOrderControlled.value) {
|
|
572
|
+
internalColumnOrder.value = resolveColumnOrder(columns, internalColumnOrder.value)
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
{ immediate: true },
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
let viewportResizeObserver: ResizeObserver | null = null
|
|
579
|
+
|
|
580
|
+
/** keep-alive 恢复用:不受 DOM 清零 / 恢复期伪 scroll 事件干扰 */
|
|
581
|
+
let preservedScrollTop = 0
|
|
582
|
+
let preservedScrollLeft = 0
|
|
583
|
+
/** 正在把保存的滚动写回 DOM;期间忽略 scroll 事件,避免被清零覆盖 */
|
|
584
|
+
let restoringScroll = false
|
|
585
|
+
let restoreGeneration = 0
|
|
586
|
+
|
|
587
|
+
function syncMeasuredViewportSize(): void {
|
|
588
|
+
const el = scrollContainerRef.value
|
|
589
|
+
if (!el) return
|
|
590
|
+
const nextH = el.clientHeight
|
|
591
|
+
if (nextH > 0 && nextH !== measuredViewportHeight.value) {
|
|
592
|
+
measuredViewportHeight.value = nextH
|
|
593
|
+
}
|
|
594
|
+
const nextW = el.clientWidth
|
|
595
|
+
if (nextW > 0 && nextW !== measuredViewportWidth.value) {
|
|
596
|
+
measuredViewportWidth.value = nextW
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 失活前快照滚动。
|
|
602
|
+
* DOM 脱离文档后 scrollTop 常为 0——切勿用 0 覆盖 onScroll 已写入的位置。
|
|
603
|
+
*/
|
|
604
|
+
function captureScrollFromDom(): void {
|
|
605
|
+
const el = scrollContainerRef.value
|
|
606
|
+
if (el) {
|
|
607
|
+
if (el.scrollTop > 0) scrollTop.value = el.scrollTop
|
|
608
|
+
if (el.scrollLeft > 0) scrollLeft.value = el.scrollLeft
|
|
609
|
+
}
|
|
610
|
+
if (scrollTop.value > 0) preservedScrollTop = scrollTop.value
|
|
611
|
+
if (scrollLeft.value > 0) preservedScrollLeft = scrollLeft.value
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** 将内部保存的滚动写回 DOM,并同步 reactive(驱动虚拟切片)。 */
|
|
615
|
+
function restoreScrollToDom(): void {
|
|
616
|
+
const el = scrollContainerRef.value
|
|
617
|
+
if (!el) return
|
|
618
|
+
syncMeasuredViewportSize()
|
|
619
|
+
const top = preservedScrollTop
|
|
620
|
+
const left = preservedScrollLeft
|
|
621
|
+
if (scrollTop.value !== top) scrollTop.value = top
|
|
622
|
+
if (scrollLeft.value !== left) scrollLeft.value = left
|
|
623
|
+
if (el.scrollTop !== top) el.scrollTop = top
|
|
624
|
+
if (el.scrollLeft !== left) el.scrollLeft = left
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* 布局稳定后多次写回滚动:keep-alive 切回首帧 clientHeight 常为 0,
|
|
629
|
+
* 需等 nextTick / rAF / 视口测稳后再设 scrollTop,否则会被浏览器钳成 0。
|
|
630
|
+
*/
|
|
631
|
+
function scheduleVirtualLayoutSync(): void {
|
|
632
|
+
restoreGeneration += 1
|
|
633
|
+
const gen = restoreGeneration
|
|
634
|
+
restoringScroll = true
|
|
635
|
+
const apply = (): void => {
|
|
636
|
+
if (gen !== restoreGeneration) return
|
|
637
|
+
restoreScrollToDom()
|
|
638
|
+
}
|
|
639
|
+
apply()
|
|
640
|
+
void nextTick(() => {
|
|
641
|
+
apply()
|
|
642
|
+
requestAnimationFrame(() => {
|
|
643
|
+
apply()
|
|
644
|
+
requestAnimationFrame(() => {
|
|
645
|
+
apply()
|
|
646
|
+
if (gen === restoreGeneration) restoringScroll = false
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function resetScrollPosition(): void {
|
|
653
|
+
preservedScrollTop = 0
|
|
654
|
+
preservedScrollLeft = 0
|
|
655
|
+
scrollTop.value = 0
|
|
656
|
+
scrollLeft.value = 0
|
|
657
|
+
const el = scrollContainerRef.value
|
|
658
|
+
if (el) {
|
|
659
|
+
el.scrollTop = 0
|
|
660
|
+
el.scrollLeft = 0
|
|
661
|
+
}
|
|
662
|
+
scheduleVirtualLayoutSync()
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const dragColumnOffset = computed(() => (showRowDragHandle.value ? 40 : 0))
|
|
666
|
+
const expandColumnOffset = computed(() => dragColumnOffset.value + (props.expandable ? 40 : 0))
|
|
667
|
+
/** 左侧选择列仅 checkbox / radio 显示;row 模式靠点击行多选 */
|
|
668
|
+
const showSelectColumn = computed(
|
|
669
|
+
() => props.selectable && props.selectionType !== 'row',
|
|
670
|
+
)
|
|
671
|
+
const isRowSelection = computed(() => props.selectable && props.selectionType === 'row')
|
|
672
|
+
const selectColumnOffset = computed(
|
|
673
|
+
() => expandColumnOffset.value + (showSelectColumn.value ? 40 : 0),
|
|
674
|
+
)
|
|
675
|
+
/** 行点击多选的 Shift 锚点 */
|
|
676
|
+
const selectionAnchorKey = ref<string | null>(null)
|
|
677
|
+
|
|
678
|
+
const fixedRowHeight = computed(() => resolveTableRowHeight(resolvedSize.value, props.rowHeight))
|
|
679
|
+
const scrollContainerRef = ref<HTMLElement | null>(null)
|
|
680
|
+
const measuredViewportHeight = ref(0)
|
|
681
|
+
const measuredViewportWidth = ref(0)
|
|
682
|
+
const viewportHeight = computed(() => {
|
|
683
|
+
if (typeof props.height === 'number' && props.height > 0) return props.height
|
|
684
|
+
if (typeof props.height === 'string') {
|
|
685
|
+
const parsed = Number.parseInt(props.height, 10)
|
|
686
|
+
if (Number.isFinite(parsed) && parsed > 0 && !props.height.includes('%')) return parsed
|
|
687
|
+
}
|
|
688
|
+
if (measuredViewportHeight.value > 0) return measuredViewportHeight.value
|
|
689
|
+
const el = scrollContainerRef.value
|
|
690
|
+
if (el && el.clientHeight > 0) return el.clientHeight
|
|
691
|
+
return 320
|
|
692
|
+
})
|
|
693
|
+
const tableMinWidth = computed(() => resolveScrollWidth(props.scrollX))
|
|
694
|
+
|
|
695
|
+
onMounted(() => {
|
|
696
|
+
scheduleVirtualLayoutSync()
|
|
697
|
+
const el = scrollContainerRef.value
|
|
698
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
699
|
+
viewportResizeObserver = new ResizeObserver(() => {
|
|
700
|
+
const prevH = measuredViewportHeight.value
|
|
701
|
+
syncMeasuredViewportSize()
|
|
702
|
+
// keep-alive 切回:视口从 0→实高时再写一次,否则虚拟切片按错误高度算完后滚动会被钳掉
|
|
703
|
+
if (
|
|
704
|
+
prevH === 0 &&
|
|
705
|
+
measuredViewportHeight.value > 0 &&
|
|
706
|
+
(preservedScrollTop > 0 || preservedScrollLeft > 0)
|
|
707
|
+
) {
|
|
708
|
+
restoringScroll = true
|
|
709
|
+
restoreScrollToDom()
|
|
710
|
+
requestAnimationFrame(() => {
|
|
711
|
+
restoringScroll = false
|
|
712
|
+
})
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
viewportResizeObserver.observe(el)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
onActivated(() => {
|
|
719
|
+
scheduleVirtualLayoutSync()
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
onDeactivated(() => {
|
|
723
|
+
captureScrollFromDom()
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
/** 失活同步快照:须在 DOM scrollTop 被清零前(或依赖 onScroll 已写入的值)。 */
|
|
727
|
+
watch(
|
|
728
|
+
() => props.layoutActive,
|
|
729
|
+
(active, prev) => {
|
|
730
|
+
if (active === false && prev !== false) {
|
|
731
|
+
captureScrollFromDom()
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{ flush: 'sync' },
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
/** 激活后再测视口并写回滚动(需等布局稳定)。 */
|
|
738
|
+
watch(
|
|
739
|
+
() => props.layoutActive,
|
|
740
|
+
(active) => {
|
|
741
|
+
if (active) scheduleVirtualLayoutSync()
|
|
742
|
+
},
|
|
743
|
+
{ flush: 'post' },
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
watch(
|
|
747
|
+
() => props.viewKey,
|
|
748
|
+
(_next, prev) => {
|
|
749
|
+
if (prev === undefined) return
|
|
750
|
+
resetScrollPosition()
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
/** 前缀列固定宽度(与 CSS 一致),用于 resizable 模式精确计算表格总宽 */
|
|
755
|
+
const PREFIX_COL_WIDTH = {
|
|
756
|
+
drag: 40,
|
|
757
|
+
expand: 40,
|
|
758
|
+
select: 40,
|
|
759
|
+
status: 52,
|
|
760
|
+
} as const
|
|
761
|
+
|
|
762
|
+
/** 拖拽会话中的列宽快照(仅在拖拽开始时写入一次,供 colgroup 初始渲染) */
|
|
763
|
+
const resizePaintWidths = ref<Record<string, number> | null>(null)
|
|
764
|
+
/** 是否处于列宽拖拽中(切换 fixed 布局,移动过程走 DOM 直写避免整表重渲染) */
|
|
765
|
+
const isColumnResizing = ref(false)
|
|
766
|
+
const tableRef = ref<HTMLTableElement | null>(null)
|
|
767
|
+
|
|
768
|
+
/** 拖拽中的实时列宽(非响应式,避免 mousemove 触发整表更新) */
|
|
769
|
+
let activeDragWidths: Record<string, number> | null = null
|
|
770
|
+
|
|
771
|
+
/** 样式计算用:拖拽初始帧用快照,否则用持久化列宽 */
|
|
772
|
+
const effectiveColumnWidths = computed(() => resizePaintWidths.value ?? resolvedColumnWidths.value)
|
|
773
|
+
|
|
774
|
+
const dataColumnWidth = (column: RsTableColumn<T>): number =>
|
|
775
|
+
resolveColumnPixelWidth(column, effectiveColumnWidths.value)
|
|
776
|
+
|
|
777
|
+
/** 左/右冻结列始终实体渲染;中间列参与横向虚拟化 */
|
|
778
|
+
const leftFixedDataColumns = computed(() => displayColumns.value.filter((c) => c.fixed === 'left'))
|
|
779
|
+
const rightFixedDataColumns = computed(() => displayColumns.value.filter((c) => c.fixed === 'right'))
|
|
780
|
+
const fluidDataColumns = computed(() => displayColumns.value.filter((c) => !c.fixed))
|
|
781
|
+
|
|
782
|
+
const virtualColumnsEnabled = computed(() => {
|
|
783
|
+
if (props.virtualColumns === false) return false
|
|
784
|
+
if (isColumnResizing.value) return false
|
|
785
|
+
if (props.virtualColumns === true) return fluidDataColumns.value.length > 0
|
|
786
|
+
// 'auto':按列数阈值
|
|
787
|
+
const threshold = props.virtualColumnsAutoThreshold ?? 0
|
|
788
|
+
return threshold > 0 && fluidDataColumns.value.length >= threshold
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const fluidLeadingOffset = computed(() => {
|
|
792
|
+
let sum = 0
|
|
793
|
+
if (showRowDragHandle.value) sum += PREFIX_COL_WIDTH.drag
|
|
794
|
+
if (props.expandable) sum += PREFIX_COL_WIDTH.expand
|
|
795
|
+
if (showSelectColumn.value) sum += PREFIX_COL_WIDTH.select
|
|
796
|
+
if (showEditGutterColumn.value) sum += resolvedGutterWidth.value
|
|
797
|
+
else if (showIndexColumn.value) sum += resolvedIndexWidth.value
|
|
798
|
+
if (showRowStatusColumn.value) sum += PREFIX_COL_WIDTH.status
|
|
799
|
+
for (const col of leftFixedDataColumns.value) sum += dataColumnWidth(col)
|
|
800
|
+
return sum
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
const columnVirtualSlice = computed(() => {
|
|
804
|
+
const fluidCount = fluidDataColumns.value.length
|
|
805
|
+
if (!virtualColumnsEnabled.value) {
|
|
806
|
+
return {
|
|
807
|
+
columns: displayColumns.value,
|
|
808
|
+
paddingLeft: 0,
|
|
809
|
+
paddingRight: 0,
|
|
810
|
+
startIndex: 0,
|
|
811
|
+
endIndex: fluidCount,
|
|
812
|
+
fluidCount,
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const viewport = Math.max(measuredViewportWidth.value || 320, 120)
|
|
816
|
+
const midScroll = Math.max(0, scrollLeft.value - fluidLeadingOffset.value)
|
|
817
|
+
const midViewport = Math.max(120, viewport - Math.min(viewport - 40, fluidLeadingOffset.value))
|
|
818
|
+
const sliced = sliceVirtualColumns(fluidDataColumns.value, {
|
|
819
|
+
scrollLeft: midScroll,
|
|
820
|
+
viewportWidth: midViewport,
|
|
821
|
+
getWidth: dataColumnWidth,
|
|
822
|
+
overscan: props.virtualColumnOverscan ?? 2,
|
|
823
|
+
})
|
|
824
|
+
return {
|
|
825
|
+
columns: [
|
|
826
|
+
...leftFixedDataColumns.value,
|
|
827
|
+
...sliced.columns,
|
|
828
|
+
...rightFixedDataColumns.value,
|
|
829
|
+
],
|
|
830
|
+
paddingLeft: sliced.paddingLeft,
|
|
831
|
+
paddingRight: sliced.paddingRight,
|
|
832
|
+
startIndex: sliced.startIndex,
|
|
833
|
+
endIndex: sliced.endIndex,
|
|
834
|
+
fluidCount,
|
|
835
|
+
}
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
const visibleDataColumns = computed(() => columnVirtualSlice.value.columns)
|
|
839
|
+
const columnPadLeft = computed(() => columnVirtualSlice.value.paddingLeft)
|
|
840
|
+
const columnPadRight = computed(() => columnVirtualSlice.value.paddingRight)
|
|
841
|
+
|
|
842
|
+
const useScrollContainer = computed(
|
|
843
|
+
() => virtualScrollEnabled.value || props.infinite || virtualColumnsEnabled.value,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
const scrollContainerStyle = computed(() => {
|
|
847
|
+
const style: Record<string, string> = {}
|
|
848
|
+
if (useScrollContainer.value && !props.fill) {
|
|
849
|
+
const resolvedHeight = resolveVirtualListHeight(props.height, 320)
|
|
850
|
+
if (resolvedHeight) {
|
|
851
|
+
style.height = resolvedHeight
|
|
852
|
+
style.maxHeight = resolvedHeight
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (showEditGutterColumn.value) {
|
|
856
|
+
style['--rs-table-gutter-width'] = `${resolvedGutterWidth.value}px`
|
|
857
|
+
}
|
|
858
|
+
if (showIndexColumn.value) {
|
|
859
|
+
style['--rs-table-index-width'] = `${resolvedIndexWidth.value}px`
|
|
860
|
+
}
|
|
861
|
+
return Object.keys(style).length ? style : undefined
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
/** resizable 且列宽稳定时,或拖拽会话中:表格内容最小宽度 = 各列宽度之和(不含拉满补宽) */
|
|
865
|
+
const resizableTableWidth = computed(() => {
|
|
866
|
+
if (!props.resizable) return undefined
|
|
867
|
+
if (!resizePaintWidths.value && props.columnLayout === 'auto' && !useStableColumnWidths.value) {
|
|
868
|
+
return undefined
|
|
869
|
+
}
|
|
870
|
+
let sum = 0
|
|
871
|
+
if (showRowDragHandle.value) sum += PREFIX_COL_WIDTH.drag
|
|
872
|
+
if (props.expandable) sum += PREFIX_COL_WIDTH.expand
|
|
873
|
+
if (showSelectColumn.value) sum += PREFIX_COL_WIDTH.select
|
|
874
|
+
if (showEditGutterColumn.value) sum += resolvedGutterWidth.value
|
|
875
|
+
else if (showIndexColumn.value) sum += resolvedIndexWidth.value
|
|
876
|
+
const widths = effectiveColumnWidths.value
|
|
877
|
+
for (const col of displayColumns.value) {
|
|
878
|
+
sum += widths[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
|
|
879
|
+
}
|
|
880
|
+
return `${sum}px`
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* 列总宽不足视口时,把剩余像素均分到数据列(前缀列不动)。
|
|
885
|
+
* 避免 fixed + resizable 把 table 锁成「列宽之和」,右侧露出表头灰底。
|
|
886
|
+
*/
|
|
887
|
+
const emptyFillExtras: Record<string, number> = Object.freeze({})
|
|
888
|
+
const useFixedColumnLayout = computed(
|
|
889
|
+
() => useStableColumnWidths.value || isColumnResizing.value,
|
|
890
|
+
)
|
|
891
|
+
const columnFillExtras = computed((): Record<string, number> => {
|
|
892
|
+
if (isColumnResizing.value) return emptyFillExtras
|
|
893
|
+
if (!useFixedColumnLayout.value) return emptyFillExtras
|
|
894
|
+
if (virtualColumnsEnabled.value) return emptyFillExtras
|
|
895
|
+
const viewportW = measuredViewportWidth.value
|
|
896
|
+
if (viewportW <= 0) return emptyFillExtras
|
|
897
|
+
const cols = displayColumns.value
|
|
898
|
+
if (cols.length === 0) return emptyFillExtras
|
|
899
|
+
|
|
900
|
+
let sum = measurePrefixColumnWidth()
|
|
901
|
+
if (showRowStatusColumn.value) sum += PREFIX_COL_WIDTH.status
|
|
902
|
+
const widths = effectiveColumnWidths.value
|
|
903
|
+
for (const col of cols) {
|
|
904
|
+
sum += widths[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
|
|
905
|
+
}
|
|
906
|
+
// 预留 1px,避免边框/亚像素导致偶发横向条
|
|
907
|
+
const excess = Math.floor(viewportW - sum) - 1
|
|
908
|
+
if (excess <= 0) return emptyFillExtras
|
|
909
|
+
|
|
910
|
+
const extras: Record<string, number> = {}
|
|
911
|
+
const base = Math.floor(excess / cols.length)
|
|
912
|
+
let rem = excess - base * cols.length
|
|
913
|
+
for (const col of cols) {
|
|
914
|
+
extras[col.key] = base + (rem > 0 ? 1 : 0)
|
|
915
|
+
if (rem > 0) rem -= 1
|
|
916
|
+
}
|
|
917
|
+
return extras
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
/** 渲染用列宽 = 持久化/拖拽列宽 + 拉满补宽 */
|
|
921
|
+
const renderColumnWidths = computed(() => {
|
|
922
|
+
const extras = columnFillExtras.value
|
|
923
|
+
const base = effectiveColumnWidths.value
|
|
924
|
+
if (extras === emptyFillExtras) return base
|
|
925
|
+
const next: Record<string, number> = { ...base }
|
|
926
|
+
for (const col of displayColumns.value) {
|
|
927
|
+
const extra = extras[col.key]
|
|
928
|
+
if (!extra) continue
|
|
929
|
+
const cur = next[col.key] ?? parseColumnWidth(col.width ?? col.minWidth)
|
|
930
|
+
next[col.key] = cur + extra
|
|
931
|
+
}
|
|
932
|
+
return next
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
const tableInlineStyle = computed(() => {
|
|
936
|
+
if (isColumnResizing.value) {
|
|
937
|
+
return undefined
|
|
938
|
+
}
|
|
939
|
+
if (props.resizable) {
|
|
940
|
+
if (props.columnLayout === 'auto' && !useStableColumnWidths.value) {
|
|
941
|
+
return tableMinWidth.value
|
|
942
|
+
? { width: '100%', minWidth: tableMinWidth.value }
|
|
943
|
+
: { width: '100%' }
|
|
944
|
+
}
|
|
945
|
+
const width = resizableTableWidth.value
|
|
946
|
+
if (!width) return undefined
|
|
947
|
+
// 列少时 width:100% 拉满容器;列多时靠 minWidth 撑开并横向滚动
|
|
948
|
+
return { width: '100%', minWidth: tableMinWidth.value ?? width }
|
|
949
|
+
}
|
|
950
|
+
return tableMinWidth.value ? { minWidth: tableMinWidth.value } : undefined
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
/** 列最小/显式宽度之和(不含可任意压缩的流体列)。用于判断是否需要横向滚动。 */
|
|
954
|
+
function estimateRequiredTableWidth(): number {
|
|
955
|
+
let sum = measurePrefixColumnWidth()
|
|
956
|
+
if (showRowStatusColumn.value) sum += PREFIX_COL_WIDTH.status
|
|
957
|
+
const widths = effectiveColumnWidths.value
|
|
958
|
+
for (const col of displayColumns.value) {
|
|
959
|
+
const stored = widths[col.key]
|
|
960
|
+
if (typeof stored === 'number') {
|
|
961
|
+
sum += stored
|
|
962
|
+
continue
|
|
963
|
+
}
|
|
964
|
+
if (typeof col.width === 'number') {
|
|
965
|
+
sum += col.width
|
|
966
|
+
continue
|
|
967
|
+
}
|
|
968
|
+
if (col.minWidth !== undefined) {
|
|
969
|
+
sum += typeof col.minWidth === 'number' ? col.minWidth : parseColumnWidth(col.minWidth, 0)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return sum
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** 仅当列宽确需超过容器时开启横向滚动(避免纵向条引发的伪横向条)。 */
|
|
976
|
+
const overflowXEnabled = computed(() => {
|
|
977
|
+
if (virtualColumnsEnabled.value) return true
|
|
978
|
+
if (props.scrollX != null && props.scrollX !== '') return true
|
|
979
|
+
const viewportW = measuredViewportWidth.value
|
|
980
|
+
if (viewportW <= 0) return false
|
|
981
|
+
if (useFixedColumnLayout.value) {
|
|
982
|
+
const raw = resizableTableWidth.value
|
|
983
|
+
const sum = raw ? Number.parseInt(raw, 10) : estimateRequiredTableWidth()
|
|
984
|
+
return Number.isFinite(sum) && sum > viewportW
|
|
985
|
+
}
|
|
986
|
+
return estimateRequiredTableWidth() > viewportW
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
function measurePrefixColumnWidth(): number {
|
|
990
|
+
let sum = 0
|
|
991
|
+
if (showRowDragHandle.value) sum += PREFIX_COL_WIDTH.drag
|
|
992
|
+
if (props.expandable) sum += PREFIX_COL_WIDTH.expand
|
|
993
|
+
if (showSelectColumn.value) sum += PREFIX_COL_WIDTH.select
|
|
994
|
+
if (showEditGutterColumn.value) sum += resolvedGutterWidth.value
|
|
995
|
+
else if (showIndexColumn.value) sum += resolvedIndexWidth.value
|
|
996
|
+
return sum
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function applyTableColumnWidths(table: HTMLTableElement, widths: Record<string, number>): void {
|
|
1000
|
+
let sum = measurePrefixColumnWidth()
|
|
1001
|
+
for (const column of displayColumns.value) {
|
|
1002
|
+
const col = table.querySelector<HTMLTableColElement>(`colgroup col[data-col-key="${column.key}"]`)
|
|
1003
|
+
const width = widths[column.key] ?? parseColumnWidth(column.width ?? column.minWidth)
|
|
1004
|
+
if (col) col.style.width = `${width}px`
|
|
1005
|
+
const th = table.querySelector<HTMLTableCellElement>(`thead th[data-col-key="${column.key}"]`)
|
|
1006
|
+
if (th) th.style.width = `${width}px`
|
|
1007
|
+
sum += width
|
|
1008
|
+
}
|
|
1009
|
+
table.style.width = `${sum}px`
|
|
1010
|
+
const minWidth = tableMinWidth.value
|
|
1011
|
+
if (minWidth) table.style.minWidth = minWidth
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function resolvedDataColumnWidth(key: string, fallback?: number | string): number {
|
|
1015
|
+
const stored = renderColumnWidths.value[key]
|
|
1016
|
+
if (typeof stored === 'number') return stored
|
|
1017
|
+
return parseColumnWidth(fallback)
|
|
1018
|
+
}
|
|
1019
|
+
const fixedColumnStyles = computed(() =>
|
|
1020
|
+
resolveFixedColumnStyles(displayColumns.value, renderColumnWidths.value, {
|
|
1021
|
+
selectable: showSelectColumn.value,
|
|
1022
|
+
showIndex: showIndexColumn.value,
|
|
1023
|
+
showEditGutter: showEditGutterColumn.value,
|
|
1024
|
+
gutterWidth: resolvedGutterWidth.value,
|
|
1025
|
+
indexWidth: resolvedIndexWidth.value,
|
|
1026
|
+
expandable: props.expandable,
|
|
1027
|
+
rowDraggable: showRowDragHandle.value,
|
|
1028
|
+
}),
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
const tableEntries = computed(() =>
|
|
1032
|
+
injectExpandRows(
|
|
1033
|
+
buildTableEntries(props.data, displayColumns.value, {
|
|
1034
|
+
sort: sortState.value,
|
|
1035
|
+
sorts: sortsState.value,
|
|
1036
|
+
multiSort: props.multiSort,
|
|
1037
|
+
filterText: props.filterText,
|
|
1038
|
+
filterKeys: props.filterKeys,
|
|
1039
|
+
columnFilters: columnFiltersState.value,
|
|
1040
|
+
groupBy: props.groupBy,
|
|
1041
|
+
groupLabel: props.groupLabel,
|
|
1042
|
+
remoteSort: props.remoteSort,
|
|
1043
|
+
}),
|
|
1044
|
+
expandedKeySet.value,
|
|
1045
|
+
props.rowKey,
|
|
1046
|
+
props.rowExpandable,
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
const virtualHeightModel = computed(() =>
|
|
1051
|
+
buildVirtualHeightModel(
|
|
1052
|
+
tableEntries.value,
|
|
1053
|
+
fixedRowHeight.value,
|
|
1054
|
+
undefined,
|
|
1055
|
+
(props.expandRowHeight ?? undefined) as 80 | undefined,
|
|
1056
|
+
),
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
const virtualSlice = computed(() => {
|
|
1060
|
+
if (!virtualScrollEnabled.value) {
|
|
1061
|
+
return { entries: tableEntries.value, paddingTop: 0, paddingBottom: 0 }
|
|
1062
|
+
}
|
|
1063
|
+
return sliceVirtualHeightModel(
|
|
1064
|
+
virtualHeightModel.value,
|
|
1065
|
+
scrollTop.value,
|
|
1066
|
+
viewportHeight.value,
|
|
1067
|
+
fixedRowHeight.value,
|
|
1068
|
+
props.overscan,
|
|
1069
|
+
)
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
/** DEV:行/列虚拟是否生效 + 可视切片,便于对照 heap 里 `<tr>` / `<td>` 数量 */
|
|
1073
|
+
if (import.meta.env.DEV) {
|
|
1074
|
+
watch(
|
|
1075
|
+
() => {
|
|
1076
|
+
const enabled = virtualScrollEnabled.value
|
|
1077
|
+
const slice = virtualSlice.value
|
|
1078
|
+
const total = tableEntries.value.length
|
|
1079
|
+
const rendered = enabled ? slice.entries.length : total
|
|
1080
|
+
return {
|
|
1081
|
+
enabled,
|
|
1082
|
+
prop: props.virtual,
|
|
1083
|
+
threshold: props.virtualAutoThreshold,
|
|
1084
|
+
fill: props.fill,
|
|
1085
|
+
infinite: props.infinite,
|
|
1086
|
+
rows: props.data.length,
|
|
1087
|
+
entries: total,
|
|
1088
|
+
rendered,
|
|
1089
|
+
rowSlice: enabled
|
|
1090
|
+
? `pad ${Math.round(slice.paddingTop)}px + ${rendered} + pad ${Math.round(slice.paddingBottom)}px`
|
|
1091
|
+
: 'all',
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
(state, prev) => {
|
|
1095
|
+
if (
|
|
1096
|
+
prev &&
|
|
1097
|
+
state.enabled === prev.enabled &&
|
|
1098
|
+
state.rows === prev.rows &&
|
|
1099
|
+
state.rendered === prev.rendered &&
|
|
1100
|
+
state.rowSlice === prev.rowSlice
|
|
1101
|
+
) {
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
console.info('[RsTable:virtual-rows]', state)
|
|
1105
|
+
},
|
|
1106
|
+
{ immediate: true },
|
|
1107
|
+
)
|
|
1108
|
+
watch(
|
|
1109
|
+
() => {
|
|
1110
|
+
const slice = columnVirtualSlice.value
|
|
1111
|
+
const enabled = virtualColumnsEnabled.value
|
|
1112
|
+
return {
|
|
1113
|
+
enabled,
|
|
1114
|
+
prop: props.virtualColumns,
|
|
1115
|
+
threshold: props.virtualColumnsAutoThreshold,
|
|
1116
|
+
fluidCols: slice.fluidCount,
|
|
1117
|
+
renderedCols: slice.columns.length,
|
|
1118
|
+
fluidSlice: enabled ? `${slice.startIndex}..${slice.endIndex}` : 'all',
|
|
1119
|
+
padLeft: Math.round(slice.paddingLeft),
|
|
1120
|
+
padRight: Math.round(slice.paddingRight),
|
|
1121
|
+
resizing: isColumnResizing.value,
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
(state, prev) => {
|
|
1125
|
+
if (
|
|
1126
|
+
prev &&
|
|
1127
|
+
state.enabled === prev.enabled &&
|
|
1128
|
+
state.fluidCols === prev.fluidCols &&
|
|
1129
|
+
state.fluidSlice === prev.fluidSlice &&
|
|
1130
|
+
state.renderedCols === prev.renderedCols &&
|
|
1131
|
+
state.resizing === prev.resizing
|
|
1132
|
+
) {
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1135
|
+
console.info('[RsTable:virtual-cols]', state)
|
|
1136
|
+
},
|
|
1137
|
+
{ immediate: true },
|
|
1138
|
+
)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const visibleEntries = computed(() => virtualSlice.value.entries)
|
|
1142
|
+
const hasData = computed(() => dataRows.value.length > 0)
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* 单次遍历 tableEntries,产出 dataRows、selectableRowKeys、rowKeyByIndex。
|
|
1146
|
+
* - 仅依赖 tableEntries(排序/过滤/数据变化),不依赖 selectedKeySet,
|
|
1147
|
+
* 避免选中操作触发不必要的全量重遍历。
|
|
1148
|
+
* - rowKeyByIndex:rowIndex → rowKey,供模板和内部函数 O(1) 查询,
|
|
1149
|
+
* 消除每行 8+ 次重复 resolveRowKey 调用。
|
|
1150
|
+
*/
|
|
1151
|
+
const rowDerivedState = computed(() => {
|
|
1152
|
+
const rows: Extract<RsTableRowEntry<T>, { type: 'row' }>[] = []
|
|
1153
|
+
const selKeys: string[] = []
|
|
1154
|
+
const keyByIndex = new Map<number, string>()
|
|
1155
|
+
for (const entry of tableEntries.value) {
|
|
1156
|
+
if (entry.type !== 'row') continue
|
|
1157
|
+
rows.push(entry)
|
|
1158
|
+
const key = resolveRowKey(entry.row, entry.rowIndex, props.rowKey)
|
|
1159
|
+
keyByIndex.set(entry.rowIndex, key)
|
|
1160
|
+
const disabled = isTableRowDisabled(entry.row)
|
|
1161
|
+
if (!disabled && (props.rowSelectable ? props.rowSelectable(entry.row, entry.rowIndex) : true)) {
|
|
1162
|
+
selKeys.push(key)
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return { rows, selKeys, keyByIndex }
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
const dataRows = computed(() => rowDerivedState.value.rows)
|
|
1169
|
+
const selectableRowKeys = computed(() => rowDerivedState.value.selKeys)
|
|
1170
|
+
/** rowIndex → rowKey 缓存,消除模板热路径重复 resolveRowKey */
|
|
1171
|
+
const rowKeyByIndex = computed(() => rowDerivedState.value.keyByIndex)
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* 当前已选行对象(供 contextMenuItems 使用)。
|
|
1175
|
+
* 依赖 dataRows + selectedKeySet,选中变化时单独重算,
|
|
1176
|
+
* 不触发 dataRows/selectableRowKeys 的重遍历。
|
|
1177
|
+
*/
|
|
1178
|
+
const selectedRows = computed<T[]>(() => {
|
|
1179
|
+
const keySet = selectedKeySet.value
|
|
1180
|
+
const keyMap = rowKeyByIndex.value
|
|
1181
|
+
return dataRows.value
|
|
1182
|
+
.filter((entry) => keySet.has(keyMap.get(entry.rowIndex) ?? ''))
|
|
1183
|
+
.map((entry) => entry.row)
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
const selectAllState = computed(() => resolveSelectAllState(selectedRowKeys.value, selectableRowKeys.value))
|
|
1187
|
+
|
|
1188
|
+
const bodyColspan = computed(() => {
|
|
1189
|
+
let count = visibleDataColumns.value.length
|
|
1190
|
+
if (columnPadLeft.value > 0) count += 1
|
|
1191
|
+
if (columnPadRight.value > 0) count += 1
|
|
1192
|
+
if (showRowDragHandle.value) count += 1
|
|
1193
|
+
if (props.expandable) count += 1
|
|
1194
|
+
if (showSelectColumn.value) count += 1
|
|
1195
|
+
if (showEditGutterColumn.value || showIndexColumn.value) count += 1
|
|
1196
|
+
if (showRowStatusColumn.value) count += 1
|
|
1197
|
+
return count
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
const isRadioSelection = computed(() => props.selectionType === 'radio')
|
|
1201
|
+
|
|
1202
|
+
let resizeState: {
|
|
1203
|
+
key: string
|
|
1204
|
+
startX: number
|
|
1205
|
+
startWidth: number
|
|
1206
|
+
currentWidth: number
|
|
1207
|
+
/** 拖拽开始时从 DOM 批量测量的各列宽度 */
|
|
1208
|
+
domSnapshot: Record<string, number>
|
|
1209
|
+
/** auto 模式:拖拽前已持久化的列宽 */
|
|
1210
|
+
persistedWidths: Record<string, number>
|
|
1211
|
+
} | null = null
|
|
1212
|
+
let resizeRafId = 0
|
|
1213
|
+
let latestResizeClientX = 0
|
|
1214
|
+
let resizeMoved = false
|
|
1215
|
+
|
|
1216
|
+
/** 列样式 Map:按 column.key 缓存,避免模板热路径每次创建新对象 */
|
|
1217
|
+
const columnStyleMap = computed<Map<string, Record<string, string> | undefined>>(() => {
|
|
1218
|
+
const map = new Map<string, Record<string, string> | undefined>()
|
|
1219
|
+
for (const col of displayColumns.value) {
|
|
1220
|
+
const base = resolveColumnStyle(col, renderColumnWidths.value) ?? {}
|
|
1221
|
+
const fixed = fixedCellStyle(fixedColumnStyles.value.get(col.key))
|
|
1222
|
+
let merged: Record<string, string> | undefined
|
|
1223
|
+
if (fixed) merged = { ...base, ...fixed }
|
|
1224
|
+
else if (Object.keys(base).length) merged = base
|
|
1225
|
+
map.set(col.key, merged)
|
|
1226
|
+
}
|
|
1227
|
+
return map
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
/** 表头列样式:固定列需同时 sticky top + left/right */
|
|
1231
|
+
const columnHeaderStyleMap = computed<Map<string, Record<string, string> | undefined>>(() => {
|
|
1232
|
+
const map = new Map<string, Record<string, string> | undefined>()
|
|
1233
|
+
for (const col of displayColumns.value) {
|
|
1234
|
+
const base = resolveColumnStyle(col, renderColumnWidths.value) ?? {}
|
|
1235
|
+
const fixed = fixedCellStyle(fixedColumnStyles.value.get(col.key), { header: true })
|
|
1236
|
+
let merged: Record<string, string> | undefined
|
|
1237
|
+
if (fixed) merged = { ...base, ...fixed }
|
|
1238
|
+
else if (Object.keys(base).length) merged = base
|
|
1239
|
+
map.set(col.key, merged)
|
|
1240
|
+
}
|
|
1241
|
+
return map
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
/** td 静态 class Map:列配置不变时复用,消除每行每列重复拼接 */
|
|
1245
|
+
const columnTdClassMap = computed<Map<string, string[]>>(() => {
|
|
1246
|
+
const map = new Map<string, string[]>()
|
|
1247
|
+
for (const col of displayColumns.value) {
|
|
1248
|
+
const classes: string[] = [`rs-table__cell--${col.align ?? 'left'}`]
|
|
1249
|
+
if (col.ellipsis) classes.push('rs-table__td--ellipsis')
|
|
1250
|
+
if (col.fixed) classes.push('rs-table__cell--fixed')
|
|
1251
|
+
map.set(col.key, classes)
|
|
1252
|
+
}
|
|
1253
|
+
return map
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
/** 4 个固定前缀列的 style(拖拽/展开/选择/序号),避免每行重复计算 */
|
|
1257
|
+
const dragLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: 0 }) ?? {})
|
|
1258
|
+
const expandLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: dragColumnOffset.value }) ?? {})
|
|
1259
|
+
const selectLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: expandColumnOffset.value }) ?? {})
|
|
1260
|
+
const indexLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: selectColumnOffset.value }) ?? {})
|
|
1261
|
+
const gutterLeadStyle = computed(() => fixedCellStyle({ fixed: 'left', left: selectColumnOffset.value }) ?? {})
|
|
1262
|
+
const dragLeadHeaderStyle = computed(() => fixedCellStyle({ fixed: 'left', left: 0 }, { header: true }) ?? {})
|
|
1263
|
+
const expandLeadHeaderStyle = computed(() => fixedCellStyle({ fixed: 'left', left: dragColumnOffset.value }, { header: true }) ?? {})
|
|
1264
|
+
const selectLeadHeaderStyle = computed(() => fixedCellStyle({ fixed: 'left', left: expandColumnOffset.value }, { header: true }) ?? {})
|
|
1265
|
+
const indexLeadHeaderStyle = computed(() => fixedCellStyle({ fixed: 'left', left: selectColumnOffset.value }, { header: true }) ?? {})
|
|
1266
|
+
const gutterLeadHeaderStyle = computed(() => fixedCellStyle({ fixed: 'left', left: selectColumnOffset.value }, { header: true }) ?? {})
|
|
1267
|
+
|
|
1268
|
+
function sortOrderFor(key: string): RsTableSortOrder {
|
|
1269
|
+
if (props.multiSort) return getSortOrderForKey(sortsState.value, key)
|
|
1270
|
+
return sortState.value?.key === key ? sortState.value.order : null
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function sortPriorityFor(key: string): number {
|
|
1274
|
+
if (!props.multiSort) return 0
|
|
1275
|
+
return getSortPriorityForKey(sortsState.value, key)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function sortIconName(key: string): string {
|
|
1279
|
+
const order = sortOrderFor(key)
|
|
1280
|
+
if (order === 'asc') return 'arrow-up'
|
|
1281
|
+
if (order === 'desc') return 'arrow-down'
|
|
1282
|
+
return 'arrow-up-down'
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function onHeaderClick(column: RsTableColumn<T>): void {
|
|
1286
|
+
if (!column.sortable) return
|
|
1287
|
+
if (props.multiSort) {
|
|
1288
|
+
sortsState.value = toggleMultiSortState(sortsState.value, column.key, props.maxSort)
|
|
1289
|
+
return
|
|
1290
|
+
}
|
|
1291
|
+
sortState.value = toggleSortState(sortState.value, column.key)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function onColumnDragStart(key: string, event: DragEvent): void {
|
|
1295
|
+
if (!props.columnDraggable) return
|
|
1296
|
+
dragColumnKey.value = key
|
|
1297
|
+
dropColumnKey.value = null
|
|
1298
|
+
if (!event.dataTransfer) return
|
|
1299
|
+
event.dataTransfer.setData('text/plain', key)
|
|
1300
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function onColumnDragOver(key: string, event: DragEvent): void {
|
|
1304
|
+
if (!props.columnDraggable || !dragColumnKey.value) return
|
|
1305
|
+
if (dragColumnKey.value === key) {
|
|
1306
|
+
if (dropColumnKey.value !== null) dropColumnKey.value = null
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
event.preventDefault()
|
|
1310
|
+
if (dropColumnKey.value !== key) dropColumnKey.value = key
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function onColumnDrop(key: string, event: DragEvent): void {
|
|
1314
|
+
if (!props.columnDraggable || !dragColumnKey.value) return
|
|
1315
|
+
event.preventDefault()
|
|
1316
|
+
if (dragColumnKey.value === key) return
|
|
1317
|
+
columnOrderState.value = reorderColumnKeys(columnOrderState.value, dragColumnKey.value, key)
|
|
1318
|
+
dragColumnKey.value = null
|
|
1319
|
+
dropColumnKey.value = null
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function onColumnDragEnd(): void {
|
|
1323
|
+
dragColumnKey.value = null
|
|
1324
|
+
dropColumnKey.value = null
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function onRowClick(entry: RsTableRowEntry<T>, event?: MouseEvent): void {
|
|
1328
|
+
if (entry.type !== 'row') return
|
|
1329
|
+
const rowKey = rowKeyFor(entry)
|
|
1330
|
+
if (isRowSelection.value && canSelectRow(entry) && !tableEdit.editingCell.value) {
|
|
1331
|
+
const toggle = Boolean(event && (event.ctrlKey || event.metaKey))
|
|
1332
|
+
const range = Boolean(event?.shiftKey)
|
|
1333
|
+
selectedRowKeys.value = selectRowKeysByClick(selectedRowKeys.value, rowKey, {
|
|
1334
|
+
toggle,
|
|
1335
|
+
range,
|
|
1336
|
+
orderedKeys: selectableRowKeys.value,
|
|
1337
|
+
anchorKey: selectionAnchorKey.value,
|
|
1338
|
+
})
|
|
1339
|
+
if (!range) selectionAnchorKey.value = rowKey
|
|
1340
|
+
// 清掉浏览器划选,避免与行选区叠成「拖蓝选中文字」观感
|
|
1341
|
+
if (typeof window !== 'undefined') {
|
|
1342
|
+
window.getSelection()?.removeAllRanges()
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
rowHighlight.applyRowClickHighlight(rowKey)
|
|
1346
|
+
emit('rowClick', entry.row, entry.rowIndex)
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** 行选模式下 Shift/Ctrl 按下时禁止开始原生文本选区 */
|
|
1350
|
+
function onRowSelectMouseDown(event: MouseEvent): void {
|
|
1351
|
+
if (!isRowSelection.value) return
|
|
1352
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
|
1353
|
+
event.preventDefault()
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function onRowDblclick(entry: RsTableRowEntry<T>, _event?: MouseEvent): void {
|
|
1358
|
+
if (entry.type !== 'row') return
|
|
1359
|
+
emit('rowDblclick', entry.row, entry.rowIndex)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** 当前右键行(null = 空白区域) */
|
|
1363
|
+
const ctxRow = ref<T | null>(null)
|
|
1364
|
+
const ctxRowIndex = ref<number | null>(null)
|
|
1365
|
+
const ctxColKey = ref<string | null>(null)
|
|
1366
|
+
const ctxMenuItems = ref<RsContextMenuItem[]>([])
|
|
1367
|
+
|
|
1368
|
+
function refreshCtxMenuItems(): void {
|
|
1369
|
+
if (!contextMenuEnabled.value) {
|
|
1370
|
+
ctxMenuItems.value = []
|
|
1371
|
+
return
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const items = buildDefaultTableContextMenuItems({
|
|
1375
|
+
copyCellLabel: t('table.copyCell'),
|
|
1376
|
+
copyRowLabel: t('table.copyRow'),
|
|
1377
|
+
hasRow: ctxRow.value != null,
|
|
1378
|
+
hasCell: ctxColKey.value != null,
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
const custom = props.contextMenuItems?.(ctxRow.value as T | null, selectedRows.value) ?? []
|
|
1382
|
+
if (custom.length > 0) {
|
|
1383
|
+
if (items.length > 0) {
|
|
1384
|
+
items.push({ key: '__ctx-sep', label: '', separator: true })
|
|
1385
|
+
}
|
|
1386
|
+
items.push(...custom)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
ctxMenuItems.value = items
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async function copyCtxCell(): Promise<void> {
|
|
1393
|
+
if (!ctxRow.value || ctxRowIndex.value == null || !ctxColKey.value) return
|
|
1394
|
+
const column = displayColumns.value.find((item) => item.key === ctxColKey.value)
|
|
1395
|
+
if (!column) return
|
|
1396
|
+
const key = resolveRowKey(ctxRow.value, ctxRowIndex.value, props.rowKey)
|
|
1397
|
+
const text = resolveTableCellCopyText(
|
|
1398
|
+
ctxRow.value,
|
|
1399
|
+
column,
|
|
1400
|
+
ctxRowIndex.value,
|
|
1401
|
+
tableEdit.getDraft(key, column.key),
|
|
1402
|
+
)
|
|
1403
|
+
await copyTextToClipboard(text)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function copyCtxRow(): Promise<void> {
|
|
1407
|
+
if (!ctxRow.value || ctxRowIndex.value == null) return
|
|
1408
|
+
const key = resolveRowKey(ctxRow.value, ctxRowIndex.value, props.rowKey)
|
|
1409
|
+
const text = resolveTableRowCopyText(
|
|
1410
|
+
ctxRow.value,
|
|
1411
|
+
ctxRowIndex.value,
|
|
1412
|
+
displayColumns.value,
|
|
1413
|
+
tableEdit.getDraft,
|
|
1414
|
+
key,
|
|
1415
|
+
)
|
|
1416
|
+
await copyTextToClipboard(text)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async function onCtxMenuSelect(key: string): Promise<void> {
|
|
1420
|
+
if (key === TABLE_CTX_COPY_CELL) {
|
|
1421
|
+
await copyCtxCell()
|
|
1422
|
+
emit('contextMenuSelect', key, ctxRow.value as T | null, selectedRows.value)
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1425
|
+
if (key === TABLE_CTX_COPY_ROW) {
|
|
1426
|
+
await copyCtxRow()
|
|
1427
|
+
emit('contextMenuSelect', key, ctxRow.value as T | null, selectedRows.value)
|
|
1428
|
+
return
|
|
1429
|
+
}
|
|
1430
|
+
emit('contextMenuSelect', key, ctxRow.value as T | null, selectedRows.value)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function prepareRowContextmenu(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
|
|
1434
|
+
const shouldSelect = props.selectOnContextmenu ?? props.selectable
|
|
1435
|
+
if (shouldSelect && canSelectRow(entry) && !isRowSelected(entry)) {
|
|
1436
|
+
selectedRowKeys.value = selectRowKeys(selectedRowKeys.value, rowKeyFor(entry), props.selectionType)
|
|
1437
|
+
}
|
|
1438
|
+
if (contextMenuEnabled.value) {
|
|
1439
|
+
ctxRow.value = entry.row
|
|
1440
|
+
ctxRowIndex.value = entry.rowIndex
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function onTableContextmenu(event: MouseEvent): void {
|
|
1445
|
+
if (!contextMenuEnabled.value) return
|
|
1446
|
+
const tr = (event.target as HTMLElement).closest('tr.rs-table__row')
|
|
1447
|
+
if (!tr) {
|
|
1448
|
+
ctxRow.value = null
|
|
1449
|
+
ctxRowIndex.value = null
|
|
1450
|
+
ctxColKey.value = null
|
|
1451
|
+
refreshCtxMenuItems()
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function onRowContextmenu(entry: RsTableRowEntry<T>, event: MouseEvent): void {
|
|
1456
|
+
if (entry.type !== 'row') return
|
|
1457
|
+
if ((event.target as HTMLElement).closest('.rs-table__td--data')) return
|
|
1458
|
+
prepareRowContextmenu(entry)
|
|
1459
|
+
ctxColKey.value = null
|
|
1460
|
+
refreshCtxMenuItems()
|
|
1461
|
+
emit('rowContextmenu', entry.row, entry.rowIndex, event)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function onCellContextmenu(entry: RsTableRowEntry<T>, colKey: string, event: MouseEvent): void {
|
|
1465
|
+
if (entry.type !== 'row') return
|
|
1466
|
+
prepareRowContextmenu(entry)
|
|
1467
|
+
ctxColKey.value = colKey
|
|
1468
|
+
refreshCtxMenuItems()
|
|
1469
|
+
emit('rowContextmenu', entry.row, entry.rowIndex, event)
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** 从缓存 Map 中 O(1) 取行 key,消除重复 resolveRowKey 调用 */
|
|
1473
|
+
function rowKeyFor(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): string {
|
|
1474
|
+
return rowKeyByIndex.value.get(entry.rowIndex) ?? resolveRowKey(entry.row, entry.rowIndex, props.rowKey)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const rowDrag = createTableRowDragHandlers<T>({
|
|
1478
|
+
state: rowDragState,
|
|
1479
|
+
getRowDraggable: () => props.rowDraggable,
|
|
1480
|
+
getRowDragTrigger: () => props.rowDragTrigger,
|
|
1481
|
+
getRowDropMode: () => props.rowDropMode,
|
|
1482
|
+
rowDraggableWhen: props.rowDraggableWhen,
|
|
1483
|
+
rowDropTargetWhen: props.rowDropTargetWhen,
|
|
1484
|
+
canRowDrop: props.canRowDrop,
|
|
1485
|
+
rowKeyFor: (row, index) => resolveRowKey(row, index, props.rowKey),
|
|
1486
|
+
isRowDisabled: isTableRowDisabled,
|
|
1487
|
+
getSelectedKeys: () => selectedRowKeys.value,
|
|
1488
|
+
onDragStart: (dragKeys, event) => emit('rowDragStart', dragKeys, event),
|
|
1489
|
+
onDrop: (dragKeys, dropKey, position) => emit('rowDrop', dragKeys, dropKey, position),
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
function onRowDragStart(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, event: DragEvent): void {
|
|
1493
|
+
rowDrag.onRowDragStart(entry.row, entry.rowIndex, event)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function onRowDragOver(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, event: DragEvent): void {
|
|
1497
|
+
rowDrag.onRowDragOver(entry.row, entry.rowIndex, event)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function onRowDragLeave(event: DragEvent): void {
|
|
1501
|
+
rowDrag.onRowDragLeave(event)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function onRowDrop(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, event: DragEvent): void {
|
|
1505
|
+
rowDrag.onRowDrop(entry.row, entry.rowIndex, event)
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function onRowDragEnd(): void {
|
|
1509
|
+
rowDrag.onRowDragEnd()
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function isRowDropTarget(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1513
|
+
return rowDrag.isRowDropTarget(rowKeyFor(entry))
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function isRowDragging(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1517
|
+
return rowDrag.isRowDragging(rowKeyFor(entry))
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function canDragRow(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1521
|
+
return rowDrag.canDragRow(entry.row, entry.rowIndex)
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function isRowDragByRow(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1525
|
+
return props.rowDraggable && props.rowDragTrigger === 'row' && canDragRow(entry)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function isRowSelected(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1529
|
+
return selectedKeySet.value.has(rowKeyFor(entry))
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function canSelectRow(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1533
|
+
if (isTableRowDisabled(entry.row)) return false
|
|
1534
|
+
return props.rowSelectable ? props.rowSelectable(entry.row, entry.rowIndex) : true
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function onToggleRow(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
|
|
1538
|
+
if (!canSelectRow(entry)) return
|
|
1539
|
+
selectedRowKeys.value = selectRowKeys(selectedRowKeys.value, rowKeyFor(entry), props.selectionType)
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function canExpandRow(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1543
|
+
if (!props.expandable) return false
|
|
1544
|
+
if (props.rowExpandable) return props.rowExpandable(entry.row, entry.rowIndex)
|
|
1545
|
+
return true
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function isRowExpanded(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
1549
|
+
return expandedKeySet.value.has(rowKeyFor(entry))
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function onToggleExpand(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
|
|
1553
|
+
if (!canExpandRow(entry)) return
|
|
1554
|
+
expandedRowKeys.value = toggleExpandedRowKeys(expandedRowKeys.value, rowKeyFor(entry))
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function onToggleSelectAll(): void {
|
|
1558
|
+
const select = selectAllState.value !== 'checked'
|
|
1559
|
+
selectedRowKeys.value = toggleSelectAll(selectedRowKeys.value, selectableRowKeys.value, select)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function cellTooltipEnabled(column: RsTableColumn<T>, rowIndex: number): boolean {
|
|
1563
|
+
return props.cellTooltip && columnUsesSharedTooltip(column) && !slots[column.key]
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function cellTooltipMode(column: RsTableColumn<T>, rowIndex: number): string | undefined {
|
|
1567
|
+
if (!cellTooltipEnabled(column, rowIndex)) return undefined
|
|
1568
|
+
return resolveCellTooltipMode(column) ?? undefined
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function cellTooltipText(column: RsTableColumn<T>, row: T, rowIndex: number): string | undefined {
|
|
1572
|
+
if (!cellTooltipEnabled(column, rowIndex)) return undefined
|
|
1573
|
+
const text = resolveCellTooltipText(column, row, rowIndex)
|
|
1574
|
+
return text || undefined
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function cellTooltipFallbackTitle(column: RsTableColumn<T>, row: T, rowIndex: number): string | undefined {
|
|
1578
|
+
if (props.cellTooltip || !column.ellipsis || slots[column.key]) return undefined
|
|
1579
|
+
const text = resolveCellTooltipText(column, row, rowIndex)
|
|
1580
|
+
return text || undefined
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function hasColumnSlot(key: string): boolean {
|
|
1584
|
+
return Boolean(slots[key])
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function hasEditSlot(key: string): boolean {
|
|
1588
|
+
return Boolean(slots[`edit-${key}`])
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function resolveColumnByKey(colKey: string): RsTableColumn<T> | undefined {
|
|
1592
|
+
return displayColumns.value.find((column) => column.key === colKey)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function findRowEntryByKey(rowKey: string): Extract<RsTableRowEntry<T>, { type: 'row' }> | undefined {
|
|
1596
|
+
return dataRows.value.find((item) => rowKeyByIndex.value.get(item.rowIndex) === rowKey)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function buildEditableCellList() {
|
|
1600
|
+
const result: Array<{ rowKey: string; colKey: string; rowIndex: number }> = []
|
|
1601
|
+
for (const entry of dataRows.value) {
|
|
1602
|
+
const rowKey = rowKeyFor(entry)
|
|
1603
|
+
for (const column of displayColumns.value) {
|
|
1604
|
+
if (!isColumnEditable(column, entry.row, entry.rowIndex, props.editable)) continue
|
|
1605
|
+
result.push({ rowKey, colKey: column.key, rowIndex: entry.rowIndex })
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return result
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function onCellStartEdit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, colKey: string): void {
|
|
1612
|
+
const column = resolveColumnByKey(colKey)
|
|
1613
|
+
if (!column) return
|
|
1614
|
+
if (isBooleanToggleColumn(column)) return
|
|
1615
|
+
if (!isColumnEditable(column, entry.row, entry.rowIndex, props.editable)) return
|
|
1616
|
+
const key = rowKeyFor(entry)
|
|
1617
|
+
const initialText = resolveCellEditText(entry.row, column, entry.rowIndex, tableEdit.getDraft(key, colKey))
|
|
1618
|
+
const editorOpts = resolveColumnEditorOptions(column, entry.row, entry.rowIndex)
|
|
1619
|
+
if (editorOpts.presentation === 'dialog') {
|
|
1620
|
+
emit('cellEditDialog', entry.row, column, entry.rowIndex, initialText)
|
|
1621
|
+
return
|
|
1622
|
+
}
|
|
1623
|
+
tableEdit.startEdit({ rowKey: key, colKey, rowIndex: entry.rowIndex }, initialText)
|
|
1624
|
+
emit('cellEditStart', entry.row, column, entry.rowIndex)
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function onCellUpdateDraft(_entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, _colKey: string, value: string): void {
|
|
1628
|
+
tableEdit.updateDraft(value)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function onCellCancelEdit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>, colKey: string): void {
|
|
1632
|
+
const column = resolveColumnByKey(colKey)
|
|
1633
|
+
tableEdit.cancelEdit()
|
|
1634
|
+
if (column) emit('cellEditCancel', entry.row, column, entry.rowIndex)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function onCellCommitEdit(
|
|
1638
|
+
entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
|
|
1639
|
+
colKey: string,
|
|
1640
|
+
rawText: string,
|
|
1641
|
+
options?: { navigate?: RsTableCellNavigateDirection; skipUndo?: boolean; skipBatch?: boolean },
|
|
1642
|
+
): Promise<boolean> {
|
|
1643
|
+
const column = resolveColumnByKey(colKey)
|
|
1644
|
+
if (!column) return false
|
|
1645
|
+
const allowNull = isColumnNullable(column, props.allowNull)
|
|
1646
|
+
const parsed = parseCellEditInput(rawText, entry.row, column, entry.rowIndex, { allowNull })
|
|
1647
|
+
const rowKey = rowKeyFor(entry)
|
|
1648
|
+
|
|
1649
|
+
tableEdit.setValidating(rowKey, colKey, true)
|
|
1650
|
+
let error: string | null = null
|
|
1651
|
+
try {
|
|
1652
|
+
error = await validateCellValueAsync(parsed, entry.row, column, entry.rowIndex)
|
|
1653
|
+
} finally {
|
|
1654
|
+
tableEdit.setValidating(rowKey, colKey, false)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (error) {
|
|
1658
|
+
tableEdit.updateDraft(rawText)
|
|
1659
|
+
tableEdit.setCellError(rowKey, colKey, error)
|
|
1660
|
+
emit('cellEditInvalid', entry.row, column, entry.rowIndex, error, parsed)
|
|
1661
|
+
return false
|
|
1662
|
+
}
|
|
1663
|
+
tableEdit.clearCellError(rowKey, colKey)
|
|
1664
|
+
|
|
1665
|
+
const batchEnabled =
|
|
1666
|
+
props.editBatch &&
|
|
1667
|
+
props.selectable &&
|
|
1668
|
+
props.selectionType !== 'radio' &&
|
|
1669
|
+
!props.rowCommit &&
|
|
1670
|
+
!options?.skipBatch
|
|
1671
|
+
|
|
1672
|
+
const targets = batchEnabled
|
|
1673
|
+
? listBatchColumnTargets({
|
|
1674
|
+
rows: dataRows.value.map((item) => ({
|
|
1675
|
+
row: item.row,
|
|
1676
|
+
rowIndex: item.rowIndex,
|
|
1677
|
+
rowKey: rowKeyFor(item),
|
|
1678
|
+
})),
|
|
1679
|
+
column,
|
|
1680
|
+
tableEditable: props.editable,
|
|
1681
|
+
selectedKeys: selectedRowKeys.value,
|
|
1682
|
+
anchorRowKey: rowKey,
|
|
1683
|
+
})
|
|
1684
|
+
: [{ row: entry.row, rowIndex: entry.rowIndex, rowKey }]
|
|
1685
|
+
|
|
1686
|
+
if (props.rowCommit) {
|
|
1687
|
+
const previous = resolveColumnRawValue(entry.row, column, entry.rowIndex)
|
|
1688
|
+
if (!isCellValueChanged(previous, parsed)) {
|
|
1689
|
+
tableEdit.unstageCell(rowKey, colKey)
|
|
1690
|
+
tableEdit.cancelEdit()
|
|
1691
|
+
if (options?.navigate) moveEditFocus(entry, colKey, options.navigate, false)
|
|
1692
|
+
return true
|
|
1693
|
+
}
|
|
1694
|
+
tableEdit.stageCell({
|
|
1695
|
+
rowKey,
|
|
1696
|
+
colKey,
|
|
1697
|
+
rowIndex: entry.rowIndex,
|
|
1698
|
+
draft: rawText,
|
|
1699
|
+
original: previous,
|
|
1700
|
+
})
|
|
1701
|
+
if (options?.navigate) moveEditFocus(entry, colKey, options.navigate, true)
|
|
1702
|
+
return true
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
tableEdit.cancelEdit()
|
|
1706
|
+
|
|
1707
|
+
const undoItems: Array<{
|
|
1708
|
+
rowKey: string
|
|
1709
|
+
colKey: string
|
|
1710
|
+
rowIndex: number
|
|
1711
|
+
previous: unknown
|
|
1712
|
+
next: unknown
|
|
1713
|
+
}> = []
|
|
1714
|
+
const batchChanges: Array<{ row: T; index: number; value: unknown; previous: unknown }> = []
|
|
1715
|
+
|
|
1716
|
+
for (const target of targets) {
|
|
1717
|
+
const previous = resolveColumnRawValue(target.row, column, target.rowIndex)
|
|
1718
|
+
if (!isCellValueChanged(previous, parsed)) continue
|
|
1719
|
+
undoItems.push({
|
|
1720
|
+
rowKey: target.rowKey,
|
|
1721
|
+
colKey,
|
|
1722
|
+
rowIndex: target.rowIndex,
|
|
1723
|
+
previous,
|
|
1724
|
+
next: parsed,
|
|
1725
|
+
})
|
|
1726
|
+
batchChanges.push({
|
|
1727
|
+
row: target.row,
|
|
1728
|
+
index: target.rowIndex,
|
|
1729
|
+
value: parsed,
|
|
1730
|
+
previous,
|
|
1731
|
+
})
|
|
1732
|
+
if (target.rowKey === rowKey) {
|
|
1733
|
+
emit('cellEditCommit', target.row, column, target.rowIndex, parsed, previous)
|
|
1734
|
+
} else {
|
|
1735
|
+
emit('cellEditCommit', target.row, column, target.rowIndex, parsed, previous)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (batchChanges.length > 1) {
|
|
1740
|
+
emit('cellEditBatchCommit', column, batchChanges)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (!options?.skipUndo && props.editUndo && undoItems.length) {
|
|
1744
|
+
tableEdit.pushUndo({ items: undoItems })
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (options?.navigate) moveEditFocus(entry, colKey, options.navigate, true)
|
|
1748
|
+
return true
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function moveEditFocus(
|
|
1752
|
+
entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
|
|
1753
|
+
colKey: string,
|
|
1754
|
+
direction: RsTableCellNavigateDirection,
|
|
1755
|
+
startEditNext: boolean,
|
|
1756
|
+
): void {
|
|
1757
|
+
if (!props.editKeyboard) return
|
|
1758
|
+
const cells = buildEditableCellList()
|
|
1759
|
+
const next = navigateEditableCell(
|
|
1760
|
+
cells,
|
|
1761
|
+
rowKeyFor(entry),
|
|
1762
|
+
colKey,
|
|
1763
|
+
direction,
|
|
1764
|
+
displayColumns.value.length || 1,
|
|
1765
|
+
)
|
|
1766
|
+
if (!next) return
|
|
1767
|
+
const nextEntry = findRowEntryByKey(next.rowKey)
|
|
1768
|
+
if (!nextEntry) return
|
|
1769
|
+
tableEdit.setFocusCell(next)
|
|
1770
|
+
rowHighlight.applyRowClickHighlight(next.rowKey)
|
|
1771
|
+
if (startEditNext) {
|
|
1772
|
+
const column = resolveColumnByKey(next.colKey)
|
|
1773
|
+
if (column && isBooleanToggleColumn(column)) return
|
|
1774
|
+
onCellStartEdit(nextEntry, next.colKey)
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
async function onCellNavigate(
|
|
1779
|
+
entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
|
|
1780
|
+
colKey: string,
|
|
1781
|
+
direction: RsTableCellNavigateDirection,
|
|
1782
|
+
): Promise<void> {
|
|
1783
|
+
const draft = tableEdit.editingDraft.value
|
|
1784
|
+
await onCellCommitEdit(entry, colKey, draft, { navigate: direction })
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function onCellClick(entry: RsTableRowEntry<T>, colKey: string, event?: MouseEvent): void {
|
|
1788
|
+
if (entry.type !== 'row') return
|
|
1789
|
+
onRowClick(entry, event)
|
|
1790
|
+
if (!props.editable) return
|
|
1791
|
+
const column = resolveColumnByKey(colKey)
|
|
1792
|
+
if (!column) return
|
|
1793
|
+
tableEdit.setFocusCell({
|
|
1794
|
+
rowKey: rowKeyFor(entry),
|
|
1795
|
+
colKey,
|
|
1796
|
+
rowIndex: entry.rowIndex,
|
|
1797
|
+
})
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* 单元格双击:可编辑且触发方式为 dblclick 时进入编辑;
|
|
1802
|
+
* 否则发出 cellView(供只读大字段查看),并转发 rowDblclick(FTP 进目录等)。
|
|
1803
|
+
* 数据单元格上 click/dblclick 使用 .stop,必须在此桥接,否则行级事件收不到。
|
|
1804
|
+
*/
|
|
1805
|
+
function onCellDblclick(entry: RsTableRowEntry<T>, colKey: string, event?: MouseEvent): void {
|
|
1806
|
+
if (entry.type !== 'row') return
|
|
1807
|
+
const column = resolveColumnByKey(colKey)
|
|
1808
|
+
if (
|
|
1809
|
+
column &&
|
|
1810
|
+
props.editable &&
|
|
1811
|
+
resolveColumnEditTrigger(column, props.editTrigger) === 'dblclick' &&
|
|
1812
|
+
isColumnEditable(column, entry.row, entry.rowIndex, props.editable) &&
|
|
1813
|
+
!isBooleanToggleColumn(column)
|
|
1814
|
+
) {
|
|
1815
|
+
onCellStartEdit(entry, colKey)
|
|
1816
|
+
return
|
|
1817
|
+
}
|
|
1818
|
+
if (column) {
|
|
1819
|
+
emit('cellView', entry.row, column, entry.rowIndex)
|
|
1820
|
+
}
|
|
1821
|
+
onRowDblclick(entry, event)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function applyUndoRedoValue(
|
|
1825
|
+
entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
|
|
1826
|
+
colKey: string,
|
|
1827
|
+
value: unknown,
|
|
1828
|
+
): void {
|
|
1829
|
+
const column = resolveColumnByKey(colKey)
|
|
1830
|
+
if (!column) return
|
|
1831
|
+
const previous = resolveColumnRawValue(entry.row, column, entry.rowIndex)
|
|
1832
|
+
emit('cellEditCommit', entry.row, column, entry.rowIndex, value, previous)
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function onEditUndo(): void {
|
|
1836
|
+
if (!props.editUndo) return
|
|
1837
|
+
const entry = tableEdit.undo()
|
|
1838
|
+
if (!entry) return
|
|
1839
|
+
for (const item of entry.items) {
|
|
1840
|
+
const rowEntry = findRowEntryByKey(item.rowKey)
|
|
1841
|
+
if (!rowEntry) continue
|
|
1842
|
+
applyUndoRedoValue(rowEntry, item.colKey, item.previous)
|
|
1843
|
+
}
|
|
1844
|
+
emit('cellEditUndo', entry)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function onEditRedo(): void {
|
|
1848
|
+
if (!props.editUndo) return
|
|
1849
|
+
const entry = tableEdit.redo()
|
|
1850
|
+
if (!entry) return
|
|
1851
|
+
for (const item of entry.items) {
|
|
1852
|
+
const rowEntry = findRowEntryByKey(item.rowKey)
|
|
1853
|
+
if (!rowEntry) continue
|
|
1854
|
+
applyUndoRedoValue(rowEntry, item.colKey, item.next)
|
|
1855
|
+
}
|
|
1856
|
+
emit('cellEditRedo', entry)
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function onTableKeydown(event: KeyboardEvent): void {
|
|
1860
|
+
if (!props.editable || !props.editKeyboard) return
|
|
1861
|
+
const target = event.target as HTMLElement | null
|
|
1862
|
+
if (target?.closest('input, textarea, select, [contenteditable="true"]')) {
|
|
1863
|
+
// 编辑器内部自行处理;仅拦截全局撤销
|
|
1864
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z' && !event.shiftKey) {
|
|
1865
|
+
if (!tableEdit.editingCell.value) {
|
|
1866
|
+
event.preventDefault()
|
|
1867
|
+
onEditUndo()
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (
|
|
1871
|
+
(event.ctrlKey || event.metaKey) &&
|
|
1872
|
+
(event.key.toLowerCase() === 'y' || (event.key.toLowerCase() === 'z' && event.shiftKey))
|
|
1873
|
+
) {
|
|
1874
|
+
if (!tableEdit.editingCell.value) {
|
|
1875
|
+
event.preventDefault()
|
|
1876
|
+
onEditRedo()
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z' && !event.shiftKey) {
|
|
1883
|
+
event.preventDefault()
|
|
1884
|
+
onEditUndo()
|
|
1885
|
+
return
|
|
1886
|
+
}
|
|
1887
|
+
if (
|
|
1888
|
+
(event.ctrlKey || event.metaKey) &&
|
|
1889
|
+
(event.key.toLowerCase() === 'y' || (event.key.toLowerCase() === 'z' && event.shiftKey))
|
|
1890
|
+
) {
|
|
1891
|
+
event.preventDefault()
|
|
1892
|
+
onEditRedo()
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const focus = tableEdit.focusCell.value
|
|
1897
|
+
if (!focus) return
|
|
1898
|
+
const entry = findRowEntryByKey(focus.rowKey)
|
|
1899
|
+
if (!entry) return
|
|
1900
|
+
const column = resolveColumnByKey(focus.colKey)
|
|
1901
|
+
if (!column) return
|
|
1902
|
+
|
|
1903
|
+
if (event.key === 'F2' || event.key === 'Enter') {
|
|
1904
|
+
event.preventDefault()
|
|
1905
|
+
if (isBooleanToggleColumn(column)) {
|
|
1906
|
+
const next = !coerceBooleanFromRow(entry, column)
|
|
1907
|
+
onCellCommitEdit(entry, focus.colKey, next ? 'true' : 'false')
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1910
|
+
onCellStartEdit(entry, focus.colKey)
|
|
1911
|
+
return
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (event.key === 'Escape') {
|
|
1915
|
+
event.preventDefault()
|
|
1916
|
+
tableEdit.cancelEdit()
|
|
1917
|
+
return
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
1921
|
+
if (!isColumnEditable(column, entry.row, entry.rowIndex, props.editable)) return
|
|
1922
|
+
event.preventDefault()
|
|
1923
|
+
const allowNull = isColumnNullable(column, props.allowNull)
|
|
1924
|
+
onCellCommitEdit(entry, focus.colKey, allowNull ? nullToEditText() : '')
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (event.key === 'Tab') {
|
|
1929
|
+
event.preventDefault()
|
|
1930
|
+
moveEditFocus(entry, focus.colKey, event.shiftKey ? 'prev' : 'next', false)
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
if (event.key === 'ArrowRight') {
|
|
1934
|
+
event.preventDefault()
|
|
1935
|
+
moveEditFocus(entry, focus.colKey, 'next', false)
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
if (event.key === 'ArrowLeft') {
|
|
1939
|
+
event.preventDefault()
|
|
1940
|
+
moveEditFocus(entry, focus.colKey, 'prev', false)
|
|
1941
|
+
return
|
|
1942
|
+
}
|
|
1943
|
+
if (event.key === 'ArrowDown') {
|
|
1944
|
+
event.preventDefault()
|
|
1945
|
+
moveEditFocus(entry, focus.colKey, 'down', false)
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
if (event.key === 'ArrowUp') {
|
|
1949
|
+
event.preventDefault()
|
|
1950
|
+
moveEditFocus(entry, focus.colKey, 'up', false)
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function coerceBooleanFromRow(
|
|
1955
|
+
entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
|
|
1956
|
+
column: RsTableColumn<T>,
|
|
1957
|
+
): boolean {
|
|
1958
|
+
const raw = resolveColumnRawValue(entry.row, column, entry.rowIndex)
|
|
1959
|
+
if (raw == null) return false
|
|
1960
|
+
if (typeof raw === 'boolean') return raw
|
|
1961
|
+
return String(raw).toLowerCase() === 'true' || raw === 1 || raw === '1'
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function onTablePaste(event: ClipboardEvent): void {
|
|
1965
|
+
if (!props.editable || !props.editPaste) return
|
|
1966
|
+
if (tableEdit.editingCell.value) return
|
|
1967
|
+
const focus = tableEdit.focusCell.value
|
|
1968
|
+
if (!focus) return
|
|
1969
|
+
const text = event.clipboardData?.getData('text/plain')
|
|
1970
|
+
if (!text) return
|
|
1971
|
+
const grid = parseClipboardGrid(text)
|
|
1972
|
+
if (!grid.length) return
|
|
1973
|
+
event.preventDefault()
|
|
1974
|
+
|
|
1975
|
+
const startEntry = findRowEntryByKey(focus.rowKey)
|
|
1976
|
+
if (!startEntry) return
|
|
1977
|
+
const startRowPos = dataRows.value.findIndex((item) => rowKeyFor(item) === focus.rowKey)
|
|
1978
|
+
const startColIndex = displayColumns.value.findIndex((column) => column.key === focus.colKey)
|
|
1979
|
+
if (startRowPos < 0 || startColIndex < 0) return
|
|
1980
|
+
|
|
1981
|
+
for (let r = 0; r < grid.length; r += 1) {
|
|
1982
|
+
const rowEntry = dataRows.value[startRowPos + r]
|
|
1983
|
+
if (!rowEntry) break
|
|
1984
|
+
const line = grid[r] ?? []
|
|
1985
|
+
for (let c = 0; c < line.length; c += 1) {
|
|
1986
|
+
const column = displayColumns.value[startColIndex + c]
|
|
1987
|
+
if (!column) break
|
|
1988
|
+
if (!isColumnEditable(column, rowEntry.row, rowEntry.rowIndex, props.editable)) continue
|
|
1989
|
+
onCellCommitEdit(rowEntry, column.key, line[c] ?? '')
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function rejectRowEdit(rowKey: string, reason?: string): void {
|
|
1995
|
+
const entry = findRowEntryByKey(rowKey)
|
|
1996
|
+
tableEdit.rollbackRow(rowKey)
|
|
1997
|
+
if (entry) emit('cellEditReject', entry.row, entry.rowIndex, reason)
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function onRowCommitEdit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): Promise<void> {
|
|
2001
|
+
const rowKey = rowKeyFor(entry)
|
|
2002
|
+
// date/datetime 等:面板确认只改 editingDraft,不会自动 stage;✓ 前先刷入 staged
|
|
2003
|
+
const editing = tableEdit.editingCell.value
|
|
2004
|
+
if (editing?.rowKey === rowKey) {
|
|
2005
|
+
const ok = await onCellCommitEdit(entry, editing.colKey, tableEdit.editingDraft.value)
|
|
2006
|
+
if (!ok) return
|
|
2007
|
+
}
|
|
2008
|
+
const staged = tableEdit.commitRow(rowKey)
|
|
2009
|
+
if (!staged.length) return
|
|
2010
|
+
const changes = staged.map((item) => {
|
|
2011
|
+
const column = resolveColumnByKey(item.colKey)
|
|
2012
|
+
const allowNull = column ? isColumnNullable(column, props.allowNull) : props.allowNull
|
|
2013
|
+
const value = column
|
|
2014
|
+
? parseCellEditInput(item.draft, entry.row, column, entry.rowIndex, { allowNull })
|
|
2015
|
+
: item.draft
|
|
2016
|
+
return {
|
|
2017
|
+
colKey: item.colKey,
|
|
2018
|
+
value,
|
|
2019
|
+
previous: item.original,
|
|
2020
|
+
}
|
|
2021
|
+
})
|
|
2022
|
+
emit('rowEditCommit', entry.row, entry.rowIndex, changes)
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function onRowRollbackEdit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
|
|
2026
|
+
if (!props.rowCommit && isExternalRowPending(entry)) {
|
|
2027
|
+
emit('rowEditRollback', entry.row, entry.rowIndex)
|
|
2028
|
+
return
|
|
2029
|
+
}
|
|
2030
|
+
tableEdit.rollbackRow(rowKeyFor(entry))
|
|
2031
|
+
emit('rowEditRollback', entry.row, entry.rowIndex)
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function isExternalRowPending(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
2035
|
+
return Boolean(props.rowPending?.(entry.row, entry.rowIndex))
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function rowEditPending(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
|
|
2039
|
+
if (isExternalRowPending(entry)) return true
|
|
2040
|
+
const rowKey = rowKeyFor(entry)
|
|
2041
|
+
return isRowEditPending(rowKey, entry.row, entry.rowIndex, {
|
|
2042
|
+
isRowDirty: tableEdit.isRowDirty,
|
|
2043
|
+
editingCell: tableEdit.editingCell.value,
|
|
2044
|
+
editingDraft: tableEdit.editingDraft.value,
|
|
2045
|
+
resolveColumn: resolveColumnByKey,
|
|
2046
|
+
})
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function onGutterCommit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
|
|
2050
|
+
if (props.rowCommit) {
|
|
2051
|
+
void onRowCommitEdit(entry)
|
|
2052
|
+
return
|
|
2053
|
+
}
|
|
2054
|
+
if (isExternalRowPending(entry)) {
|
|
2055
|
+
emit('rowEditCommit', entry.row, entry.rowIndex, [])
|
|
2056
|
+
return
|
|
2057
|
+
}
|
|
2058
|
+
const editing = tableEdit.editingCell.value
|
|
2059
|
+
if (editing?.rowKey !== rowKeyFor(entry)) return
|
|
2060
|
+
void onCellCommitEdit(entry, editing.colKey, tableEdit.editingDraft.value)
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function getRowByKey(rowKey: string): T | undefined {
|
|
2064
|
+
const entry = dataRows.value.find((item) => rowKeyByIndex.value.get(item.rowIndex) === rowKey)
|
|
2065
|
+
return entry?.row
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function getRowByIndex(index: number): T | undefined {
|
|
2069
|
+
return dataRows.value.find((item) => item.rowIndex === index)?.row
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function getColumnValues(colKey: string): unknown[] {
|
|
2073
|
+
const column = resolveColumnByKey(colKey)
|
|
2074
|
+
if (!column) return []
|
|
2075
|
+
return dataRows.value.map((entry) => resolveColumnRawValue(entry.row, column, entry.rowIndex))
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function getCellValueByKey(rowKey: string, colKey: string): unknown {
|
|
2079
|
+
const row = getRowByKey(rowKey)
|
|
2080
|
+
const column = resolveColumnByKey(colKey)
|
|
2081
|
+
if (!row || !column) return undefined
|
|
2082
|
+
const entry = dataRows.value.find((item) => rowKeyByIndex.value.get(item.rowIndex) === rowKey)
|
|
2083
|
+
if (!entry) return undefined
|
|
2084
|
+
return resolveColumnRawValue(entry.row, column, entry.rowIndex)
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
defineExpose({
|
|
2088
|
+
getRowByKey,
|
|
2089
|
+
getRowByIndex,
|
|
2090
|
+
getColumnValues,
|
|
2091
|
+
getCellValue: getCellValueByKey,
|
|
2092
|
+
getHighlightedRowKey: () => rowHighlight.highlightedKey.value,
|
|
2093
|
+
setHighlightedRowKey: rowHighlight.setHighlightedKey,
|
|
2094
|
+
cancelCellEdit: () => tableEdit.cancelEdit(),
|
|
2095
|
+
cancelAllEdits: () => tableEdit.cancelAll(),
|
|
2096
|
+
getDirtyCellKeys: () => [...tableEdit.stagedMap.value.keys()],
|
|
2097
|
+
getCellError: tableEdit.getCellError,
|
|
2098
|
+
setCellError: tableEdit.setCellError,
|
|
2099
|
+
/** 弹窗等外部编辑器写回草稿(需配合 row-commit,由用户手动点行提交) */
|
|
2100
|
+
stageCell: tableEdit.stageCell,
|
|
2101
|
+
rejectRowEdit,
|
|
2102
|
+
undoEdit: onEditUndo,
|
|
2103
|
+
redoEdit: onEditRedo,
|
|
2104
|
+
commitRowEdits: (rowKey: string) => {
|
|
2105
|
+
const entry = dataRows.value.find((item) => rowKeyByIndex.value.get(item.rowIndex) === rowKey)
|
|
2106
|
+
if (entry) void onRowCommitEdit(entry)
|
|
2107
|
+
},
|
|
2108
|
+
rollbackRowEdits: (rowKey: string) => {
|
|
2109
|
+
const entry = dataRows.value.find((item) => rowKeyByIndex.value.get(item.rowIndex) === rowKey)
|
|
2110
|
+
if (entry) onRowRollbackEdit(entry)
|
|
2111
|
+
},
|
|
2112
|
+
})
|
|
2113
|
+
|
|
2114
|
+
/** RAF handle,用于节流滚动更新;组件卸载时取消,防止内存泄漏 */
|
|
2115
|
+
let scrollRafId = 0
|
|
2116
|
+
|
|
2117
|
+
function onScroll(event: Event): void {
|
|
2118
|
+
hideCellTooltip()
|
|
2119
|
+
// 保留编辑草稿:虚拟滚动卸载行后滚回仍可恢复编辑态
|
|
2120
|
+
const element = event.target as HTMLElement
|
|
2121
|
+
// 无限加载判断不节流,保证触底灵敏
|
|
2122
|
+
if (props.infinite && !props.loading && !props.loadingMore && props.hasMore && !loadMoreLocked.value) {
|
|
2123
|
+
if (isNearScrollBottom(element.scrollTop, element.scrollHeight, element.clientHeight, props.infiniteDistance)) {
|
|
2124
|
+
loadMoreLocked.value = true
|
|
2125
|
+
emit('loadMore')
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
// 始终记录滚动(含非虚拟),供 keep-alive / layoutActive 切回后恢复
|
|
2129
|
+
if (scrollRafId) return
|
|
2130
|
+
scrollRafId = requestAnimationFrame(() => {
|
|
2131
|
+
// 恢复窗口内浏览器常先抛出 scrollTop=0 的伪事件,忽略以免冲掉已保存位置
|
|
2132
|
+
if (restoringScroll) {
|
|
2133
|
+
scrollRafId = 0
|
|
2134
|
+
return
|
|
2135
|
+
}
|
|
2136
|
+
scrollTop.value = element.scrollTop
|
|
2137
|
+
scrollLeft.value = element.scrollLeft
|
|
2138
|
+
preservedScrollTop = element.scrollTop
|
|
2139
|
+
preservedScrollLeft = element.scrollLeft
|
|
2140
|
+
scrollRafId = 0
|
|
2141
|
+
})
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
watch(
|
|
2145
|
+
() => [props.loadingMore, props.hasMore] as const,
|
|
2146
|
+
([loadingMore, hasMore]) => {
|
|
2147
|
+
if (!loadingMore || !hasMore) loadMoreLocked.value = false
|
|
2148
|
+
},
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
function beginResizeSession(): void {
|
|
2152
|
+
if (!resizeState || isColumnResizing.value) return
|
|
2153
|
+
isColumnResizing.value = true
|
|
2154
|
+
activeDragWidths = { ...resizeState.domSnapshot }
|
|
2155
|
+
resizePaintWidths.value = { ...activeDragWidths }
|
|
2156
|
+
void nextTick(() => {
|
|
2157
|
+
if (tableRef.value && activeDragWidths) applyTableColumnWidths(tableRef.value, activeDragWidths)
|
|
2158
|
+
})
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
function onResizeMove(event: MouseEvent): void {
|
|
2162
|
+
if (!resizeState) return
|
|
2163
|
+
latestResizeClientX = event.clientX
|
|
2164
|
+
if (!resizeMoved && Math.abs(latestResizeClientX - resizeState.startX) < 1) return
|
|
2165
|
+
if (!resizeMoved) {
|
|
2166
|
+
resizeMoved = true
|
|
2167
|
+
beginResizeSession()
|
|
2168
|
+
}
|
|
2169
|
+
if (resizeRafId !== 0) return
|
|
2170
|
+
resizeRafId = requestAnimationFrame(() => {
|
|
2171
|
+
resizeRafId = 0
|
|
2172
|
+
if (!resizeState || !activeDragWidths) return
|
|
2173
|
+
const nextWidth = clampColumnWidth(
|
|
2174
|
+
resizeState.startWidth + latestResizeClientX - resizeState.startX,
|
|
2175
|
+
props.minColumnWidth,
|
|
2176
|
+
props.maxColumnWidth,
|
|
2177
|
+
)
|
|
2178
|
+
resizeState.currentWidth = nextWidth
|
|
2179
|
+
activeDragWidths = { ...resizeState.domSnapshot, [resizeState.key]: nextWidth }
|
|
2180
|
+
if (tableRef.value) applyTableColumnWidths(tableRef.value, activeDragWidths)
|
|
2181
|
+
})
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function stopResize(): void {
|
|
2185
|
+
if (resizeState && resizeMoved) {
|
|
2186
|
+
const finalWidth = clampColumnWidth(
|
|
2187
|
+
resizeState.startWidth + latestResizeClientX - resizeState.startX,
|
|
2188
|
+
props.minColumnWidth,
|
|
2189
|
+
props.maxColumnWidth,
|
|
2190
|
+
)
|
|
2191
|
+
if (finalWidth !== resizeState.startWidth) {
|
|
2192
|
+
const merged = { ...resizeState.domSnapshot, [resizeState.key]: finalWidth }
|
|
2193
|
+
resolvedColumnWidths.value = merged
|
|
2194
|
+
emit('columnResize', resizeState.key, finalWidth)
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (tableRef.value) {
|
|
2198
|
+
tableRef.value.style.width = ''
|
|
2199
|
+
tableRef.value.style.minWidth = ''
|
|
2200
|
+
for (const th of tableRef.value.querySelectorAll<HTMLTableCellElement>('thead th[data-col-key]')) {
|
|
2201
|
+
th.style.width = ''
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
isColumnResizing.value = false
|
|
2205
|
+
resizePaintWidths.value = null
|
|
2206
|
+
activeDragWidths = null
|
|
2207
|
+
resizeState = null
|
|
2208
|
+
resizeMoved = false
|
|
2209
|
+
if (resizeRafId !== 0) {
|
|
2210
|
+
cancelAnimationFrame(resizeRafId)
|
|
2211
|
+
resizeRafId = 0
|
|
2212
|
+
}
|
|
2213
|
+
document.body.style.cursor = ''
|
|
2214
|
+
document.body.style.userSelect = ''
|
|
2215
|
+
document.removeEventListener('mousemove', onResizeMove)
|
|
2216
|
+
document.removeEventListener('mouseup', stopResize, { capture: true })
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function onResizeStart(key: string, event: MouseEvent): void {
|
|
2220
|
+
if (!props.resizable) return
|
|
2221
|
+
|
|
2222
|
+
const thEl = (event.currentTarget as HTMLElement).closest('th') as HTMLTableCellElement | null
|
|
2223
|
+
if (!thEl) return
|
|
2224
|
+
const tableEl = thEl.closest('table') as HTMLTableElement | null
|
|
2225
|
+
if (!tableEl) return
|
|
2226
|
+
|
|
2227
|
+
const dataThs = Array.from(tableEl.querySelectorAll<HTMLTableCellElement>('thead th[data-col-key]'))
|
|
2228
|
+
const domSnapshot: Record<string, number> = {}
|
|
2229
|
+
for (const th of dataThs) {
|
|
2230
|
+
domSnapshot[th.dataset.colKey!] = th.offsetWidth
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const startWidth = domSnapshot[key] ?? thEl.offsetWidth
|
|
2234
|
+
latestResizeClientX = event.clientX
|
|
2235
|
+
resizeMoved = false
|
|
2236
|
+
resizeState = {
|
|
2237
|
+
key,
|
|
2238
|
+
startX: event.clientX,
|
|
2239
|
+
startWidth,
|
|
2240
|
+
currentWidth: startWidth,
|
|
2241
|
+
domSnapshot,
|
|
2242
|
+
persistedWidths: { ...resolvedColumnWidths.value },
|
|
2243
|
+
}
|
|
2244
|
+
document.body.style.cursor = 'col-resize'
|
|
2245
|
+
document.body.style.userSelect = 'none'
|
|
2246
|
+
document.addEventListener('mousemove', onResizeMove)
|
|
2247
|
+
document.addEventListener('mouseup', stopResize, { capture: true })
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
onUnmounted(() => {
|
|
2251
|
+
stopResize()
|
|
2252
|
+
viewportResizeObserver?.disconnect()
|
|
2253
|
+
viewportResizeObserver = null
|
|
2254
|
+
if (scrollRafId) {
|
|
2255
|
+
cancelAnimationFrame(scrollRafId)
|
|
2256
|
+
scrollRafId = 0
|
|
2257
|
+
}
|
|
2258
|
+
})
|
|
2259
|
+
</script>
|
|
2260
|
+
|
|
2261
|
+
<template>
|
|
2262
|
+
<RsContextMenu
|
|
2263
|
+
:disabled="!contextMenuEnabled"
|
|
2264
|
+
:items="ctxMenuItems"
|
|
2265
|
+
@select="onCtxMenuSelect"
|
|
2266
|
+
>
|
|
2267
|
+
<!-- shell 作为 ContextMenuTrigger 宿主;::before 伪元素铺满 shell 捕获空白区右键 -->
|
|
2268
|
+
<div
|
|
2269
|
+
class="rs-table-shell"
|
|
2270
|
+
:class="{ 'rs-table-shell--ctx': contextMenuEnabled, 'rs-table-shell--fill': fill }"
|
|
2271
|
+
tabindex="0"
|
|
2272
|
+
v-bind="$attrs"
|
|
2273
|
+
@contextmenu="onTableContextmenu"
|
|
2274
|
+
@keydown="onTableKeydown"
|
|
2275
|
+
@paste="onTablePaste"
|
|
2276
|
+
>
|
|
2277
|
+
<div
|
|
2278
|
+
ref="scrollContainerRef"
|
|
2279
|
+
class="rs-table rs-native-scrollbar"
|
|
2280
|
+
:class="{
|
|
2281
|
+
'rs-table--bordered': bordered,
|
|
2282
|
+
'rs-table--column-bordered': columnBordered,
|
|
2283
|
+
'rs-table--rounded': rounded,
|
|
2284
|
+
'rs-table--fill': fill,
|
|
2285
|
+
'rs-table--compact': compact,
|
|
2286
|
+
[`rs-table--${resolvedSize}`]: true,
|
|
2287
|
+
'rs-table--virtual': virtualScrollEnabled,
|
|
2288
|
+
'rs-table--virtual-cols': virtualColumnsEnabled,
|
|
2289
|
+
'rs-table--content-visibility': !virtualScrollEnabled && data.length >= 30,
|
|
2290
|
+
'rs-table--infinite': infinite,
|
|
2291
|
+
'rs-table--resizable': resizable,
|
|
2292
|
+
'rs-table--col-auto': resizable && columnLayout === 'auto',
|
|
2293
|
+
'rs-table--col-fixed': useFixedColumnLayout,
|
|
2294
|
+
'rs-table--striped': striped,
|
|
2295
|
+
'rs-table--selectable': selectable,
|
|
2296
|
+
'rs-table--row-select': isRowSelection,
|
|
2297
|
+
'rs-table--expandable': expandable,
|
|
2298
|
+
'rs-table--draggable': rowDraggable || columnDraggable,
|
|
2299
|
+
'rs-table--scroll-x': !!scrollX,
|
|
2300
|
+
'rs-table--overflow-x': overflowXEnabled,
|
|
2301
|
+
'rs-table--ctx': contextMenuEnabled,
|
|
2302
|
+
'rs-table--edit-gutter': showEditGutterColumn,
|
|
2303
|
+
}"
|
|
2304
|
+
:style="scrollContainerStyle"
|
|
2305
|
+
@scroll.passive="onScroll"
|
|
2306
|
+
>
|
|
2307
|
+
<table ref="tableRef" class="rs-table__table" :style="tableInlineStyle">
|
|
2308
|
+
<colgroup v-if="useFixedColumnLayout">
|
|
2309
|
+
<col v-if="showRowDragHandle" :style="{ width: `${PREFIX_COL_WIDTH.drag}px` }">
|
|
2310
|
+
<col v-if="expandable" :style="{ width: `${PREFIX_COL_WIDTH.expand}px` }">
|
|
2311
|
+
<col v-if="showSelectColumn" :style="{ width: `${PREFIX_COL_WIDTH.select}px` }">
|
|
2312
|
+
<col v-if="showEditGutterColumn" data-col-key="gutter" :style="{ width: `${resolvedGutterWidth}px` }">
|
|
2313
|
+
<col v-else-if="showIndexColumn" :style="{ width: `${resolvedIndexWidth}px` }">
|
|
2314
|
+
<col v-if="showRowStatusColumn" :style="{ width: `${PREFIX_COL_WIDTH.status}px` }">
|
|
2315
|
+
<col
|
|
2316
|
+
v-if="columnPadLeft > 0"
|
|
2317
|
+
class="rs-table__col-pad"
|
|
2318
|
+
:style="{ width: `${columnPadLeft}px` }"
|
|
2319
|
+
>
|
|
2320
|
+
<col
|
|
2321
|
+
v-for="column in visibleDataColumns"
|
|
2322
|
+
:key="column.key"
|
|
2323
|
+
:data-col-key="column.key"
|
|
2324
|
+
:style="{ width: `${resolvedDataColumnWidth(column.key, column.width ?? column.minWidth)}px` }"
|
|
2325
|
+
>
|
|
2326
|
+
<col
|
|
2327
|
+
v-if="columnPadRight > 0"
|
|
2328
|
+
class="rs-table__col-pad"
|
|
2329
|
+
:style="{ width: `${columnPadRight}px` }"
|
|
2330
|
+
>
|
|
2331
|
+
</colgroup>
|
|
2332
|
+
<thead
|
|
2333
|
+
v-if="showHeader"
|
|
2334
|
+
class="rs-table__head"
|
|
2335
|
+
@pointerover.passive="onCellTipOver"
|
|
2336
|
+
@pointerout.passive="onCellTipOut"
|
|
2337
|
+
>
|
|
2338
|
+
<tr>
|
|
2339
|
+
<th
|
|
2340
|
+
v-if="showRowDragHandle"
|
|
2341
|
+
class="rs-table__th rs-table__th--drag"
|
|
2342
|
+
:style="dragLeadHeaderStyle"
|
|
2343
|
+
/>
|
|
2344
|
+
<th
|
|
2345
|
+
v-if="expandable"
|
|
2346
|
+
class="rs-table__th rs-table__th--expand"
|
|
2347
|
+
:style="expandLeadHeaderStyle"
|
|
2348
|
+
/>
|
|
2349
|
+
<th
|
|
2350
|
+
v-if="showSelectColumn"
|
|
2351
|
+
class="rs-table__th rs-table__th--selection"
|
|
2352
|
+
:style="selectLeadHeaderStyle"
|
|
2353
|
+
>
|
|
2354
|
+
<label
|
|
2355
|
+
v-if="!isRadioSelection"
|
|
2356
|
+
class="rs-table__checkbox"
|
|
2357
|
+
:class="{
|
|
2358
|
+
'rs-table__checkbox--checked': selectAllState === 'checked',
|
|
2359
|
+
'rs-table__checkbox--indeterminate': selectAllState === 'indeterminate',
|
|
2360
|
+
}"
|
|
2361
|
+
>
|
|
2362
|
+
<input
|
|
2363
|
+
type="checkbox"
|
|
2364
|
+
class="rs-table__checkbox-input"
|
|
2365
|
+
:checked="selectAllState === 'checked'"
|
|
2366
|
+
:aria-label="t('table.selectAll')"
|
|
2367
|
+
@change="onToggleSelectAll"
|
|
2368
|
+
>
|
|
2369
|
+
<span class="rs-table__checkbox-box" aria-hidden="true" />
|
|
2370
|
+
</label>
|
|
2371
|
+
</th>
|
|
2372
|
+
<th
|
|
2373
|
+
v-if="showEditGutterColumn"
|
|
2374
|
+
class="rs-table__th rs-table__th--gutter rs-table__cell--center"
|
|
2375
|
+
:style="gutterLeadHeaderStyle"
|
|
2376
|
+
/>
|
|
2377
|
+
<th
|
|
2378
|
+
v-else-if="showIndexColumn"
|
|
2379
|
+
class="rs-table__th rs-table__th--index rs-table__cell--center"
|
|
2380
|
+
:style="indexLeadHeaderStyle"
|
|
2381
|
+
>
|
|
2382
|
+
{{ t('table.index') }}
|
|
2383
|
+
</th>
|
|
2384
|
+
<th
|
|
2385
|
+
v-if="showRowStatusColumn"
|
|
2386
|
+
class="rs-table__th rs-table__th--status rs-table__cell--center"
|
|
2387
|
+
>
|
|
2388
|
+
{{ t('table.rowStatus') }}
|
|
2389
|
+
</th>
|
|
2390
|
+
<th
|
|
2391
|
+
v-if="columnPadLeft > 0"
|
|
2392
|
+
class="rs-table__th rs-table__col-pad"
|
|
2393
|
+
:style="{ width: `${columnPadLeft}px`, minWidth: `${columnPadLeft}px` }"
|
|
2394
|
+
/>
|
|
2395
|
+
<th
|
|
2396
|
+
v-for="column in visibleDataColumns"
|
|
2397
|
+
:key="column.key"
|
|
2398
|
+
:data-col-key="column.key"
|
|
2399
|
+
class="rs-table__th"
|
|
2400
|
+
:class="[
|
|
2401
|
+
`rs-table__cell--${column.align ?? 'left'}`,
|
|
2402
|
+
{ 'rs-table__th--sortable': column.sortable },
|
|
2403
|
+
{ 'rs-table__th--filterable': column.filterable },
|
|
2404
|
+
{ 'rs-table__cell--fixed': column.fixed },
|
|
2405
|
+
{ 'rs-table__th--dragging': dragColumnKey === column.key },
|
|
2406
|
+
{ 'rs-table__th--drop-target': dropColumnKey === column.key },
|
|
2407
|
+
]"
|
|
2408
|
+
:style="columnHeaderStyleMap.get(column.key)"
|
|
2409
|
+
@dragover="onColumnDragOver(column.key, $event)"
|
|
2410
|
+
@drop="onColumnDrop(column.key, $event)"
|
|
2411
|
+
>
|
|
2412
|
+
<span
|
|
2413
|
+
v-if="columnDraggable"
|
|
2414
|
+
class="rs-table__column-drag-handle"
|
|
2415
|
+
draggable="true"
|
|
2416
|
+
:aria-label="t('table.dragColumn')"
|
|
2417
|
+
@click.stop
|
|
2418
|
+
@dragstart.stop="onColumnDragStart(column.key, $event)"
|
|
2419
|
+
@dragend.stop="onColumnDragEnd"
|
|
2420
|
+
/>
|
|
2421
|
+
<slot :name="`header-${column.key}`" :column="column">
|
|
2422
|
+
<span
|
|
2423
|
+
v-if="column.headerTip"
|
|
2424
|
+
class="rs-table__th-label rs-table__th-label--tip"
|
|
2425
|
+
:data-rs-table-header-tip="column.headerTip"
|
|
2426
|
+
>{{ column.title }}</span>
|
|
2427
|
+
<span v-else class="rs-table__th-label">{{ column.title }}</span>
|
|
2428
|
+
</slot>
|
|
2429
|
+
<span v-if="column.filterable || column.sortable" class="rs-table__header-actions">
|
|
2430
|
+
<RsTableHeaderFilter
|
|
2431
|
+
v-if="column.filterable"
|
|
2432
|
+
:model-value="columnFilterValue(column.key)"
|
|
2433
|
+
:column-title="column.title"
|
|
2434
|
+
:filter-label="t('table.filterColumn')"
|
|
2435
|
+
:placeholder="t('table.filterPlaceholder')"
|
|
2436
|
+
:clear-label="t('table.filterClear')"
|
|
2437
|
+
:apply-label="t('table.filterApply')"
|
|
2438
|
+
:active="isColumnFilterActive(columnFiltersState, column.key)"
|
|
2439
|
+
@update:model-value="updateColumnFilter(column.key, $event)"
|
|
2440
|
+
/>
|
|
2441
|
+
<button
|
|
2442
|
+
v-if="column.sortable"
|
|
2443
|
+
type="button"
|
|
2444
|
+
class="rs-table__sort"
|
|
2445
|
+
:class="{ 'rs-table__sort--active': !!sortOrderFor(column.key) }"
|
|
2446
|
+
:title="column.title"
|
|
2447
|
+
:aria-label="column.title"
|
|
2448
|
+
@click.stop="onHeaderClick(column)"
|
|
2449
|
+
>
|
|
2450
|
+
<RsIcon :name="sortIconName(column.key)" size="sm" />
|
|
2451
|
+
<span v-if="multiSort && sortPriorityFor(column.key) > 1" class="rs-table__sort-priority">
|
|
2452
|
+
{{ sortPriorityFor(column.key) }}
|
|
2453
|
+
</span>
|
|
2454
|
+
</button>
|
|
2455
|
+
</span>
|
|
2456
|
+
<span
|
|
2457
|
+
v-if="resizable"
|
|
2458
|
+
class="rs-table__resize-handle"
|
|
2459
|
+
@mousedown.stop="onResizeStart(column.key, $event)"
|
|
2460
|
+
@click.stop
|
|
2461
|
+
/>
|
|
2462
|
+
</th>
|
|
2463
|
+
<th
|
|
2464
|
+
v-if="columnPadRight > 0"
|
|
2465
|
+
class="rs-table__th rs-table__col-pad"
|
|
2466
|
+
:style="{ width: `${columnPadRight}px`, minWidth: `${columnPadRight}px` }"
|
|
2467
|
+
/>
|
|
2468
|
+
</tr>
|
|
2469
|
+
</thead>
|
|
2470
|
+
<tbody
|
|
2471
|
+
@pointerover.passive="onCellTipOver"
|
|
2472
|
+
@pointerout.passive="onCellTipOut"
|
|
2473
|
+
>
|
|
2474
|
+
<tr v-if="loading">
|
|
2475
|
+
<td class="rs-table__empty" :colspan="bodyColspan">{{ t('table.loading') }}</td>
|
|
2476
|
+
</tr>
|
|
2477
|
+
<tr v-else-if="!hasData">
|
|
2478
|
+
<td class="rs-table__empty" :colspan="bodyColspan">
|
|
2479
|
+
<slot name="empty">{{ t('table.empty') }}</slot>
|
|
2480
|
+
</td>
|
|
2481
|
+
</tr>
|
|
2482
|
+
<template v-else>
|
|
2483
|
+
<tr
|
|
2484
|
+
v-if="virtualScrollEnabled && virtualSlice.paddingTop > 0"
|
|
2485
|
+
class="rs-table__virtual-pad"
|
|
2486
|
+
>
|
|
2487
|
+
<td :colspan="bodyColspan" :style="{ height: `${virtualSlice.paddingTop}px` }" />
|
|
2488
|
+
</tr>
|
|
2489
|
+
<template v-for="entry in visibleEntries" :key="resolveEntryKey(entry, rowKey)">
|
|
2490
|
+
<tr v-if="entry.type === 'group'" class="rs-table__group-row">
|
|
2491
|
+
<td class="rs-table__group-cell" :colspan="bodyColspan">
|
|
2492
|
+
<slot name="group" :key="entry.key" :label="entry.label">
|
|
2493
|
+
{{ entry.label }}
|
|
2494
|
+
</slot>
|
|
2495
|
+
</td>
|
|
2496
|
+
</tr>
|
|
2497
|
+
<tr
|
|
2498
|
+
v-else-if="entry.type === 'expand'"
|
|
2499
|
+
class="rs-table__expand-row"
|
|
2500
|
+
>
|
|
2501
|
+
<td class="rs-table__expand-cell" :colspan="bodyColspan">
|
|
2502
|
+
<slot name="expand" :row="entry.row" :index="entry.rowIndex" />
|
|
2503
|
+
</td>
|
|
2504
|
+
</tr>
|
|
2505
|
+
<RsTableBodyRow
|
|
2506
|
+
v-else
|
|
2507
|
+
v-memo="[
|
|
2508
|
+
rowKeyFor(entry),
|
|
2509
|
+
/* 行对象引用:父级不可变更新后必须失效,否则布尔勾选等提交不刷新 */
|
|
2510
|
+
entry.row,
|
|
2511
|
+
isRowSelected(entry),
|
|
2512
|
+
isRowExpanded(entry),
|
|
2513
|
+
rowHighlight.isHighlighted(rowKeyFor(entry)),
|
|
2514
|
+
rowEditPending(entry),
|
|
2515
|
+
tableEdit.stagedMap.value.size,
|
|
2516
|
+
tableEdit.editingCell.value?.rowKey === rowKeyFor(entry)
|
|
2517
|
+
? tableEdit.editingDraft.value
|
|
2518
|
+
: '',
|
|
2519
|
+
activeEditCellKey,
|
|
2520
|
+
activeFocusCellKey,
|
|
2521
|
+
activeErrorMapSize,
|
|
2522
|
+
activeValidatingMapSize,
|
|
2523
|
+
dragRowKeys.join(','),
|
|
2524
|
+
dropRowTargetKey,
|
|
2525
|
+
dropRowPosition,
|
|
2526
|
+
columnPadLeft,
|
|
2527
|
+
columnPadRight,
|
|
2528
|
+
visibleDataColumns.map((c) => c.key).join('\0'),
|
|
2529
|
+
]"
|
|
2530
|
+
:row="entry.row"
|
|
2531
|
+
:row-index="entry.rowIndex"
|
|
2532
|
+
:row-key="rowKeyFor(entry)"
|
|
2533
|
+
:columns="visibleDataColumns"
|
|
2534
|
+
:column-pad-left="columnPadLeft"
|
|
2535
|
+
:column-pad-right="columnPadRight"
|
|
2536
|
+
:column-td-class-map="columnTdClassMap"
|
|
2537
|
+
:column-style-map="columnStyleMap"
|
|
2538
|
+
:show-row-drag-handle="showRowDragHandle"
|
|
2539
|
+
:expandable="expandable"
|
|
2540
|
+
:selectable="showSelectColumn"
|
|
2541
|
+
:show-index="showIndexColumn"
|
|
2542
|
+
:show-edit-gutter="showEditGutterColumn"
|
|
2543
|
+
:striped="striped"
|
|
2544
|
+
:selection-type="selectionType"
|
|
2545
|
+
:is-radio-selection="isRadioSelection"
|
|
2546
|
+
:table-editable="editable"
|
|
2547
|
+
:edit-trigger="editTrigger"
|
|
2548
|
+
:row-commit="rowCommit"
|
|
2549
|
+
:allow-null="allowNull"
|
|
2550
|
+
:focus-mode="editFocusMode"
|
|
2551
|
+
:null-label="nullLabel"
|
|
2552
|
+
:show-row-status="showRowStatusColumn"
|
|
2553
|
+
:selected="isRowSelected(entry)"
|
|
2554
|
+
:highlighted="rowHighlight.isHighlighted(rowKeyFor(entry))"
|
|
2555
|
+
:expanded="isRowExpanded(entry)"
|
|
2556
|
+
:disabled="isTableRowDisabled(entry.row)"
|
|
2557
|
+
:row-drag-by-row="isRowDragByRow(entry)"
|
|
2558
|
+
:dragging="isRowDragging(entry)"
|
|
2559
|
+
:drop-target="isRowDropTarget(entry)"
|
|
2560
|
+
:drop-position="isRowDropTarget(entry) ? dropRowPosition : null"
|
|
2561
|
+
:drag-lead-style="dragLeadStyle"
|
|
2562
|
+
:expand-lead-style="expandLeadStyle"
|
|
2563
|
+
:select-lead-style="selectLeadStyle"
|
|
2564
|
+
:index-lead-style="indexLeadStyle"
|
|
2565
|
+
:gutter-lead-style="gutterLeadStyle"
|
|
2566
|
+
:can-expand="canExpandRow(entry)"
|
|
2567
|
+
:can-select="canSelectRow(entry)"
|
|
2568
|
+
:row-edit-pending="rowEditPending(entry)"
|
|
2569
|
+
:row-dirty="tableEdit.isRowDirty(rowKeyFor(entry))"
|
|
2570
|
+
:show-gutter-rollback="rowCommit || isExternalRowPending(entry)"
|
|
2571
|
+
:editing-col-key="tableEdit.editingCell.value?.rowKey === rowKeyFor(entry) ? tableEdit.editingCell.value.colKey : null"
|
|
2572
|
+
:focus-col-key="tableEdit.focusCell.value?.rowKey === rowKeyFor(entry) ? tableEdit.focusCell.value.colKey : null"
|
|
2573
|
+
:has-column-slot="hasColumnSlot"
|
|
2574
|
+
:has-edit-slot="hasEditSlot"
|
|
2575
|
+
:cell-tooltip-enabled="cellTooltipEnabled"
|
|
2576
|
+
:cell-tooltip-mode="cellTooltipMode"
|
|
2577
|
+
:cell-tooltip-text="cellTooltipText"
|
|
2578
|
+
:cell-tooltip-fallback-title="cellTooltipFallbackTitle"
|
|
2579
|
+
:get-cell-draft="tableEdit.getDraft"
|
|
2580
|
+
:is-cell-dirty="tableEdit.isDirty"
|
|
2581
|
+
:get-cell-error="tableEdit.getCellError"
|
|
2582
|
+
:is-cell-validating="tableEdit.isValidating"
|
|
2583
|
+
:drag-row-label="t('table.dragRow')"
|
|
2584
|
+
:expand-row-label="t('table.expandRow')"
|
|
2585
|
+
:collapse-row-label="t('table.collapseRow')"
|
|
2586
|
+
:select-row-label="t('table.selectRow')"
|
|
2587
|
+
:row-commit-label="t('table.rowCommit')"
|
|
2588
|
+
:row-rollback-label="t('table.rowRollback')"
|
|
2589
|
+
:gutter-commit-hint="t('table.gutterCommit')"
|
|
2590
|
+
@click="onRowClick(entry, $event)"
|
|
2591
|
+
@mousedown="onRowSelectMouseDown"
|
|
2592
|
+
@cell-click="(colKey, event) => onCellClick(entry, colKey, event)"
|
|
2593
|
+
@cell-dblclick="(colKey, event) => onCellDblclick(entry, colKey, event)"
|
|
2594
|
+
@cell-contextmenu="(colKey, event) => onCellContextmenu(entry, colKey, event)"
|
|
2595
|
+
@dblclick="onRowDblclick(entry, $event)"
|
|
2596
|
+
@contextmenu="onRowContextmenu(entry, $event)"
|
|
2597
|
+
@dragover="onRowDragOver(entry, $event)"
|
|
2598
|
+
@dragleave="onRowDragLeave"
|
|
2599
|
+
@drop="onRowDrop(entry, $event)"
|
|
2600
|
+
@row-drag-start="onRowDragStart(entry, $event)"
|
|
2601
|
+
@row-drag-end="onRowDragEnd"
|
|
2602
|
+
@toggle-expand="onToggleExpand(entry)"
|
|
2603
|
+
@toggle-select="onToggleRow(entry)"
|
|
2604
|
+
@cell-start-edit="onCellStartEdit(entry, $event)"
|
|
2605
|
+
@cell-commit="(colKey, value) => onCellCommitEdit(entry, colKey, value)"
|
|
2606
|
+
@cell-cancel="onCellCancelEdit(entry, $event)"
|
|
2607
|
+
@cell-update-draft="(colKey, value) => onCellUpdateDraft(entry, colKey, value)"
|
|
2608
|
+
@cell-navigate="(colKey, direction) => onCellNavigate(entry, colKey, direction)"
|
|
2609
|
+
@row-commit="onRowCommitEdit(entry)"
|
|
2610
|
+
@row-rollback="onRowRollbackEdit(entry)"
|
|
2611
|
+
@gutter-commit="onGutterCommit(entry)"
|
|
2612
|
+
>
|
|
2613
|
+
<template v-for="column in visibleDataColumns" #[column.key]="slotProps">
|
|
2614
|
+
<slot :name="column.key" v-bind="slotProps" />
|
|
2615
|
+
</template>
|
|
2616
|
+
<template
|
|
2617
|
+
v-for="column in visibleDataColumns"
|
|
2618
|
+
:key="`edit-${column.key}`"
|
|
2619
|
+
#[`edit-${column.key}`]="slotProps"
|
|
2620
|
+
>
|
|
2621
|
+
<slot :name="`edit-${column.key}`" v-bind="slotProps" />
|
|
2622
|
+
</template>
|
|
2623
|
+
</RsTableBodyRow>
|
|
2624
|
+
</template>
|
|
2625
|
+
<tr
|
|
2626
|
+
v-if="virtualScrollEnabled && virtualSlice.paddingBottom > 0"
|
|
2627
|
+
class="rs-table__virtual-pad"
|
|
2628
|
+
>
|
|
2629
|
+
<td :colspan="bodyColspan" :style="{ height: `${virtualSlice.paddingBottom}px` }" />
|
|
2630
|
+
</tr>
|
|
2631
|
+
<tr v-if="infinite && loadingMore">
|
|
2632
|
+
<td class="rs-table__empty rs-table__empty--more" :colspan="bodyColspan">
|
|
2633
|
+
{{ t('table.loadingMore') }}
|
|
2634
|
+
</td>
|
|
2635
|
+
</tr>
|
|
2636
|
+
</template>
|
|
2637
|
+
</tbody>
|
|
2638
|
+
<tfoot v-if="$slots.summary" class="rs-table__foot">
|
|
2639
|
+
<tr>
|
|
2640
|
+
<td class="rs-table__summary" :colspan="bodyColspan">
|
|
2641
|
+
<slot name="summary" />
|
|
2642
|
+
</td>
|
|
2643
|
+
</tr>
|
|
2644
|
+
</tfoot>
|
|
2645
|
+
</table>
|
|
2646
|
+
</div>
|
|
2647
|
+
<Teleport to="body">
|
|
2648
|
+
<div
|
|
2649
|
+
v-if="cellTipState.visible"
|
|
2650
|
+
ref="sharedTipRef"
|
|
2651
|
+
class="rs-table__shared-tip"
|
|
2652
|
+
:class="{
|
|
2653
|
+
'rs-table__shared-tip--ready': cellTipState.ready,
|
|
2654
|
+
'rs-table__shared-tip--header': cellTipState.kind === 'header',
|
|
2655
|
+
}"
|
|
2656
|
+
:style="cellTipState.style"
|
|
2657
|
+
role="tooltip"
|
|
2658
|
+
>
|
|
2659
|
+
{{ cellTipState.text }}
|
|
2660
|
+
</div>
|
|
2661
|
+
</Teleport>
|
|
2662
|
+
</div>
|
|
2663
|
+
</RsContextMenu>
|
|
2664
|
+
</template>
|