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,53 @@
1
+ import type { RsContextMenuItem } from '../context-menu-utils'
2
+ import type { RsTableColumn, RsTableRowData } from '../table-utils'
3
+ import { formatCellDisplayValue, resolveCellEditText, resolveColumnRawValue } from './table-edit-utils'
4
+
5
+ export const TABLE_CTX_COPY_CELL = 'copyCell'
6
+ export const TABLE_CTX_COPY_ROW = 'copyRow'
7
+
8
+ export function buildDefaultTableContextMenuItems(options: {
9
+ copyCellLabel: string
10
+ copyRowLabel: string
11
+ hasRow: boolean
12
+ hasCell: boolean
13
+ }): RsContextMenuItem[] {
14
+ const items: RsContextMenuItem[] = []
15
+ if (options.hasRow && options.hasCell) {
16
+ items.push({ key: TABLE_CTX_COPY_CELL, label: options.copyCellLabel, icon: 'copy' })
17
+ }
18
+ if (options.hasRow) {
19
+ items.push({ key: TABLE_CTX_COPY_ROW, label: options.copyRowLabel, icon: 'copy' })
20
+ }
21
+ return items
22
+ }
23
+
24
+ export function resolveTableCellCopyText<T extends RsTableRowData>(
25
+ row: T,
26
+ column: RsTableColumn<T>,
27
+ rowIndex: number,
28
+ draft?: string,
29
+ ): string {
30
+ return resolveCellEditText(row, column, rowIndex, draft)
31
+ }
32
+
33
+ export function resolveTableRowCopyText<T extends RsTableRowData>(
34
+ row: T,
35
+ rowIndex: number,
36
+ columns: RsTableColumn<T>[],
37
+ getDraft?: (rowKey: string, colKey: string) => string | undefined,
38
+ rowKey?: string,
39
+ ): string {
40
+ return columns
41
+ .map((column) => {
42
+ const draft = rowKey && getDraft ? getDraft(rowKey, column.key) : undefined
43
+ if (draft !== undefined) {
44
+ return formatCellDisplayValue(draft, row, column, rowIndex)
45
+ }
46
+ return formatCellDisplayValue(resolveColumnRawValue(row, column, rowIndex), row, column, rowIndex)
47
+ })
48
+ .join('\t')
49
+ }
50
+
51
+ export function isBuiltInTableContextMenuKey(key: string): boolean {
52
+ return key === TABLE_CTX_COPY_CELL || key === TABLE_CTX_COPY_ROW
53
+ }
@@ -0,0 +1,496 @@
1
+ import type { VNodeChild } from 'vue'
2
+ import { isVNode } from 'vue'
3
+ import type {
4
+ RsTableCellValueType,
5
+ RsTableColumn,
6
+ RsTableColumnEditorOptionsResolved,
7
+ RsTableRowData,
8
+ } from '../table-utils'
9
+ import { getCellValue } from '../table-utils'
10
+ import { formatDateTimeValue, formatDateValue } from '../date-picker-utils'
11
+ import {
12
+ formatIsoUtcToLocal,
13
+ looksLikeIsoDateTimeWithTz,
14
+ parseLocalDateTimeToUtcIso,
15
+ } from '../../lib/iso-local-datetime'
16
+
17
+ export type RsTableCellCommitTrigger = 'blur' | 'enter' | 'change' | 'manual'
18
+ export type RsTableCellEditTrigger = 'click' | 'dblclick'
19
+ export type RsTableCellEditFocusMode = 'end' | 'select' | 'start'
20
+ export type RsTableCellNavigateDirection = 'next' | 'prev' | 'up' | 'down'
21
+ export type RsTableCellEditorInputType =
22
+ | 'text'
23
+ | 'number'
24
+ | 'date'
25
+ | 'datetime-local'
26
+ | 'textarea'
27
+
28
+ /** 编辑草稿中的显式 NULL 哨兵(与空字符串区分) */
29
+ export const RS_TABLE_NULL_DRAFT = '__rs_null__'
30
+
31
+ export function isNullDraft(text: string | undefined | null): boolean {
32
+ return text === RS_TABLE_NULL_DRAFT
33
+ }
34
+
35
+ export function nullToEditText(): string {
36
+ return RS_TABLE_NULL_DRAFT
37
+ }
38
+
39
+ export function resolveCellEditorInputType(
40
+ valueType: RsTableCellValueType = 'text',
41
+ ): RsTableCellEditorInputType {
42
+ switch (valueType) {
43
+ case 'number':
44
+ return 'number'
45
+ case 'date':
46
+ return 'date'
47
+ case 'datetime':
48
+ return 'datetime-local'
49
+ case 'textarea':
50
+ return 'textarea'
51
+ default:
52
+ return 'text'
53
+ }
54
+ }
55
+
56
+ export function resolveColumnValueType<T extends RsTableRowData>(
57
+ column: RsTableColumn<T>,
58
+ ): RsTableCellValueType {
59
+ return column.valueType ?? 'text'
60
+ }
61
+
62
+ export function resolveColumnEditorOptions<T extends RsTableRowData>(
63
+ column: RsTableColumn<T>,
64
+ row?: T,
65
+ index = 0,
66
+ ): RsTableColumnEditorOptionsResolved {
67
+ const raw = column.editorOptions ?? {}
68
+ const { options: rawOptions, ...rest } = raw
69
+ let options: RsTableColumnEditorOptionsResolved['options']
70
+ if (typeof rawOptions === 'function') {
71
+ if (row != null) {
72
+ options = rawOptions(row, index)
73
+ } else {
74
+ options = []
75
+ }
76
+ } else {
77
+ options = rawOptions
78
+ }
79
+ return { ...rest, options }
80
+ }
81
+
82
+ /** 布尔列:展示态直接勾选切换,不挂载编辑器(大表友好)。 */
83
+ export function isBooleanToggleColumn<T extends RsTableRowData>(
84
+ column: RsTableColumn<T>,
85
+ ): boolean {
86
+ return resolveColumnValueType(column) === 'boolean'
87
+ }
88
+
89
+ /** 浮层类编辑器(select/date):点选即编辑,避免再双击。 */
90
+ export function usesOverlayEditor(valueType: RsTableCellValueType): boolean {
91
+ return valueType === 'select' || valueType === 'date' || valueType === 'datetime'
92
+ }
93
+
94
+ export function resolveColumnEditTrigger<T extends RsTableRowData>(
95
+ column: RsTableColumn<T>,
96
+ tableDefault: RsTableCellEditTrigger,
97
+ ): RsTableCellEditTrigger {
98
+ if (column.editTrigger) return column.editTrigger
99
+ const valueType = resolveColumnValueType(column)
100
+ if (usesOverlayEditor(valueType)) return 'click'
101
+ return tableDefault
102
+ }
103
+
104
+ export function resolveColumnCommitOn<T extends RsTableRowData>(
105
+ column: RsTableColumn<T>,
106
+ rowCommit: boolean,
107
+ ): RsTableCellCommitTrigger {
108
+ if (rowCommit) return 'manual'
109
+ if (column.commitOn) return column.commitOn
110
+ const valueType = resolveColumnValueType(column)
111
+ if (valueType === 'boolean' || valueType === 'select') return 'change'
112
+ // 日期/时间:非 rowCommit 时面板确认只写草稿,Enter/Tab 再提交(避免一点确认就写库)
113
+ // rowCommit 走上方 manual:面板关闭即 stage,行首 ✓ 才写库
114
+ if (valueType === 'date' || valueType === 'datetime') return 'enter'
115
+ return 'blur'
116
+ }
117
+
118
+ /** 单元格数据值(编辑 / 复制 / 对比);不含 `render` 展示结果。 */
119
+ export function resolveColumnRawValue<T extends RsTableRowData>(
120
+ row: T,
121
+ column: RsTableColumn<T>,
122
+ _index: number,
123
+ ): unknown {
124
+ return getCellValue(row, column)
125
+ }
126
+
127
+ /** 将 `render` / 展示内容压成纯文本(tooltip、无障碍等);VNode 取文本子节点。 */
128
+ export function plainTextFromCellDisplay(content: VNodeChild): string {
129
+ if (content == null || content === false || content === true) return ''
130
+ if (typeof content === 'string' || typeof content === 'number') return String(content)
131
+ if (Array.isArray(content)) {
132
+ return content.map((item) => plainTextFromCellDisplay(item as VNodeChild)).join('')
133
+ }
134
+ if (isVNode(content)) {
135
+ return plainTextFromCellDisplay(content.children as VNodeChild)
136
+ }
137
+ return ''
138
+ }
139
+
140
+ /**
141
+ * 单元格展示内容:优先 `column.render`(支持 VNode),否则 formatter / 原始值。
142
+ * `draft` 存在时展示编辑草稿的格式化文本。
143
+ */
144
+ export function resolveColumnDisplayContent<T extends RsTableRowData>(
145
+ row: T,
146
+ column: RsTableColumn<T>,
147
+ index: number,
148
+ options?: { draft?: string; nullLabel?: string },
149
+ ): VNodeChild {
150
+ if (options?.draft !== undefined) {
151
+ return formatCellDisplayValue(options.draft, row, column, index, {
152
+ nullLabel: options.nullLabel,
153
+ })
154
+ }
155
+ if (column.render) {
156
+ return column.render(row, index)
157
+ }
158
+ return formatCellDisplayValue(getCellValue(row, column), row, column, index, {
159
+ nullLabel: options?.nullLabel,
160
+ })
161
+ }
162
+
163
+ export function isColumnEditable<T extends RsTableRowData>(
164
+ column: RsTableColumn<T>,
165
+ row: T,
166
+ index: number,
167
+ tableEditable: boolean,
168
+ ): boolean {
169
+ if (!tableEditable) return false
170
+ if (typeof column.editable === 'function') return column.editable(row, index)
171
+ if (column.editable === false) return false
172
+ if (column.editable === true) return true
173
+ return tableEditable
174
+ }
175
+
176
+ export function isColumnNullable<T extends RsTableRowData>(
177
+ column: RsTableColumn<T>,
178
+ tableAllowNull: boolean,
179
+ ): boolean {
180
+ if (column.nullable === false) return false
181
+ if (column.nullable === true) return true
182
+ return tableAllowNull
183
+ }
184
+
185
+ export function coerceBoolean(value: unknown): boolean {
186
+ if (typeof value === 'boolean') return value
187
+ if (typeof value === 'number') return value !== 0
188
+ const text = String(value ?? '')
189
+ .trim()
190
+ .toLowerCase()
191
+ // 含 PG bool 文本 't' / 'f'
192
+ if (['false', '0', 'no', 'n', '否', 'f', ''].includes(text)) return false
193
+ return ['true', '1', 'yes', 'y', '是', 't', '✓', '✔', '√'].includes(text)
194
+ }
195
+
196
+ export function booleanToEditText(value: unknown): string {
197
+ return coerceBoolean(value) ? 'true' : 'false'
198
+ }
199
+
200
+ /** 去掉千分位等展示噪音,便于 number 解析 */
201
+ export function stripNumberDecorations(input: string): string {
202
+ return String(input ?? '').replace(/,/g, '').replace(/\s/g, '').trim()
203
+ }
204
+
205
+ export function formatCellDisplayValue<T extends RsTableRowData>(
206
+ value: unknown,
207
+ row: T,
208
+ column: RsTableColumn<T>,
209
+ index: number,
210
+ options?: { nullLabel?: string },
211
+ ): string {
212
+ if (column.formatter) return column.formatter(value, row, index)
213
+ if (value === null || value === undefined) {
214
+ return options?.nullLabel ?? '(NULL)'
215
+ }
216
+ if (isNullDraft(String(value))) return options?.nullLabel ?? '(NULL)'
217
+ const valueType = resolveColumnValueType(column)
218
+ if (valueType === 'boolean') {
219
+ return coerceBoolean(value) ? '✓' : ''
220
+ }
221
+ // 与编辑态一致:带时区 ISO → 本地;其余 date/datetime 规范格式
222
+ if (valueType === 'date') {
223
+ const raw = String(value)
224
+ const local = formatIsoUtcToLocal(raw)
225
+ if (local) return local.slice(0, 10)
226
+ return formatDateValue(raw) || raw
227
+ }
228
+ if (valueType === 'datetime') {
229
+ const raw = String(value)
230
+ const local = formatIsoUtcToLocal(raw)
231
+ if (local) return local
232
+ return formatDateTimeValue(raw) || raw
233
+ }
234
+ if (typeof value === 'string') {
235
+ const local = formatIsoUtcToLocal(value)
236
+ if (local) return local
237
+ }
238
+ return cellValueToText(value)
239
+ }
240
+
241
+ /** 单元格值 → 纯文本(对象用 JSON,避免 `[object Object]`)。 */
242
+ export function cellValueToText(value: unknown): string {
243
+ if (value === null || value === undefined) return ''
244
+ if (typeof value === 'string') return value
245
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
246
+ return String(value)
247
+ }
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 resolveCellEditText<T extends RsTableRowData>(
259
+ row: T,
260
+ column: RsTableColumn<T>,
261
+ index: number,
262
+ draft?: string,
263
+ ): string {
264
+ if (draft !== undefined) return draft
265
+ const raw = resolveColumnRawValue(row, column, index)
266
+ if (raw === null || raw === undefined) return RS_TABLE_NULL_DRAFT
267
+ const valueType = resolveColumnValueType(column)
268
+ if (valueType === 'boolean') return booleanToEditText(raw)
269
+ // 带时区 ISO → 本地墙钟(含毫秒),与展示一致,便于写回还原
270
+ if (typeof raw === 'string') {
271
+ const local = formatIsoUtcToLocal(raw)
272
+ if (local) {
273
+ if (valueType === 'date') return local.slice(0, 10)
274
+ return local
275
+ }
276
+ }
277
+ // 日期/日期时间:规范为选择器可解析格式,保证弹层选中态与双向绑定一致
278
+ if (valueType === 'date') {
279
+ return formatDateValue(String(raw)) || String(raw)
280
+ }
281
+ if (valueType === 'datetime') {
282
+ return formatDateTimeValue(String(raw)) || String(raw)
283
+ }
284
+ if (column.formatter) {
285
+ return cellValueToText(raw)
286
+ }
287
+ // 行数据 → 编辑草稿:number 列等在此转为文本(草稿协议为 string)
288
+ return cellValueToText(raw)
289
+ }
290
+
291
+ export function parseCellEditInput<T extends RsTableRowData>(
292
+ input: string,
293
+ row: T,
294
+ column: RsTableColumn<T>,
295
+ index: number,
296
+ options?: { allowNull?: boolean },
297
+ ): unknown {
298
+ if (isNullDraft(input)) return null
299
+ const allowNull = options?.allowNull ?? isColumnNullable(column, true)
300
+ if (column.emptyAsNull && allowNull && input.trim() === '') return null
301
+ if (column.parser) return column.parser(input, row, index)
302
+ const valueType = resolveColumnValueType(column)
303
+ if (valueType === 'boolean') return coerceBoolean(input)
304
+ if (valueType === 'number') {
305
+ const cleaned = stripNumberDecorations(input)
306
+ if (!cleaned) return allowNull ? null : ''
307
+ const parsed = Number(cleaned)
308
+ return Number.isNaN(parsed) ? input : parsed
309
+ }
310
+
311
+ const trimmed = input.trim()
312
+ const previous = resolveColumnRawValue(row, column, index)
313
+ // 原值是带时区 ISO:本地草稿写回 UTC,未改秒则保留原始毫秒/更长小数
314
+ if (typeof previous === 'string' && looksLikeIsoDateTimeWithTz(previous.trim())) {
315
+ if (!trimmed) return allowNull ? null : ''
316
+ const utc = parseLocalDateTimeToUtcIso(trimmed, previous)
317
+ if (utc) return utc
318
+ }
319
+
320
+ if (valueType === 'datetime' && column.editorOptions?.timezone === 'utc') {
321
+ if (!trimmed) return allowNull ? null : ''
322
+ const utc = parseLocalDateTimeToUtcIso(trimmed)
323
+ if (utc) return utc
324
+ if (/Z$/i.test(trimmed) || /[+-]\d{2}:?\d{2}$/.test(trimmed)) return trimmed
325
+ return `${trimmed.replace(' ', 'T')}Z`
326
+ }
327
+ return input
328
+ }
329
+
330
+ export function validateCellValue<T extends RsTableRowData>(
331
+ value: unknown,
332
+ row: T,
333
+ column: RsTableColumn<T>,
334
+ index: number,
335
+ ): string | null {
336
+ if (!column.validator) return null
337
+ const result = column.validator(value, row, index)
338
+ if (result instanceof Promise) {
339
+ // 同步路径:异步 validator 请用 validateCellValueAsync
340
+ return null
341
+ }
342
+ return result
343
+ }
344
+
345
+ export async function validateCellValueAsync<T extends RsTableRowData>(
346
+ value: unknown,
347
+ row: T,
348
+ column: RsTableColumn<T>,
349
+ index: number,
350
+ ): Promise<string | null> {
351
+ if (!column.validator) return null
352
+ return column.validator(value, row, index)
353
+ }
354
+
355
+ export function isAsyncValidator<T extends RsTableRowData>(column: RsTableColumn<T>): boolean {
356
+ // 运行期无法静态判定;提交时若返回 Promise 即按异步处理
357
+ return typeof column.validator === 'function'
358
+ }
359
+
360
+ /** 多行同列批量填充目标 */
361
+ export function listBatchColumnTargets<T extends RsTableRowData>(options: {
362
+ rows: Array<{ row: T; rowIndex: number; rowKey: string }>
363
+ column: RsTableColumn<T>
364
+ tableEditable: boolean
365
+ selectedKeys: string[]
366
+ anchorRowKey: string
367
+ }): Array<{ row: T; rowIndex: number; rowKey: string }> {
368
+ const selected = new Set(options.selectedKeys)
369
+ if (selected.size <= 1 || !selected.has(options.anchorRowKey)) {
370
+ const anchor = options.rows.find((item) => item.rowKey === options.anchorRowKey)
371
+ return anchor ? [anchor] : []
372
+ }
373
+ return options.rows.filter(
374
+ (item) =>
375
+ selected.has(item.rowKey) &&
376
+ isColumnEditable(options.column, item.row, item.rowIndex, options.tableEditable),
377
+ )
378
+ }
379
+
380
+ export function isCellValueChanged(previous: unknown, next: unknown): boolean {
381
+ if (previous === next) return false
382
+ if (previous == null && next == null) return false
383
+ if (typeof previous === 'boolean' || typeof next === 'boolean') {
384
+ return coerceBoolean(previous) !== coerceBoolean(next)
385
+ }
386
+ return cellValueToText(previous) !== cellValueToText(next)
387
+ }
388
+
389
+ export function isRowEditPending<T extends RsTableRowData>(
390
+ rowKey: string,
391
+ row: T,
392
+ rowIndex: number,
393
+ options: {
394
+ isRowDirty: (rowKey: string) => boolean
395
+ editingCell: { rowKey: string; colKey: string } | null
396
+ editingDraft: string
397
+ resolveColumn: (colKey: string) => RsTableColumn<T> | undefined
398
+ },
399
+ ): boolean {
400
+ if (options.isRowDirty(rowKey)) return true
401
+ const editing = options.editingCell
402
+ if (!editing || editing.rowKey !== rowKey) return false
403
+ const column = options.resolveColumn(editing.colKey)
404
+ if (!column) return false
405
+ const baseline = resolveCellEditText(row, column, rowIndex)
406
+ return isCellValueChanged(baseline, options.editingDraft)
407
+ }
408
+
409
+ export interface RsTableEditableCellRef {
410
+ rowKey: string
411
+ colKey: string
412
+ rowIndex: number
413
+ }
414
+
415
+ /** 按显示列顺序收集可编辑单元格(用于 Tab / 方向键) */
416
+ export function listEditableCells<T extends RsTableRowData>(options: {
417
+ rows: T[]
418
+ columns: RsTableColumn<T>[]
419
+ tableEditable: boolean
420
+ rowKeyOf: (row: T, index: number) => string
421
+ skipBooleanToggle?: boolean
422
+ }): RsTableEditableCellRef[] {
423
+ const result: RsTableEditableCellRef[] = []
424
+ for (let rowIndex = 0; rowIndex < options.rows.length; rowIndex += 1) {
425
+ const row = options.rows[rowIndex]!
426
+ const rowKey = options.rowKeyOf(row, rowIndex)
427
+ for (const column of options.columns) {
428
+ if (!isColumnEditable(column, row, rowIndex, options.tableEditable)) continue
429
+ if (options.skipBooleanToggle && isBooleanToggleColumn(column)) continue
430
+ result.push({ rowKey, colKey: column.key, rowIndex })
431
+ }
432
+ }
433
+ return result
434
+ }
435
+
436
+ export function findEditableCellIndex(
437
+ cells: RsTableEditableCellRef[],
438
+ rowKey: string,
439
+ colKey: string,
440
+ ): number {
441
+ return cells.findIndex((cell) => cell.rowKey === rowKey && cell.colKey === colKey)
442
+ }
443
+
444
+ export function navigateEditableCell(
445
+ cells: RsTableEditableCellRef[],
446
+ rowKey: string,
447
+ colKey: string,
448
+ direction: RsTableCellNavigateDirection,
449
+ columnsPerRow: number,
450
+ ): RsTableEditableCellRef | null {
451
+ if (!cells.length || columnsPerRow <= 0) return null
452
+ const index = findEditableCellIndex(cells, rowKey, colKey)
453
+ if (index < 0) return cells[0] ?? null
454
+ if (direction === 'next') return cells[(index + 1) % cells.length] ?? null
455
+ if (direction === 'prev') return cells[(index - 1 + cells.length) % cells.length] ?? null
456
+
457
+ const current = cells[index]!
458
+ const colKeys = [...new Set(cells.map((cell) => cell.colKey))]
459
+ const colPos = colKeys.indexOf(current.colKey)
460
+ const rowDelta = direction === 'down' ? 1 : -1
461
+ const targetRow = current.rowIndex + rowDelta
462
+ const sameCol = cells.find((cell) => cell.rowIndex === targetRow && cell.colKey === current.colKey)
463
+ if (sameCol) return sameCol
464
+ // 回退:同列最近的可编辑格
465
+ const candidates = cells.filter((cell) => cell.colKey === current.colKey)
466
+ if (!candidates.length) return null
467
+ if (direction === 'down') {
468
+ return candidates.find((cell) => cell.rowIndex > current.rowIndex) ?? candidates[0] ?? null
469
+ }
470
+ const prev = [...candidates].reverse().find((cell) => cell.rowIndex < current.rowIndex)
471
+ return prev ?? candidates[candidates.length - 1] ?? null
472
+ }
473
+
474
+ /** 解析剪贴板 TSV/CSV(制表符优先) */
475
+ export function parseClipboardGrid(text: string): string[][] {
476
+ const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
477
+ const lines = normalized.split('\n')
478
+ while (lines.length && lines[lines.length - 1] === '') lines.pop()
479
+ return lines.map((line) => line.split('\t'))
480
+ }
481
+
482
+ export function applyFocusMode(
483
+ input: HTMLInputElement | HTMLTextAreaElement,
484
+ mode: RsTableCellEditFocusMode,
485
+ ): void {
486
+ const end = input.value.length
487
+ if (mode === 'select') {
488
+ input.select()
489
+ return
490
+ }
491
+ if (mode === 'start') {
492
+ input.setSelectionRange(0, 0)
493
+ return
494
+ }
495
+ input.setSelectionRange(end, end)
496
+ }
@@ -0,0 +1,12 @@
1
+ export function makeCellKey(rowKey: string, colKey: string): string {
2
+ return `${rowKey}:${colKey}`
3
+ }
4
+
5
+ export function parseCellKey(cellKey: string): { rowKey: string; colKey: string } {
6
+ const index = cellKey.indexOf(':')
7
+ if (index < 0) return { rowKey: cellKey, colKey: '' }
8
+ return {
9
+ rowKey: cellKey.slice(0, index),
10
+ colKey: cellKey.slice(index + 1),
11
+ }
12
+ }