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.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. 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
+ }