niuma-ui 1.0.1 → 1.1.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 (190) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/package.json +10 -2
  3. package/src/__tests__/RsButton.spec.ts +47 -0
  4. package/src/__tests__/RsCard.spec.ts +160 -1
  5. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  6. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  7. package/src/__tests__/RsDatePicker.spec.ts +21 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +178 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +250 -6
  14. package/src/__tests__/RsFormItem.spec.ts +478 -0
  15. package/src/__tests__/RsFormList.spec.ts +109 -0
  16. package/src/__tests__/RsInput.spec.ts +108 -1
  17. package/src/__tests__/RsInputNumber.spec.ts +33 -0
  18. package/src/__tests__/RsMenu.spec.ts +406 -14
  19. package/src/__tests__/RsPagination.spec.ts +67 -2
  20. package/src/__tests__/RsSelect.spec.ts +465 -2
  21. package/src/__tests__/RsSwitch.spec.ts +74 -0
  22. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  23. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  24. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  25. package/src/__tests__/RsTable.spec.ts +63 -8
  26. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  27. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  28. package/src/__tests__/RsTabs.spec.ts +102 -4
  29. package/src/__tests__/RsTimePicker.spec.ts +20 -0
  30. package/src/__tests__/RsTooltip.spec.ts +43 -0
  31. package/src/__tests__/RsUpload.spec.ts +62 -6
  32. package/src/__tests__/createRsDialog.spec.ts +145 -0
  33. package/src/__tests__/css-token.spec.ts +51 -0
  34. package/src/__tests__/date-picker-utils.spec.ts +49 -0
  35. package/src/__tests__/dialog-utils.spec.ts +43 -0
  36. package/src/__tests__/form-path.spec.ts +46 -0
  37. package/src/__tests__/form-rules.spec.ts +139 -0
  38. package/src/__tests__/perf-budgets.ts +32 -0
  39. package/src/__tests__/rs-table-api.spec.ts +206 -0
  40. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  41. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  42. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  43. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  44. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  45. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  46. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  47. package/src/__tests__/table-utils.spec.ts +124 -0
  48. package/src/__tests__/theme.spec.ts +27 -0
  49. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  50. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  51. package/src/components/RsAlert.vue +212 -0
  52. package/src/components/RsAvatar.vue +1 -1
  53. package/src/components/RsBadge.vue +1 -1
  54. package/src/components/RsBreadcrumb.vue +1 -1
  55. package/src/components/RsButton.vue +147 -23
  56. package/src/components/RsCalendarGrid.vue +3 -3
  57. package/src/components/RsCard.vue +204 -20
  58. package/src/components/RsCodeBlock.vue +4 -4
  59. package/src/components/RsCodeEditor.vue +8 -8
  60. package/src/components/RsConfirmDialog.vue +217 -30
  61. package/src/components/RsContextMenu.vue +57 -17
  62. package/src/components/RsDatePicker.vue +316 -39
  63. package/src/components/RsDateTimePicker.vue +27 -2
  64. package/src/components/RsDescriptions.vue +186 -0
  65. package/src/components/RsDescriptionsItem.vue +32 -0
  66. package/src/components/RsDialog.vue +374 -52
  67. package/src/components/RsDivider.vue +89 -0
  68. package/src/components/RsDrawer.vue +559 -34
  69. package/src/components/RsDropdown.vue +34 -3
  70. package/src/components/RsDynamicTags.vue +531 -0
  71. package/src/components/RsEmpty.vue +1 -1
  72. package/src/components/RsForm.vue +164 -8
  73. package/src/components/RsFormItem.vue +423 -0
  74. package/src/components/RsFormItemControl.vue +21 -0
  75. package/src/components/RsFormList.vue +117 -0
  76. package/src/components/RsInput.vue +491 -100
  77. package/src/components/RsInputNumber.vue +77 -27
  78. package/src/components/RsLabel.vue +20 -5
  79. package/src/components/RsLink.vue +1 -1
  80. package/src/components/RsLoadingBar.vue +151 -0
  81. package/src/components/RsMarkdown.vue +5 -5
  82. package/src/components/RsMenu.vue +74 -16
  83. package/src/components/RsMenuItems.vue +147 -9
  84. package/src/components/RsMonacoEditor.vue +9 -3
  85. package/src/components/RsPagination.vue +93 -14
  86. package/src/components/RsProseEditor.vue +3 -3
  87. package/src/components/RsRadio.vue +81 -0
  88. package/src/components/RsRadioItem.vue +156 -0
  89. package/src/components/RsSelect.vue +372 -642
  90. package/src/components/RsSidebarGroup.vue +1 -1
  91. package/src/components/RsSplitPane.vue +90 -1
  92. package/src/components/RsStatCard.vue +2 -2
  93. package/src/components/RsSteps.vue +2 -2
  94. package/src/components/RsSwitch.vue +192 -0
  95. package/src/components/RsTable.vue +653 -2555
  96. package/src/components/RsTabs.vue +422 -90
  97. package/src/components/RsTag.vue +191 -0
  98. package/src/components/RsTerminal.vue +58 -19
  99. package/src/components/RsTimePicker.vue +72 -9
  100. package/src/components/RsTimePickerColumns.vue +137 -78
  101. package/src/components/RsToaster.vue +64 -24
  102. package/src/components/RsTooltip.vue +10 -1
  103. package/src/components/RsTooltipProvider.vue +11 -1
  104. package/src/components/RsTree.vue +45 -27
  105. package/src/components/RsUpload.vue +147 -14
  106. package/src/components/RsVNodeHost.vue +20 -0
  107. package/src/components/button-utils.ts +39 -0
  108. package/src/components/context-menu-utils.ts +6 -0
  109. package/src/components/date-picker-utils.ts +195 -4
  110. package/src/components/descriptions-utils.ts +24 -0
  111. package/src/components/dialog-utils.ts +182 -0
  112. package/src/components/dialog-window.ts +74 -9
  113. package/src/components/drawer-utils.ts +73 -0
  114. package/src/components/form-item-control.ts +55 -0
  115. package/src/components/form-path.ts +92 -0
  116. package/src/components/form-rules.ts +311 -0
  117. package/src/components/form-utils.ts +145 -3
  118. package/src/components/menu-utils.ts +11 -0
  119. package/src/components/overlay-utils.ts +3 -0
  120. package/src/components/radio-utils.ts +12 -0
  121. package/src/components/reka.ts +5 -0
  122. package/src/components/select-utils.ts +285 -6
  123. package/src/components/split-pane-utils.ts +65 -12
  124. package/src/components/table/RsTableBody.vue +217 -0
  125. package/src/components/table/RsTableBodyRow.vue +90 -1
  126. package/src/components/table/RsTableColGroup.vue +41 -0
  127. package/src/components/table/RsTableHeader.vue +158 -0
  128. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  129. package/src/components/table/RsTableSummaryRow.vue +52 -0
  130. package/src/components/table/rs-table-api.ts +99 -0
  131. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  132. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  133. package/src/components/table/rs-table-feature-host.ts +178 -0
  134. package/src/components/table/rs-table-grid-nav.ts +122 -0
  135. package/src/components/table/rs-table-header-types.ts +47 -0
  136. package/src/components/table/rs-table-module-registry.ts +72 -0
  137. package/src/components/table/rs-table-props.ts +274 -0
  138. package/src/components/table/rs-table-view-context.ts +258 -0
  139. package/src/components/table/rs-table.css +85 -7
  140. package/src/components/table/table-chart-utils.ts +165 -0
  141. package/src/components/table/table-features.ts +284 -0
  142. package/src/components/table/table-summary-utils.ts +194 -0
  143. package/src/components/table-utils.ts +452 -2
  144. package/src/components/tabs-utils.ts +142 -2
  145. package/src/components/time-picker-utils.ts +76 -3
  146. package/src/components/upload-utils.ts +122 -0
  147. package/src/components/use-rs-select.ts +436 -0
  148. package/src/components/virtual-list-utils.ts +15 -2
  149. package/src/composables/assembleRsTableApi.ts +79 -0
  150. package/src/composables/bindRsTableViewContext.ts +269 -0
  151. package/src/composables/createRsDialog.ts +357 -0
  152. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  153. package/src/composables/useRsConfig.ts +10 -3
  154. package/src/composables/useRsI18n.ts +16 -5
  155. package/src/composables/useRsLoadingBar.ts +36 -0
  156. package/src/composables/useRsTable.ts +134 -0
  157. package/src/composables/useRsTableA11y.ts +52 -0
  158. package/src/composables/useRsTableChartBridge.ts +79 -0
  159. package/src/composables/useRsTableColumnLayout.ts +320 -0
  160. package/src/composables/useRsTableColumnResize.ts +244 -0
  161. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  162. package/src/composables/useRsTableColumns.ts +200 -0
  163. package/src/composables/useRsTableContextMenu.ts +257 -0
  164. package/src/composables/useRsTableCore.ts +256 -0
  165. package/src/composables/useRsTableEditActions.ts +606 -0
  166. package/src/composables/useRsTableEditLayer.ts +251 -0
  167. package/src/composables/useRsTableEngine.ts +357 -0
  168. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  169. package/src/composables/useRsTableHeadless.ts +137 -0
  170. package/src/composables/useRsTableInteraction.ts +433 -0
  171. package/src/composables/useRsTableScrollHost.ts +75 -0
  172. package/src/composables/useRsTableScrollLayout.ts +255 -0
  173. package/src/composables/useRsTableSelectionSource.ts +89 -0
  174. package/src/composables/useRsTableShell.ts +416 -0
  175. package/src/composables/useRsTableShellChrome.ts +170 -0
  176. package/src/composables/useRsTableSummary.ts +113 -0
  177. package/src/composables/useRsTableViewProvide.ts +263 -0
  178. package/src/composables/useRsTableVirtual.ts +154 -0
  179. package/src/composables/useRsTabsNav.ts +4 -0
  180. package/src/composables/useTableEdit.ts +1 -1
  181. package/src/index.ts +378 -10
  182. package/src/locale/interpolate.ts +32 -0
  183. package/src/locale/messages.ts +40 -0
  184. package/src/styles.css +365 -56
  185. package/src/theme/brand.example.css +24 -2
  186. package/src/theme/css-token.ts +107 -0
  187. package/src/theme/presets.ts +4 -0
  188. package/src/theme/types.ts +68 -0
  189. package/src/components/RsTableColgroup.vue +0 -17
  190. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -58,6 +58,11 @@ export {
58
58
  PopoverPortal,
59
59
  PopoverRoot,
60
60
  PopoverTrigger,
61
+ RadioGroupIndicator,
62
+ RadioGroupItem,
63
+ RadioGroupRoot,
64
+ SwitchRoot,
65
+ SwitchThumb,
61
66
  PaginationEllipsis,
62
67
  PaginationFirst,
63
68
  PaginationLast,
@@ -1,7 +1,24 @@
1
+ export type RsSelectValue = string | number
2
+
3
+ /** 过滤 / 回显比对字段名,默认 label;可传 value 或 fieldNames 映射后的键 */
4
+ export type RsSelectOptionFilterProp = string
5
+
6
+ export type RsSelectPlacement = 'top' | 'bottom' | 'left' | 'right'
7
+
8
+ export type RsSelectStatus = 'error' | 'warning' | ''
9
+
10
+ /** 自定义过滤(对齐 Ant Design filterOption)。返回 false 则隐藏该项。 */
11
+ export type RsSelectFilterOption = (query: string, option: RsSelectOption) => boolean
12
+
13
+ /** 过滤后排序(对齐 Ant Design filterSort) */
14
+ export type RsSelectFilterSort = (a: RsSelectOption, b: RsSelectOption) => number
15
+
1
16
  export interface RsSelectOption {
2
17
  label: string
3
- value: string
18
+ value: RsSelectValue
4
19
  disabled?: boolean
20
+ /** 原生 title;悬停展示补充说明 */
21
+ title?: string
5
22
  }
6
23
 
7
24
  export interface RsSelectOptionGroup {
@@ -11,6 +28,51 @@ export interface RsSelectOptionGroup {
11
28
 
12
29
  export type RsSelectOptions = ReadonlyArray<RsSelectOption | RsSelectOptionGroup>
13
30
 
31
+ /** 未正规化的选项(配合 fieldNames 可用 name/id/children 等业务字段) */
32
+ export type RsSelectOptionInput = RsSelectOption | RsSelectOptionGroup | Record<string, unknown>
33
+
34
+ export type RsSelectOptionsInput = ReadonlyArray<RsSelectOptionInput>
35
+
36
+ /** 对齐 Ant Design fieldNames */
37
+ export interface RsSelectFieldNames {
38
+ label?: string
39
+ value?: string
40
+ options?: string
41
+ disabled?: string
42
+ groupLabel?: string
43
+ }
44
+
45
+ export type RsSelectGetPopupContainer = (triggerNode?: HTMLElement) => string | HTMLElement | undefined
46
+
47
+ /** labelInValue 时的 v-model 项(对齐 Ant Design) */
48
+ export interface RsSelectLabeledValue {
49
+ label: string
50
+ value: RsSelectValue
51
+ }
52
+
53
+ export type RsSelectModelValue =
54
+ | RsSelectValue
55
+ | RsSelectValue[]
56
+ | RsSelectLabeledValue
57
+ | RsSelectLabeledValue[]
58
+ | ''
59
+
60
+ /**
61
+ * Reka ComboboxItem 禁止 value 为空串(空串表示未选中 / placeholder)。
62
+ * 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
63
+ */
64
+ export const RS_SELECT_EMPTY_VALUE = '__rs_select_empty__'
65
+
66
+ /** 业务 value → ComboboxItem token(空串走哨兵;数字转为十进制字符串) */
67
+ export function toComboboxValue(value: RsSelectValue): string {
68
+ return value === '' ? RS_SELECT_EMPTY_VALUE : String(value)
69
+ }
70
+
71
+ /** ComboboxItem token → 业务字符串(仅还原空串哨兵,不恢复 number) */
72
+ export function fromComboboxValue(value: string): string {
73
+ return value === RS_SELECT_EMPTY_VALUE ? '' : value
74
+ }
75
+
14
76
  export function isSelectOptionGroup(
15
77
  item: RsSelectOption | RsSelectOptionGroup,
16
78
  ): item is RsSelectOptionGroup {
@@ -29,10 +91,40 @@ export function flattenSelectOptions(options: RsSelectOptions): RsSelectOption[]
29
91
  return result
30
92
  }
31
93
 
94
+ export function findSelectOption(
95
+ options: RsSelectOptions,
96
+ token: string,
97
+ ): RsSelectOption | undefined {
98
+ const raw = fromComboboxValue(token)
99
+ return flattenSelectOptions(options).find((opt) => String(opt.value) === String(raw))
100
+ }
101
+
102
+ /**
103
+ * Combobox token → 业务 value。
104
+ * 优先返回 options 里的原始类型(number 0 保持为 0);手输/未知项保持字符串。
105
+ */
106
+ export function restoreSelectValue(
107
+ token: string,
108
+ options: RsSelectOptions,
109
+ ): RsSelectValue {
110
+ const matched = findSelectOption(options, token)
111
+ if (matched) return matched.value
112
+ return fromComboboxValue(token)
113
+ }
114
+
115
+ export function restoreSelectValues(
116
+ tokens: string[],
117
+ options: RsSelectOptions,
118
+ ): RsSelectValue[] {
119
+ return tokens.map((token) => restoreSelectValue(token, options))
120
+ }
121
+
32
122
  export function buildOptionLabelMap(options: RsSelectOptions): Map<string, string> {
33
123
  const map = new Map<string, string>()
34
124
  for (const opt of flattenSelectOptions(options)) {
35
- map.set(opt.value, opt.label)
125
+ const key = toComboboxValue(opt.value)
126
+ map.set(key, opt.label)
127
+ if (opt.value === '') map.set('', opt.label)
36
128
  }
37
129
  return map
38
130
  }
@@ -40,27 +132,130 @@ export function buildOptionLabelMap(options: RsSelectOptions): Map<string, strin
40
132
  export function buildOptionDisabledMap(options: RsSelectOptions): Map<string, boolean> {
41
133
  const map = new Map<string, boolean>()
42
134
  for (const opt of flattenSelectOptions(options)) {
43
- map.set(opt.value, Boolean(opt.disabled))
135
+ const key = toComboboxValue(opt.value)
136
+ map.set(key, Boolean(opt.disabled))
44
137
  }
45
138
  return map
46
139
  }
47
140
 
141
+ function optionFieldText(option: RsSelectOption, prop: string): string {
142
+ if (prop === 'value') return String(option.value)
143
+ if (prop === 'label' || !prop) return String(option.label)
144
+ const extra = Reflect.get(option, prop)
145
+ return extra == null ? String(option.label) : String(extra)
146
+ }
147
+
148
+ function defaultMatchOption(
149
+ option: RsSelectOption,
150
+ query: string,
151
+ contains: (text: string, search: string) => boolean,
152
+ optionFilterProp: RsSelectOptionFilterProp,
153
+ ): boolean {
154
+ return contains(optionFieldText(option, optionFilterProp), query)
155
+ }
156
+
157
+ function coerceSelectValue(raw: unknown): RsSelectValue {
158
+ if (typeof raw === 'number' || typeof raw === 'string') return raw
159
+ if (raw == null) return ''
160
+ return String(raw)
161
+ }
162
+
163
+ function mapSelectOption(
164
+ raw: Record<string, unknown>,
165
+ names: Required<Pick<RsSelectFieldNames, 'label' | 'value' | 'disabled'>>,
166
+ ): RsSelectOption {
167
+ const value = coerceSelectValue(raw[names.value])
168
+ const label = raw[names.label]
169
+ return {
170
+ ...raw,
171
+ value,
172
+ label: label == null ? String(value) : String(label),
173
+ disabled: Boolean(raw[names.disabled]),
174
+ title: typeof raw.title === 'string' ? raw.title : undefined,
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 按 fieldNames 把业务选项正规化为 { label, value, disabled }。
180
+ * 默认键即为 label / value / options,既有调用无需改。
181
+ */
182
+ export function normalizeSelectOptions(
183
+ options: RsSelectOptionsInput,
184
+ fieldNames?: RsSelectFieldNames,
185
+ ): RsSelectOptions {
186
+ const labelKey = fieldNames?.label ?? 'label'
187
+ const valueKey = fieldNames?.value ?? 'value'
188
+ const optionsKey = fieldNames?.options ?? 'options'
189
+ const disabledKey = fieldNames?.disabled ?? 'disabled'
190
+ const groupLabelKey = fieldNames?.groupLabel ?? labelKey
191
+ const result: Array<RsSelectOption | RsSelectOptionGroup> = []
192
+
193
+ for (const item of options) {
194
+ if (!item || typeof item !== 'object') continue
195
+ const raw = item as Record<string, unknown>
196
+ const nested = raw[optionsKey]
197
+ if (Array.isArray(nested)) {
198
+ const children: RsSelectOption[] = []
199
+ for (const child of nested) {
200
+ if (!child || typeof child !== 'object') continue
201
+ children.push(mapSelectOption(child as Record<string, unknown>, {
202
+ label: labelKey,
203
+ value: valueKey,
204
+ disabled: disabledKey,
205
+ }))
206
+ }
207
+ result.push({
208
+ label: String(raw[groupLabelKey] ?? ''),
209
+ options: children,
210
+ })
211
+ continue
212
+ }
213
+ result.push(mapSelectOption(raw, { label: labelKey, value: valueKey, disabled: disabledKey }))
214
+ }
215
+ return result
216
+ }
217
+
218
+ /** 触发器 / tag 展示文案,对齐 Ant optionLabelProp */
219
+ export function optionDisplayLabel(option: RsSelectOption, prop = 'label'): string {
220
+ return optionFieldText(option, prop)
221
+ }
222
+
223
+ export function comboboxBindingToTokens(value: unknown, multiple: boolean): string[] {
224
+ if (multiple) return Array.isArray(value) ? value.map(String) : []
225
+ if (value == null || value === '') return []
226
+ return [String(value)]
227
+ }
228
+
229
+ export function selectModelEntries(value: unknown): unknown[] {
230
+ if (Array.isArray(value)) return value
231
+ if (value === '' || value == null) return []
232
+ return [value]
233
+ }
234
+
48
235
  export function filterSelectOptions(
49
236
  options: RsSelectOptions,
50
237
  query: string,
51
238
  contains: (text: string, search: string) => boolean,
239
+ filterOption?: RsSelectFilterOption | boolean,
240
+ optionFilterProp: RsSelectOptionFilterProp = 'label',
52
241
  ): RsSelectOptions {
242
+ if (filterOption === false) return options
53
243
  const trimmed = query.trim()
54
244
  if (!trimmed) return options
55
245
 
246
+ const match =
247
+ typeof filterOption === 'function'
248
+ ? (opt: RsSelectOption) => filterOption(trimmed, opt)
249
+ : (opt: RsSelectOption) => defaultMatchOption(opt, trimmed, contains, optionFilterProp)
250
+
56
251
  const result: Array<RsSelectOption | RsSelectOptionGroup> = []
57
252
  for (const item of options) {
58
253
  if (isSelectOptionGroup(item)) {
59
- const filtered = item.options.filter((opt) => contains(opt.label, trimmed))
254
+ const filtered = item.options.filter(match)
60
255
  if (filtered.length > 0) {
61
256
  result.push({ label: item.label, options: filtered })
62
257
  }
63
- } else if (contains(item.label, trimmed)) {
258
+ } else if (match(item)) {
64
259
  result.push(item)
65
260
  }
66
261
  }
@@ -68,5 +263,89 @@ export function filterSelectOptions(
68
263
  }
69
264
 
70
265
  export function flattenSelectValues(options: RsSelectOptions): string[] {
71
- return flattenSelectOptions(options).map((opt) => opt.value)
266
+ return flattenSelectOptions(options).map((opt) => toComboboxValue(opt.value))
267
+ }
268
+
269
+ export function toSelectedTokens(value: unknown, multiple: boolean): string[] {
270
+ const entries = multiple
271
+ ? selectModelEntries(value)
272
+ : Array.isArray(value) || value === '' || value == null
273
+ ? []
274
+ : [value]
275
+ const tokens: string[] = []
276
+ for (const entry of entries) {
277
+ const raw = unwrapSelectEntry(entry)
278
+ if (raw === null) continue
279
+ tokens.push(toComboboxValue(raw))
280
+ }
281
+ return tokens
282
+ }
283
+
284
+ export function isSelectLabeledValue(value: unknown): value is RsSelectLabeledValue {
285
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && 'value' in value
286
+ }
287
+
288
+ export function unwrapSelectEntry(item: unknown): RsSelectValue | null {
289
+ if (item == null || item === '') return null
290
+ if (isSelectLabeledValue(item)) return item.value
291
+ if (typeof item === 'string' || typeof item === 'number') return item
292
+ return null
293
+ }
294
+
295
+ export function toLabeledValue(
296
+ value: RsSelectValue,
297
+ options: RsSelectOptions,
298
+ ): RsSelectLabeledValue {
299
+ const matched = flattenSelectOptions(options).find(
300
+ (opt) => String(opt.value) === String(value),
301
+ )
302
+ return {
303
+ value: matched ? matched.value : value,
304
+ label: matched?.label ?? String(value),
305
+ }
306
+ }
307
+
308
+ export function packSelectModel(
309
+ values: RsSelectValue[],
310
+ options: RsSelectOptions,
311
+ multiple: boolean,
312
+ labelInValue: boolean,
313
+ ): RsSelectModelValue {
314
+ if (multiple) {
315
+ return labelInValue ? values.map((value) => toLabeledValue(value, options)) : values
316
+ }
317
+ const first = values[0]
318
+ if (first === undefined) return ''
319
+ return labelInValue ? toLabeledValue(first, options) : first
320
+ }
321
+
322
+ export function sortSelectOptions(
323
+ options: RsSelectOptions,
324
+ sort?: RsSelectFilterSort,
325
+ ): RsSelectOptions {
326
+ if (!sort) return options
327
+ const next: Array<RsSelectOption | RsSelectOptionGroup> = []
328
+ const flats: RsSelectOption[] = []
329
+ const flushFlats = () => {
330
+ if (!flats.length) return
331
+ next.push(...[...flats].sort(sort))
332
+ flats.length = 0
333
+ }
334
+ for (const item of options) {
335
+ if (isSelectOptionGroup(item)) {
336
+ flushFlats()
337
+ next.push({ label: item.label, options: [...item.options].sort(sort) })
338
+ } else {
339
+ flats.push(item)
340
+ }
341
+ }
342
+ flushFlats()
343
+ return next as RsSelectOptions
344
+ }
345
+
346
+ export function splitByTokenSeparators(raw: string, separators: string[]): string[] | null {
347
+ if (!separators.length) return null
348
+ const found = separators.find((sep) => sep && raw.includes(sep))
349
+ if (!found) return null
350
+ return raw.split(found)
72
351
  }
@@ -1,15 +1,23 @@
1
1
  /** 面板分割方向:horizontal 左右排布(拖动横向)· vertical 上下排布(拖动纵向) */
2
2
  export type RsSplitOrientation = 'horizontal' | 'vertical'
3
3
 
4
- /** 单个面板定义;尺寸单位统一为「占比百分比」(0~100) */
4
+ /** 面板尺寸:数字为占比百分比;'auto' 按内容自适应(不占百分比配额) */
5
+ export type RsSplitPaneSize = number | 'auto'
6
+
7
+ /** 单个面板定义;百分比尺寸单位为 0~100 */
5
8
  export interface RsSplitPaneItem {
6
9
  /** 唯一标识,用于插槽名与 v-model:sizes 的顺序映射 */
7
10
  key: string
8
- /** 初始尺寸(百分比);缺省时在剩余空间内均分 */
9
- size?: number
10
- /** 最小尺寸(百分比),默认 0 */
11
+ /**
12
+ * 初始尺寸
13
+ * - number:占比百分比
14
+ * - 'auto':按内容自适应(flex-basis: auto)
15
+ * - 缺省:在「非 auto」面板的剩余百分比内均分
16
+ */
17
+ size?: RsSplitPaneSize
18
+ /** 最小尺寸(百分比),默认 0;auto 面板可作为 min-height/min-width % */
11
19
  min?: number
12
- /** 最大尺寸(百分比),默认 100 */
20
+ /** 最大尺寸(百分比),默认 100;auto 面板可作为 max-height/max-width % */
13
21
  max?: number
14
22
  /** 是否可折叠:拖动越过阈值时吸附到 collapsedSize */
15
23
  collapsible?: boolean
@@ -36,6 +44,16 @@ export function roundSize(value: number, precision = 4): number {
36
44
  return Math.round(value * factor) / factor
37
45
  }
38
46
 
47
+ /** 是否为内容自适应尺寸 */
48
+ export function isRsSplitPaneAutoSize(size: RsSplitPaneSize | undefined): size is 'auto' {
49
+ return size === 'auto'
50
+ }
51
+
52
+ /** 各面板是否声明为 auto(忽略受控 provided 时的瞬时物化) */
53
+ export function resolveSplitAutoFlags(panes: RsSplitPaneItem[]): boolean[] {
54
+ return panes.map((pane) => isRsSplitPaneAutoSize(pane.size))
55
+ }
56
+
39
57
  /** 将面板定义解析为规范化约束,保证 0 ≤ min ≤ max ≤ 100 且 collapsedSize ≤ min */
40
58
  export function resolveSplitConstraints(panes: RsSplitPaneItem[]): RsSplitConstraint[] {
41
59
  return panes.map((pane) => {
@@ -65,8 +83,8 @@ function rescaleTo100(sizes: number[]): number[] {
65
83
  }
66
84
 
67
85
  /**
68
- * 计算初始尺寸数组(总和 100)。
69
- * 优先使用 provided(受控值),其次面板自身的 size,未指定的项在剩余空间内均分。
86
+ * 计算初始尺寸数组(非 auto 面板总和 100;auto 位为 0 占位,样式层不使用 flex-grow)。
87
+ * 优先使用 provided(受控值),其次面板自身的 number size,未指定的非 auto 项在剩余空间内均分。
70
88
  */
71
89
  export function normalizeSplitSizes(
72
90
  panes: RsSplitPaneItem[],
@@ -75,18 +93,53 @@ export function normalizeSplitSizes(
75
93
  const count = panes.length
76
94
  if (count === 0) return []
77
95
 
96
+ const autoFlags = resolveSplitAutoFlags(panes)
97
+ // 受控 sizes 一旦提供完整数组,视为已全部物化为百分比(含原 auto)
98
+ const fullyControlled =
99
+ Array.isArray(provided) &&
100
+ provided.length === count &&
101
+ provided.every((value) => typeof value === 'number' && Number.isFinite(value))
102
+
103
+ if (fullyControlled) {
104
+ return rescaleTo100(provided!.map((value) => Math.max(0, value))).map((value) =>
105
+ roundSize(value),
106
+ )
107
+ }
108
+
78
109
  const seed: (number | null)[] = panes.map((pane, index) => {
79
- const candidate = provided?.[index] ?? pane.size
110
+ if (autoFlags[index]) return 0
111
+ const candidate = provided?.[index] ?? (typeof pane.size === 'number' ? pane.size : null)
80
112
  return typeof candidate === 'number' && Number.isFinite(candidate)
81
113
  ? Math.max(0, candidate)
82
114
  : null
83
115
  })
84
116
 
85
- const missing = seed.filter((value) => value === null).length
86
- const knownSum = seed.reduce<number>((acc, value) => acc + (value ?? 0), 0)
117
+ const flexibleIndices = panes
118
+ .map((_, index) => index)
119
+ .filter((index) => !autoFlags[index])
120
+
121
+ if (flexibleIndices.length === 0) {
122
+ // 全部 auto:占位全 0,由内容撑开
123
+ return panes.map(() => 0)
124
+ }
125
+
126
+ const missing = flexibleIndices.filter((index) => seed[index] === null).length
127
+ const knownSum = flexibleIndices.reduce<number>((acc, index) => acc + (seed[index] ?? 0), 0)
87
128
  const each = missing > 0 ? Math.max(0, 100 - knownSum) / missing : 0
88
- const filled = seed.map((value) => value ?? each)
89
- return rescaleTo100(filled).map((value) => roundSize(value))
129
+
130
+ const filled = seed.map((value, index) => {
131
+ if (autoFlags[index]) return 0
132
+ return value ?? each
133
+ })
134
+
135
+ // 只对非 auto 部分缩放到 100
136
+ const flexibleValues = flexibleIndices.map((index) => filled[index] ?? 0)
137
+ const scaledFlexible = rescaleTo100(flexibleValues)
138
+ const next = filled.slice()
139
+ flexibleIndices.forEach((paneIndex, i) => {
140
+ next[paneIndex] = roundSize(scaledFlexible[i] ?? 0)
141
+ })
142
+ return next
90
143
  }
91
144
 
92
145
  /** 判断某面板当前是否处于折叠态 */
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts" generic="T extends import('../table-utils').RsTableRowData">
2
+ /**
3
+ * RsTable 表体视图:通过 ViewContext inject 取状态(多表互不串扰)。
4
+ * 行级 v-memo 保留,避免抽离后丢失细粒度跳过渲染。
5
+ */
6
+ import { isTableRowDisabled, resolveEntryKey } from '../table-utils'
7
+ import type { RsTableRowEntry } from '../table-utils'
8
+ import RsTableBodyRow from './RsTableBodyRow.vue'
9
+ import { useRsTableView } from './rs-table-view-context'
10
+
11
+ const view = useRsTableView<T>()
12
+
13
+ type RowEntry = Extract<RsTableRowEntry<T>, { type: 'row' }>
14
+
15
+ function editingColKeyFor(entry: RowEntry): string | null {
16
+ const key = view.rowKeyFor(entry)
17
+ return view.editingCell?.rowKey === key ? view.editingCell.colKey : null
18
+ }
19
+
20
+ function focusColKeyFor(entry: RowEntry): string | null {
21
+ const key = view.rowKeyFor(entry)
22
+ return view.focusCell?.rowKey === key ? view.focusCell.colKey : null
23
+ }
24
+
25
+ function memoEditingDraft(entry: RowEntry): string {
26
+ return view.editingCell?.rowKey === view.rowKeyFor(entry) ? view.editingDraft : ''
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <tbody
32
+ @pointerover.passive="view.onBodyPointerOver($event)"
33
+ @pointerout.passive="view.onBodyPointerOut($event)"
34
+ >
35
+ <tr v-if="view.loading">
36
+ <td class="rs-table__empty" :colspan="view.bodyColspan">
37
+ <div class="rs-table__empty-content" :style="view.emptyContentStyle">
38
+ {{ view.bodyLabels.loading }}
39
+ </div>
40
+ </td>
41
+ </tr>
42
+ <tr v-else-if="!view.hasData">
43
+ <td class="rs-table__empty" :colspan="view.bodyColspan">
44
+ <div class="rs-table__empty-content" :style="view.emptyContentStyle">
45
+ <slot name="empty">{{ view.bodyLabels.empty }}</slot>
46
+ </div>
47
+ </td>
48
+ </tr>
49
+ <template v-else>
50
+ <tr
51
+ v-if="view.virtualScrollEnabled && view.virtualSlice.paddingTop > 0"
52
+ class="rs-table__virtual-pad"
53
+ >
54
+ <td :colspan="view.bodyColspan" :style="{ height: `${view.virtualSlice.paddingTop}px` }" />
55
+ </tr>
56
+ <template
57
+ v-for="entry in view.visibleEntries"
58
+ :key="resolveEntryKey(entry, view.rowKey)"
59
+ >
60
+ <tr v-if="entry.type === 'group'" class="rs-table__group-row">
61
+ <td class="rs-table__group-cell" :colspan="view.bodyColspan">
62
+ <slot name="group" :key="entry.key" :label="entry.label">
63
+ {{ entry.label }}
64
+ </slot>
65
+ </td>
66
+ </tr>
67
+ <tr v-else-if="entry.type === 'expand'" class="rs-table__expand-row">
68
+ <td class="rs-table__expand-cell" :colspan="view.bodyColspan">
69
+ <slot name="expand" :row="entry.row" :index="entry.rowIndex" />
70
+ </td>
71
+ </tr>
72
+ <RsTableBodyRow
73
+ v-else
74
+ v-memo="[
75
+ view.rowKeyFor(entry),
76
+ entry.row,
77
+ view.isRowSelected(entry),
78
+ view.isRowIndeterminate(entry),
79
+ view.isRowExpanded(entry),
80
+ view.isHighlighted(view.rowKeyFor(entry)),
81
+ view.rowEditPending(entry),
82
+ view.stagedMapSize,
83
+ memoEditingDraft(entry),
84
+ view.activeEditCellKey,
85
+ view.activeFocusCellKey,
86
+ view.activeErrorMapSize,
87
+ view.activeValidatingMapSize,
88
+ view.dragRowKeys.join(','),
89
+ view.dropRowTargetKey,
90
+ view.dropRowPosition,
91
+ view.columnPadLeft,
92
+ view.columnPadRight,
93
+ view.treeDepthOf(entry),
94
+ view.canExpandRow(entry),
95
+ view.treeLoadingKeySet.has(view.rowKeyFor(entry)),
96
+ view.visibleDataColumns.map((c) => c.key).join('\0'),
97
+ view.instanceId,
98
+ ]"
99
+ :row="entry.row"
100
+ :row-index="entry.rowIndex"
101
+ :row-key="view.rowKeyFor(entry)"
102
+ :columns="view.visibleDataColumns"
103
+ :column-pad-left="view.columnPadLeft"
104
+ :column-pad-right="view.columnPadRight"
105
+ :column-td-class-map="view.columnTdClassMap"
106
+ :column-style-map="view.columnStyleMap"
107
+ :show-row-drag-handle="view.showRowDragHandle"
108
+ :expandable="view.detailExpandable"
109
+ :tree-mode="view.treeMode"
110
+ :tree-depth="view.treeDepthOf(entry)"
111
+ :tree-has-children="view.canExpandRow(entry)"
112
+ :tree-indent="view.treeIndentPx"
113
+ :tree-column-key="view.treeExpandColumnKey"
114
+ :tree-loading="view.treeLoadingKeySet.has(view.rowKeyFor(entry))"
115
+ :selectable="view.showSelectColumn"
116
+ :show-index="view.showIndexColumn"
117
+ :show-edit-gutter="view.showEditGutterColumn"
118
+ :striped="view.striped"
119
+ :selection-type="view.selectionType"
120
+ :is-radio-selection="view.isRadioSelection"
121
+ :table-editable="view.editable"
122
+ :edit-trigger="view.editTrigger"
123
+ :row-commit="view.rowCommit"
124
+ :allow-null="view.allowNull"
125
+ :focus-mode="view.editFocusMode"
126
+ :null-label="view.nullLabel"
127
+ :show-row-status="view.showRowStatusColumn"
128
+ :selected="view.isRowSelected(entry)"
129
+ :indeterminate="view.isRowIndeterminate(entry)"
130
+ :highlighted="view.isHighlighted(view.rowKeyFor(entry))"
131
+ :expanded="view.isRowExpanded(entry)"
132
+ :disabled="isTableRowDisabled(entry.row)"
133
+ :row-drag-by-row="view.isRowDragByRow(entry)"
134
+ :dragging="view.isRowDragging(entry)"
135
+ :drop-target="view.isRowDropTarget(entry)"
136
+ :drop-position="view.isRowDropTarget(entry) ? view.dropRowPosition : null"
137
+ :drag-lead-style="view.dragLeadStyle"
138
+ :expand-lead-style="view.expandLeadStyle"
139
+ :select-lead-style="view.selectLeadStyle"
140
+ :index-lead-style="view.indexLeadStyle"
141
+ :gutter-lead-style="view.gutterLeadStyle"
142
+ :can-expand="view.canExpandRow(entry)"
143
+ :can-select="view.canSelectRow(entry)"
144
+ :row-edit-pending="view.rowEditPending(entry)"
145
+ :row-dirty="view.isRowDirty(view.rowKeyFor(entry))"
146
+ :show-gutter-rollback="view.rowCommit || view.isExternalRowPending(entry)"
147
+ :editing-col-key="editingColKeyFor(entry)"
148
+ :focus-col-key="focusColKeyFor(entry)"
149
+ :has-column-slot="view.hasColumnSlot"
150
+ :has-edit-slot="view.hasEditSlot"
151
+ :cell-tooltip-enabled="view.cellTooltipEnabled"
152
+ :cell-tooltip-mode="view.cellTooltipMode"
153
+ :cell-tooltip-text="view.cellTooltipText"
154
+ :cell-tooltip-fallback-title="view.cellTooltipFallbackTitle"
155
+ :get-cell-draft="view.getCellDraft"
156
+ :is-cell-dirty="view.isCellDirty"
157
+ :get-cell-error="view.getCellError"
158
+ :is-cell-validating="view.isCellValidating"
159
+ :drag-row-label="view.bodyLabels.dragRow"
160
+ :expand-row-label="view.bodyLabels.expandRow"
161
+ :collapse-row-label="view.bodyLabels.collapseRow"
162
+ :select-row-label="view.bodyLabels.selectRow"
163
+ :row-commit-label="view.bodyLabels.rowCommit"
164
+ :row-rollback-label="view.bodyLabels.rowRollback"
165
+ :gutter-commit-hint="view.bodyLabels.gutterCommit"
166
+ @click="view.onRowClick(entry, $event)"
167
+ @mousedown="view.onRowSelectMouseDown($event)"
168
+ @cell-click="(colKey, event) => view.onCellClick(entry, colKey, event)"
169
+ @cell-dblclick="(colKey, event) => view.onCellDblclick(entry, colKey, event)"
170
+ @cell-contextmenu="(colKey, event) => view.onCellContextmenu(entry, colKey, event)"
171
+ @dblclick="view.onRowDblclick(entry, $event)"
172
+ @contextmenu="view.onRowContextmenu(entry, $event)"
173
+ @dragover="view.onRowDragOver(entry, $event)"
174
+ @dragleave="view.onRowDragLeave($event)"
175
+ @drop="view.onRowDrop(entry, $event)"
176
+ @row-drag-start="view.onRowDragStart(entry, $event)"
177
+ @row-drag-end="view.onRowDragEnd()"
178
+ @toggle-expand="view.onToggleExpand(entry)"
179
+ @toggle-select="view.onToggleSelect(entry)"
180
+ @cell-start-edit="view.onCellStartEdit(entry, $event)"
181
+ @cell-commit="(colKey, value) => view.onCellCommit(entry, colKey, value)"
182
+ @cell-cancel="view.onCellCancel(entry, $event)"
183
+ @cell-update-draft="(colKey, value) => view.onCellUpdateDraft(entry, colKey, value)"
184
+ @cell-navigate="(colKey, direction) => view.onCellNavigate(entry, colKey, direction)"
185
+ @row-commit="view.onRowCommit(entry)"
186
+ @row-rollback="view.onRowRollback(entry)"
187
+ @gutter-commit="view.onGutterCommit(entry)"
188
+ >
189
+ <template v-for="column in view.visibleDataColumns" #[column.key]="slotProps">
190
+ <slot :name="column.key" v-bind="slotProps" />
191
+ </template>
192
+ <template
193
+ v-for="column in view.visibleDataColumns"
194
+ :key="`edit-${column.key}`"
195
+ #[`edit-${column.key}`]="slotProps"
196
+ >
197
+ <slot :name="`edit-${column.key}`" v-bind="slotProps" />
198
+ </template>
199
+ </RsTableBodyRow>
200
+ </template>
201
+ <tr
202
+ v-if="view.virtualScrollEnabled && view.virtualSlice.paddingBottom > 0"
203
+ class="rs-table__virtual-pad"
204
+ >
205
+ <td
206
+ :colspan="view.bodyColspan"
207
+ :style="{ height: `${view.virtualSlice.paddingBottom}px` }"
208
+ />
209
+ </tr>
210
+ <tr v-if="view.infinite && view.loadingMore">
211
+ <td class="rs-table__empty rs-table__empty--more" :colspan="view.bodyColspan">
212
+ {{ view.bodyLabels.loadingMore }}
213
+ </td>
214
+ </tr>
215
+ </template>
216
+ </tbody>
217
+ </template>