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
@@ -0,0 +1,436 @@
1
+ import { computed, nextTick, watch, type ModelRef } from 'vue'
2
+ import type { RsTranslateFn } from '../composables/useRsI18n'
3
+ import { useFilter } from './reka'
4
+ import {
5
+ buildOptionDisabledMap,
6
+ buildOptionLabelMap,
7
+ comboboxBindingToTokens,
8
+ filterSelectOptions,
9
+ findSelectOption,
10
+ flattenSelectOptions,
11
+ flattenSelectValues,
12
+ fromComboboxValue,
13
+ isSelectLabeledValue,
14
+ normalizeSelectOptions,
15
+ optionDisplayLabel,
16
+ packSelectModel,
17
+ restoreSelectValue,
18
+ restoreSelectValues,
19
+ selectModelEntries,
20
+ sortSelectOptions,
21
+ splitByTokenSeparators,
22
+ toComboboxValue,
23
+ toSelectedTokens,
24
+ type RsSelectFieldNames,
25
+ type RsSelectFilterOption,
26
+ type RsSelectFilterSort,
27
+ type RsSelectModelValue,
28
+ type RsSelectOption,
29
+ type RsSelectOptionFilterProp,
30
+ type RsSelectOptionsInput,
31
+ type RsSelectValue,
32
+ } from './select-utils'
33
+
34
+ /** RsSelect 行为层入参(不含尺寸 / 表单外观) */
35
+ export interface RsSelectEngineProps {
36
+ options: RsSelectOptionsInput
37
+ fieldNames?: RsSelectFieldNames
38
+ optionLabelProp?: string
39
+ optionFilterProp?: RsSelectOptionFilterProp
40
+ filterOption?: RsSelectFilterOption | boolean
41
+ filterSort?: RsSelectFilterSort
42
+ searchable?: boolean
43
+ creatable?: boolean
44
+ multiple?: boolean
45
+ remote?: boolean
46
+ virtual?: boolean
47
+ virtualThreshold?: number
48
+ maxTagCount?: number
49
+ maxTagPlaceholder?: string | ((omitted: number) => string)
50
+ maxTagTooltip?: boolean
51
+ maxTagTextLength?: number
52
+ multipleLimit?: number
53
+ labelInValue?: boolean
54
+ tokenSeparators?: string[]
55
+ autoClearSearchValue?: boolean
56
+ searchPlaceholder?: string
57
+ emptyText?: string
58
+ loadingText?: string
59
+ placeholder?: string
60
+ }
61
+
62
+ export type RsSelectEngineEmit = {
63
+ (e: 'search', query: string): void
64
+ (e: 'select', value: RsSelectValue, option?: RsSelectOption): void
65
+ (e: 'deselect', value: RsSelectValue, option?: RsSelectOption): void
66
+ (e: 'clear'): void
67
+ }
68
+
69
+ /**
70
+ * Select 过滤、选中、多选 tag、creatable / 分隔符提交。
71
+ * 组件模板只消费返回值;表单校验仍留在 SFC。
72
+ */
73
+ export function useRsSelect(
74
+ props: RsSelectEngineProps,
75
+ model: ModelRef<RsSelectModelValue>,
76
+ open: ModelRef<boolean>,
77
+ searchQuery: ModelRef<string>,
78
+ emit: RsSelectEngineEmit,
79
+ t: RsTranslateFn,
80
+ ) {
81
+ const { contains } = useFilter({ sensitivity: 'base' })
82
+
83
+ const normalizedOptions = computed(() =>
84
+ normalizeSelectOptions(props.options, props.fieldNames),
85
+ )
86
+
87
+ const resolvedPlaceholder = computed(() => props.placeholder ?? t('select.placeholder'))
88
+ const resolvedSearchPlaceholder = computed(
89
+ () => props.searchPlaceholder ?? t('select.searchPlaceholder'),
90
+ )
91
+ const resolvedEmptyText = computed(() => props.emptyText ?? t('select.empty'))
92
+ const resolvedLoadingText = computed(() => props.loadingText ?? t('select.loading'))
93
+ const isSearchable = computed(() => Boolean(props.searchable || props.creatable))
94
+
95
+ const labelMap = computed(() => buildOptionLabelMap(normalizedOptions.value))
96
+ const disabledMap = computed(() => buildOptionDisabledMap(normalizedOptions.value))
97
+ const flatOptions = computed(() => flattenSelectOptions(normalizedOptions.value))
98
+ const flatCount = computed(() => flatOptions.value.length)
99
+ const useVirtual = computed(
100
+ () => Boolean(props.virtual) || flatCount.value > (props.virtualThreshold ?? 50),
101
+ )
102
+
103
+ const useManualFilter = computed(
104
+ () =>
105
+ Boolean(props.remote) ||
106
+ useVirtual.value ||
107
+ typeof props.filterOption === 'function' ||
108
+ props.filterOption === false ||
109
+ Boolean(props.optionFilterProp && props.optionFilterProp !== 'label') ||
110
+ Boolean(props.filterSort) ||
111
+ Boolean(props.fieldNames),
112
+ )
113
+
114
+ const displayOptions = computed(() => {
115
+ let next = normalizedOptions.value
116
+ if (!props.remote && props.filterOption !== false) {
117
+ if (useManualFilter.value || props.filterOption) {
118
+ next = filterSelectOptions(
119
+ normalizedOptions.value,
120
+ searchQuery.value,
121
+ contains,
122
+ props.filterOption,
123
+ props.optionFilterProp,
124
+ )
125
+ }
126
+ }
127
+ return sortSelectOptions(next, props.filterSort)
128
+ })
129
+
130
+ const createValue = computed(() => searchQuery.value.trim())
131
+
132
+ const canCreate = computed(() => {
133
+ if (!props.creatable) return false
134
+ const q = createValue.value
135
+ if (!q) return false
136
+ const lower = q.toLowerCase()
137
+ return !flatOptions.value.some(
138
+ (opt) =>
139
+ String(opt.value).toLowerCase() === lower || String(opt.label).toLowerCase() === lower,
140
+ )
141
+ })
142
+
143
+ const createOptionLabel = computed(() =>
144
+ t('select.createOption', '使用 “{query}”').replace('{query}', createValue.value),
145
+ )
146
+
147
+ const virtualValues = computed(() => {
148
+ const values = flattenSelectValues(displayOptions.value)
149
+ if (!canCreate.value) return values
150
+ return [createValue.value, ...values.filter((item) => String(item) !== createValue.value)]
151
+ })
152
+
153
+ const selectedValues = computed(() => toSelectedTokens(model.value, Boolean(props.multiple)))
154
+ const hasValue = computed(() => selectedValues.value.length > 0)
155
+
156
+ const visibleTagTokens = computed(() => {
157
+ const all = selectedValues.value
158
+ if (!props.multiple || props.maxTagCount == null || all.length <= props.maxTagCount) {
159
+ return all
160
+ }
161
+ return all.slice(0, Math.max(0, props.maxTagCount))
162
+ })
163
+
164
+ const omittedTagCount = computed(() =>
165
+ Math.max(0, selectedValues.value.length - visibleTagTokens.value.length),
166
+ )
167
+
168
+ const omittedTagLabel = computed(() => {
169
+ const count = omittedTagCount.value
170
+ if (typeof props.maxTagPlaceholder === 'function') return props.maxTagPlaceholder(count)
171
+ if (props.maxTagPlaceholder) return props.maxTagPlaceholder
172
+ return t('select.maxTagPlaceholder', { count })
173
+ })
174
+
175
+ function optionFromToken(token: string): RsSelectOption | undefined {
176
+ return findSelectOption(normalizedOptions.value, String(token))
177
+ }
178
+
179
+ function tokenLabel(token: string): string {
180
+ if (props.labelInValue) {
181
+ for (const entry of selectModelEntries(model.value)) {
182
+ if (
183
+ isSelectLabeledValue(entry) &&
184
+ toComboboxValue(entry.value) === token &&
185
+ entry.label
186
+ ) {
187
+ return entry.label
188
+ }
189
+ }
190
+ }
191
+ const option = optionFromToken(token)
192
+ if (option) return optionDisplayLabel(option, props.optionLabelProp)
193
+ return labelMap.value.get(token) ?? fromComboboxValue(token)
194
+ }
195
+
196
+ const omittedTagTitle = computed(() => {
197
+ if (!props.maxTagTooltip) return omittedTagLabel.value
198
+ const omitted = selectedValues.value.slice(visibleTagTokens.value.length)
199
+ if (!omitted.length) return omittedTagLabel.value
200
+ return omitted.map((token) => tokenLabel(token)).join('、')
201
+ })
202
+
203
+ function truncateTagLabel(label: string): string {
204
+ const max = props.maxTagTextLength
205
+ if (!max || max < 1 || label.length <= max) return label
206
+ return `${label.slice(0, max)}…`
207
+ }
208
+
209
+ const atMultipleLimit = computed(
210
+ () =>
211
+ Boolean(props.multiple) &&
212
+ props.multipleLimit != null &&
213
+ props.multipleLimit > 0 &&
214
+ selectedValues.value.length >= props.multipleLimit,
215
+ )
216
+
217
+ function isOptionLimited(option: RsSelectOption): boolean {
218
+ if (!atMultipleLimit.value) return false
219
+ return !selectedValues.value.includes(toComboboxValue(option.value))
220
+ }
221
+
222
+ function isTokenDisabled(token: string): boolean {
223
+ if (disabledMap.value.get(String(token))) return true
224
+ const option = optionFromToken(token)
225
+ return option ? isOptionLimited(option) : false
226
+ }
227
+
228
+ const singleDisplayLabel = computed(() => {
229
+ if (props.multiple || !hasValue.value) return ''
230
+ return tokenLabel(selectedValues.value[0]!)
231
+ })
232
+
233
+ function writeTokens(tokens: string[]): void {
234
+ const values = restoreSelectValues(tokens, normalizedOptions.value)
235
+ model.value = packSelectModel(
236
+ values,
237
+ normalizedOptions.value,
238
+ Boolean(props.multiple),
239
+ Boolean(props.labelInValue),
240
+ )
241
+ }
242
+
243
+ function emitSelectionDiff(prev: string[], next: string[]): void {
244
+ const prevSet = new Set(prev)
245
+ const nextSet = new Set(next)
246
+ for (const token of next) {
247
+ if (prevSet.has(token)) continue
248
+ const value = restoreSelectValue(token, normalizedOptions.value)
249
+ emit('select', value, findSelectOption(normalizedOptions.value, token))
250
+ }
251
+ for (const token of prev) {
252
+ if (nextSet.has(token)) continue
253
+ const value = restoreSelectValue(token, normalizedOptions.value)
254
+ emit('deselect', value, findSelectOption(normalizedOptions.value, token))
255
+ }
256
+ }
257
+
258
+ const comboboxModel = computed<string | string[] | undefined>({
259
+ get() {
260
+ if (props.multiple) return selectedValues.value.map((token) => token)
261
+ return hasValue.value ? selectedValues.value[0] : undefined
262
+ },
263
+ set(value) {
264
+ const prev = selectedValues.value
265
+ const tokens = comboboxBindingToTokens(value, Boolean(props.multiple))
266
+ emitSelectionDiff(prev, tokens)
267
+ writeTokens(tokens)
268
+ if (props.autoClearSearchValue) searchQuery.value = ''
269
+ },
270
+ })
271
+
272
+ function currentValues(): RsSelectValue[] {
273
+ return restoreSelectValues(selectedValues.value, normalizedOptions.value)
274
+ }
275
+
276
+ function commitCreatedValue(value: string): void {
277
+ const next = value.trim()
278
+ if (!next) return
279
+ const values = currentValues()
280
+ if (values.some((item) => String(item) === next)) return
281
+ if (props.multiple) {
282
+ if (atMultipleLimit.value) return
283
+ const prev = selectedValues.value
284
+ const packed = packSelectModel(
285
+ [...values, next],
286
+ normalizedOptions.value,
287
+ true,
288
+ Boolean(props.labelInValue),
289
+ )
290
+ model.value = packed
291
+ emitSelectionDiff(prev, toSelectedTokens(packed, true))
292
+ } else {
293
+ const prev = selectedValues.value
294
+ model.value = packSelectModel(
295
+ [next],
296
+ normalizedOptions.value,
297
+ false,
298
+ Boolean(props.labelInValue),
299
+ )
300
+ emitSelectionDiff(prev, toSelectedTokens(model.value, false))
301
+ open.value = false
302
+ }
303
+ if (props.autoClearSearchValue) searchQuery.value = ''
304
+ }
305
+
306
+ function consumeTokenSeparators(raw: string): void {
307
+ if (!props.multiple && !props.creatable) return
308
+ const parts = splitByTokenSeparators(raw, props.tokenSeparators ?? [])
309
+ if (!parts) return
310
+ const rest = parts.pop() ?? ''
311
+ for (const part of parts) {
312
+ const token = part.trim()
313
+ if (!token) continue
314
+ const matched = flatOptions.value.find(
315
+ (opt) => String(opt.value) === token || String(opt.label) === token,
316
+ )
317
+ if (matched) {
318
+ const prev = selectedValues.value
319
+ const key = toComboboxValue(matched.value)
320
+ if (prev.includes(key)) continue
321
+ if (props.multiple && atMultipleLimit.value) continue
322
+ const nextTokens = props.multiple ? [...prev, key] : [key]
323
+ emitSelectionDiff(prev, nextTokens)
324
+ writeTokens(nextTokens)
325
+ if (!props.multiple) open.value = false
326
+ } else if (props.creatable) {
327
+ commitCreatedValue(token)
328
+ }
329
+ }
330
+ if (rest !== raw) searchQuery.value = rest
331
+ }
332
+
333
+ watch(searchQuery, (query) => {
334
+ if (props.remote) emit('search', query)
335
+ if (props.tokenSeparators?.length) consumeTokenSeparators(query)
336
+ })
337
+
338
+ watch(open, async (isOpen) => {
339
+ if (isOpen) {
340
+ const pending = searchQuery.value
341
+ await nextTick()
342
+ await nextTick()
343
+ // ComboboxInput 挂载时会 resetSearchTerm;受控 searchValue 需要写回
344
+ if (pending) searchQuery.value = pending
345
+ return
346
+ }
347
+ if (props.autoClearSearchValue) searchQuery.value = ''
348
+ })
349
+
350
+ function onSearchKeydown(event: KeyboardEvent): void {
351
+ if (event.key !== 'Enter' || event.isComposing) return
352
+ if (!canCreate.value) return
353
+ event.preventDefault()
354
+ event.stopPropagation()
355
+ commitCreatedValue(createValue.value)
356
+ }
357
+
358
+ function onClear(event: MouseEvent): void {
359
+ event.preventDefault()
360
+ event.stopPropagation()
361
+ emitSelectionDiff(selectedValues.value, [])
362
+ emit('clear')
363
+ model.value = props.multiple ? [] : ''
364
+ }
365
+
366
+ function removeTag(value: string, event: MouseEvent): void {
367
+ event.preventDefault()
368
+ event.stopPropagation()
369
+ if (!props.multiple) return
370
+ const prev = selectedValues.value
371
+ const tokens = prev.filter((item) => item !== value)
372
+ emitSelectionDiff(prev, tokens)
373
+ writeTokens(tokens)
374
+ }
375
+
376
+ function setValue(value: unknown): void {
377
+ if (props.multiple) {
378
+ const list = Array.isArray(value) ? value : value !== '' && value != null ? [value] : []
379
+ writeTokens(toSelectedTokens(list, true))
380
+ return
381
+ }
382
+ writeTokens(toSelectedTokens(value, false))
383
+ }
384
+
385
+ function restoreTokenValue(token: string): RsSelectValue {
386
+ return restoreSelectValue(token, normalizedOptions.value)
387
+ }
388
+
389
+ function optionOrCreate(token: string): RsSelectOption {
390
+ return (
391
+ optionFromToken(token) ?? {
392
+ label: fromComboboxValue(token),
393
+ value: restoreTokenValue(token),
394
+ }
395
+ )
396
+ }
397
+
398
+ return {
399
+ normalizedOptions,
400
+ resolvedPlaceholder,
401
+ resolvedSearchPlaceholder,
402
+ resolvedEmptyText,
403
+ resolvedLoadingText,
404
+ isSearchable,
405
+ labelMap,
406
+ useVirtual,
407
+ useManualFilter,
408
+ displayOptions,
409
+ createValue,
410
+ canCreate,
411
+ createOptionLabel,
412
+ virtualValues,
413
+ selectedValues,
414
+ hasValue,
415
+ visibleTagTokens,
416
+ omittedTagCount,
417
+ omittedTagLabel,
418
+ omittedTagTitle,
419
+ singleDisplayLabel,
420
+ comboboxModel,
421
+ tokenLabel,
422
+ truncateTagLabel,
423
+ optionFromToken,
424
+ optionOrCreate,
425
+ isOptionLimited,
426
+ isTokenDisabled,
427
+ restoreTokenValue,
428
+ onSearchKeydown,
429
+ onClear,
430
+ removeTag,
431
+ setValue,
432
+ resetSearch: () => {
433
+ searchQuery.value = ''
434
+ },
435
+ }
436
+ }
@@ -1,4 +1,9 @@
1
1
  import { inject, provide, ref, type InjectionKey, type Ref } from 'vue'
