niuma-ui 1.0.2 → 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 (85) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/RsDatePicker.spec.ts +21 -0
  4. package/src/__tests__/RsDialog.spec.ts +61 -0
  5. package/src/__tests__/RsForm.spec.ts +5 -5
  6. package/src/__tests__/RsFormItem.spec.ts +478 -0
  7. package/src/__tests__/RsFormList.spec.ts +109 -0
  8. package/src/__tests__/RsInput.spec.ts +93 -0
  9. package/src/__tests__/RsInputNumber.spec.ts +33 -0
  10. package/src/__tests__/RsSelect.spec.ts +423 -1
  11. package/src/__tests__/RsSwitch.spec.ts +30 -0
  12. package/src/__tests__/RsTimePicker.spec.ts +9 -0
  13. package/src/__tests__/RsTooltip.spec.ts +43 -0
  14. package/src/__tests__/css-token.spec.ts +51 -0
  15. package/src/__tests__/date-picker-utils.spec.ts +49 -0
  16. package/src/__tests__/dialog-utils.spec.ts +43 -0
  17. package/src/__tests__/form-path.spec.ts +46 -0
  18. package/src/__tests__/form-rules.spec.ts +63 -0
  19. package/src/__tests__/theme.spec.ts +2 -0
  20. package/src/components/RsAlert.vue +1 -1
  21. package/src/components/RsAvatar.vue +1 -1
  22. package/src/components/RsBadge.vue +1 -1
  23. package/src/components/RsBreadcrumb.vue +1 -1
  24. package/src/components/RsButton.vue +4 -4
  25. package/src/components/RsCalendarGrid.vue +3 -3
  26. package/src/components/RsCard.vue +4 -4
  27. package/src/components/RsCodeBlock.vue +4 -4
  28. package/src/components/RsCodeEditor.vue +8 -8
  29. package/src/components/RsConfirmDialog.vue +2 -2
  30. package/src/components/RsContextMenu.vue +6 -4
  31. package/src/components/RsDatePicker.vue +75 -31
  32. package/src/components/RsDateTimePicker.vue +9 -1
  33. package/src/components/RsDescriptions.vue +2 -2
  34. package/src/components/RsDialog.vue +26 -16
  35. package/src/components/RsDrawer.vue +1 -1
  36. package/src/components/RsDropdown.vue +1 -1
  37. package/src/components/RsDynamicTags.vue +18 -4
  38. package/src/components/RsEmpty.vue +1 -1
  39. package/src/components/RsForm.vue +68 -1
  40. package/src/components/RsFormItem.vue +423 -0
  41. package/src/components/RsFormItemControl.vue +21 -0
  42. package/src/components/RsFormList.vue +117 -0
  43. package/src/components/RsInput.vue +411 -18
  44. package/src/components/RsInputNumber.vue +13 -4
  45. package/src/components/RsLink.vue +1 -1
  46. package/src/components/RsMarkdown.vue +5 -5
  47. package/src/components/RsMenu.vue +2 -2
  48. package/src/components/RsMonacoEditor.vue +9 -3
  49. package/src/components/RsPagination.vue +1 -1
  50. package/src/components/RsProseEditor.vue +3 -3
  51. package/src/components/RsSelect.vue +332 -611
  52. package/src/components/RsSidebarGroup.vue +1 -1
  53. package/src/components/RsSplitPane.vue +22 -1
  54. package/src/components/RsStatCard.vue +2 -2
  55. package/src/components/RsSteps.vue +2 -2
  56. package/src/components/RsSwitch.vue +20 -7
  57. package/src/components/RsTabs.vue +9 -9
  58. package/src/components/RsTag.vue +1 -1
  59. package/src/components/RsTerminal.vue +58 -19
  60. package/src/components/RsTimePicker.vue +28 -4
  61. package/src/components/RsTimePickerColumns.vue +4 -4
  62. package/src/components/RsToaster.vue +7 -7
  63. package/src/components/RsTooltip.vue +10 -1
  64. package/src/components/RsTooltipProvider.vue +11 -1
  65. package/src/components/RsTree.vue +6 -6
  66. package/src/components/RsUpload.vue +2 -2
  67. package/src/components/date-picker-utils.ts +113 -2
  68. package/src/components/dialog-utils.ts +37 -2
  69. package/src/components/form-item-control.ts +55 -0
  70. package/src/components/form-path.ts +92 -0
  71. package/src/components/form-rules.ts +64 -10
  72. package/src/components/form-utils.ts +94 -2
  73. package/src/components/select-utils.ts +272 -12
  74. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  75. package/src/components/table/rs-table.css +1 -1
  76. package/src/components/use-rs-select.ts +436 -0
  77. package/src/composables/useRsConfig.ts +10 -3
  78. package/src/composables/useRsI18n.ts +16 -5
  79. package/src/index.ts +69 -1
  80. package/src/locale/interpolate.ts +32 -0
  81. package/src/locale/messages.ts +10 -0
  82. package/src/styles.css +68 -12
  83. package/src/theme/brand.example.css +4 -1
  84. package/src/theme/css-token.ts +107 -0
  85. package/src/theme/types.ts +1 -0
