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
|
@@ -30,11 +30,18 @@ withDefaults(
|
|
|
30
30
|
withSeconds?: boolean
|
|
31
31
|
minDate?: string
|
|
32
32
|
maxDate?: string
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* 绑定值格式(展示仍为墙钟)。
|
|
35
|
+
* string:YYYY-MM-DD HH:mm:ss;timestamp:毫秒;iso:本地偏移 RFC3339;
|
|
36
|
+
* 亦可传入 dayjs 模板。
|
|
37
|
+
*/
|
|
34
38
|
valueFormat?: RsDatePickerValueFormat
|
|
35
39
|
labelPosition?: RsDateTimePickerLabelPosition
|
|
36
40
|
shortcuts?: RsDatePickerShortcut[]
|
|
37
41
|
size?: RsComponentSize
|
|
42
|
+
id?: string
|
|
43
|
+
invalid?: boolean
|
|
44
|
+
showValidateMessage?: boolean
|
|
38
45
|
}>(),
|
|
39
46
|
{
|
|
40
47
|
disabled: false,
|
|
@@ -43,6 +50,7 @@ withDefaults(
|
|
|
43
50
|
withSeconds: false,
|
|
44
51
|
valueFormat: 'string',
|
|
45
52
|
labelPosition: 'top',
|
|
53
|
+
showValidateMessage: true,
|
|
46
54
|
},
|
|
47
55
|
)
|
|
48
56
|
</script>
|
|
@@ -95,7 +95,7 @@ const gridStyle = computed(() => ({
|
|
|
95
95
|
|
|
96
96
|
.rs-descriptions__header {
|
|
97
97
|
font-size: var(--rs-font-size-base);
|
|
98
|
-
font-weight:
|
|
98
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
99
99
|
line-height: var(--rs-line-height-tight);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -145,7 +145,7 @@ const gridStyle = computed(() => ({
|
|
|
145
145
|
|
|
146
146
|
.rs-descriptions__label {
|
|
147
147
|
color: var(--rs-descriptions-label-fg, var(--rs-muted));
|
|
148
|
-
font-weight:
|
|
148
|
+
font-weight: var(--rs-font-weight-medium);
|
|
149
149
|
word-break: break-word;
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
isRsDialogWidthPreset,
|
|
26
26
|
resolveDialogOverlayStyle,
|
|
27
27
|
resolveRsDialogCssWidth,
|
|
28
|
+
resolveRsDialogWidthPx,
|
|
28
29
|
runRsDialogBeforeClose,
|
|
29
30
|
type RsDialogBeforeClose,
|
|
30
31
|
type RsDialogCloseReason,
|
|
@@ -43,6 +44,11 @@ const props = withDefaults(
|
|
|
43
44
|
/** 标题文案;也可用 `#title` / `#header` 插槽。缺省时用空格满足无障碍 Title */
|
|
44
45
|
title?: string
|
|
45
46
|
description?: string
|
|
47
|
+
/**
|
|
48
|
+
* 宽度:`sm` / `md` / `lg`,或 number(px) / `px` / `rem` / `%`。
|
|
49
|
+
* window:打开时折成像素后居中(`%` 相对视口扣除 inset),之后可拖拽缩放。
|
|
50
|
+
* confirm:自定义宽度按 CSS 写入。
|
|
51
|
+
*/
|
|
46
52
|
width?: RsDialogWidth
|
|
47
53
|
tone?: RsFeedbackTone
|
|
48
54
|
/**
|
|
@@ -73,6 +79,9 @@ const props = withDefaults(
|
|
|
73
79
|
* - string / HTMLElement:Teleport 到指定节点
|
|
74
80
|
* - false:禁用 Teleport,就地渲染(不挂到 body / 全局挂载点)
|
|
75
81
|
* - undefined:走 Reka 默认(通常为 body,或 ConfigProvider.teleportTo)
|
|
82
|
+
*
|
|
83
|
+
* 全屏时若未禁用 Teleport,会临时改挂到默认目标(通常为 body),
|
|
84
|
+
* 避免业务容器内的层叠上下文(如 isolation:isolate)低于应用顶栏。
|
|
76
85
|
*/
|
|
77
86
|
teleportTo?: string | HTMLElement | false
|
|
78
87
|
/**
|
|
@@ -150,16 +159,7 @@ const widthPreset = computed<RsDialogWidthPreset>(() =>
|
|
|
150
159
|
isRsDialogWidthPreset(props.width) ? props.width : 'md',
|
|
151
160
|
)
|
|
152
161
|
|
|
153
|
-
const initialWidthPx = computed(() =>
|
|
154
|
-
if (typeof props.width === 'number' && Number.isFinite(props.width) && props.width > 0) {
|
|
155
|
-
return props.width
|
|
156
|
-
}
|
|
157
|
-
if (typeof props.width === 'string') {
|
|
158
|
-
const trimmed = props.width.trim()
|
|
159
|
-
if (/^\d+(\.\d+)?px$/i.test(trimmed)) return Number.parseFloat(trimmed)
|
|
160
|
-
}
|
|
161
|
-
return undefined
|
|
162
|
-
})
|
|
162
|
+
const initialWidthPx = computed(() => resolveRsDialogWidthPx(props.width))
|
|
163
163
|
|
|
164
164
|
const customCssWidth = computed(() => resolveRsDialogCssWidth(props.width))
|
|
165
165
|
|
|
@@ -276,13 +276,12 @@ const contentClass = computed(() => {
|
|
|
276
276
|
|
|
277
277
|
const contentStyle = computed(() => {
|
|
278
278
|
const base = isWindowLayout.value ? dialogStyle.value : undefined
|
|
279
|
+
// window 宽度只走 bounds 像素,避免 `90%` 覆盖 CSS 后与 left/top 脱节
|
|
280
|
+
if (isWindowLayout.value) return base
|
|
279
281
|
const cssW = customCssWidth.value
|
|
280
|
-
// 数值/px 已通过 initialWidth 进入 window bounds;其它自定义宽度用 CSS 覆盖
|
|
281
282
|
if (!cssW) return base
|
|
282
|
-
if (isWindowLayout.value && initialWidthPx.value != null) return base
|
|
283
283
|
return {
|
|
284
284
|
...(base ?? {}),
|
|
285
|
-
width: isConfirmLayout.value ? undefined : cssW,
|
|
286
285
|
maxWidth: cssW,
|
|
287
286
|
}
|
|
288
287
|
})
|
|
@@ -373,6 +372,17 @@ const {
|
|
|
373
372
|
boundsTransition: toRef(props, 'boundsTransition'),
|
|
374
373
|
})
|
|
375
374
|
|
|
375
|
+
/**
|
|
376
|
+
* 全屏时跳出业务挂载点,挂到默认 portal(通常 body),
|
|
377
|
+
* 还原后仍回到 props.teleportTo,以保留页签内浮层的生命周期绑定。
|
|
378
|
+
*/
|
|
379
|
+
const resolvedTeleportTo = computed(() => {
|
|
380
|
+
if (isFullscreen.value && props.teleportTo !== false) {
|
|
381
|
+
return undefined
|
|
382
|
+
}
|
|
383
|
+
return props.teleportTo
|
|
384
|
+
})
|
|
385
|
+
|
|
376
386
|
const contentRef = ref<{ $el?: HTMLElement } | HTMLElement | null>(null)
|
|
377
387
|
|
|
378
388
|
watch(
|
|
@@ -401,8 +411,8 @@ defineExpose({
|
|
|
401
411
|
<template>
|
|
402
412
|
<DialogRoot :open="open" :modal="modal" @update:open="onUpdateOpen">
|
|
403
413
|
<DialogPortal
|
|
404
|
-
:disabled="
|
|
405
|
-
:to="
|
|
414
|
+
:disabled="resolvedTeleportTo === false"
|
|
415
|
+
:to="resolvedTeleportTo === false ? undefined : resolvedTeleportTo"
|
|
406
416
|
>
|
|
407
417
|
<DialogOverlay v-if="showOverlay" class="rs-dialog__overlay rs-motion-reduce" :style="overlayStyle" />
|
|
408
418
|
<DialogContent
|
|
@@ -680,7 +690,7 @@ defineExpose({
|
|
|
680
690
|
align-items: center;
|
|
681
691
|
gap: var(--rs-space-sm);
|
|
682
692
|
font-size: var(--rs-font-size-base);
|
|
683
|
-
font-weight:
|
|
693
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
684
694
|
letter-spacing: -0.015em;
|
|
685
695
|
line-height: var(--rs-line-height-tight, 1.3);
|
|
686
696
|
color: var(--rs-dialog-title-fg);
|
|
@@ -663,7 +663,7 @@ defineExpose({
|
|
|
663
663
|
align-items: center;
|
|
664
664
|
gap: var(--rs-space-sm);
|
|
665
665
|
font-size: var(--rs-font-size-base);
|
|
666
|
-
font-weight:
|
|
666
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
667
667
|
line-height: var(--rs-line-height-tight, 1.3);
|
|
668
668
|
color: var(--rs-drawer-title-fg, var(--rs-text));
|
|
669
669
|
overflow: hidden;
|
|
@@ -218,7 +218,7 @@ function onSelect(value: AcceptableValue) {
|
|
|
218
218
|
display: block;
|
|
219
219
|
padding: 0.375rem var(--rs-space-md) 0.125rem;
|
|
220
220
|
font-size: var(--rs-font-size-xs);
|
|
221
|
-
font-weight:
|
|
221
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
222
222
|
color: var(--rs-muted);
|
|
223
223
|
letter-spacing: 0.04em;
|
|
224
224
|
line-height: var(--rs-line-height-tight);
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
|
3
3
|
import { useRsI18n } from '../composables/useRsI18n'
|
|
4
4
|
import type { RsComponentSize, RsRadius } from '../theme/types'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
isRsFormItemBoundControl,
|
|
7
|
+
useRsFormContext,
|
|
8
|
+
useRsFormField,
|
|
9
|
+
useRsFormItemContext,
|
|
10
|
+
} from './form-utils'
|
|
6
11
|
import {
|
|
7
12
|
buildLocalInputRules,
|
|
8
13
|
runFormFieldRules,
|
|
@@ -23,6 +28,8 @@ const props = withDefaults(
|
|
|
23
28
|
defineProps<{
|
|
24
29
|
/** 字段名:匹配 RsForm.rules[name] */
|
|
25
30
|
name?: string
|
|
31
|
+
id?: string
|
|
32
|
+
invalid?: boolean
|
|
26
33
|
placeholder?: string
|
|
27
34
|
disabled?: boolean
|
|
28
35
|
required?: boolean
|
|
@@ -63,6 +70,10 @@ const emit = defineEmits<{
|
|
|
63
70
|
|
|
64
71
|
const { t } = useRsI18n()
|
|
65
72
|
const formContext = useRsFormContext()
|
|
73
|
+
const formItem = useRsFormItemContext()
|
|
74
|
+
const boundToItem = computed(() =>
|
|
75
|
+
isRsFormItemBoundControl(formItem, { id: props.id, name: props.name }),
|
|
76
|
+
)
|
|
66
77
|
const draft = ref('')
|
|
67
78
|
const editing = ref(props.inputMode === 'always')
|
|
68
79
|
const composing = ref(false)
|
|
@@ -93,7 +104,10 @@ const showTrigger = computed(
|
|
|
93
104
|
!editing.value,
|
|
94
105
|
)
|
|
95
106
|
|
|
96
|
-
const hasError = computed(() =>
|
|
107
|
+
const hasError = computed(() =>
|
|
108
|
+
Boolean(props.invalid || (boundToItem.value && formItem?.invalid.value) || autoMessage.value),
|
|
109
|
+
)
|
|
110
|
+
const visibleMessage = computed(() => (boundToItem.value ? '' : autoMessage.value))
|
|
97
111
|
|
|
98
112
|
const rootClass = computed(() => [
|
|
99
113
|
'rs-dynamic-tags',
|
|
@@ -339,11 +353,11 @@ onBeforeUnmount(() => {
|
|
|
339
353
|
</button>
|
|
340
354
|
</div>
|
|
341
355
|
<p
|
|
342
|
-
v-if="
|
|
356
|
+
v-if="visibleMessage"
|
|
343
357
|
class="rs-dynamic-tags-field__error"
|
|
344
358
|
role="alert"
|
|
345
359
|
>
|
|
346
|
-
{{
|
|
360
|
+
{{ visibleMessage }}
|
|
347
361
|
</p>
|
|
348
362
|
</div>
|
|
349
363
|
</template>
|
|
@@ -13,7 +13,13 @@ import type {
|
|
|
13
13
|
RsFormValidationResult,
|
|
14
14
|
} from './form-utils'
|
|
15
15
|
import { cloneFormFieldValue, provideRsFormContext, resolveFieldRules } from './form-utils'
|
|
16
|
-
import type { RsFormRuleTrigger, RsFormRules } from './form-rules'
|
|
16
|
+
import type { RsFormRuleTrigger, RsFormRules, RsFormValidateMessages } from './form-rules'
|
|
17
|
+
import {
|
|
18
|
+
getByNamePath,
|
|
19
|
+
namePathKey,
|
|
20
|
+
setByNamePath,
|
|
21
|
+
type RsFormNamePath,
|
|
22
|
+
} from './form-path'
|
|
17
23
|
|
|
18
24
|
const props = withDefaults(
|
|
19
25
|
defineProps<{
|
|
@@ -29,6 +35,13 @@ const props = withDefaults(
|
|
|
29
35
|
* 与字段自身 required/rule/validator 合并执行。
|
|
30
36
|
*/
|
|
31
37
|
rules?: RsFormRules
|
|
38
|
+
/**
|
|
39
|
+
* 表单数据中枢(对齐 Ant Design Form.model)。
|
|
40
|
+
* Form.Item 声明 name 后读写此对象;也可继续在控件上 v-model。
|
|
41
|
+
*/
|
|
42
|
+
model?: Record<string, unknown>
|
|
43
|
+
/** 覆盖默认校验文案模板,支持 `{label}` */
|
|
44
|
+
validateMessages?: RsFormValidateMessages
|
|
32
45
|
/** 原生 form 提交时自动跑一遍校验(默认 true) */
|
|
33
46
|
validateOnSubmit?: boolean
|
|
34
47
|
/**
|
|
@@ -62,6 +75,8 @@ const labelWidthRef = computed(() => props.labelWidth)
|
|
|
62
75
|
const labelAlignRef = computed(() => props.labelAlign)
|
|
63
76
|
const sizeRef = computed(() => props.size)
|
|
64
77
|
const rulesRef = toRef(props, 'rules')
|
|
78
|
+
const modelRef = toRef(props, 'model')
|
|
79
|
+
const validateMessagesRef = computed(() => props.validateMessages)
|
|
65
80
|
const initialValues = ref(new Map<symbol, unknown>())
|
|
66
81
|
|
|
67
82
|
function registerField(id: symbol, field: RsFormFieldExpose): void {
|
|
@@ -78,6 +93,48 @@ function getFieldRules(name?: string) {
|
|
|
78
93
|
return resolveFieldRules(props.rules, name)
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
function getFieldValue(name: RsFormNamePath): unknown {
|
|
97
|
+
if (modelRef.value) return getByNamePath(modelRef.value, name)
|
|
98
|
+
const key = namePathKey(name)
|
|
99
|
+
const field = Array.from(fields.values()).find((item) => item.name === key)
|
|
100
|
+
return field?.getValue()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setFieldValue(name: RsFormNamePath, value: unknown): void {
|
|
104
|
+
if (modelRef.value) {
|
|
105
|
+
setByNamePath(modelRef.value, name, value)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const key = namePathKey(name)
|
|
109
|
+
const field = Array.from(fields.values()).find((item) => item.name === key)
|
|
110
|
+
field?.setValue(value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getFieldsValue(): Record<string, unknown> {
|
|
114
|
+
if (modelRef.value) return { ...modelRef.value }
|
|
115
|
+
const values: Record<string, unknown> = {}
|
|
116
|
+
fields.forEach((field) => {
|
|
117
|
+
if (!field.name) return
|
|
118
|
+
values[field.name] = field.getValue()
|
|
119
|
+
})
|
|
120
|
+
return values
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setFieldsValue(values: Record<string, unknown>): void {
|
|
124
|
+
Object.entries(values).forEach(([name, value]) => {
|
|
125
|
+
setFieldValue(name, value)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scrollToField(name: RsFormNamePath): void {
|
|
130
|
+
if (typeof document === 'undefined') return
|
|
131
|
+
const key = namePathKey(name)
|
|
132
|
+
const el = document.querySelector(`[data-rs-form-item="${CSS.escape(key)}"]`)
|
|
133
|
+
if (el instanceof HTMLElement) {
|
|
134
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
81
138
|
function renderError(ctx: RsFormErrorRenderContext) {
|
|
82
139
|
if (slots.error) return slots.error(ctx)
|
|
83
140
|
return props.errorRender?.(ctx)
|
|
@@ -94,6 +151,11 @@ provideRsFormContext({
|
|
|
94
151
|
renderError,
|
|
95
152
|
registerField,
|
|
96
153
|
unregisterField,
|
|
154
|
+
model: modelRef,
|
|
155
|
+
validateMessages: validateMessagesRef,
|
|
156
|
+
getFieldValue,
|
|
157
|
+
setFieldValue,
|
|
158
|
+
getFieldsValue,
|
|
97
159
|
})
|
|
98
160
|
|
|
99
161
|
function collectResult(
|
|
@@ -169,6 +231,11 @@ defineExpose({
|
|
|
169
231
|
validateField,
|
|
170
232
|
clearValidation,
|
|
171
233
|
resetFields,
|
|
234
|
+
getFieldsValue,
|
|
235
|
+
setFieldsValue,
|
|
236
|
+
getFieldValue,
|
|
237
|
+
setFieldValue,
|
|
238
|
+
scrollToField,
|
|
172
239
|
})
|
|
173
240
|
</script>
|
|
174
241
|
|