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.
Files changed (88) hide show
  1. package/CHANGELOG.md +37 -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/RsTableCellEditor.vue +13 -5
  75. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  76. package/src/components/table/rs-table.css +1 -1
  77. package/src/components/use-rs-select.ts +436 -0
  78. package/src/composables/useRsConfig.ts +10 -3
  79. package/src/composables/useRsI18n.ts +16 -5
  80. package/src/composables/useRsTableCore.ts +3 -1
  81. package/src/composables/useRsTableHeadless.ts +10 -2
  82. package/src/index.ts +69 -1
  83. package/src/locale/interpolate.ts +32 -0
  84. package/src/locale/messages.ts +10 -0
  85. package/src/styles.css +68 -12
  86. package/src/theme/brand.example.css +4 -1
  87. package/src/theme/css-token.ts +107 -0
  88. 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 { useRsFormContext, useRsFormField } from './form-utils'
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
- /** string:YYYY-MM-DD[/HH:mm:ss];timestamp:毫秒时间戳 / [start, end] */
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
- if (useTimestamp.value) {
127
- if (!isTimestampRange(value)) return { ...EMPTY_DATE_RANGE }
165
+ const opts = convertOptions.value
166
+ if (isTimestampRange(value)) {
128
167
  return {
129
- start: formatTimestampValue(value[0], props.withTime),
130
- end: formatTimestampValue(value[1], props.withTime),
168
+ start: formatTimestampValue(value[0], opts.withTime),
169
+ end: formatTimestampValue(value[1], opts.withTime),
131
170
  }
132
171
  }
133
- return isRangeModel(value) ? value : { ...EMPTY_DATE_RANGE }
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 = 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
- const value = model.value
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
- if (useTimestamp.value) {
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
- if (useTimestamp.value) {
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
- :id="fieldId"
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': !!autoMessage,
621
+ 'rs-date-picker__trigger--invalid': isInvalid,
586
622
  }"
587
623
  :disabled="disabled"
588
- :aria-invalid="autoMessage ? true : undefined"
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="autoMessage" class="rs-date-picker-field__error" role="alert">
732
- {{ autoMessage }}
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: 600;
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: 500;
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
- /** string:YYYY-MM-DD HH:mm:ss;timestamp:毫秒时间戳 / [start, end] */
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: 600;
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: 500;
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="teleportTo === false"
405
- :to="teleportTo === false ? undefined : teleportTo"
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: 600;
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: 600;
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: 600;
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 { useRsFormContext, useRsFormField } from './form-utils'
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(() => Boolean(autoMessage.value))
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="autoMessage"
356
+ v-if="visibleMessage"
343
357
  class="rs-dynamic-tags-field__error"
344
358
  role="alert"
345
359
  >
346
- {{ autoMessage }}
360
+ {{ visibleMessage }}
347
361
  </p>
348
362
  </div>
349
363
  </template>
@@ -90,7 +90,7 @@ const rootStyle = computed(() => ({
90
90
  .rs-empty__title {
91
91
  margin: 0;
92
92
  font-size: var(--rs-font-size-sm);
93
- font-weight: 500;
93
+ font-weight: var(--rs-font-weight-medium);
94
94
  color: var(--rs-text);
95
95
  }
96
96
 
@@ -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