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,950 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type RsTableColumnAlign = 'left' | 'center' | 'right'
|
|
4
|
+
export type RsTableColumnFixed = 'left' | 'right'
|
|
5
|
+
export type RsTableSortOrder = 'asc' | 'desc' | null
|
|
6
|
+
/**
|
|
7
|
+
* 行选择模式:
|
|
8
|
+
* - checkbox / radio:左侧选择列
|
|
9
|
+
* - row:无选择列,点击行多选(Ctrl/Cmd 切换、Shift 范围)
|
|
10
|
+
*/
|
|
11
|
+
export type RsTableSelectionType = 'checkbox' | 'radio' | 'row'
|
|
12
|
+
export type RsTableSize = 'sm' | 'md' | 'lg'
|
|
13
|
+
|
|
14
|
+
/** 行数据最小约束:任意非原始类型对象(interface / type 均可) */
|
|
15
|
+
export type RsTableRowData = object
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 可选约定字段(非强制)。
|
|
19
|
+
* - 未传 `rowKey` 时,`resolveRowKey` 依次回退 `id` → `key` → 行索引
|
|
20
|
+
* - `disabled === true` 时行不可选
|
|
21
|
+
*/
|
|
22
|
+
export interface RsTableRowConvention {
|
|
23
|
+
id?: string | number
|
|
24
|
+
key?: string | number
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 行字段访问器:列 key( keyof 字符串键)、任意路径字符串、或自定义函数。
|
|
30
|
+
* 用于 `rowKey`、`groupBy` 等配置。
|
|
31
|
+
*/
|
|
32
|
+
export type RsTableFieldAccessor<T extends RsTableRowData = RsTableRowData> =
|
|
33
|
+
| Extract<keyof T, string>
|
|
34
|
+
| string
|
|
35
|
+
| ((row: T) => string)
|
|
36
|
+
|
|
37
|
+
/** 行唯一键配置 */
|
|
38
|
+
export type RsTableRowKey<T extends RsTableRowData = RsTableRowData> = RsTableFieldAccessor<T>
|
|
39
|
+
|
|
40
|
+
/** 分组字段配置 */
|
|
41
|
+
export type RsTableGroupBy<T extends RsTableRowData = RsTableRowData> = RsTableFieldAccessor<T>
|
|
42
|
+
|
|
43
|
+
/** 单元格 render 返回值(文本、数字或 Vue VNode) */
|
|
44
|
+
export type RsTableCellRenderResult = VNodeChild
|
|
45
|
+
|
|
46
|
+
/** 单元格编辑/展示值类型 */
|
|
47
|
+
export type RsTableCellValueType =
|
|
48
|
+
| 'text'
|
|
49
|
+
| 'number'
|
|
50
|
+
| 'date'
|
|
51
|
+
| 'datetime'
|
|
52
|
+
| 'boolean'
|
|
53
|
+
| 'select'
|
|
54
|
+
| 'textarea'
|
|
55
|
+
|
|
56
|
+
/** 列级编辑器选项(仅编辑挂载时读取,不影响展示路径)。 */
|
|
57
|
+
export interface RsTableColumnEditorOptions {
|
|
58
|
+
/**
|
|
59
|
+
* select:选项列表;也可按行返回(如外键引用表随 schema 变化)。
|
|
60
|
+
* 函数形式在进入编辑态时按当前行解析。
|
|
61
|
+
*/
|
|
62
|
+
options?:
|
|
63
|
+
| import('./select-utils').RsSelectOptions
|
|
64
|
+
| ((row: RsTableRowData, index: number) => import('./select-utils').RsSelectOptions)
|
|
65
|
+
/** select:可搜索 */
|
|
66
|
+
searchable?: boolean
|
|
67
|
+
/**
|
|
68
|
+
* select:允许搜索框手输自定义值(Enter / 「使用 xxx」)。
|
|
69
|
+
* 适合类型名、厂商扩展类型等预设列表外的输入。
|
|
70
|
+
*/
|
|
71
|
+
creatable?: boolean
|
|
72
|
+
/**
|
|
73
|
+
* select:多选。草稿仍为字符串,选项间以 `, ` 拼接;
|
|
74
|
+
* 多选时在下拉关闭时提交(避免每点一项就结束编辑)。
|
|
75
|
+
*/
|
|
76
|
+
multiple?: boolean
|
|
77
|
+
/** select / date:可清空;表格单元格 select 默认 false(仅选择,无清除 X) */
|
|
78
|
+
clearable?: boolean
|
|
79
|
+
/** number:最小值(对齐 InputNumber min) */
|
|
80
|
+
min?: number
|
|
81
|
+
/** number:最大值 */
|
|
82
|
+
max?: number
|
|
83
|
+
/** number:步进 */
|
|
84
|
+
step?: number | string
|
|
85
|
+
/** number:小数位 */
|
|
86
|
+
precision?: number
|
|
87
|
+
/** number:是否显示步进按钮;inline 单元格默认 false */
|
|
88
|
+
controls?: boolean
|
|
89
|
+
/** datetime:是否含秒 */
|
|
90
|
+
withSeconds?: boolean
|
|
91
|
+
/** datetime:时区策略(utc 提交时补 Z) */
|
|
92
|
+
timezone?: 'local' | 'utc'
|
|
93
|
+
/** textarea:可见行数 */
|
|
94
|
+
rows?: number
|
|
95
|
+
/**
|
|
96
|
+
* 编辑呈现:inline 为单元格内编辑;dialog 由外层弹窗承接(大字段)。
|
|
97
|
+
* 为 dialog 时表格不进入行内编辑态,改为发出 cellEditDialog。
|
|
98
|
+
*/
|
|
99
|
+
presentation?: 'inline' | 'dialog'
|
|
100
|
+
/** 进入编辑时光标策略覆盖 */
|
|
101
|
+
focusMode?: 'end' | 'select' | 'start'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 已按行解析后的编辑器选项(options 仅为列表,供单元格编辑器使用)。 */
|
|
105
|
+
export type RsTableColumnEditorOptionsResolved = Omit<RsTableColumnEditorOptions, 'options'> & {
|
|
106
|
+
options?: import('./select-utils').RsSelectOptions
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readConvention(row: object): Partial<RsTableRowConvention> {
|
|
110
|
+
const record = row as Record<string, unknown>
|
|
111
|
+
const result: Partial<RsTableRowConvention> = {}
|
|
112
|
+
const { id, key, disabled } = record
|
|
113
|
+
if (typeof id === 'string' || typeof id === 'number') result.id = id
|
|
114
|
+
if (typeof key === 'string' || typeof key === 'number') result.key = key
|
|
115
|
+
if (typeof disabled === 'boolean') result.disabled = disabled
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatComparableValue(value: unknown): string {
|
|
120
|
+
if (value === null || value === undefined) return ''
|
|
121
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
122
|
+
return String(value)
|
|
123
|
+
}
|
|
124
|
+
if (value instanceof Date) return value.toISOString()
|
|
125
|
+
return JSON.stringify(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readRowKeyFallback(row: object): string | undefined {
|
|
129
|
+
const { id, key } = readConvention(row)
|
|
130
|
+
if (typeof id === 'string' || typeof id === 'number') return String(id)
|
|
131
|
+
if (typeof key === 'string' || typeof key === 'number') return String(key)
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface RsTableSortState {
|
|
136
|
+
key: string
|
|
137
|
+
order: Exclude<RsTableSortOrder, null>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface RsTableColumn<T extends RsTableRowData = Record<string, unknown>> {
|
|
141
|
+
key: string
|
|
142
|
+
title: string
|
|
143
|
+
dataIndex?: Extract<keyof T, string> | string
|
|
144
|
+
width?: number | string
|
|
145
|
+
minWidth?: number | string
|
|
146
|
+
align?: RsTableColumnAlign
|
|
147
|
+
sortable?: boolean
|
|
148
|
+
sorter?: (left: T, right: T) => number
|
|
149
|
+
/** 表头显示筛选图标,配合表格 `columnFilters` 使用 */
|
|
150
|
+
filterable?: boolean
|
|
151
|
+
/** 自定义列筛选;默认对单元格值做包含匹配 */
|
|
152
|
+
filter?: (value: unknown, row: T, query: string) => boolean
|
|
153
|
+
fixed?: RsTableColumnFixed
|
|
154
|
+
ellipsis?: boolean
|
|
155
|
+
/**
|
|
156
|
+
* 悬停提示文案。启用表格级共享 Tooltip(整表单浮层),适合大表场景。
|
|
157
|
+
* 与 `ellipsis` 独立:未省略时也会显示。
|
|
158
|
+
*/
|
|
159
|
+
tooltip?: (row: T, index: number) => string | null | undefined
|
|
160
|
+
/** 表头原生 title(宽表勿逐列挂 RsTooltip,避免成百实例) */
|
|
161
|
+
headerTip?: string
|
|
162
|
+
/** 是否允许行内编辑;也可在表格级设置 editable */
|
|
163
|
+
editable?: boolean | ((row: T, index: number) => boolean)
|
|
164
|
+
/** 单元格值类型,控制行内编辑控件(默认 text) */
|
|
165
|
+
valueType?: RsTableCellValueType
|
|
166
|
+
/** 编辑器附加选项(select options 等);仅进入编辑态时使用 */
|
|
167
|
+
editorOptions?: RsTableColumnEditorOptions
|
|
168
|
+
/** 展示格式化 */
|
|
169
|
+
formatter?: (value: unknown, row: T, index: number) => string
|
|
170
|
+
/** 编辑输入解析 */
|
|
171
|
+
parser?: (input: string, row: T, index: number) => unknown
|
|
172
|
+
/** 编辑校验,返回错误文案;支持异步 */
|
|
173
|
+
validator?: (
|
|
174
|
+
value: unknown,
|
|
175
|
+
row: T,
|
|
176
|
+
index: number,
|
|
177
|
+
) => string | null | Promise<string | null>
|
|
178
|
+
/** 是否允许 NULL(覆盖表格 allowNull) */
|
|
179
|
+
nullable?: boolean
|
|
180
|
+
/** 空字符串提交为 null(需 nullable) */
|
|
181
|
+
emptyAsNull?: boolean
|
|
182
|
+
/** 提交时机 */
|
|
183
|
+
commitOn?: 'blur' | 'enter' | 'change' | 'manual'
|
|
184
|
+
/** 进入编辑的触发方式 */
|
|
185
|
+
editTrigger?: 'click' | 'dblclick'
|
|
186
|
+
/**
|
|
187
|
+
* 自定义单元格展示内容(string / number / VNode 等 `VNodeChild`)。
|
|
188
|
+
* 仅影响展示层;排序、筛选、编辑、复制仍读取 `dataIndex` / `key` 字段。
|
|
189
|
+
* 纯文本格式化请优先用 `formatter`。
|
|
190
|
+
*/
|
|
191
|
+
render?: (row: T, index: number) => RsTableCellRenderResult
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type RsTableSelectAllState = 'checked' | 'indeterminate' | 'unchecked'
|
|
195
|
+
|
|
196
|
+
export type RsTableRowEntry<T extends RsTableRowData = Record<string, unknown>> =
|
|
197
|
+
| { type: 'row'; row: T; rowIndex: number }
|
|
198
|
+
| { type: 'group'; key: string; label: string }
|
|
199
|
+
| { type: 'expand'; row: T; rowIndex: number; rowKey: string }
|
|
200
|
+
|
|
201
|
+
export const TABLE_ROW_HEIGHT = {
|
|
202
|
+
sm: 33,
|
|
203
|
+
md: 41,
|
|
204
|
+
lg: 48,
|
|
205
|
+
group: 36,
|
|
206
|
+
expand: 80,
|
|
207
|
+
} as const
|
|
208
|
+
|
|
209
|
+
export interface RsTableFixedCellStyle {
|
|
210
|
+
fixed: RsTableColumnFixed
|
|
211
|
+
left?: number
|
|
212
|
+
right?: number
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getCellValue<T extends RsTableRowData>(row: T, column: RsTableColumn<T>): unknown {
|
|
216
|
+
return row[(column.dataIndex ?? column.key) as keyof T]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type RsTableCellTooltipMode = 'overflow' | 'always'
|
|
220
|
+
|
|
221
|
+
export function columnUsesSharedTooltip<T extends RsTableRowData>(column: RsTableColumn<T>): boolean {
|
|
222
|
+
return Boolean(column.ellipsis || column.tooltip)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function resolveCellTooltipMode<T extends RsTableRowData>(
|
|
226
|
+
column: RsTableColumn<T>,
|
|
227
|
+
): RsTableCellTooltipMode | null {
|
|
228
|
+
if (column.tooltip) return 'always'
|
|
229
|
+
if (column.ellipsis) return 'overflow'
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function resolveCellTooltipText<T extends RsTableRowData>(
|
|
234
|
+
column: RsTableColumn<T>,
|
|
235
|
+
row: T,
|
|
236
|
+
index: number,
|
|
237
|
+
): string {
|
|
238
|
+
if (column.tooltip) {
|
|
239
|
+
const tip = column.tooltip(row, index)
|
|
240
|
+
if (tip != null && tip !== '') return String(tip)
|
|
241
|
+
return ''
|
|
242
|
+
}
|
|
243
|
+
const value = getCellValue(row, column)
|
|
244
|
+
if (column.formatter) {
|
|
245
|
+
return column.formatter(value, row, index)
|
|
246
|
+
}
|
|
247
|
+
if (value == null) return ''
|
|
248
|
+
if (typeof value === 'object') {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.stringify(value)
|
|
251
|
+
} catch {
|
|
252
|
+
return Object.prototype.toString.call(value)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return String(value)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function compareTableValues(a: unknown, b: unknown): number {
|
|
259
|
+
if (a === b) return 0
|
|
260
|
+
if (a === undefined || a === null) return -1
|
|
261
|
+
if (b === undefined || b === null) return 1
|
|
262
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b
|
|
263
|
+
return formatComparableValue(a).localeCompare(formatComparableValue(b))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function toggleSortState(current: RsTableSortState | null, key: string): RsTableSortState | null {
|
|
267
|
+
if (current?.key !== key) return { key, order: 'asc' }
|
|
268
|
+
if (current.order === 'asc') return { key, order: 'desc' }
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function toggleMultiSortState(
|
|
273
|
+
sorts: readonly RsTableSortState[],
|
|
274
|
+
key: string,
|
|
275
|
+
maxSort = 3,
|
|
276
|
+
): RsTableSortState[] {
|
|
277
|
+
const existingIndex = sorts.findIndex((item) => item.key === key)
|
|
278
|
+
if (existingIndex < 0) {
|
|
279
|
+
const next = [...sorts, { key, order: 'asc' as const }]
|
|
280
|
+
return next.length > maxSort ? next.slice(next.length - maxSort) : next
|
|
281
|
+
}
|
|
282
|
+
const existing = sorts[existingIndex]
|
|
283
|
+
if (!existing) return [...sorts]
|
|
284
|
+
if (existing.order === 'asc') {
|
|
285
|
+
return sorts.map((item, index) => (index === existingIndex ? { key, order: 'desc' as const } : item))
|
|
286
|
+
}
|
|
287
|
+
return sorts.filter((_, index) => index !== existingIndex)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function getSortOrderForKey(sorts: readonly RsTableSortState[], key: string): RsTableSortOrder {
|
|
291
|
+
return sorts.find((item) => item.key === key)?.order ?? null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getSortPriorityForKey(sorts: readonly RsTableSortState[], key: string): number {
|
|
295
|
+
const index = sorts.findIndex((item) => item.key === key)
|
|
296
|
+
return index >= 0 ? index + 1 : 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function compareTableRowsBySort<T extends RsTableRowData>(
|
|
300
|
+
left: T,
|
|
301
|
+
right: T,
|
|
302
|
+
column: RsTableColumn<T>,
|
|
303
|
+
order: Exclude<RsTableSortOrder, null>,
|
|
304
|
+
): number {
|
|
305
|
+
const direction = order === 'asc' ? 1 : -1
|
|
306
|
+
if (column.sorter) return column.sorter(left, right) * direction
|
|
307
|
+
return compareTableValues(getCellValue(left, column), getCellValue(right, column)) * direction
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function sortTableRows<T extends RsTableRowData>(
|
|
311
|
+
rows: readonly T[],
|
|
312
|
+
columns: readonly RsTableColumn<T>[],
|
|
313
|
+
sort: RsTableSortState | null,
|
|
314
|
+
): T[] {
|
|
315
|
+
if (!sort) return rows as T[]
|
|
316
|
+
const column = columns.find((item) => item.key === sort.key)
|
|
317
|
+
if (!column) return rows as T[]
|
|
318
|
+
return [...rows].sort((left, right) => compareTableRowsBySort(left, right, column, sort.order))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function sortTableRowsMulti<T extends RsTableRowData>(
|
|
322
|
+
rows: readonly T[],
|
|
323
|
+
columns: readonly RsTableColumn<T>[],
|
|
324
|
+
sorts: readonly RsTableSortState[],
|
|
325
|
+
): T[] {
|
|
326
|
+
if (sorts.length === 0) return rows as T[]
|
|
327
|
+
return [...rows].sort((left, right) => {
|
|
328
|
+
for (const sort of sorts) {
|
|
329
|
+
const column = columns.find((item) => item.key === sort.key)
|
|
330
|
+
if (!column) continue
|
|
331
|
+
const compare = compareTableRowsBySort(left, right, column, sort.order)
|
|
332
|
+
if (compare !== 0) return compare
|
|
333
|
+
}
|
|
334
|
+
return 0
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function filterTableRows<T extends RsTableRowData>(
|
|
339
|
+
rows: readonly T[],
|
|
340
|
+
query: string,
|
|
341
|
+
columns: readonly RsTableColumn<T>[],
|
|
342
|
+
keys?: string[],
|
|
343
|
+
): T[] {
|
|
344
|
+
const trimmed = query.trim()
|
|
345
|
+
if (!trimmed) return rows as T[]
|
|
346
|
+
const searchKeys = keys ?? columns.map((column) => column.key)
|
|
347
|
+
const lower = trimmed.toLowerCase()
|
|
348
|
+
return rows.filter((row) =>
|
|
349
|
+
searchKeys.some((key) => {
|
|
350
|
+
const column = columns.find((item) => item.key === key)
|
|
351
|
+
const value = column ? getCellValue(row, column) : row[key as keyof T]
|
|
352
|
+
return formatComparableValue(value).toLowerCase().includes(lower)
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function filterTableRowsByColumnFilters<T extends RsTableRowData>(
|
|
358
|
+
rows: readonly T[],
|
|
359
|
+
columns: readonly RsTableColumn<T>[],
|
|
360
|
+
filters: Record<string, string>,
|
|
361
|
+
): T[] {
|
|
362
|
+
const active = Object.entries(filters)
|
|
363
|
+
.map(([key, query]) => [key, query.trim()] as const)
|
|
364
|
+
.filter(([, query]) => query.length > 0)
|
|
365
|
+
if (!active.length) return rows as T[]
|
|
366
|
+
|
|
367
|
+
const columnMap = new Map(columns.map((column) => [column.key, column]))
|
|
368
|
+
return rows.filter((row) =>
|
|
369
|
+
active.every(([key, query]) => {
|
|
370
|
+
const column = columnMap.get(key)
|
|
371
|
+
if (!column) return true
|
|
372
|
+
const value = getCellValue(row, column)
|
|
373
|
+
if (column.filter) return column.filter(value, row, query)
|
|
374
|
+
const lower = query.toLowerCase()
|
|
375
|
+
return formatComparableValue(value).toLowerCase().includes(lower)
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function isColumnFilterActive(filters: Record<string, string>, key: string): boolean {
|
|
381
|
+
return Boolean(filters[key]?.trim())
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function resolveGroupKey<T extends RsTableRowData>(
|
|
385
|
+
row: T,
|
|
386
|
+
groupBy: RsTableGroupBy<T>,
|
|
387
|
+
): string {
|
|
388
|
+
if (typeof groupBy === 'function') return groupBy(row)
|
|
389
|
+
return String(row[groupBy as keyof T] ?? '')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function groupTableRows<T extends RsTableRowData>(
|
|
393
|
+
rows: readonly T[],
|
|
394
|
+
groupBy: RsTableGroupBy<T>,
|
|
395
|
+
labelFormatter?: (key: string) => string,
|
|
396
|
+
): RsTableRowEntry<T>[] {
|
|
397
|
+
const result: RsTableRowEntry<T>[] = []
|
|
398
|
+
let lastKey: string | null = null
|
|
399
|
+
rows.forEach((row, rowIndex) => {
|
|
400
|
+
const key = resolveGroupKey(row, groupBy)
|
|
401
|
+
if (key !== lastKey) {
|
|
402
|
+
result.push({ type: 'group', key, label: labelFormatter?.(key) ?? key })
|
|
403
|
+
lastKey = key
|
|
404
|
+
}
|
|
405
|
+
result.push({ type: 'row', row, rowIndex })
|
|
406
|
+
})
|
|
407
|
+
return result
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function resolveColumnOrder<T extends RsTableRowData>(
|
|
411
|
+
columns: readonly RsTableColumn<T>[],
|
|
412
|
+
order?: readonly string[],
|
|
413
|
+
): string[] {
|
|
414
|
+
const keys = columns.map((column) => column.key)
|
|
415
|
+
if (!order?.length) return keys
|
|
416
|
+
const valid = order.filter((key) => keys.includes(key))
|
|
417
|
+
const missing = keys.filter((key) => !valid.includes(key))
|
|
418
|
+
return [...valid, ...missing]
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function resolveOrderedColumns<T extends RsTableRowData>(
|
|
422
|
+
columns: readonly RsTableColumn<T>[],
|
|
423
|
+
order?: readonly string[],
|
|
424
|
+
): RsTableColumn<T>[] {
|
|
425
|
+
const map = new Map(columns.map((column) => [column.key, column]))
|
|
426
|
+
return resolveColumnOrder(columns, order)
|
|
427
|
+
.map((key) => map.get(key))
|
|
428
|
+
.filter((column): column is RsTableColumn<T> => column !== undefined)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function reorderColumnKeys(order: readonly string[], dragKey: string, dropKey: string): string[] {
|
|
432
|
+
if (dragKey === dropKey) return [...order]
|
|
433
|
+
const next = order.filter((key) => key !== dragKey)
|
|
434
|
+
const dropIndex = next.indexOf(dropKey)
|
|
435
|
+
if (dropIndex < 0) return [...order]
|
|
436
|
+
next.splice(dropIndex, 0, dragKey)
|
|
437
|
+
return next
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export type RsTableRowDropPosition = 'before' | 'after' | 'into'
|
|
441
|
+
|
|
442
|
+
export function reorderTableRows<T>(
|
|
443
|
+
rows: readonly T[],
|
|
444
|
+
dragIndex: number,
|
|
445
|
+
dropIndex: number,
|
|
446
|
+
position: RsTableRowDropPosition,
|
|
447
|
+
): T[] {
|
|
448
|
+
if (dragIndex < 0 || dropIndex < 0 || dragIndex === dropIndex) return [...rows]
|
|
449
|
+
const next = [...rows]
|
|
450
|
+
const [moved] = next.splice(dragIndex, 1)
|
|
451
|
+
if (moved === undefined) return [...rows]
|
|
452
|
+
let targetIndex = dropIndex
|
|
453
|
+
if (dragIndex < dropIndex) targetIndex -= 1
|
|
454
|
+
if (position === 'after') targetIndex += 1
|
|
455
|
+
next.splice(targetIndex, 0, moved)
|
|
456
|
+
return next
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function buildTableEntries<T extends RsTableRowData>(
|
|
460
|
+
rows: readonly T[],
|
|
461
|
+
columns: readonly RsTableColumn<T>[],
|
|
462
|
+
options: {
|
|
463
|
+
sort?: RsTableSortState | null
|
|
464
|
+
sorts?: readonly RsTableSortState[]
|
|
465
|
+
multiSort?: boolean
|
|
466
|
+
filterText?: string
|
|
467
|
+
filterKeys?: string[]
|
|
468
|
+
columnFilters?: Record<string, string>
|
|
469
|
+
groupBy?: RsTableGroupBy<T>
|
|
470
|
+
groupLabel?: (key: string) => string
|
|
471
|
+
remoteSort?: boolean
|
|
472
|
+
} = {},
|
|
473
|
+
): RsTableRowEntry<T>[] {
|
|
474
|
+
let processed: readonly T[] = rows
|
|
475
|
+
const filterText = options.filterText ?? ''
|
|
476
|
+
if (filterText.trim()) {
|
|
477
|
+
processed = filterTableRows(processed, filterText, columns, options.filterKeys)
|
|
478
|
+
}
|
|
479
|
+
const columnFilters = options.columnFilters ?? {}
|
|
480
|
+
if (Object.values(columnFilters).some((q) => q.trim())) {
|
|
481
|
+
processed = filterTableRowsByColumnFilters(processed, columns, columnFilters)
|
|
482
|
+
}
|
|
483
|
+
if (!options.remoteSort) {
|
|
484
|
+
if (options.multiSort && options.sorts?.length) {
|
|
485
|
+
processed = sortTableRowsMulti(processed, columns, options.sorts)
|
|
486
|
+
} else if (options.sort) {
|
|
487
|
+
processed = sortTableRows(processed, columns, options.sort)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (options.groupBy) {
|
|
491
|
+
const groupBy = options.groupBy
|
|
492
|
+
processed = [...processed].sort((left, right) =>
|
|
493
|
+
compareTableValues(resolveGroupKey(left, groupBy), resolveGroupKey(right, groupBy)),
|
|
494
|
+
)
|
|
495
|
+
return groupTableRows(processed, groupBy, options.groupLabel)
|
|
496
|
+
}
|
|
497
|
+
return processed.map((row, rowIndex) => ({ type: 'row' as const, row, rowIndex }))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function injectExpandRows<T extends RsTableRowData>(
|
|
501
|
+
entries: readonly RsTableRowEntry<T>[],
|
|
502
|
+
expandedKeys: ReadonlySet<string>,
|
|
503
|
+
rowKey?: RsTableRowKey<T>,
|
|
504
|
+
rowExpandable?: (row: T, index: number) => boolean,
|
|
505
|
+
): RsTableRowEntry<T>[] {
|
|
506
|
+
const result: RsTableRowEntry<T>[] = []
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
result.push(entry)
|
|
509
|
+
if (entry.type !== 'row') continue
|
|
510
|
+
if (rowExpandable && !rowExpandable(entry.row, entry.rowIndex)) continue
|
|
511
|
+
const key = resolveRowKey(entry.row, entry.rowIndex, rowKey)
|
|
512
|
+
if (expandedKeys.has(key)) {
|
|
513
|
+
result.push({ type: 'expand', row: entry.row, rowIndex: entry.rowIndex, rowKey: key })
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return result
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function toggleExpandedRowKeys(keys: readonly string[], key: string): string[] {
|
|
520
|
+
const next = new Set(keys)
|
|
521
|
+
if (next.has(key)) next.delete(key)
|
|
522
|
+
else next.add(key)
|
|
523
|
+
return [...next]
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function resolveTableSize(compact: boolean, size: RsTableSize = 'md'): RsTableSize {
|
|
527
|
+
if (compact) return 'sm'
|
|
528
|
+
return size
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function resolveTableRowHeight(size: RsTableSize = 'md', custom?: number): number {
|
|
532
|
+
if (custom !== undefined) return custom
|
|
533
|
+
return TABLE_ROW_HEIGHT[size]
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function resolveTableVirtualEnabled(options: {
|
|
537
|
+
virtual?: boolean
|
|
538
|
+
infinite?: boolean
|
|
539
|
+
virtualOnInfinite?: boolean
|
|
540
|
+
}): boolean {
|
|
541
|
+
if (options.virtual) return true
|
|
542
|
+
if (options.infinite && options.virtualOnInfinite !== false) return true
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function isNearScrollBottom(
|
|
547
|
+
scrollTop: number,
|
|
548
|
+
scrollHeight: number,
|
|
549
|
+
clientHeight: number,
|
|
550
|
+
distance = 80,
|
|
551
|
+
): boolean {
|
|
552
|
+
return scrollHeight - scrollTop - clientHeight <= distance
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function entryHeight<T extends RsTableRowData>(
|
|
556
|
+
entry: RsTableRowEntry<T>,
|
|
557
|
+
rowHeight: number,
|
|
558
|
+
groupRowHeight = TABLE_ROW_HEIGHT.group,
|
|
559
|
+
expandRowHeight = TABLE_ROW_HEIGHT.expand,
|
|
560
|
+
): number {
|
|
561
|
+
if (entry.type === 'group') return groupRowHeight
|
|
562
|
+
if (entry.type === 'expand') return expandRowHeight
|
|
563
|
+
return rowHeight
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function resolveScrollWidth(scrollX?: number | string, _columns?: readonly RsTableColumn[]): string | undefined {
|
|
567
|
+
if (scrollX === undefined) return undefined
|
|
568
|
+
if (typeof scrollX === 'number') return `${scrollX}px`
|
|
569
|
+
return scrollX
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function resolveLeadingColumnWidth(options: {
|
|
573
|
+
selectable?: boolean
|
|
574
|
+
showIndex?: boolean
|
|
575
|
+
showEditGutter?: boolean
|
|
576
|
+
gutterWidth?: number
|
|
577
|
+
indexWidth?: number
|
|
578
|
+
expandable?: boolean
|
|
579
|
+
rowDraggable?: boolean
|
|
580
|
+
}): number {
|
|
581
|
+
let width = 0
|
|
582
|
+
if (options.rowDraggable) width += 40
|
|
583
|
+
if (options.expandable) width += 40
|
|
584
|
+
if (options.selectable) width += 40
|
|
585
|
+
if (options.showEditGutter) width += options.gutterWidth ?? 32
|
|
586
|
+
else if (options.showIndex) width += options.indexWidth ?? 56
|
|
587
|
+
return width
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function resolveFixedColumnStyles<T extends RsTableRowData>(
|
|
591
|
+
columns: readonly RsTableColumn<T>[],
|
|
592
|
+
widths: Record<string, number | string>,
|
|
593
|
+
options: {
|
|
594
|
+
selectable?: boolean
|
|
595
|
+
showIndex?: boolean
|
|
596
|
+
showEditGutter?: boolean
|
|
597
|
+
gutterWidth?: number
|
|
598
|
+
indexWidth?: number
|
|
599
|
+
expandable?: boolean
|
|
600
|
+
rowDraggable?: boolean
|
|
601
|
+
} = {},
|
|
602
|
+
): Map<string, RsTableFixedCellStyle> {
|
|
603
|
+
const result = new Map<string, RsTableFixedCellStyle>()
|
|
604
|
+
const leading = resolveLeadingColumnWidth(options)
|
|
605
|
+
let leftOffset = leading
|
|
606
|
+
|
|
607
|
+
for (const column of columns) {
|
|
608
|
+
if (column.fixed !== 'left') continue
|
|
609
|
+
result.set(column.key, { fixed: 'left', left: leftOffset })
|
|
610
|
+
leftOffset += parseColumnWidth(widths[column.key] ?? column.width)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let rightOffset = 0
|
|
614
|
+
for (let index = columns.length - 1; index >= 0; index -= 1) {
|
|
615
|
+
const column = columns[index]
|
|
616
|
+
if (column?.fixed !== 'right') continue
|
|
617
|
+
result.set(column.key, { fixed: 'right', right: rightOffset })
|
|
618
|
+
rightOffset += parseColumnWidth(widths[column.key] ?? column.width)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return result
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function fixedCellStyle(
|
|
625
|
+
style?: RsTableFixedCellStyle,
|
|
626
|
+
options?: { header?: boolean },
|
|
627
|
+
): Record<string, string> | undefined {
|
|
628
|
+
if (!style) return undefined
|
|
629
|
+
const result: Record<string, string> = {
|
|
630
|
+
position: 'sticky',
|
|
631
|
+
zIndex: options?.header ? '4' : '2',
|
|
632
|
+
}
|
|
633
|
+
if (options?.header) result.top = '0'
|
|
634
|
+
if (style.fixed === 'left' && style.left !== undefined) {
|
|
635
|
+
result.left = `${style.left}px`
|
|
636
|
+
return result
|
|
637
|
+
}
|
|
638
|
+
if (style.fixed === 'right' && style.right !== undefined) {
|
|
639
|
+
result.right = `${style.right}px`
|
|
640
|
+
return result
|
|
641
|
+
}
|
|
642
|
+
return undefined
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function selectRowKeys(
|
|
646
|
+
selectedKeys: readonly string[],
|
|
647
|
+
key: string,
|
|
648
|
+
selectionType: RsTableSelectionType,
|
|
649
|
+
): string[] {
|
|
650
|
+
if (selectionType === 'radio') return [key]
|
|
651
|
+
return toggleRowSelection(selectedKeys, key)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** 行点击多选(plain / Ctrl·Cmd / Shift),供 selectionType=`row` 使用。 */
|
|
655
|
+
export function selectRowKeysByClick(
|
|
656
|
+
selectedKeys: readonly string[],
|
|
657
|
+
key: string,
|
|
658
|
+
options: {
|
|
659
|
+
toggle: boolean
|
|
660
|
+
range: boolean
|
|
661
|
+
/** 有序行键(当前可见数据顺序),用于 Shift 范围选 */
|
|
662
|
+
orderedKeys: readonly string[]
|
|
663
|
+
/** Shift 范围的锚点行键 */
|
|
664
|
+
anchorKey?: string | null
|
|
665
|
+
},
|
|
666
|
+
): string[] {
|
|
667
|
+
if (options.range && options.orderedKeys.length > 0) {
|
|
668
|
+
const anchor = options.anchorKey && options.orderedKeys.includes(options.anchorKey)
|
|
669
|
+
? options.anchorKey
|
|
670
|
+
: key
|
|
671
|
+
const from = options.orderedKeys.indexOf(anchor)
|
|
672
|
+
const to = options.orderedKeys.indexOf(key)
|
|
673
|
+
if (from < 0 || to < 0) return [key]
|
|
674
|
+
const start = Math.min(from, to)
|
|
675
|
+
const end = Math.max(from, to)
|
|
676
|
+
return options.orderedKeys.slice(start, end + 1)
|
|
677
|
+
}
|
|
678
|
+
if (options.toggle) return toggleRowSelection(selectedKeys, key)
|
|
679
|
+
return [key]
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function sliceVirtualTableEntries<T extends RsTableRowData>(
|
|
683
|
+
entries: readonly RsTableRowEntry<T>[],
|
|
684
|
+
scrollTop: number,
|
|
685
|
+
viewportHeight: number,
|
|
686
|
+
rowHeight: number,
|
|
687
|
+
groupRowHeight = TABLE_ROW_HEIGHT.group,
|
|
688
|
+
overscan = 4,
|
|
689
|
+
expandRowHeight = TABLE_ROW_HEIGHT.expand,
|
|
690
|
+
): {
|
|
691
|
+
entries: RsTableRowEntry<T>[]
|
|
692
|
+
paddingTop: number
|
|
693
|
+
paddingBottom: number
|
|
694
|
+
} {
|
|
695
|
+
if (entries.length === 0) return { entries: [], paddingTop: 0, paddingBottom: 0 }
|
|
696
|
+
const model = buildVirtualHeightModel(entries, rowHeight, groupRowHeight, expandRowHeight)
|
|
697
|
+
return sliceVirtualHeightModel(model, scrollTop, viewportHeight, rowHeight, overscan)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** 预计算行高前缀和,供虚拟滚动复用,避免每次 scroll 全量 map/reduce */
|
|
701
|
+
export function buildVirtualHeightModel<T extends RsTableRowData>(
|
|
702
|
+
entries: readonly RsTableRowEntry<T>[],
|
|
703
|
+
rowHeight: number,
|
|
704
|
+
groupRowHeight = TABLE_ROW_HEIGHT.group,
|
|
705
|
+
expandRowHeight = TABLE_ROW_HEIGHT.expand,
|
|
706
|
+
): {
|
|
707
|
+
entries: readonly RsTableRowEntry<T>[]
|
|
708
|
+
prefix: number[]
|
|
709
|
+
total: number
|
|
710
|
+
} {
|
|
711
|
+
const prefix = new Array<number>(entries.length + 1)
|
|
712
|
+
prefix[0] = 0
|
|
713
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
714
|
+
prefix[index + 1] = prefix[index] + entryHeight(entries[index], rowHeight, groupRowHeight, expandRowHeight)
|
|
715
|
+
}
|
|
716
|
+
return { entries, prefix, total: prefix[entries.length] ?? 0 }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function findVirtualStartIndex(prefix: readonly number[], targetTop: number): number {
|
|
720
|
+
let low = 0
|
|
721
|
+
let high = prefix.length - 2
|
|
722
|
+
while (low < high) {
|
|
723
|
+
const mid = Math.floor((low + high) / 2)
|
|
724
|
+
if ((prefix[mid + 1] ?? 0) <= targetTop) low = mid + 1
|
|
725
|
+
else high = mid
|
|
726
|
+
}
|
|
727
|
+
return low
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function sliceVirtualHeightModel<T extends RsTableRowData>(
|
|
731
|
+
model: {
|
|
732
|
+
entries: readonly RsTableRowEntry<T>[]
|
|
733
|
+
prefix: readonly number[]
|
|
734
|
+
total: number
|
|
735
|
+
},
|
|
736
|
+
scrollTop: number,
|
|
737
|
+
viewportHeight: number,
|
|
738
|
+
rowHeight: number,
|
|
739
|
+
overscan = 4,
|
|
740
|
+
): {
|
|
741
|
+
entries: RsTableRowEntry<T>[]
|
|
742
|
+
paddingTop: number
|
|
743
|
+
paddingBottom: number
|
|
744
|
+
} {
|
|
745
|
+
const { entries, prefix, total } = model
|
|
746
|
+
if (entries.length === 0) return { entries: [], paddingTop: 0, paddingBottom: 0 }
|
|
747
|
+
|
|
748
|
+
const overscanPx = overscan * rowHeight
|
|
749
|
+
const targetTop = Math.max(0, scrollTop - overscanPx)
|
|
750
|
+
const start = findVirtualStartIndex(prefix, targetTop)
|
|
751
|
+
const paddingTop = prefix[start] ?? 0
|
|
752
|
+
|
|
753
|
+
const maxVisible = viewportHeight + overscanPx * 2
|
|
754
|
+
let end = start
|
|
755
|
+
while (end < entries.length && (prefix[end] ?? 0) - paddingTop < maxVisible) {
|
|
756
|
+
end += 1
|
|
757
|
+
}
|
|
758
|
+
if (end === start) end = Math.min(start + 1, entries.length)
|
|
759
|
+
|
|
760
|
+
const renderedBottom = prefix[end] ?? total
|
|
761
|
+
const paddingBottom = Math.max(0, total - renderedBottom)
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
entries: entries.slice(start, end) as RsTableRowEntry<T>[],
|
|
765
|
+
paddingTop,
|
|
766
|
+
paddingBottom,
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** 列像素宽:优先已测量/配置 widths,其次 column.width / minWidth */
|
|
771
|
+
export function resolveColumnPixelWidth<T extends RsTableRowData>(
|
|
772
|
+
column: RsTableColumn<T>,
|
|
773
|
+
widths?: Record<string, number | string>,
|
|
774
|
+
fallback = 120,
|
|
775
|
+
): number {
|
|
776
|
+
const fromMap = widths?.[column.key]
|
|
777
|
+
if (fromMap !== undefined) return parseColumnWidth(fromMap, fallback)
|
|
778
|
+
return parseColumnWidth(column.width ?? column.minWidth, fallback)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 横向列虚拟切片(仅针对可横滚的流体列)。
|
|
783
|
+
* 调用方负责:冻结列始终实体渲染;`scrollLeft` 为相对流体区起点的偏移
|
|
784
|
+
* (即容器 scrollLeft 减去前缀列与左冻结列宽度)。
|
|
785
|
+
*/
|
|
786
|
+
export function sliceVirtualColumns<T extends RsTableRowData>(
|
|
787
|
+
columns: readonly RsTableColumn<T>[],
|
|
788
|
+
options: {
|
|
789
|
+
scrollLeft: number
|
|
790
|
+
viewportWidth: number
|
|
791
|
+
getWidth: (column: RsTableColumn<T>) => number
|
|
792
|
+
overscan?: number
|
|
793
|
+
},
|
|
794
|
+
): {
|
|
795
|
+
columns: RsTableColumn<T>[]
|
|
796
|
+
paddingLeft: number
|
|
797
|
+
paddingRight: number
|
|
798
|
+
startIndex: number
|
|
799
|
+
endIndex: number
|
|
800
|
+
} {
|
|
801
|
+
const n = columns.length
|
|
802
|
+
if (n === 0) {
|
|
803
|
+
return { columns: [], paddingLeft: 0, paddingRight: 0, startIndex: 0, endIndex: 0 }
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const overscan = options.overscan ?? 2
|
|
807
|
+
const prefix: number[] = new Array(n + 1)
|
|
808
|
+
prefix[0] = 0
|
|
809
|
+
for (let i = 0; i < n; i += 1) {
|
|
810
|
+
prefix[i + 1] = (prefix[i] ?? 0) + Math.max(1, options.getWidth(columns[i]!))
|
|
811
|
+
}
|
|
812
|
+
const total = prefix[n] ?? 0
|
|
813
|
+
|
|
814
|
+
const viewLeft = Math.max(0, options.scrollLeft)
|
|
815
|
+
const viewRight = viewLeft + Math.max(1, options.viewportWidth)
|
|
816
|
+
|
|
817
|
+
let start = 0
|
|
818
|
+
while (start < n && (prefix[start + 1] ?? 0) <= viewLeft) start += 1
|
|
819
|
+
start = Math.max(0, start - overscan)
|
|
820
|
+
|
|
821
|
+
let end = start
|
|
822
|
+
while (end < n && (prefix[end] ?? 0) < viewRight) end += 1
|
|
823
|
+
end = Math.min(n, Math.max(end + overscan, start + 1))
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
columns: columns.slice(start, end) as RsTableColumn<T>[],
|
|
827
|
+
paddingLeft: prefix[start] ?? 0,
|
|
828
|
+
paddingRight: Math.max(0, total - (prefix[end] ?? total)),
|
|
829
|
+
startIndex: start,
|
|
830
|
+
endIndex: end,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function resolveRowKey<T extends RsTableRowData>(row: T, index: number, rowKey?: RsTableRowKey<T>): string {
|
|
835
|
+
if (typeof rowKey === 'function') return rowKey(row)
|
|
836
|
+
if (rowKey) return String(row[rowKey as keyof T])
|
|
837
|
+
return readRowKeyFallback(row) ?? String(index)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function resolveEntryKey<T extends RsTableRowData>(
|
|
841
|
+
entry: RsTableRowEntry<T>,
|
|
842
|
+
rowKey?: RsTableRowKey<T>,
|
|
843
|
+
): string {
|
|
844
|
+
if (entry.type === 'group') return `group:${entry.key}`
|
|
845
|
+
if (entry.type === 'expand') return `expand:${entry.rowKey}`
|
|
846
|
+
return resolveRowKey(entry.row, entry.rowIndex, rowKey)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function resolveColumnStyle<T extends RsTableRowData = Record<string, unknown>>(
|
|
850
|
+
column: RsTableColumn<T>,
|
|
851
|
+
widths?: Record<string, number | string>,
|
|
852
|
+
): Record<string, string> | undefined {
|
|
853
|
+
const style: Record<string, string> = {}
|
|
854
|
+
const width = widths?.[column.key] ?? column.width
|
|
855
|
+
if (width !== undefined) {
|
|
856
|
+
style.width = typeof width === 'number' ? `${width}px` : width
|
|
857
|
+
}
|
|
858
|
+
if (column.minWidth !== undefined) {
|
|
859
|
+
style.minWidth = typeof column.minWidth === 'number' ? `${column.minWidth}px` : column.minWidth
|
|
860
|
+
}
|
|
861
|
+
return Object.keys(style).length ? style : undefined
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export function clampColumnWidth(width: number, min = 48, max = 640): number {
|
|
865
|
+
return Math.min(max, Math.max(min, width))
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export function parseColumnWidth(width: number | string | undefined, fallback = 120): number {
|
|
869
|
+
if (typeof width === 'number') return width
|
|
870
|
+
if (typeof width === 'string' && width.endsWith('px')) return Number.parseInt(width, 10) || fallback
|
|
871
|
+
return fallback
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function createInitialColumnWidths<T extends RsTableRowData>(
|
|
875
|
+
columns: readonly RsTableColumn<T>[],
|
|
876
|
+
overrides?: Record<string, number | string>,
|
|
877
|
+
options?: { forceAll?: boolean },
|
|
878
|
+
): Record<string, number> {
|
|
879
|
+
const result: Record<string, number> = {}
|
|
880
|
+
for (const column of columns) {
|
|
881
|
+
const override = overrides?.[column.key]
|
|
882
|
+
if (typeof override === 'number') {
|
|
883
|
+
result[column.key] = override
|
|
884
|
+
continue
|
|
885
|
+
}
|
|
886
|
+
if (typeof override === 'string' && override.endsWith('px')) {
|
|
887
|
+
result[column.key] = Number.parseInt(override, 10) || parseColumnWidth(column.width)
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
if (typeof column.width === 'number') {
|
|
891
|
+
result[column.key] = column.width
|
|
892
|
+
continue
|
|
893
|
+
}
|
|
894
|
+
// resizable 模式下,确保所有列都有显式宽度,避免 table-layout:auto 重分配
|
|
895
|
+
if (options?.forceAll) {
|
|
896
|
+
result[column.key] = parseColumnWidth(column.width ?? column.minWidth)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return result
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function isTableRowDisabled<T extends RsTableRowData>(row: T): boolean {
|
|
903
|
+
return readConvention(row).disabled === true
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export function resolveSelectableRowKeys<T extends RsTableRowData>(
|
|
907
|
+
rows: readonly T[],
|
|
908
|
+
rowKey?: RsTableRowKey<T>,
|
|
909
|
+
rowSelectable?: (row: T, index: number) => boolean,
|
|
910
|
+
): string[] {
|
|
911
|
+
return rows
|
|
912
|
+
.map((row, index) => ({ row, index }))
|
|
913
|
+
.filter(({ row, index }) => {
|
|
914
|
+
if (isTableRowDisabled(row)) return false
|
|
915
|
+
return rowSelectable ? rowSelectable(row, index) : true
|
|
916
|
+
})
|
|
917
|
+
.map(({ row, index }) => resolveRowKey(row, index, rowKey))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export function resolveSelectAllState(
|
|
921
|
+
selectedKeys: readonly string[],
|
|
922
|
+
allKeys: readonly string[],
|
|
923
|
+
): RsTableSelectAllState {
|
|
924
|
+
if (allKeys.length === 0) return 'unchecked'
|
|
925
|
+
const selectedCount = allKeys.filter((key) => selectedKeys.includes(key)).length
|
|
926
|
+
if (selectedCount === 0) return 'unchecked'
|
|
927
|
+
if (selectedCount === allKeys.length) return 'checked'
|
|
928
|
+
return 'indeterminate'
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function toggleRowSelection(selectedKeys: readonly string[], key: string): string[] {
|
|
932
|
+
const next = new Set(selectedKeys)
|
|
933
|
+
if (next.has(key)) next.delete(key)
|
|
934
|
+
else next.add(key)
|
|
935
|
+
return [...next]
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export function toggleSelectAll(
|
|
939
|
+
selectedKeys: readonly string[],
|
|
940
|
+
allKeys: readonly string[],
|
|
941
|
+
select: boolean,
|
|
942
|
+
): string[] {
|
|
943
|
+
const next = new Set(selectedKeys)
|
|
944
|
+
if (select) {
|
|
945
|
+
for (const key of allKeys) next.add(key)
|
|
946
|
+
} else {
|
|
947
|
+
for (const key of allKeys) next.delete(key)
|
|
948
|
+
}
|
|
949
|
+
return [...next]
|
|
950
|
+
}
|