feat: 同步精简接口并统一表单校验
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { NForm, type FormInst, type FormRules, type FormValidationError } from 'naive-ui'
|
||||
|
||||
/** 统一使用 Naive UI 校验,屏蔽浏览器原生气泡;不改变请求和费用确认职责。 */
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
model: Record<string, unknown>
|
||||
rules?: FormRules
|
||||
disabled?: boolean
|
||||
validateOnChange?: boolean
|
||||
resetKey?: unknown
|
||||
}>(),
|
||||
{ validateOnChange: false }
|
||||
)
|
||||
const emit = defineEmits<{ submit: []; invalid: [errors: FormValidationError[]] }>()
|
||||
const formRef = ref<(FormInst & { $el: HTMLFormElement }) | null>(null)
|
||||
let revision = 0
|
||||
let validating = false
|
||||
let submitted = false
|
||||
onBeforeUnmount(() => {
|
||||
revision++
|
||||
})
|
||||
watch(
|
||||
() => props.resetKey,
|
||||
() => {
|
||||
revision++
|
||||
submitted = false
|
||||
formRef.value?.restoreValidation()
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
/** 验证等待期间切换目标或修改输入不继续执行旧的提交。 */
|
||||
async function validate(action?: () => unknown) {
|
||||
if (props.disabled || validating || !formRef.value) return false
|
||||
const version = revision
|
||||
const snapshot = JSON.stringify(props.model)
|
||||
submitted = true
|
||||
validating = true
|
||||
try {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (errors) {
|
||||
if (version === revision && Array.isArray(errors)) {
|
||||
emit('invalid', errors)
|
||||
// 等待调用方展开高级参数,再将焦点交给第一个错误字段。
|
||||
await nextTick()
|
||||
if (version === revision) {
|
||||
formRef.value?.$el
|
||||
.querySelector<HTMLElement>(
|
||||
'.n-form-item-blank--error input, .n-form-item-blank--error textarea'
|
||||
)
|
||||
?.focus()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (version !== revision || props.disabled || snapshot !== JSON.stringify(props.model)) return false
|
||||
if (action) await action()
|
||||
return true
|
||||
} finally {
|
||||
validating = false
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => props.model,
|
||||
async () => {
|
||||
if (!submitted && !props.validateOnChange) return
|
||||
await nextTick()
|
||||
// 这里只更新字段反馈,不提交业务操作。
|
||||
void formRef.value?.validate().catch(() => {})
|
||||
},
|
||||
{ deep: true, flush: 'post' }
|
||||
)
|
||||
|
||||
async function submit() {
|
||||
await validate(() => emit('submit'))
|
||||
}
|
||||
defineExpose({ validate, restoreValidation: () => formRef.value?.restoreValidation() })
|
||||
</script>
|
||||
<template>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
class="app-form"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:disabled="disabled"
|
||||
label-placement="top"
|
||||
require-mark-placement="right"
|
||||
novalidate
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<slot :validate="validate" />
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 标签与错误由 Naive UI 排版;窄列中的反馈换行不挤压相邻字段。 */
|
||||
.app-form .n-form-item {
|
||||
min-width: 0;
|
||||
}
|
||||
.app-form .n-form-item-blank > .n-input-number,
|
||||
.app-form .n-form-item-blank > .n-select {
|
||||
width: 100%;
|
||||
}
|
||||
.app-form .n-form-item-feedback {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.app-form .form-controls {
|
||||
align-items: start;
|
||||
}
|
||||
.app-form .form-controls > .n-checkbox,
|
||||
.app-form .form-controls > .n-button,
|
||||
.app-form .form-controls > .confirm-action {
|
||||
align-self: start;
|
||||
margin-top: 29px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,11 @@ const expanded = ref<string[]>([])
|
||||
function toggle() {
|
||||
expanded.value = expanded.value.length ? [] : ['details']
|
||||
}
|
||||
defineExpose({
|
||||
expand: () => {
|
||||
expanded.value = ['details']
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NCollapse v-model:expanded-names="expanded" class="detail-disclosure"
|
||||
|
||||
Reference in New Issue
Block a user