niuma-ui 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/RsDatePicker.spec.ts +21 -0
  4. package/src/__tests__/RsDialog.spec.ts +61 -0
  5. package/src/__tests__/RsForm.spec.ts +5 -5
  6. package/src/__tests__/RsFormItem.spec.ts +478 -0
  7. package/src/__tests__/RsFormList.spec.ts +109 -0
  8. package/src/__tests__/RsInput.spec.ts +93 -0
  9. package/src/__tests__/RsInputNumber.spec.ts +33 -0
  10. package/src/__tests__/RsSelect.spec.ts +423 -1
  11. package/src/__tests__/RsSwitch.spec.ts +30 -0
  12. package/src/__tests__/RsTimePicker.spec.ts +9 -0
  13. package/src/__tests__/RsTooltip.spec.ts +43 -0
  14. package/src/__tests__/css-token.spec.ts +51 -0
  15. package/src/__tests__/date-picker-utils.spec.ts +49 -0
  16. package/src/__tests__/dialog-utils.spec.ts +43 -0
  17. package/src/__tests__/form-path.spec.ts +46 -0
  18. package/src/__tests__/form-rules.spec.ts +63 -0
  19. package/src/__tests__/theme.spec.ts +2 -0
  20. package/src/components/RsAlert.vue +1 -1
  21. package/src/components/RsAvatar.vue +1 -1
  22. package/src/components/RsBadge.vue +1 -1
  23. package/src/components/RsBreadcrumb.vue +1 -1
  24. package/src/components/RsButton.vue +4 -4
  25. package/src/components/RsCalendarGrid.vue +3 -3
  26. package/src/components/RsCard.vue +4 -4
  27. package/src/components/RsCodeBlock.vue +4 -4
  28. package/src/components/RsCodeEditor.vue +8 -8
  29. package/src/components/RsConfirmDialog.vue +2 -2
  30. package/src/components/RsContextMenu.vue +6 -4
  31. package/src/components/RsDatePicker.vue +75 -31
  32. package/src/components/RsDateTimePicker.vue +9 -1
  33. package/src/components/RsDescriptions.vue +2 -2
  34. package/src/components/RsDialog.vue +26 -16
  35. package/src/components/RsDrawer.vue +1 -1
  36. package/src/components/RsDropdown.vue +1 -1
  37. package/src/components/RsDynamicTags.vue +18 -4
  38. package/src/components/RsEmpty.vue +1 -1
  39. package/src/components/RsForm.vue +68 -1
  40. package/src/components/RsFormItem.vue +423 -0
  41. package/src/components/RsFormItemControl.vue +21 -0
  42. package/src/components/RsFormList.vue +117 -0
  43. package/src/components/RsInput.vue +411 -18
  44. package/src/components/RsInputNumber.vue +13 -4
  45. package/src/components/RsLink.vue +1 -1
  46. package/src/components/RsMarkdown.vue +5 -5
  47. package/src/components/RsMenu.vue +2 -2
  48. package/src/components/RsMonacoEditor.vue +9 -3
  49. package/src/components/RsPagination.vue +1 -1
  50. package/src/components/RsProseEditor.vue +3 -3
  51. package/src/components/RsSelect.vue +332 -611
  52. package/src/components/RsSidebarGroup.vue +1 -1
  53. package/src/components/RsSplitPane.vue +22 -1
  54. package/src/components/RsStatCard.vue +2 -2
  55. package/src/components/RsSteps.vue +2 -2
  56. package/src/components/RsSwitch.vue +20 -7
  57. package/src/components/RsTabs.vue +9 -9
  58. package/src/components/RsTag.vue +1 -1
  59. package/src/components/RsTerminal.vue +58 -19
  60. package/src/components/RsTimePicker.vue +28 -4
  61. package/src/components/RsTimePickerColumns.vue +4 -4
  62. package/src/components/RsToaster.vue +7 -7
  63. package/src/components/RsTooltip.vue +10 -1
  64. package/src/components/RsTooltipProvider.vue +11 -1
  65. package/src/components/RsTree.vue +6 -6
  66. package/src/components/RsUpload.vue +2 -2
  67. package/src/components/date-picker-utils.ts +113 -2
  68. package/src/components/dialog-utils.ts +37 -2
  69. package/src/components/form-item-control.ts +55 -0
  70. package/src/components/form-path.ts +92 -0
  71. package/src/components/form-rules.ts +64 -10
  72. package/src/components/form-utils.ts +94 -2
  73. package/src/components/select-utils.ts +272 -12
  74. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  75. package/src/components/table/rs-table.css +1 -1
  76. package/src/components/use-rs-select.ts +436 -0
  77. package/src/composables/useRsConfig.ts +10 -3
  78. package/src/composables/useRsI18n.ts +16 -5
  79. package/src/index.ts +69 -1
  80. package/src/locale/interpolate.ts +32 -0
  81. package/src/locale/messages.ts +10 -0
  82. package/src/styles.css +68 -12
  83. package/src/theme/brand.example.css +4 -1
  84. package/src/theme/css-token.ts +107 -0
  85. package/src/theme/types.ts +1 -0
@@ -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