2
+ import {
3
+ interpolateRsMessage,
4
+ resolveRsTranslateArgs,
5
+ type RsTranslateFn,
6
+ } from '../locale/interpolate'
2
7
  import { localeMap } from '../locale/messages'
3
8
  import { defaultLocale, type RsLocale } from '../locale/types'
4
9
  import type { RsComponentSize, RsRadius, RsThemeMode } from '../theme/types'
@@ -17,7 +22,7 @@ export interface RsConfigContext {
17
22
  setLocale: (locale: RsLocale) => void
18
23
  setControlSize: (size: RsComponentSize) => void
19
24
  setControlRadius: (radius: RsRadius | undefined) => void
20
- t: (key: string, fallback?: string) => string
25
+ t: RsTranslateFn
21
26
  }
22
27
 
23
28
  export const rsConfigKey: InjectionKey<RsConfigContext> = Symbol('rs-config')
@@ -65,8 +70,10 @@ export function createRsConfigState(
65
70
  controlRadius.value = radius
66
71
  }
67
72
 
68
- function t(key: string, fallback?: string) {
69
- return localeMap[locale.value][key] ?? fallback ?? key
73
+ const t: RsTranslateFn = (key, fallbackOrVars, vars) => {
74
+ const parsed = resolveRsTranslateArgs(fallbackOrVars, vars)
75
+ const raw = localeMap[locale.value][key] ?? parsed.fallback ?? key
76
+ return interpolateRsMessage(raw, parsed.vars)
70
77
  }
71
78
 
72
79
  if (typeof document !== 'undefined') {
@@ -1,22 +1,33 @@
1
1
  import { computed, inject } from 'vue'
2
+ import { interpolateRsMessage, resolveRsTranslateArgs, type RsI18nVars, type RsTranslateFn } from '../locale/interpolate'
2
3
  import { localeMap } from '../locale/messages'
3
4
  import { defaultLocale, type RsLocale } from '../locale/types'
4
5
  import { rsConfigKey } from './useRsConfig'
5
6
 
6
- export type RsTranslateFn = (key: string, fallback?: string) => string
7
+ export type { RsI18nVars, RsTranslateFn }
8
+
9
+ function translate(
10
+ locale: RsLocale,
11
+ key: string,
12
+ fallbackOrVars?: string | RsI18nVars,
13
+ vars?: RsI18nVars,
14
+ ): string {
15
+ const parsed = resolveRsTranslateArgs(fallbackOrVars, vars)
16
+ const raw = localeMap[locale][key] ?? parsed.fallback ?? key
17
+ return interpolateRsMessage(raw, parsed.vars)
18
+ }
7
19
 
8
20
  /** 组件内安全取文案:Provider 内跟随 locale,否则回退 defaultLocale */
9
21
  export function useRsI18n() {
10
22
  const ctx = inject(rsConfigKey, null)
11
23
  const locale = computed<RsLocale>(() => ctx?.locale.value ?? defaultLocale)
12
24
 
13
- function t(key: string, fallback?: string): string {
14
- return localeMap[locale.value][key] ?? fallback ?? key
15
- }
25
+ const t: RsTranslateFn = (key, fallbackOrVars, vars) =>
26
+ translate(locale.value, key, fallbackOrVars, vars)
16
27
 
17
28
  return { t, locale }
18
29
  }
19
30
 
20
31
  export function createTranslator(locale: RsLocale): RsTranslateFn {
21
- return (key, fallback) => localeMap[locale][key] ?? fallback ?? key
32
+ return (key, fallbackOrVars, vars) => translate(locale, key, fallbackOrVars, vars)
22
33
  }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  } from './components/button-utils'
12
12
  export { default as RsCheckbox } from './components/RsCheckbox.vue'
13
13
  export { default as RsSwitch } from './components/RsSwitch.vue'
14
+ export type { RsSwitchValue } from './components/RsSwitch.vue'
14
15
  export { default as RsRadio } from './components/RsRadio.vue'
15
16
  export { default as RsRadioItem } from './components/RsRadioItem.vue'
16
17
  export type { RsRadioValue } from './components/radio-utils'
@@ -89,6 +90,7 @@ export {
89
90
  isRsDialogWidthPreset,
90
91
  resolveDialogOverlayStyle,
91
92
  resolveRsDialogCssWidth,
93
+ resolveRsDialogWidthPx,
92
94
  runRsConfirmBeforeClose,
93
95
  runRsDialogBeforeClose,
94
96
  } from './components/dialog-utils'
@@ -107,6 +109,8 @@ export {
107
109
  runRsDrawerBeforeClose,
108
110
  } from './components/drawer-utils'
109
111
  export { default as RsForm } from './components/RsForm.vue'
112
+ export { default as RsFormItem } from './components/RsFormItem.vue'
113
+ export { default as RsFormList } from './components/RsFormList.vue'
110
114
  export { default as RsToaster } from './components/RsToaster.vue'
111
115
  export { default as RsDatePicker } from './components/RsDatePicker.vue'
112
116
  export { default as RsDateTimePicker } from './components/RsDateTimePicker.vue'
@@ -205,11 +209,33 @@ export type { RsContextMenuItem } from './components/context-menu-utils'
205
209
  export type { RsDropdownItem, RsDropdownItemGroup, RsDropdownItems } from './components/dropdown-utils'
206
210
  export type { RsMenuItem, RsMenuItemGroup, RsMenuItems } from './components/menu-utils'
207
211
  export type { RsScrollbarOrientation, RsScrollbarType } from './components/scrollbar-utils'
208
- export type { RsSelectOption, RsSelectOptionGroup, RsSelectOptions } from './components/select-utils'
212
+ export type {
213
+ RsSelectFieldNames,
214
+ RsSelectFilterOption,
215
+ RsSelectFilterSort,
216
+ RsSelectGetPopupContainer,
217
+ RsSelectLabeledValue,
218
+ RsSelectModelValue,
219
+ RsSelectOption,
220
+ RsSelectOptionFilterProp,
221
+ RsSelectOptionGroup,
222
+ RsSelectOptionInput,
223
+ RsSelectOptions,
224
+ RsSelectOptionsInput,
225
+ RsSelectPlacement,
226
+ RsSelectStatus,
227
+ RsSelectValue,
228
+ } from './components/select-utils'
209
229
  export {
210
230
  RS_SELECT_EMPTY_VALUE,
211
231
  fromComboboxValue,
232
+ isSelectLabeledValue,
233
+ normalizeSelectOptions,
234
+ optionDisplayLabel,
235
+ packSelectModel,
236
+ restoreSelectValue,
212
237
  toComboboxValue,
238
+ unwrapSelectEntry,
213
239
  } from './components/select-utils'
214
240
  export type { RsFeedbackTone, RsToastPosition, RsToastType } from './components/overlay-utils'
215
241
  export { RS_TOAST_DEFAULT_GAP, RS_TOAST_DEFAULT_POSITION, rsToastPositions, rsFeedbackIconClass } from './components/overlay-utils'
@@ -218,27 +244,51 @@ export type {
218
244
  RsFormErrorRender,
219
245
  RsFormErrorRenderContext,
220
246
  RsFormFieldExpose,
247
+ RsFormItemContext,
248
+ RsFormListContext,
249
+ RsFormListField,
250
+ RsFormListOperations,
221
251
  RsFormFieldValidationResult,
222
252
  RsFormGap,
223
253
  RsFormLabelAlign,
224
254
  RsFormLabelPosition,
225
255
  RsFormMaxWidth,
226
256
  RsFormSize,
257
+ RsFormValidateStatus,
227
258
  RsFormValidationResult,
228
259
  } from './components/form-utils'
229
260
  export {
230
261
  cloneFormFieldValue,
262
+ isRsFormItemBoundControl,
263
+ provideRsFormItemContext,
264
+ provideRsFormListContext,
231
265
  RS_FORM_INJECTION_KEY,
266
+ RS_FORM_ITEM_INJECTION_KEY,
267
+ RS_FORM_LIST_INJECTION_KEY,
232
268
  resolveFieldRules,
233
269
  useRsFormContext,
234
270
  useRsFormField,
271
+ useRsFormItemContext,
272
+ useRsFormListContext,
235
273
  } from './components/form-utils'
274
+ export type { RsFormNamePath } from './components/form-path'
275
+ export {
276
+ concatNamePath,
277
+ getByNamePath,
278
+ hasByNamePath,
279
+ isIndexSegment,
280
+ namePathKey,
281
+ normalizeNamePath,
282
+ setByNamePath,
283
+ } from './components/form-path'
236
284
  export type {
237
285
  RsFormRuleItem,
238
286
  RsFormRuleMessage,
239
287
  RsFormRules,
240
288
  RsFormRuleTrigger,
241
289
  RsFormRuleValidateResult,
290
+ RsFormValidateMessages,
291
+ RsFormValidatorContext,
242
292
  } from './components/form-rules'
243
293
  export {
244
294
  buildLocalInputRules,
@@ -596,7 +646,9 @@ export type {
596
646
  RsDatePickerModelValue,
597
647
  RsDatePickerShortcut,
598
648
  RsDatePickerTimestampRange,
649
+ RsDatePickerValueConvertOptions,
599
650
  RsDatePickerValueFormat,
651
+ RsDatePickerValueFormatPreset,
600
652
  RsDateRangeValue,
601
653
  RsParsedDate,
602
654
  RsParsedDateTime,
@@ -609,11 +661,15 @@ export {
609
661
  formatDateTimeDisplay,
610
662
  formatDateTimeValue,
611
663
  formatDateValue,
664
+ fromInternalPickerValue,
612
665
  isDateRangeEmpty,
613
666
  isDateRangeOrdered,
614
667
  isDateTimeRangeOrdered,
615
668
  parseDateTimeValue,
616
669
  parseDateValue,
670
+ RS_DATE_PICKER_VALUE_FORMAT_PRESETS,
671
+ toInternalPickerValue,
672
+ toRangeEndpointString,
617
673
  } from './components/date-picker-utils'
618
674
  export {
619
675
  RS_DATE_FORMAT,
@@ -723,3 +779,15 @@ export { useResolvedRsComponentSize, resolveRsComponentSize } from './components
723
779
  export { useResolvedRsRadius, rsRadiusCss } from './components/resolve-radius'
724
780
  export { themePresets } from './theme/presets'
725
781
  export { applyTheme } from './theme/apply'
782
+ export {
783
+ parseCssLengthToPx,
784
+ readCodeFontFamily,
785
+ readCodeFontSizePx,
786
+ readCssLengthPx,
787
+ readCssVar,
788
+ readRootFontSizePx,
789
+ readTerminalFontFamily,
790
+ readTerminalFontSizePx,
791
+ readTerminalFontWeight,
792
+ readTerminalFontWeightBold,
793
+ } from './theme/css-token'
@@ -0,0 +1,32 @@
1
+ export type RsI18nVars = Record<string, string | number>
2
+
3
+ export type RsTranslateFn = (
4
+ key: string,
5
+ fallbackOrVars?: string | RsI18nVars,
6
+ vars?: RsI18nVars,
7
+ ) => string
8
+
9
+ /**
10
+ * 将文案中的 `{name}` 替换为 vars。
11
+ * 对标 Ant Design / vue-i18n 的命名插值。
12
+ */
13
+ export function interpolateRsMessage(template: string, vars?: RsI18nVars): string {
14
+ if (!vars) return template
15
+ return template.replace(/\{(\w+)\}/g, (matched, key: string) => {
16
+ const value = vars[key]
17
+ return value == null ? matched : String(value)
18
+ })
19
+ }
20
+
21
+ /**
22
+ * 解析 t() 的第 2、3 参:既兼容 `t(key, fallback)`,也支持 `t(key, vars)`。
23
+ */
24
+ export function resolveRsTranslateArgs(
25
+ fallbackOrVars?: string | RsI18nVars,
26
+ vars?: RsI18nVars,
27
+ ): { fallback?: string; vars?: RsI18nVars } {
28
+ if (fallbackOrVars && typeof fallbackOrVars === 'object') {
29
+ return { vars: fallbackOrVars }
30
+ }
31
+ return { fallback: fallbackOrVars, vars }
32
+ }