@@ -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,18 +28,47 @@ 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
+
14
60
  /**
15
61
  * Reka ComboboxItem 禁止 value 为空串(空串表示未选中 / placeholder)。
16
62
  * 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
17
63
  */
18
64
  export const RS_SELECT_EMPTY_VALUE = '__rs_select_empty__'
19
65
 
20
- /** 业务 value → ComboboxItem value(仅处理空串) */
21
- export function toComboboxValue(value: string): string {
22
- return value === '' ? RS_SELECT_EMPTY_VALUE : value
66
+ /** 业务 value → ComboboxItem token(空串走哨兵;数字转为十进制字符串) */
67
+ export function toComboboxValue(value: RsSelectValue): string {
68
+ return value === '' ? RS_SELECT_EMPTY_VALUE : String(value)
23
69
  }
24
70
 
25
- /** ComboboxItem value → 业务 value(仅处理空串哨兵) */
71
+ /** ComboboxItem token → 业务字符串(仅还原空串哨兵,不恢复 number) */
26
72
  export function fromComboboxValue(value: string): string {
27
73
  return value === RS_SELECT_EMPTY_VALUE ? '' : value
28
74
  }
@@ -45,11 +91,40 @@ export function flattenSelectOptions(options: RsSelectOptions): RsSelectOption[]
45
91
  return result
46
92
  }
47
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
+
48
122
  export function buildOptionLabelMap(options: RsSelectOptions): Map<string, string> {
49
123
  const map = new Map<string, string>()
50
124
  for (const opt of flattenSelectOptions(options)) {
51
- map.set(opt.value, opt.label)
52
- if (opt.value === '') map.set(RS_SELECT_EMPTY_VALUE, opt.label)
125
+ const key = toComboboxValue(opt.value)
126
+ map.set(key, opt.label)
127
+ if (opt.value === '') map.set('', opt.label)
53
128
  }
54
129
  return map
55
130
  }
@@ -57,29 +132,130 @@ export function buildOptionLabelMap(options: RsSelectOptions): Map<string, strin
57
132
  export function buildOptionDisabledMap(options: RsSelectOptions): Map<string, boolean> {
58
133
  const map = new Map<string, boolean>()
59
134
  for (const opt of flattenSelectOptions(options)) {
60
- const disabled = Boolean(opt.disabled)
61
- map.set(opt.value, disabled)
62
- if (opt.value === '') map.set(RS_SELECT_EMPTY_VALUE, disabled)
135
+ const key = toComboboxValue(opt.value)
136
+ map.set(key, Boolean(opt.disabled))
63
137
  }
64
138
  return map
65
139
  }
66
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
+
67
235
  export function filterSelectOptions(
68
236
  options: RsSelectOptions,
69
237
  query: string,
70
238
  contains: (text: string, search: string) => boolean,
239
+ filterOption?: RsSelectFilterOption | boolean,
240
+ optionFilterProp: RsSelectOptionFilterProp = 'label',
71
241
  ): RsSelectOptions {
242
+ if (filterOption === false) return options
72
243
  const trimmed = query.trim()
73
244
  if (!trimmed) return options
74
245
 
246
+ const match =
247
+ typeof filterOption === 'function'
248
+ ? (opt: RsSelectOption) => filterOption(trimmed, opt)
249
+ : (opt: RsSelectOption) => defaultMatchOption(opt, trimmed, contains, optionFilterProp)
250
+
75
251
  const result: Array<RsSelectOption | RsSelectOptionGroup> = []
76
252
  for (const item of options) {
77
253
  if (isSelectOptionGroup(item)) {
78
- const filtered = item.options.filter((opt) => contains(opt.label, trimmed))
254
+ const filtered = item.options.filter(match)
79
255
  if (filtered.length > 0) {
80
256
  result.push({ label: item.label, options: filtered })
81
257
  }
82
- } else if (contains(item.label, trimmed)) {
258
+ } else if (match(item)) {
83
259
  result.push(item)
84
260
  }
85
261
  }
@@ -89,3 +265,87 @@ export function filterSelectOptions(
89
265
  export function flattenSelectValues(options: RsSelectOptions): string[] {
90
266
  return flattenSelectOptions(options).map((opt) => toComboboxValue(opt.value))
91
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)
351
+ }
@@ -87,7 +87,7 @@ function onEditorEnter(): void {
87
87
  .rs-table-header-filter__title {
88
88
  margin: 0 0 0.5rem;
89
89
  font-size: var(--rs-font-size-sm);
90
- font-weight: 600;
90
+ font-weight: var(--rs-font-weight-semibold);
91
91
  color: var(--rs-fg);
92
92
  }
93
93
  .rs-table-header-filter__field {
@@ -778,7 +778,7 @@
778
778
  padding: var(--rs-space-xs) var(--rs-space-md);
779
779
  color: var(--rs-table-muted-fg);
780
780
  font-size: var(--rs-font-size-xs);
781
- font-weight: 600;
781
+ font-weight: var(--rs-font-weight-semibold);
782
782
  }
783
783
  .rs-table__virtual-pad td {
784
784
  padding: 0;