niuma-ui 1.0.2 → 1.1.1
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 +37 -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/RsTableCellEditor.vue +13 -5
- 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/composables/useRsTableCore.ts +3 -1
- package/src/composables/useRsTableHeadless.ts +10 -2
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useId, watch } from 'vue'
|
|
2
|
+
import { computed, ref, useAttrs, useId, watch } from 'vue'
|
|
3
3
|
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from './reka'
|
|
4
4
|
import RsCalendarGrid from './RsCalendarGrid.vue'
|
|
5
5
|
import RsIcon from './RsIcon.vue'
|
|
@@ -7,7 +7,12 @@ import RsTimePicker from './RsTimePicker.vue'
|
|
|
7
7
|
import { useRsI18n } from '../composables/useRsI18n'
|
|
8
8
|
import { RS_COMPONENT_SIZE_ICON_PX, type RsComponentSize } from '../theme/types'
|
|
9
9
|
import { useResolvedRsComponentSize } from './resolve-size'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isRsFormItemBoundControl,
|
|
12
|
+
useRsFormContext,
|
|
13
|
+
useRsFormField,
|
|
14
|
+
useRsFormItemContext,
|
|
15
|
+
} from './form-utils'
|
|
11
16
|
import {
|
|
12
17
|
buildLocalInputRules,
|
|
13
18
|
runFormFieldRules,
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
getNextMonth,
|
|
27
32
|
getNowDateTime,
|
|
28
33
|
formatTimestampValue,
|
|
34
|
+
fromInternalPickerValue,
|
|
29
35
|
getTodayDate,
|
|
30
36
|
isDateRangeEmpty,
|
|
31
37
|
isDateRangeOrdered,
|
|
@@ -35,6 +41,8 @@ import {
|
|
|
35
41
|
parseDateTimeValue,
|
|
36
42
|
parseDateValue,
|
|
37
43
|
parseTimestampValue,
|
|
44
|
+
toInternalPickerValue,
|
|
45
|
+
toRangeEndpointString,
|
|
38
46
|
type RsDatePickerModelValue,
|
|
39
47
|
type RsDatePickerShortcut,
|
|
40
48
|
type RsDatePickerValueFormat,
|
|
@@ -55,6 +63,8 @@ function isRangeModel(value: RsDatePickerModelValue): value is RsDateRangeValue
|
|
|
55
63
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && ('start' in value || 'end' in value)
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
defineOptions({ inheritAttrs: false })
|
|
67
|
+
|
|
58
68
|
const model = defineModel<RsDatePickerModelValue>({ default: '' })
|
|
59
69
|
const open = defineModel<boolean>('open', { default: false })
|
|
60
70
|
|
|
@@ -75,12 +85,19 @@ const props = withDefaults(
|
|
|
75
85
|
* 是否选择秒。未传时:withTime 为 true 则默认带秒,纯日期为 false。
|
|
76
86
|
*/
|
|
77
87
|
withSeconds?: boolean
|
|
78
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* 绑定值格式(展示仍为墙钟)。
|
|
90
|
+
* string:YYYY-MM-DD[/ HH:mm:ss];timestamp:毫秒;iso:本地偏移 RFC3339;
|
|
91
|
+
* 亦可传入 dayjs 模板(对齐 Element Plus value-format)。
|
|
92
|
+
*/
|
|
79
93
|
valueFormat?: RsDatePickerValueFormat
|
|
80
94
|
labelPosition?: RsDatePickerLabelPosition
|
|
81
95
|
/** 底部快捷选项(有值时替代「今天/现在」链接) */
|
|
82
96
|
shortcuts?: RsDatePickerShortcut[]
|
|
83
97
|
size?: RsComponentSize
|
|
98
|
+
id?: string
|
|
99
|
+
invalid?: boolean
|
|
100
|
+
showValidateMessage?: boolean
|
|
84
101
|
}>(),
|
|
85
102
|
{
|
|
86
103
|
disabled: false,
|
|
@@ -89,10 +106,12 @@ const props = withDefaults(
|
|
|
89
106
|
withTime: false,
|
|
90
107
|
valueFormat: 'string',
|
|
91
108
|
labelPosition: 'top',
|
|
109
|
+
showValidateMessage: true,
|
|
92
110
|
},
|
|
93
111
|
)
|
|
94
112
|
|
|
95
113
|
const fieldId = useId()
|
|
114
|
+
const attrs = useAttrs()
|
|
96
115
|
const { t } = useRsI18n()
|
|
97
116
|
const resolvedSize = useResolvedRsComponentSize(() => props.size)
|
|
98
117
|
const triggerIconSize = computed(() => RS_COMPONENT_SIZE_ICON_PX[resolvedSize.value])
|
|
@@ -101,7 +120,22 @@ const resolvedWithSeconds = computed(() =>
|
|
|
101
120
|
props.withSeconds !== undefined ? props.withSeconds : props.withTime,
|
|
102
121
|
)
|
|
103
122
|
const formContext = useRsFormContext()
|
|
123
|
+
const formItem = useRsFormItemContext()
|
|
124
|
+
const boundToItem = computed(() =>
|
|
125
|
+
isRsFormItemBoundControl(formItem, { id: props.id, name: props.name }),
|
|
126
|
+
)
|
|
104
127
|
const autoMessage = ref('')
|
|
128
|
+
const isInvalid = computed(() =>
|
|
129
|
+
Boolean(
|
|
130
|
+
props.invalid ||
|
|
131
|
+
(boundToItem.value && formItem?.invalid.value) ||
|
|
132
|
+
(!boundToItem.value && autoMessage.value),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
const triggerId = computed(() => props.id || fieldId)
|
|
136
|
+
const visibleMessage = computed(() =>
|
|
137
|
+
boundToItem.value || props.showValidateMessage === false ? '' : autoMessage.value,
|
|
138
|
+
)
|
|
105
139
|
|
|
106
140
|
const viewYear = ref(getTodayDate().year)
|
|
107
141
|
const viewMonth = ref(getTodayDate().month)
|
|
@@ -117,47 +151,52 @@ const draftTime = ref('')
|
|
|
117
151
|
const draftStartTime = ref('')
|
|
118
152
|
const draftEndTime = ref('')
|
|
119
153
|
|
|
154
|
+
const convertOptions = computed(() => ({
|
|
155
|
+
valueFormat: props.valueFormat ?? 'string',
|
|
156
|
+
withTime: props.withTime,
|
|
157
|
+
}))
|
|
158
|
+
|
|
120
159
|
const useTimestamp = computed(() => props.valueFormat === 'timestamp')
|
|
121
160
|
|
|
122
161
|
const rangeModel = computed<RsDateRangeValue>({
|
|
123
162
|
get() {
|
|
124
163
|
if (!props.range) return { ...EMPTY_DATE_RANGE }
|
|
125
164
|
const value = model.value
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
const opts = convertOptions.value
|
|
166
|
+
if (isTimestampRange(value)) {
|
|
128
167
|
return {
|
|
129
|
-
start: formatTimestampValue(value[0],
|
|
130
|
-
end: formatTimestampValue(value[1],
|
|
168
|
+
start: formatTimestampValue(value[0], opts.withTime),
|
|
169
|
+
end: formatTimestampValue(value[1], opts.withTime),
|
|
131
170
|
}
|
|
132
171
|
}
|
|
133
|
-
|
|
172
|
+
if (!isRangeModel(value)) return { ...EMPTY_DATE_RANGE }
|
|
173
|
+
return {
|
|
174
|
+
start: toInternalPickerValue(value.start, opts) || undefined,
|
|
175
|
+
end: toInternalPickerValue(value.end, opts) || undefined,
|
|
176
|
+
}
|
|
134
177
|
},
|
|
135
178
|
set(value) {
|
|
179
|
+
const opts = convertOptions.value
|
|
136
180
|
if (useTimestamp.value) {
|
|
137
181
|
const start = parseTimestampValue(value.start)
|
|
138
182
|
const end = parseTimestampValue(value.end)
|
|
139
183
|
model.value = start != null && end != null ? [start, end] : null
|
|
140
184
|
return
|
|
141
185
|
}
|
|
142
|
-
model.value =
|
|
186
|
+
model.value = {
|
|
187
|
+
start: toRangeEndpointString(fromInternalPickerValue(value.start || '', opts)),
|
|
188
|
+
end: toRangeEndpointString(fromInternalPickerValue(value.end || '', opts)),
|
|
189
|
+
}
|
|
143
190
|
},
|
|
144
191
|
})
|
|
145
192
|
|
|
146
193
|
const singleModel = computed({
|
|
147
194
|
get() {
|
|
148
195
|
if (props.range) return ''
|
|
149
|
-
|
|
150
|
-
if (useTimestamp.value) {
|
|
151
|
-
return typeof value === 'number' ? formatTimestampValue(value, props.withTime) : ''
|
|
152
|
-
}
|
|
153
|
-
return typeof value === 'string' ? value : ''
|
|
196
|
+
return toInternalPickerValue(model.value, convertOptions.value)
|
|
154
197
|
},
|
|
155
198
|
set(value: string) {
|
|
156
|
-
|
|
157
|
-
model.value = value ? parseTimestampValue(value) : null
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
model.value = value
|
|
199
|
+
model.value = fromInternalPickerValue(value, convertOptions.value)
|
|
161
200
|
},
|
|
162
201
|
})
|
|
163
202
|
|
|
@@ -422,11 +461,7 @@ function clearSelection(): void {
|
|
|
422
461
|
draftEnd.value = null
|
|
423
462
|
if (props.withTime) resetRangeTimeDrafts()
|
|
424
463
|
} else {
|
|
425
|
-
|
|
426
|
-
model.value = null
|
|
427
|
-
} else {
|
|
428
|
-
singleModel.value = ''
|
|
429
|
-
}
|
|
464
|
+
singleModel.value = ''
|
|
430
465
|
draftDate.value = null
|
|
431
466
|
if (props.withTime) draftTime.value = currentTimeValue()
|
|
432
467
|
}
|
|
@@ -577,15 +612,16 @@ watch(open, (isOpen) => {
|
|
|
577
612
|
<div class="rs-date-picker" :class="`rs-date-picker--${resolvedSize}`">
|
|
578
613
|
<PopoverRoot v-model:open="open">
|
|
579
614
|
<PopoverTrigger
|
|
580
|
-
|
|
615
|
+
v-bind="attrs"
|
|
616
|
+
:id="triggerId"
|
|
581
617
|
type="button"
|
|
582
618
|
class="rs-date-picker__trigger"
|
|
583
619
|
:class="{
|
|
584
620
|
'rs-date-picker__trigger--placeholder': isEmpty,
|
|
585
|
-
'rs-date-picker__trigger--invalid':
|
|
621
|
+
'rs-date-picker__trigger--invalid': isInvalid,
|
|
586
622
|
}"
|
|
587
623
|
:disabled="disabled"
|
|
588
|
-
:aria-invalid="
|
|
624
|
+
:aria-invalid="isInvalid || undefined"
|
|
589
625
|
>
|
|
590
626
|
<span class="rs-date-picker__leading">
|
|
591
627
|
<RsIcon :name="triggerIcon" :size="triggerIconSize" class="rs-date-picker__icon" />
|
|
@@ -728,8 +764,8 @@ watch(open, (isOpen) => {
|
|
|
728
764
|
</PopoverRoot>
|
|
729
765
|
</div>
|
|
730
766
|
|
|
731
|
-
<p v-if="
|
|
732
|
-
{{
|
|
767
|
+
<p v-if="visibleMessage" class="rs-date-picker-field__error" role="alert">
|
|
768
|
+
{{ visibleMessage }}
|
|
733
769
|
</p>
|
|
734
770
|
<span v-if="hint" class="rs-field__hint">{{ hint }}</span>
|
|
735
771
|
</div>
|
|
@@ -758,6 +794,7 @@ watch(open, (isOpen) => {
|
|
|
758
794
|
line-height: var(--rs-line-height-tight);
|
|
759
795
|
text-align: left;
|
|
760
796
|
cursor: pointer;
|
|
797
|
+
outline: none;
|
|
761
798
|
box-shadow: var(--rs-input-shadow, none);
|
|
762
799
|
transition:
|
|
763
800
|
border-color var(--rs-transition-fast),
|
|
@@ -803,6 +840,13 @@ watch(open, (isOpen) => {
|
|
|
803
840
|
.rs-date-picker__trigger--invalid {
|
|
804
841
|
border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
805
842
|
}
|
|
843
|
+
.rs-date-picker__trigger--invalid:focus-visible {
|
|
844
|
+
border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
845
|
+
box-shadow:
|
|
846
|
+
var(--rs-input-shadow, none),
|
|
847
|
+
0 0 0 var(--rs-focus-ring-width, 2px)
|
|
848
|
+
color-mix(in srgb, var(--rs-danger, #dc2626) 14%, transparent);
|
|
849
|
+
}
|
|
806
850
|
.rs-date-picker-field__error {
|
|
807
851
|
margin: var(--rs-space-xs) 0 0;
|
|
808
852
|
color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
@@ -874,7 +918,7 @@ watch(open, (isOpen) => {
|
|
|
874
918
|
}
|
|
875
919
|
.rs-date-picker__pane-title {
|
|
876
920
|
font-size: var(--rs-font-size-xs);
|
|
877
|
-
font-weight:
|
|
921
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
878
922
|
color: var(--rs-muted);
|
|
879
923
|
}
|
|
880
924
|
.rs-date-picker__footer {
|
|
@@ -979,7 +1023,7 @@ watch(open, (isOpen) => {
|
|
|
979
1023
|
.rs-date-picker__time-label {
|
|
980
1024
|
flex-shrink: 0;
|
|
981
1025
|
font-size: var(--rs-font-size-xs);
|
|
982
|
-
font-weight:
|
|
1026
|
+
font-weight: var(--rs-font-weight-medium);
|
|
983
1027
|
color: var(--rs-muted);
|
|
984
1028
|
}
|
|
985
1029
|
.rs-date-picker__time-picker {
|
|
@@ -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
|
|