niuma-ui 1.0.0 → 1.0.2

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 (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, toRef, useSlots } from 'vue'
3
3
  import type {
4
+ RsFormErrorRender,
5
+ RsFormErrorRenderContext,
4
6
  RsFormFieldExpose,
7
+ RsFormFieldValidationResult,
5
8
  RsFormGap,
6
9
  RsFormLabelAlign,
7
10
  RsFormLabelPosition,
@@ -9,7 +12,8 @@ import type {
9
12
  RsFormSize,
10
13
  RsFormValidationResult,
11
14
  } from './form-utils'
12
- import { cloneFormFieldValue, provideRsFormContext } from './form-utils'
15
+ import { cloneFormFieldValue, provideRsFormContext, resolveFieldRules } from './form-utils'
16
+ import type { RsFormRuleTrigger, RsFormRules } from './form-rules'
13
17
 
14
18
  const props = withDefaults(
15
19
  defineProps<{
@@ -20,6 +24,18 @@ const props = withDefaults(
20
24
  size?: RsFormSize
21
25
  gap?: RsFormGap
22
26
  maxWidth?: RsFormMaxWidth
27
+ /**
28
+ * 集中声明校验规则(按字段 name 匹配)。
29
+ * 与字段自身 required/rule/validator 合并执行。
30
+ */
31
+ rules?: RsFormRules
32
+ /** 原生 form 提交时自动跑一遍校验(默认 true) */
33
+ validateOnSubmit?: boolean
34
+ /**
35
+ * 自定义错误渲染(函数式)。
36
+ * 也可用 `#error="{ name, message, value }"` 插槽,插槽优先。
37
+ */
38
+ errorRender?: RsFormErrorRender
23
39
  }>(),
24
40
  {
25
41
  disabled: false,
@@ -29,15 +45,23 @@ const props = withDefaults(
29
45
  size: 'md',
30
46
  gap: 'md',
31
47
  maxWidth: 'full',
48
+ validateOnSubmit: true,
32
49
  },
33
50
  )
34
51
 
52
+ const emit = defineEmits<{
53
+ submit: [event: Event]
54
+ validate: [result: RsFormValidationResult]
55
+ }>()
56
+
57
+ const slots = useSlots()
35
58
  const fields = new Map<symbol, RsFormFieldExpose>()
36
59
  const disabledRef = computed(() => props.disabled)
37
60
  const labelPositionRef = computed(() => props.labelPosition)
38
61
  const labelWidthRef = computed(() => props.labelWidth)
39
62
  const labelAlignRef = computed(() => props.labelAlign)
40
63
  const sizeRef = computed(() => props.size)
64
+ const rulesRef = toRef(props, 'rules')
41
65
  const initialValues = ref(new Map<symbol, unknown>())
42
66
 
43
67
  function registerField(id: symbol, field: RsFormFieldExpose): void {
@@ -50,36 +74,99 @@ function unregisterField(id: symbol): void {
50
74
  initialValues.value.delete(id)
51
75
  }
52
76
 
77
+ function getFieldRules(name?: string) {
78
+ return resolveFieldRules(props.rules, name)
79
+ }
80
+
81
+ function renderError(ctx: RsFormErrorRenderContext) {
82
+ if (slots.error) return slots.error(ctx)
83
+ return props.errorRender?.(ctx)
84
+ }
85
+
53
86
  provideRsFormContext({
54
87
  disabled: disabledRef,
55
88
  labelPosition: labelPositionRef,
56
89
  labelWidth: labelWidthRef,
57
90
  labelAlign: labelAlignRef,
58
91
  size: sizeRef,
92
+ rules: rulesRef,
93
+ getFieldRules,
94
+ renderError,
59
95
  registerField,
60
96
  unregisterField,
61
97
  })
62
98
 
63
- async function validate(): Promise<RsFormValidationResult> {
99
+ function collectResult(
100
+ results: RsFormFieldValidationResult[],
101
+ ): RsFormValidationResult {
102
+ const errors: Record<string, string> = {}
103
+ for (const result of results) {
104
+ if (!result.valid && result.name && result.message && !errors[result.name]) {
105
+ errors[result.name] = result.message
106
+ }
107
+ }
108
+ return {
109
+ valid: results.every((result) => result.valid),
110
+ errors,
111
+ }
112
+ }
113
+
114
+ /** 校验全部字段;可传 trigger 过滤规则 */
115
+ async function validate(trigger: RsFormRuleTrigger = 'submit'): Promise<RsFormValidationResult> {
64
116
  const results = await Promise.all(
65
- Array.from(fields.values()).map((field) => field.validate?.() ?? { valid: true }),
117
+ Array.from(fields.values()).map(
118
+ (field) => field.validate?.(trigger) ?? { valid: true, name: field.name },
119
+ ),
66
120
  )
67
- return { valid: results.every((result) => result.valid) }
121
+ const aggregated = collectResult(results)
122
+ emit('validate', aggregated)
123
+ return aggregated
68
124
  }
69
125
 
70
- function clearValidation(): void {
71
- fields.forEach((field) => field.clearValidation?.())
126
+ /** 按 name 校验单个字段 */
127
+ async function validateField(
128
+ name: string,
129
+ trigger: RsFormRuleTrigger = 'submit',
130
+ ): Promise<RsFormFieldValidationResult> {
131
+ const field = Array.from(fields.values()).find((item) => item.name === name)
132
+ if (!field) return { valid: true, name }
133
+ const result = (await field.validate?.(trigger)) ?? { valid: true, name }
134
+ return { ...result, name }
72
135
  }
73
136
 
74
- function resetFields(): void {
137
+ function clearValidation(names?: string | string[]): void {
138
+ const nameSet = names
139
+ ? new Set(Array.isArray(names) ? names : [names])
140
+ : null
141
+ fields.forEach((field) => {
142
+ if (nameSet && (!field.name || !nameSet.has(field.name))) return
143
+ field.clearValidation?.()
144
+ })
145
+ }
146
+
147
+ function resetFields(names?: string | string[]): void {
148
+ const nameSet = names
149
+ ? new Set(Array.isArray(names) ? names : [names])
150
+ : null
75
151
  fields.forEach((field, id) => {
152
+ if (nameSet && (!field.name || !nameSet.has(field.name))) return
76
153
  field.setValue(cloneFormFieldValue(initialValues.value.get(id)))
77
154
  field.clearValidation?.()
78
155
  })
79
156
  }
80
157
 
158
+ async function onSubmit(event: Event): Promise<void> {
159
+ if (props.validateOnSubmit) {
160
+ event.preventDefault()
161
+ const result = await validate('submit')
162
+ if (!result.valid) return
163
+ }
164
+ emit('submit', event)
165
+ }
166
+
81
167
  defineExpose({
82
168
  validate,
169
+ validateField,
83
170
  clearValidation,
84
171
  resetFields,
85
172
  })
@@ -93,7 +180,9 @@ defineExpose({
93
180
  `rs-form--max-${maxWidth}`,
94
181
  `rs-form--label-${labelPosition}`,
95
182
  ]"
183
+ @submit="onSubmit"
96
184
  >
185
+ <!-- 默认插槽:表单字段;#error 不在此渲染,经 Context 注入到字段错误区 -->
97
186
  <slot />
98
187
  </form>
99
188
  </template>
@@ -9,25 +9,24 @@ import { useResolvedRsComponentSize } from './resolve-size'
9
9
  import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
10
10
 
11
11
  import {
12
-
13
- runInputValidation,
14
-
15
12
  type RsInputRule,
16
-
17
13
  type RsInputValidateTrigger,
18
-
19
14
  } from './input-rules'
20
15
 
21
16
  import {
22
-
23
17
  useRsFormContext,
24
-
25
18
  useRsFormField,
26
-
27
19
  type RsFormLabelPosition,
28
-
29
20
  } from './form-utils'
30
21
 
22
+ import {
23
+ buildLocalInputRules,
24
+ runFormFieldRules,
25
+ type RsFormRuleTrigger,
26
+ } from './form-rules'
27
+
28
+ import RsVNodeHost from './RsVNodeHost.vue'
29
+
31
30
  import RsIcon from './RsIcon.vue'
32
31
 
33
32
 
@@ -94,6 +93,9 @@ const props = withDefaults(
94
93
 
95
94
  validateTrigger?: RsInputValidateTrigger
96
95
 
96
+ /** 字段名:匹配 RsForm.rules[name],并对齐大厂 Form.Item name */
97
+ name?: string
98
+
97
99
  /** 文本前缀(也可用 prefix 插槽) */
98
100
 
99
101
  prefix?: string
@@ -132,6 +134,13 @@ const props = withDefaults(
132
134
 
133
135
  labelPosition?: RsFormLabelPosition
134
136
 
137
+ /**
138
+ * 浏览器自动填充。
139
+ * 默认:普通输入 off;password 用 new-password(off 常被浏览器忽略)。
140
+ * 需要记住时可显式传 on / username / current-password 等。
141
+ */
142
+ autocomplete?: string
143
+
135
144
  }>(),
136
145
 
137
146
  {
@@ -156,8 +165,6 @@ const props = withDefaults(
156
165
 
157
166
  showValidateMessage: true,
158
167
 
159
- labelPosition: 'top',
160
-
161
168
  },
162
169
 
163
170
  )
@@ -206,6 +213,12 @@ const hasPrefix = computed(() => Boolean(props.prefix || slots.prefix))
206
213
 
207
214
  const resolvedId = computed(() => props.id ?? fieldId)
208
215
 
216
+ /** 未显式传 autocomplete 时关闭快捷填充;密码框用 new-password 更稳 */
217
+ const resolvedAutocomplete = computed(() => {
218
+ if (props.autocomplete != null && props.autocomplete !== '') return props.autocomplete
219
+ return props.type === 'password' ? 'new-password' : 'off'
220
+ })
221
+
209
222
  const resolvedLabelPosition = computed(
210
223
 
211
224
  () => props.labelPosition ?? formContext?.labelPosition.value ?? 'top',
@@ -219,13 +232,21 @@ const errorId = computed(() => `${resolvedId.value}-error`)
219
232
 
220
233
 
221
234
  const displayMessage = computed(() => {
222
-
223
235
  if (props.errorMessage) return props.errorMessage
224
-
225
236
  if (props.showValidateMessage && autoInvalid.value) return autoMessage.value
226
-
227
237
  return ''
238
+ })
239
+
240
+ const errorSlotProps = computed(() => ({
241
+ name: props.name,
242
+ message: displayMessage.value,
243
+ value: model.value,
244
+ }))
228
245
 
246
+ /** Form 级 #error / errorRender;字段无 #error 时使用 */
247
+ const formErrorContent = computed(() => {
248
+ if (!displayMessage.value || !formContext?.renderError) return null
249
+ return formContext.renderError(errorSlotProps.value)
229
250
  })
230
251
 
231
252
 
@@ -316,68 +337,44 @@ const fieldStyle = computed(() => {
316
337
 
317
338
 
318
339
 
319
- function runValidate() {
320
-
321
- if (!props.rule && !props.validator && !props.required) {
340
+ async function runValidate(trigger: RsFormRuleTrigger = 'submit') {
341
+ const formRules = formContext?.getFieldRules(props.name) ?? []
342
+ const localRules = buildLocalInputRules({
343
+ required: props.required,
344
+ rule: props.rule,
345
+ validator: props.validator,
346
+ })
347
+ const rules = [...formRules, ...localRules]
322
348
 
349
+ if (!rules.length) {
323
350
  autoInvalid.value = false
324
-
325
351
  autoMessage.value = ''
326
-
327
352
  emit('validate', { valid: true })
328
-
329
353
  return true
330
-
331
354
  }
332
355
 
333
-
334
-
335
- const result = runInputValidation(
336
-
337
- model.value,
338
-
339
- {
340
-
341
- rule: props.rule,
342
-
343
- required: props.required,
344
-
345
- validator: props.validator,
346
-
347
- },
348
-
349
- t,
350
-
351
- )
352
-
353
-
356
+ const result = await runFormFieldRules(model.value, rules, { trigger, t })
354
357
 
355
358
  autoInvalid.value = !result.valid
356
-
357
359
  autoMessage.value = result.message ?? ''
358
-
359
360
  emit('validate', result)
360
-
361
361
  return result.valid
362
+ }
362
363
 
364
+ function setError(message: string): void {
365
+ autoInvalid.value = true
366
+ autoMessage.value = message
363
367
  }
364
368
 
365
369
 
366
370
 
367
371
  function onInput() {
368
-
369
372
  if (
370
-
371
373
  touched.value &&
372
-
373
374
  (props.validateTrigger === 'input' || props.validateTrigger === 'both')
374
-
375
375
  ) {
376
-
377
- runValidate()
378
-
376
+ void runValidate('change')
379
377
  }
380
-
381
378
  }
382
379
 
383
380
  /** 受控写入:始终取 DOM string value,不依赖 v-model 对 type=number 的隐式行为。 */
@@ -390,17 +387,11 @@ function onNativeInput(event: Event) {
390
387
 
391
388
 
392
389
  function onBlur(event: FocusEvent) {
393
-
394
390
  touched.value = true
395
-
396
391
  if (props.validateTrigger === 'blur' || props.validateTrigger === 'both') {
397
-
398
- runValidate()
399
-
392
+ void runValidate('blur')
400
393
  }
401
-
402
394
  emit('blur', event)
403
-
404
395
  }
405
396
 
406
397
 
@@ -431,8 +422,7 @@ function onClear() {
431
422
 
432
423
  emit('clear')
433
424
 
434
- if (touched.value) runValidate()
435
-
425
+ if (touched.value) void runValidate('change')
436
426
  }
437
427
 
438
428
 
@@ -466,27 +456,24 @@ function clearValidation(): void {
466
456
 
467
457
 
468
458
  useRsFormField(() => ({
469
-
459
+ get name() {
460
+ return props.name
461
+ },
470
462
  getValue: () => model.value,
471
-
472
463
  setValue,
473
-
474
- validate: () => ({ valid: runValidate() }),
475
-
464
+ validate: async (trigger) => {
465
+ const valid = await runValidate(trigger ?? 'submit')
466
+ return { valid, message: autoMessage.value || undefined, name: props.name }
467
+ },
476
468
  clearValidation,
477
-
469
+ setError,
478
470
  }))
479
471
 
480
-
481
-
482
472
  defineExpose({
483
-
484
473
  validate: runValidate,
485
-
486
474
  clearValidation,
487
-
488
475
  setValue,
489
-
476
+ setError,
490
477
  })
491
478
 
492
479
  </script>
@@ -585,6 +572,8 @@ defineExpose({
585
572
 
586
573
  :maxlength="maxlength"
587
574
 
575
+ :autocomplete="resolvedAutocomplete"
576
+
588
577
  :aria-invalid="isInvalid || undefined"
589
578
 
590
579
  :aria-describedby="displayMessage ? errorId : undefined"
@@ -674,19 +663,18 @@ defineExpose({
674
663
 
675
664
 
676
665
  <p
677
-
678
666
  v-if="displayMessage"
679
-
680
667
  :id="errorId"
681
-
682
668
  class="rs-input-field__error"
683
-
684
669
  role="alert"
685
-
686
670
  >
687
-
688
- {{ displayMessage }}
689
-
671
+ <slot name="error" v-bind="errorSlotProps">
672
+ <RsVNodeHost
673
+ v-if="formErrorContent !== null && formErrorContent !== undefined"
674
+ :content="formErrorContent"
675
+ />
676
+ <template v-else>{{ displayMessage }}</template>
677
+ </slot>
690
678
  </p>
691
679
 
692
680
 
@@ -755,8 +743,12 @@ defineExpose({
755
743
 
756
744
  align-items: center;
757
745
 
746
+ box-sizing: border-box;
747
+
758
748
  width: 100%;
759
749
 
750
+ height: var(--rs-control-height-md);
751
+
760
752
  min-height: var(--rs-control-height-md);
761
753
 
762
754
  border-radius: var(--rs-input-radius, var(--rs-radius-sm));
@@ -783,18 +775,24 @@ defineExpose({
783
775
 
784
776
  .rs-input-group--ssm {
785
777
 
778
+ height: var(--rs-control-height-ssm);
779
+
786
780
  min-height: var(--rs-control-height-ssm);
787
781
 
788
782
  }
789
783
 
790
784
  .rs-input-group--sm {
791
785
 
786
+ height: var(--rs-control-height-sm);
787
+
792
788
  min-height: var(--rs-control-height-sm);
793
789
 
794
790
  }
795
791
 
796
792
  .rs-input-group--lg {
797
793
 
794
+ height: var(--rs-control-height-lg);
795
+
798
796
  min-height: var(--rs-control-height-lg);
799
797
 
800
798
  }
@@ -17,6 +17,11 @@ import {
17
17
  useRsFormField,
18
18
  type RsFormLabelPosition,
19
19
  } from './form-utils'
20
+ import {
21
+ buildLocalInputRules,
22
+ runFormFieldRules,
23
+ type RsFormRuleTrigger,
24
+ } from './form-rules'
20
25
  import RsIcon from './RsIcon.vue'
21
26
  import {
22
27
  formatNumberValue,
@@ -63,6 +68,12 @@ const props = withDefaults(
63
68
  label?: string
64
69
  hint?: string
65
70
  required?: boolean
71
+ /** 字段名:匹配 RsForm.rules[name] */
72
+ name?: string
73
+ /**
74
+ * 浏览器自动填充提示;数字框默认 off,避免 Chrome 误报 username 等建议。
75
+ */
76
+ autocomplete?: string
66
77
  labelPosition?: RsFormLabelPosition
67
78
  /** 展示格式化(对齐 Ant formatter) */
68
79
  formatter?: (value: number, info: { userTyping: boolean; input: string }) => string
@@ -78,6 +89,7 @@ const props = withDefaults(
78
89
  disabled: false,
79
90
  readonly: false,
80
91
  required: false,
92
+ autocomplete: 'off',
81
93
  },
82
94
  )
83
95
 
@@ -92,6 +104,11 @@ const formContext = useRsFormContext()
92
104
  const autoId = useId()
93
105
  const resolvedId = computed(() => props.id || `rs-input-number-${autoId}`)
94
106
  const resolvedSize = useResolvedRsComponentSize(() => props.size)
107
+ const handlerIconSize = computed(() => {
108
+ if (resolvedSize.value === 'ssm' || resolvedSize.value === 'sm') return 10
109
+ if (resolvedSize.value === 'lg') return 14
110
+ return 12
111
+ })
95
112
  const resolvedRadius = useResolvedRsRadius(() => props.radius, 'sm')
96
113
  const radiusStyle = computed(() => ({
97
114
  '--rs-input-radius': rsRadiusCss(resolvedRadius.value),
@@ -264,15 +281,22 @@ function setValue(value: unknown): void {
264
281
  syncDraftFromModel(model.value)
265
282
  }
266
283
 
284
+ async function runValidate(trigger: RsFormRuleTrigger = 'submit') {
285
+ const formRules = formContext?.getFieldRules(props.name) ?? []
286
+ const localRules = buildLocalInputRules({ required: props.required })
287
+ const rules = [...formRules, ...localRules]
288
+ if (!rules.length) return { valid: true as const, name: props.name }
289
+ const result = await runFormFieldRules(model.value, rules, { trigger })
290
+ return { valid: result.valid, message: result.message, name: props.name }
291
+ }
292
+
267
293
  useRsFormField(() => ({
294
+ get name() {
295
+ return props.name
296
+ },
268
297
  getValue: () => model.value,
269
298
  setValue,
270
- validate: () => {
271
- if (props.required && model.value == null) {
272
- return { valid: false }
273
- }
274
- return { valid: true }
275
- },
299
+ validate: (trigger) => runValidate(trigger ?? 'submit'),
276
300
  clearValidation: () => undefined,
277
301
  }))
278
302
 
@@ -326,6 +350,8 @@ defineExpose({
326
350
  type="text"
327
351
  inputmode="decimal"
328
352
  role="spinbutton"
353
+ :autocomplete="autocomplete"
354
+ :name="name"
329
355
  :value="draft"
330
356
  :placeholder="placeholder"
331
357
  :disabled="resolvedDisabled"
@@ -353,7 +379,7 @@ defineExpose({
353
379
  @pointerdown.prevent
354
380
  @click="applyStep(1)"
355
381
  >
356
- <RsIcon name="chevron-up" :size="12" />
382
+ <RsIcon name="chevron-up" :size="handlerIconSize" />
357
383
  </button>
358
384
  <button
359
385
  type="button"
@@ -364,7 +390,7 @@ defineExpose({
364
390
  @pointerdown.prevent
365
391
  @click="applyStep(-1)"
366
392
  >
367
- <RsIcon name="chevron-down" :size="12" />
393
+ <RsIcon name="chevron-down" :size="handlerIconSize" />
368
394
  </button>
369
395
  </div>
370
396
  </div>
@@ -406,7 +432,9 @@ defineExpose({
406
432
  .rs-input-number {
407
433
  display: flex;
408
434
  align-items: stretch;
435
+ box-sizing: border-box;
409
436
  width: 100%;
437
+ height: var(--rs-control-height-md);
410
438
  min-height: var(--rs-control-height-md);
411
439
  border-radius: var(--rs-input-radius, var(--rs-radius-sm));
412
440
  border: 1px solid var(--rs-input-border, var(--rs-border));
@@ -419,12 +447,15 @@ defineExpose({
419
447
  overflow: hidden;
420
448
  }
421
449
  .rs-input-number--ssm {
450
+ height: var(--rs-control-height-ssm);
422
451
  min-height: var(--rs-control-height-ssm);
423
452
  }
424
453
  .rs-input-number--sm {
454
+ height: var(--rs-control-height-sm);
425
455
  min-height: var(--rs-control-height-sm);
426
456
  }
427
457
  .rs-input-number--lg {
458
+ height: var(--rs-control-height-lg);
428
459
  min-height: var(--rs-control-height-lg);
429
460
  }
430
461
  .rs-input-number:hover:not(.rs-input-number--disabled):not(.rs-input-number--readonly):not(
@@ -450,33 +481,38 @@ defineExpose({
450
481
  .rs-input-number--disabled {
451
482
  opacity: 0.55;
452
483
  cursor: not-allowed;
453
- background: var(--rs-surface-muted, var(--rs-input-bg));
484
+ background: var(--rs-surface-hover);
454
485
  }
455
486
 
456
487
  .rs-input-number__control {
457
488
  flex: 1;
489
+ align-self: stretch;
458
490
  min-width: 0;
491
+ min-height: 0;
492
+ height: 100%;
493
+ box-sizing: border-box;
494
+ margin: 0;
459
495
  border: 0;
460
496
  outline: none;
461
497
  background: transparent;
462
- color: var(--rs-fg);
463
- font: inherit;
464
- padding: 0 10px;
465
- height: 100%;
498
+ color: var(--rs-text);
499
+ font-size: var(--rs-font-size-sm);
500
+ line-height: 1;
501
+ padding: 0 var(--rs-space-md);
466
502
  appearance: textfield;
467
503
  -moz-appearance: textfield;
468
504
  }
469
505
  .rs-input-number__control--ssm {
470
- padding: 0 8px;
471
- font-size: 12px;
506
+ padding: 0 var(--rs-space-xs);
507
+ font-size: var(--rs-font-size-xs);
472
508
  }
473
509
  .rs-input-number__control--sm {
474
- padding: 0 8px;
475
- font-size: 12px;
510
+ padding: 0 var(--rs-space-sm);
511
+ font-size: var(--rs-font-size-xs);
476
512
  }
477
513
  .rs-input-number__control--lg {
478
- padding: 0 12px;
479
- font-size: 15px;
514
+ padding: 0 var(--rs-space-lg);
515
+ font-size: var(--rs-font-size-base);
480
516
  }
481
517
  .rs-input-number__control:disabled {
482
518
  cursor: not-allowed;
@@ -491,7 +527,11 @@ defineExpose({
491
527
  display: flex;
492
528
  flex-direction: column;
493
529
  flex-shrink: 0;
530
+ align-self: stretch;
531
+ box-sizing: border-box;
494
532
  width: 22px;
533
+ height: 100%;
534
+ overflow: hidden;
495
535
  border-left: 1px solid var(--rs-input-border, var(--rs-border));
496
536
  }
497
537
  .rs-input-number--ssm .rs-input-number__handlers,
@@ -499,7 +539,8 @@ defineExpose({
499
539
  width: 18px;
500
540
  }
501
541
  .rs-input-number__handler {
502
- flex: 1;
542
+ flex: 1 1 0;
543
+ min-height: 0;
503
544
  display: inline-flex;
504
545
  align-items: center;
505
546
  justify-content: center;
@@ -507,13 +548,13 @@ defineExpose({
507
548
  margin: 0;
508
549
  padding: 0;
509
550
  background: transparent;
510
- color: var(--rs-fg-muted);
551
+ color: var(--rs-muted);
511
552
  cursor: pointer;
512
553
  line-height: 1;
513
554
  }
514
555
  .rs-input-number__handler:hover:not(:disabled) {
515
- color: var(--rs-fg);
516
- background: var(--rs-surface-muted, transparent);
556
+ color: var(--rs-text);
557
+ background: var(--rs-surface-hover);
517
558
  }
518
559
  .rs-input-number__handler:disabled {
519
560
  opacity: 0.35;