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.
- package/CHANGELOG.md +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- 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
|
-
|
|
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(
|
|
117
|
+
Array.from(fields.values()).map(
|
|
118
|
+
(field) => field.validate?.(trigger) ?? { valid: true, name: field.name },
|
|
119
|
+
),
|
|
66
120
|
)
|
|
67
|
-
|
|
121
|
+
const aggregated = collectResult(results)
|
|
122
|
+
emit('validate', aggregated)
|
|
123
|
+
return aggregated
|
|
68
124
|
}
|
|
69
125
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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-
|
|
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-
|
|
463
|
-
font:
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
471
|
-
font-size:
|
|
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
|
|
475
|
-
font-size:
|
|
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
|
|
479
|
-
font-size:
|
|
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-
|
|
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-
|
|
516
|
-
background: var(--rs-surface-
|
|
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;
|