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