feat: 同步精简接口并统一表单校验

This commit is contained in:
GJ
2026-09-10 22:34:47 +08:00
parent 82d7bde631
commit 2eea0ea72e
66 changed files with 1611 additions and 1100 deletions
+119
View File
@@ -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>
+5
View File
@@ -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"