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.
- package/CHANGELOG.md +28 -1
- package/package.json +1 -1
- package/src/__tests__/RsDatePicker.spec.ts +21 -0
- package/src/__tests__/RsDialog.spec.ts +61 -0
- package/src/__tests__/RsForm.spec.ts +5 -5
- package/src/__tests__/RsFormItem.spec.ts +478 -0
- package/src/__tests__/RsFormList.spec.ts +109 -0
- package/src/__tests__/RsInput.spec.ts +93 -0
- package/src/__tests__/RsInputNumber.spec.ts +33 -0
- package/src/__tests__/RsSelect.spec.ts +423 -1
- package/src/__tests__/RsSwitch.spec.ts +30 -0
- package/src/__tests__/RsTimePicker.spec.ts +9 -0
- package/src/__tests__/RsTooltip.spec.ts +43 -0
- package/src/__tests__/css-token.spec.ts +51 -0
- package/src/__tests__/date-picker-utils.spec.ts +49 -0
- package/src/__tests__/dialog-utils.spec.ts +43 -0
- package/src/__tests__/form-path.spec.ts +46 -0
- package/src/__tests__/form-rules.spec.ts +63 -0
- package/src/__tests__/theme.spec.ts +2 -0
- package/src/components/RsAlert.vue +1 -1
- package/src/components/RsAvatar.vue +1 -1
- package/src/components/RsBadge.vue +1 -1
- package/src/components/RsBreadcrumb.vue +1 -1
- package/src/components/RsButton.vue +4 -4
- package/src/components/RsCalendarGrid.vue +3 -3
- package/src/components/RsCard.vue +4 -4
- package/src/components/RsCodeBlock.vue +4 -4
- package/src/components/RsCodeEditor.vue +8 -8
- package/src/components/RsConfirmDialog.vue +2 -2
- package/src/components/RsContextMenu.vue +6 -4
- package/src/components/RsDatePicker.vue +75 -31
- package/src/components/RsDateTimePicker.vue +9 -1
- package/src/components/RsDescriptions.vue +2 -2
- package/src/components/RsDialog.vue +26 -16
- package/src/components/RsDrawer.vue +1 -1
- package/src/components/RsDropdown.vue +1 -1
- package/src/components/RsDynamicTags.vue +18 -4
- package/src/components/RsEmpty.vue +1 -1
- package/src/components/RsForm.vue +68 -1
- package/src/components/RsFormItem.vue +423 -0
- package/src/components/RsFormItemControl.vue +21 -0
- package/src/components/RsFormList.vue +117 -0
- package/src/components/RsInput.vue +411 -18
- package/src/components/RsInputNumber.vue +13 -4
- package/src/components/RsLink.vue +1 -1
- package/src/components/RsMarkdown.vue +5 -5
- package/src/components/RsMenu.vue +2 -2
- package/src/components/RsMonacoEditor.vue +9 -3
- package/src/components/RsPagination.vue +1 -1
- package/src/components/RsProseEditor.vue +3 -3
- package/src/components/RsSelect.vue +332 -611
- package/src/components/RsSidebarGroup.vue +1 -1
- package/src/components/RsSplitPane.vue +22 -1
- package/src/components/RsStatCard.vue +2 -2
- package/src/components/RsSteps.vue +2 -2
- package/src/components/RsSwitch.vue +20 -7
- package/src/components/RsTabs.vue +9 -9
- package/src/components/RsTag.vue +1 -1
- package/src/components/RsTerminal.vue +58 -19
- package/src/components/RsTimePicker.vue +28 -4
- package/src/components/RsTimePickerColumns.vue +4 -4
- package/src/components/RsToaster.vue +7 -7
- package/src/components/RsTooltip.vue +10 -1
- package/src/components/RsTooltipProvider.vue +11 -1
- package/src/components/RsTree.vue +6 -6
- package/src/components/RsUpload.vue +2 -2
- package/src/components/date-picker-utils.ts +113 -2
- package/src/components/dialog-utils.ts +37 -2
- package/src/components/form-item-control.ts +55 -0
- package/src/components/form-path.ts +92 -0
- package/src/components/form-rules.ts +64 -10
- package/src/components/form-utils.ts +94 -2
- package/src/components/select-utils.ts +272 -12
- package/src/components/table/RsTableHeaderFilter.vue +1 -1
- package/src/components/table/rs-table.css +1 -1
- package/src/components/use-rs-select.ts +436 -0
- package/src/composables/useRsConfig.ts +10 -3
- package/src/composables/useRsI18n.ts +16 -5
- package/src/index.ts +69 -1
- package/src/locale/interpolate.ts +32 -0
- package/src/locale/messages.ts +10 -0
- package/src/styles.css +68 -12
- package/src/theme/brand.example.css +4 -1
- package/src/theme/css-token.ts +107 -0
- 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:
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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,
|
|
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 {
|
|
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
|
+
}
|