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,239 @@
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { RsTableColumn } from '../table-utils'
4
+ import RsCheckbox from '../RsCheckbox.vue'
5
+ import RsTableCellEditor from './RsTableCellEditor.vue'
6
+ import RsTableCellContent from './RsTableCellContent'
7
+ import {
8
+ booleanToEditText,
9
+ coerceBoolean,
10
+ isBooleanToggleColumn,
11
+ isColumnEditable,
12
+ isColumnNullable,
13
+ isNullDraft,
14
+ nullToEditText,
15
+ resolveCellEditText,
16
+ resolveColumnCommitOn,
17
+ resolveColumnDisplayContent,
18
+ resolveColumnEditorOptions,
19
+ resolveColumnEditTrigger,
20
+ resolveColumnRawValue,
21
+ resolveColumnValueType,
22
+ type RsTableCellCommitTrigger,
23
+ type RsTableCellEditFocusMode,
24
+ type RsTableCellEditTrigger,
25
+ type RsTableCellNavigateDirection,
26
+ } from './table-edit-utils'
27
+
28
+ const props = withDefaults(
29
+ defineProps<{
30
+ column: RsTableColumn<T>
31
+ row: T
32
+ rowIndex: number
33
+ rowKey: string
34
+ tableEditable?: boolean
35
+ editing?: boolean
36
+ dirty?: boolean
37
+ focused?: boolean
38
+ /** 编辑草稿(string);number 列也在此以文本承载,commit 时再 parse */
39
+ draft?: string
40
+ hasCustomSlot?: boolean
41
+ hasEditSlot?: boolean
42
+ editTrigger?: RsTableCellEditTrigger
43
+ rowCommit?: boolean
44
+ allowNull?: boolean
45
+ focusMode?: RsTableCellEditFocusMode
46
+ nullLabel?: string
47
+ errorMessage?: string | null
48
+ validating?: boolean
49
+ }>(),
50
+ {
51
+ tableEditable: false,
52
+ editing: false,
53
+ dirty: false,
54
+ focused: false,
55
+ hasCustomSlot: false,
56
+ hasEditSlot: false,
57
+ editTrigger: 'dblclick',
58
+ rowCommit: false,
59
+ allowNull: true,
60
+ focusMode: 'end',
61
+ nullLabel: '(NULL)',
62
+ errorMessage: null,
63
+ validating: false,
64
+ },
65
+ )
66
+
67
+ const emit = defineEmits<{
68
+ startEdit: []
69
+ commit: [value: string]
70
+ cancel: []
71
+ updateDraft: [value: string]
72
+ navigate: [direction: RsTableCellNavigateDirection]
73
+ }>()
74
+
75
+ const localDraft = ref('')
76
+
77
+ const editable = computed(() =>
78
+ isColumnEditable(props.column, props.row, props.rowIndex, props.tableEditable),
79
+ )
80
+
81
+ const valueType = computed(() => resolveColumnValueType(props.column))
82
+ const editorOptions = computed(() =>
83
+ resolveColumnEditorOptions(props.column, props.row, props.rowIndex),
84
+ )
85
+ const isBoolean = computed(() => isBooleanToggleColumn(props.column))
86
+ const nullable = computed(() => isColumnNullable(props.column, props.allowNull))
87
+ const resolvedEditTrigger = computed(() =>
88
+ resolveColumnEditTrigger(props.column, props.editTrigger),
89
+ )
90
+ const resolvedCommitOn = computed((): RsTableCellCommitTrigger =>
91
+ resolveColumnCommitOn(props.column, props.rowCommit),
92
+ )
93
+
94
+ const booleanChecked = computed(() => {
95
+ if (props.draft !== undefined) {
96
+ if (isNullDraft(props.draft)) return false
97
+ return coerceBoolean(props.draft)
98
+ }
99
+ const raw = resolveColumnRawValue(props.row, props.column, props.rowIndex)
100
+ if (raw == null) return false
101
+ return coerceBoolean(raw)
102
+ })
103
+
104
+ const isNullDisplay = computed(() => {
105
+ if (props.draft !== undefined) return isNullDraft(props.draft)
106
+ if (props.column.render) return false
107
+ const raw = resolveColumnRawValue(props.row, props.column, props.rowIndex)
108
+ return raw === null || raw === undefined
109
+ })
110
+
111
+ const displayContent = computed(() =>
112
+ resolveColumnDisplayContent(props.row, props.column, props.rowIndex, {
113
+ draft: props.draft,
114
+ nullLabel: props.nullLabel,
115
+ }),
116
+ )
117
+
118
+ watch(
119
+ () => props.editing,
120
+ (editing) => {
121
+ if (!editing) return
122
+ localDraft.value = resolveCellEditText(
123
+ props.row,
124
+ props.column,
125
+ props.rowIndex,
126
+ props.draft,
127
+ )
128
+ },
129
+ { immediate: true },
130
+ )
131
+
132
+ function onDisplayClick(): void {
133
+ if (!editable.value || isBoolean.value) return
134
+ if (resolvedEditTrigger.value === 'click') emit('startEdit')
135
+ }
136
+
137
+ function onDisplayDblClick(event: MouseEvent): void {
138
+ if (!editable.value || isBoolean.value) return
139
+ if (resolvedEditTrigger.value !== 'dblclick') return
140
+ event.stopPropagation()
141
+ emit('startEdit')
142
+ }
143
+
144
+ function onBooleanToggle(event: Event): void {
145
+ event.preventDefault()
146
+ event.stopPropagation()
147
+ if (!editable.value) return
148
+ emit('commit', booleanToEditText(!booleanChecked.value))
149
+ }
150
+
151
+ function onEditorUpdate(value: string): void {
152
+ localDraft.value = value
153
+ emit('updateDraft', value)
154
+ }
155
+
156
+ function onEditorCommit(): void {
157
+ emit('commit', localDraft.value)
158
+ }
159
+
160
+ function onEditorCancel(): void {
161
+ emit('cancel')
162
+ }
163
+
164
+ function onEditorNavigate(direction: RsTableCellNavigateDirection): void {
165
+ emit('navigate', direction)
166
+ }
167
+
168
+ function onEditorSetNull(): void {
169
+ localDraft.value = nullToEditText()
170
+ emit('updateDraft', localDraft.value)
171
+ }
172
+ </script>
173
+
174
+ <template>
175
+ <RsCheckbox
176
+ v-if="!editing && isBoolean && editable"
177
+ class="rs-table__cell-check"
178
+ :class="{
179
+ 'rs-table__cell-check--dirty': dirty,
180
+ 'rs-table__cell-check--invalid': !!errorMessage,
181
+ }"
182
+ size="sm"
183
+ :model-value="booleanChecked"
184
+ :aria-label="column.title"
185
+ @click.prevent.stop="onBooleanToggle"
186
+ @dblclick.stop
187
+ />
188
+ <span
189
+ v-else-if="!editing"
190
+ class="rs-table__cell-body"
191
+ :class="{
192
+ 'rs-table__ellipsis-text': column.ellipsis,
193
+ 'rs-table__cell-body--editable': editable,
194
+ 'rs-table__cell-body--readonly': tableEditable && !editable,
195
+ 'rs-table__cell-body--dirty': dirty,
196
+ 'rs-table__cell-body--null': isNullDisplay,
197
+ 'rs-table__cell-body--invalid': !!errorMessage,
198
+ 'rs-table__cell-body--focused': focused && !editing,
199
+ }"
200
+ :title="errorMessage || undefined"
201
+ :aria-readonly="tableEditable && !editable ? 'true' : undefined"
202
+ :aria-invalid="errorMessage ? 'true' : undefined"
203
+ @click="onDisplayClick"
204
+ @dblclick="onDisplayDblClick"
205
+ >
206
+ <slot v-if="hasCustomSlot" />
207
+ <RsTableCellContent v-else :content="displayContent" />
208
+ </span>
209
+ <div v-else-if="hasEditSlot" class="rs-table-cell-editor rs-table-cell-editor--custom">
210
+ <slot
211
+ name="editor"
212
+ :draft="localDraft"
213
+ :row="row"
214
+ :column="column"
215
+ :index="rowIndex"
216
+ :error="errorMessage"
217
+ :update="onEditorUpdate"
218
+ :commit="onEditorCommit"
219
+ :cancel="onEditorCancel"
220
+ />
221
+ </div>
222
+ <RsTableCellEditor
223
+ v-else
224
+ v-model="localDraft"
225
+ :value-type="valueType"
226
+ :editor-options="editorOptions"
227
+ size="sm"
228
+ :commit-on="resolvedCommitOn"
229
+ :focus-mode="focusMode"
230
+ :allow-null="nullable"
231
+ :error-message="errorMessage"
232
+ :validating="validating"
233
+ @update:model-value="onEditorUpdate"
234
+ @commit="onEditorCommit"
235
+ @cancel="onEditorCancel"
236
+ @navigate="onEditorNavigate"
237
+ @set-null="onEditorSetNull"
238
+ />
239
+ </template>
@@ -0,0 +1,18 @@
1
+ import { defineComponent, type PropType, type VNodeChild } from 'vue'
2
+
3
+ /**
4
+ * 渲染单元格 `VNodeChild`(string / number / VNode / 数组等)。
5
+ * 模板插值会对对象走 String(),VNode 必须经渲染函数输出。
6
+ */
7
+ export default defineComponent({
8
+ name: 'RsTableCellContent',
9
+ props: {
10
+ content: {
11
+ type: [String, Number, Boolean, Object, Array, Function] as PropType<VNodeChild>,
12
+ default: null,
13
+ },
14
+ },
15
+ setup(props) {
16
+ return () => props.content as VNodeChild
17
+ },
18
+ })
@@ -0,0 +1,351 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, ref, useId, watch } from 'vue'
3
+ import type { RsComponentSize } from '../../theme/types'
4
+ import { useRsI18n } from '../../composables/useRsI18n'
5
+ import RsDatePicker from '../RsDatePicker.vue'
6
+ import RsInput from '../RsInput.vue'
7
+ import RsInputNumber from '../RsInputNumber.vue'
8
+ import RsSelect from '../RsSelect.vue'
9
+ import type { RsSelectOptions } from '../select-utils'
10
+ import type { RsTableColumnEditorOptionsResolved, RsTableCellValueType } from '../table-utils'
11
+ import {
12
+ applyFocusMode,
13
+ isNullDraft,
14
+ nullToEditText,
15
+ resolveCellEditorInputType,
16
+ usesOverlayEditor,
17
+ type RsTableCellCommitTrigger,
18
+ type RsTableCellEditFocusMode,
19
+ type RsTableCellNavigateDirection,
20
+ } from './table-edit-utils'
21
+
22
+ const { t } = useRsI18n()
23
+
24
+ const props = withDefaults(
25
+ defineProps<{
26
+ valueType?: RsTableCellValueType
27
+ editorOptions?: RsTableColumnEditorOptionsResolved
28
+ size?: RsComponentSize
29
+ placeholder?: string
30
+ commitOn?: RsTableCellCommitTrigger
31
+ autofocus?: boolean
32
+ /** inline:表格单元格内紧凑编辑;field:Popover/表单场景完整输入框 */
33
+ variant?: 'inline' | 'field'
34
+ focusMode?: RsTableCellEditFocusMode
35
+ allowNull?: boolean
36
+ errorMessage?: string | null
37
+ validating?: boolean
38
+ }>(),
39
+ {
40
+ valueType: 'text',
41
+ size: 'sm',
42
+ commitOn: 'blur',
43
+ autofocus: true,
44
+ variant: 'inline',
45
+ focusMode: 'end',
46
+ allowNull: true,
47
+ errorMessage: null,
48
+ validating: false,
49
+ },
50
+ )
51
+
52
+ const model = defineModel<string>({ default: '' })
53
+
54
+ const emit = defineEmits<{
55
+ commit: []
56
+ cancel: []
57
+ navigate: [direction: RsTableCellNavigateDirection]
58
+ setNull: []
59
+ }>()
60
+
61
+ const rootRef = ref<HTMLElement | null>(null)
62
+ const textareaId = useId()
63
+ const textareaLabel = computed(() => props.placeholder || t('table.editTextarea'))
64
+ /** 仅 date 浮层:打开时抑制 blur 误提交;select 不做关闭特殊处理 */
65
+ const dateOverlayOpen = ref(false)
66
+ /** 多选 select:关闭下拉时再提交 */
67
+ const selectOverlayOpen = ref(false)
68
+ const composing = ref(false)
69
+
70
+ const inputType = computed(() => resolveCellEditorInputType(props.valueType))
71
+ const isSelect = computed(() => props.valueType === 'select')
72
+ const isMultipleSelect = computed(
73
+ () => isSelect.value && Boolean(props.editorOptions?.multiple),
74
+ )
75
+ const isDate = computed(() => props.valueType === 'date' || props.valueType === 'datetime')
76
+ const isTextarea = computed(() => props.valueType === 'textarea')
77
+ const isNumber = computed(() => props.valueType === 'number')
78
+ const isTextLike = computed(
79
+ () => !isSelect.value && !isDate.value && !isTextarea.value && !isNumber.value,
80
+ )
81
+ const isNullValue = computed(() => isNullDraft(model.value))
82
+ const resolvedFocusMode = computed(
83
+ (): RsTableCellEditFocusMode => props.editorOptions?.focusMode ?? props.focusMode,
84
+ )
85
+ const textareaRows = computed(() => props.editorOptions?.rows ?? 3)
86
+
87
+ const selectOptions = computed((): RsSelectOptions => props.editorOptions?.options ?? [])
88
+
89
+ /** 多选草稿:逗号分隔字符串 ↔ string[](与表设计索引列等约定一致) */
90
+ function parseMultiSelectDraft(text: string): string[] {
91
+ return String(text ?? '')
92
+ .split(/[,,]+/)
93
+ .map((s) => s.trim())
94
+ .filter(Boolean)
95
+ }
96
+
97
+ function joinMultiSelectDraft(values: string[]): string {
98
+ return values
99
+ .map((s) => String(s).trim())
100
+ .filter(Boolean)
101
+ .join(', ')
102
+ }
103
+
104
+ const selectModelValue = computed((): string | string[] => {
105
+ if (isNullValue.value) return isMultipleSelect.value ? [] : ''
106
+ if (isMultipleSelect.value) return parseMultiSelectDraft(model.value)
107
+ return model.value
108
+ })
109
+
110
+ watch(selectOverlayOpen, (isOpen, wasOpen) => {
111
+ if (wasOpen && !isOpen && isMultipleSelect.value) emit('commit')
112
+ })
113
+
114
+ const displayModel = computed({
115
+ get: () => (isNullDraft(model.value) ? '' : model.value),
116
+ set: (value: string) => {
117
+ model.value = value
118
+ },
119
+ })
120
+
121
+ /** number 列:表格草稿为 string,InputNumber stringMode 用 string | null */
122
+ const numberDraft = computed({
123
+ get: (): string | null => {
124
+ if (isNullDraft(model.value)) return null
125
+ return model.value === '' ? null : model.value
126
+ },
127
+ set: (value: string | null) => {
128
+ if (isNullDraft(model.value) && (value == null || value === '')) return
129
+ model.value = value == null ? '' : String(value)
130
+ },
131
+ })
132
+
133
+ async function focusEditor(): Promise<void> {
134
+ await nextTick()
135
+ if (isSelect.value || isDate.value) return
136
+ const el = rootRef.value?.querySelector('input, textarea') as
137
+ | HTMLInputElement
138
+ | HTMLTextAreaElement
139
+ | null
140
+ | undefined
141
+ if (!el) return
142
+ el.focus()
143
+ if (isNumber.value) {
144
+ applyFocusMode(el, resolvedFocusMode.value)
145
+ return
146
+ }
147
+ if (el instanceof HTMLInputElement && (el.type === 'number' || el.type === 'date' || el.type === 'datetime-local')) {
148
+ return
149
+ }
150
+ applyFocusMode(el, resolvedFocusMode.value)
151
+ }
152
+
153
+ onMounted(() => {
154
+ if (props.autofocus) void focusEditor()
155
+ })
156
+
157
+ function shouldCommitOnEnter(): boolean {
158
+ if (isTextarea.value && !composing.value) return false
159
+ return props.commitOn === 'enter' || props.commitOn === 'blur' || props.commitOn === 'manual'
160
+ }
161
+
162
+ function shouldCommitOnBlur(): boolean {
163
+ // select:change 提交,不做关闭/blur 特殊处理
164
+ if (props.valueType === 'select' || props.commitOn === 'change') return false
165
+ // 日期面板打开时不 blur 提交;关闭后也不靠 blur(面板「确认」只改草稿,Enter/Tab 再提交)
166
+ if (isDate.value) return false
167
+ if (usesOverlayEditor(props.valueType)) return false
168
+ return props.commitOn === 'blur' || props.commitOn === 'manual'
169
+ }
170
+
171
+ function shouldCommitOnChange(): boolean {
172
+ return props.commitOn === 'change'
173
+ }
174
+
175
+ function onPressEnter(event: KeyboardEvent): void {
176
+ if (event.isComposing || composing.value) return
177
+ if (isTextarea.value) {
178
+ if (event.ctrlKey || event.metaKey) {
179
+ event.preventDefault()
180
+ emit('commit')
181
+ }
182
+ return
183
+ }
184
+ event.preventDefault()
185
+ if (shouldCommitOnEnter()) emit('commit')
186
+ }
187
+
188
+ function onBlur(): void {
189
+ if (isDate.value && dateOverlayOpen.value) {
190
+ window.setTimeout(() => {
191
+ if (shouldCommitOnBlur()) emit('commit')
192
+ }, 0)
193
+ return
194
+ }
195
+ if (shouldCommitOnBlur()) emit('commit')
196
+ }
197
+
198
+ function onKeydown(event: KeyboardEvent): void {
199
+ if (event.isComposing || composing.value) {
200
+ if (event.key === 'Process') return
201
+ }
202
+ if (event.key === 'Enter') {
203
+ onPressEnter(event)
204
+ return
205
+ }
206
+ if (event.key === 'Escape') {
207
+ if (event.isComposing || composing.value) return
208
+ event.preventDefault()
209
+ emit('cancel')
210
+ return
211
+ }
212
+ if (event.key === 'Tab') {
213
+ if (event.isComposing || composing.value) return
214
+ event.preventDefault()
215
+ emit('navigate', event.shiftKey ? 'prev' : 'next')
216
+ return
217
+ }
218
+ if (props.allowNull && event.key === '0' && (event.ctrlKey || event.metaKey)) {
219
+ event.preventDefault()
220
+ model.value = nullToEditText()
221
+ emit('setNull')
222
+ emit('commit')
223
+ }
224
+ }
225
+
226
+ function onSelectUpdate(value: string | string[]): void {
227
+ const empty =
228
+ value == null || value === '' || (Array.isArray(value) && value.length === 0)
229
+ // 表格内下拉默认不可清除:仅选择;显式 clearable 时才允许清空
230
+ if (empty) {
231
+ if (!props.editorOptions?.clearable) return
232
+ model.value = props.allowNull ? nullToEditText() : ''
233
+ // 多选:关闭面板时再提交;单选 clearable 仍走 change
234
+ if (!isMultipleSelect.value && shouldCommitOnChange()) emit('commit')
235
+ return
236
+ }
237
+ if (isMultipleSelect.value) {
238
+ const list = Array.isArray(value) ? value.map(String) : [String(value)]
239
+ model.value = joinMultiSelectDraft(list)
240
+ // 多选不在每次勾选时提交,等下拉关闭
241
+ return
242
+ }
243
+ model.value = Array.isArray(value) ? (value[0] ?? '') : String(value ?? '')
244
+ if (shouldCommitOnChange()) emit('commit')
245
+ }
246
+
247
+ function onDateUpdate(value: string): void {
248
+ model.value = value || (props.allowNull ? nullToEditText() : '')
249
+ // rowCommit(manual):确认/清空时写入 staged(点外侧关闭不改值,不会进这里)
250
+ if (props.commitOn === 'manual') emit('commit')
251
+ }
252
+ </script>
253
+
254
+ <template>
255
+ <div
256
+ ref="rootRef"
257
+ class="rs-table-cell-editor"
258
+ :class="[
259
+ `rs-table-cell-editor--${size}`,
260
+ `rs-table-cell-editor--${valueType}`,
261
+ `rs-table-cell-editor--${variant}`,
262
+ {
263
+ 'rs-table-cell-editor--overlay': isSelect || isDate,
264
+ 'rs-table-cell-editor--invalid': !!errorMessage,
265
+ 'rs-table-cell-editor--null': isNullValue,
266
+ 'rs-table-cell-editor--validating': validating,
267
+ },
268
+ ]"
269
+ :title="errorMessage || undefined"
270
+ :aria-invalid="errorMessage ? 'true' : undefined"
271
+ :aria-busy="validating ? 'true' : undefined"
272
+ @click.stop
273
+ @dblclick.stop
274
+ >
275
+ <span v-if="validating" class="rs-table-cell-editor__validating" aria-hidden="true">…</span>
276
+ <span v-else-if="isNullValue && !isSelect && !isDate" class="rs-table-cell-editor__null-tag" aria-hidden="true">
277
+ NULL
278
+ </span>
279
+ <RsSelect
280
+ v-if="isSelect"
281
+ :model-value="selectModelValue"
282
+ v-model:open="selectOverlayOpen"
283
+ :options="selectOptions"
284
+ :size="size"
285
+ block
286
+ :multiple="isMultipleSelect"
287
+ :searchable="editorOptions?.searchable ?? true"
288
+ :creatable="editorOptions?.creatable ?? false"
289
+ :clearable="editorOptions?.clearable ?? false"
290
+ :placeholder="placeholder"
291
+ @update:model-value="onSelectUpdate"
292
+ @keydown="onKeydown"
293
+ />
294
+ <RsDatePicker
295
+ v-else-if="isDate"
296
+ :model-value="isNullValue ? '' : model"
297
+ v-model:open="dateOverlayOpen"
298
+ :with-time="valueType === 'datetime'"
299
+ :with-seconds="editorOptions?.withSeconds ?? valueType === 'datetime'"
300
+ :placeholder="placeholder"
301
+ @update:model-value="(v) => onDateUpdate(typeof v === 'string' ? v : '')"
302
+ />
303
+ <RsInputNumber
304
+ v-else-if="isNumber"
305
+ v-model="numberDraft"
306
+ string-mode
307
+ :size="size"
308
+ :placeholder="placeholder"
309
+ :disabled="validating"
310
+ :min="editorOptions?.min"
311
+ :max="editorOptions?.max"
312
+ :step="editorOptions?.step ?? 1"
313
+ :precision="editorOptions?.precision"
314
+ :controls="editorOptions?.controls ?? variant === 'field'"
315
+ :keyboard="true"
316
+ @press-enter="onPressEnter"
317
+ @blur="onBlur"
318
+ @keydown="onKeydown"
319
+ />
320
+ <template v-else-if="isTextarea">
321
+ <label class="rs-table-cell-editor__sr-label" :for="textareaId">
322
+ {{ textareaLabel }}
323
+ </label>
324
+ <textarea
325
+ :id="textareaId"
326
+ v-model="displayModel"
327
+ class="rs-table-cell-editor__textarea"
328
+ :rows="textareaRows"
329
+ :placeholder="placeholder"
330
+ :disabled="validating"
331
+ @keydown="onKeydown"
332
+ @blur="onBlur"
333
+ @compositionstart="composing = true"
334
+ @compositionend="composing = false"
335
+ />
336
+ </template>
337
+ <RsInput
338
+ v-else-if="isTextLike"
339
+ v-model="displayModel"
340
+ :type="inputType === 'textarea' ? 'text' : inputType"
341
+ :size="size"
342
+ :placeholder="placeholder"
343
+ :disabled="validating"
344
+ @press-enter="onPressEnter"
345
+ @blur="onBlur"
346
+ @keydown="onKeydown"
347
+ @compositionstart="composing = true"
348
+ @compositionend="composing = false"
349
+ />
350
+ </div>
351
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import RsIcon from '../RsIcon.vue'
3
+
4
+ defineProps<{
5
+ lineNumber: number
6
+ pending: boolean
7
+ commitLabel?: string
8
+ commitHint?: string
9
+ rollbackLabel?: string
10
+ showRollback?: boolean
11
+ }>()
12
+
13
+ const emit = defineEmits<{
14
+ commit: []
15
+ rollback: []
16
+ }>()
17
+ </script>
18
+
19
+ <template>
20
+ <div
21
+ class="rs-table-edit-gutter"
22
+ :class="{ 'rs-table-edit-gutter--pending': pending }"
23
+ >
24
+ <span
25
+ class="rs-table-edit-gutter__line"
26
+ :class="{ 'rs-table-edit-gutter__line--hidden': pending }"
27
+ aria-hidden="true"
28
+ >
29
+ {{ lineNumber }}
30
+ </span>
31
+ <div v-if="pending" class="rs-table-edit-gutter__actions">
32
+ <button
33
+ type="button"
34
+ class="rs-table-edit-gutter__commit"
35
+ :aria-label="commitLabel"
36
+ :title="commitLabel"
37
+ @click.stop="emit('commit')"
38
+ >
39
+ <RsIcon name="check" :size="12" />
40
+ <span class="rs-table-edit-gutter__commit-hint">{{ commitHint ?? commitLabel }}</span>
41
+ </button>
42
+ <button
43
+ v-if="showRollback"
44
+ type="button"
45
+ class="rs-table-edit-gutter__rollback"
46
+ :aria-label="rollbackLabel"
47
+ :title="rollbackLabel"
48
+ @click.stop="emit('rollback')"
49
+ >
50
+ <RsIcon name="x" :size="12" />
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </template>