hookehuyr

feat(表单分页): 重构下一页校验逻辑以统一处理表单提交

将下一页校验逻辑改为通过触发表单的 submit 事件统一处理
新增 navigate_next_pending 标记和 afterValidatedNavigateNext 方法
在 index.vue 的 onSubmit 中拦截下一页意图并处理翻页与状态重置
统一组件 name 属性赋值逻辑
###
# @Date: 2023-02-13 14:56:34
# @LastEditors: hookehuyr hookehuyr@gmail.com
# @LastEditTime: 2025-09-09 17:56:45
# @LastEditTime: 2025-11-27 16:16:59
# @FilePath: /data-table/.env.development
# @Description: 文件描述
###
......
自定义表单
项目目标
- 支持多页表单的分页、校验与提交流程。
技术栈
- 构建:vite;包管理:pnpm;框架:vue3(setup);移动端组件库:vant。
- 路由:router;状态:pinia;样式:less + tailwindcss(若已引入)。
功能模块
- 表单分页:通过 `usePagination` 管理页签与可视字段。
- 表单校验:使用 `van-form` 的 `@submit` 与 `@failed` 事件统一处理。
迭代变更(2025-11-27)
- 下一页校验逻辑改为触发表单的 `onSubmit`,不再直接调用 `validateCurrentPage`。
- 新增 `navigate_next_pending` 标记与 `afterValidatedNavigateNext` 方法:
- 当点击“下一页”时置标记,`onSubmit` 校验通过后执行翻页并调用 `resetValidation()` 重置后续页的错误显示状态。
- `onFailed` 时清除该标记,避免误判后续提交动作。
- `index.vue` 在 `onSubmit` 顶部拦截“下一页意图”,短路真实提交,仅做翻页与状态重置。
- 保持分页最后一页提示与按钮颜色逻辑同步。
......
......@@ -18,6 +18,8 @@ export function usePagination(formDataRef, options = {}) {
const current_page_index = ref(0)
// 每一页的导航配置(由相邻的 paginator 控件决定)
const page_nav_props_by_index = ref([])
// 下一页意图标记:通过 onSubmit 校验通过后再真正切页
const navigate_next_pending = ref(false)
/**
* 过滤后的分页
......@@ -219,11 +221,27 @@ export function usePagination(formDataRef, options = {}) {
* 下一页
* @description 验证当前页,通过后切换到下一页并执行页面切换后的回调
*/
/**
* 下一页
* @description 不直接调用 validate,而是触发表单 submit,让视图层的 onSubmit 统一处理校验
*/
const handleNext = async () => {
const ok = await validateCurrentPage()
if (!ok) return
// 标记为“下一页校验意图”,供 onSubmit 判定
navigate_next_pending.value = true
// 触发表单校验(验证失败将触发 onFailed,成功将触发 onSubmit)
await myFormRef?.value?.submit?.()
}
/**
* 校验通过后的翻页动作
* @description onSubmit 检验通过且为“下一页意图”时调用;同时重置后续页面的错误状态
*/
const afterValidatedNavigateNext = async () => {
navigate_next_pending.value = false
if (current_page_index.value < filtered_pages.value.length - 1) {
current_page_index.value += 1
// 重置所有校验状态,避免新页显示历史错误
try { myFormRef?.value?.resetValidation?.() } catch (e) {}
if (typeof afterSwitch === 'function') await afterSwitch()
await nextTick()
window.scrollTo({ top: 0 })
......@@ -248,6 +266,8 @@ export function usePagination(formDataRef, options = {}) {
buildPages,
page_nav,
is_last_page,
navigate_next_pending,
afterValidatedNavigateNext,
validateCurrentPage,
handlePrev,
handleNext,
......
......@@ -76,20 +76,19 @@ export function createComponentType(data) {
if (item.component_props.required) {
item.rules = [{ required: true, message: item.placeholder ? item.placeholder : '必填项不能为空' }]
}
// 统一名称:确保所有控件都有稳定的 name,用于 van-form 错误项定位
item.name = item.key
if (item.component_props.tag === 'input') {
item.type = 'text';
item.name = item.key;
item.component = TextField;
}
if (item.component_props.tag === 'textarea') {
item.type = 'textarea';
item.name = item.key;
// item.rows = 10;
item.autosize = true;
item.component = TextareaField;
}
if (item.component_props.tag === 'number') {
item.name = item.key;
item.component = NumberField;
}
if (item.component_props.tag === 'radio') {
......@@ -120,35 +119,27 @@ export function createComponentType(data) {
item.component = FileUploaderField;
}
if (item.component_props.tag === 'phone') {
item.name = item.key;
item.component = PhoneField;
}
if (item.component_props.tag === 'email') {
item.name = item.key;
item.component = EmailField;
}
if (item.component_props.tag === 'sign') {
item.name = item.key;
item.component = SignField;
}
if (item.component_props.tag === 'rate') {
item.name = item.key;
item.component = RatePickerField;
}
if (item.component_props.tag === 'calendar') {
item.name = item.key;
item.component = CalendarField;
}
if (item.component_props.tag === 'id_card') {
item.name = item.key;
item.component = IdentityField;
}
if (item.component_props.tag === 'desc') {
item.name = item.key;
item.component = DesField;
}
if (item.component_props.tag === 'divider') {
item.name = item.key;
item.component = DividerField;
}
// if (item.component_props.tag === 'video') {
......@@ -156,52 +147,40 @@ export function createComponentType(data) {
// item.component = VideoField;
// }
if (item.component_props.tag === 'marquee') {
item.name = item.key;
item.component = MarqueeField;
}
if (item.component_props.tag === 'contact') {
item.name = item.key;
item.component = ContactField;
}
if (item.component_props.tag === 'rule') {
item.name = item.key;
item.component = RuleField;
}
if (item.component_props.tag === 'button') {
item.name = item.key;
item.component = ButtonField;
}
if (item.component_props.tag === 'multi_rule') {
item.name = item.key;
item.value = [];
item.component = MultiRuleField;
}
if (item.component_props.tag === 'note') {
item.name = item.key;
item.component = NoteField;
}
if (item.component_props.tag === 'name') {
item.name = item.key;
item.component = NameField;
}
if (item.component_props.tag === 'gender') {
item.name = item.key;
item.component = GenderField;
}
if (item.component_props.tag === 'appointment') {
item.name = item.key;
item.component = AppointmentField;
}
if (item.component_props.tag === 'custom') {
item.name = item.key;
item.component = CustomField;
}
if (item.component_props.tag === 'group') {
item.name = item.key;
item.component = GroupField;
}
if (item.component_props.tag === 'org_picker') {
item.name = item.key;
item.component = OrgPickerField;
}
if (item.component_props.tag === 'volunteer_group') {
......
<!--
* @Date: 2022-07-18 10:22:22
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-27 15:33:12
* @LastEditTime: 2025-11-27 16:25:10
* @FilePath: /data-table/src/views/index.vue
* @Description: 首页
-->
......@@ -1191,6 +1191,12 @@ const successHandle = () => { // 表单成功提交后续操作
}
const onSubmit = async (values) => { // 表单提交回调
// TAG:下一页校验入口
// 当为“下一页意图”时,不进行真正提交,仅执行翻页并重置校验状态
if (navigate_next_pending?.value) {
await afterValidatedNavigateNext()
return false
}
// 表单数据处理
postData.value = preValidData(values);
// 合并扩展字段
......@@ -1353,6 +1359,10 @@ const onSubmit = async (values) => { // 表单提交回调
};
const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发
// TAG:当为“下一页意图”时,验证失败需清除意图标记,避免误判
if (navigate_next_pending?.value) {
navigate_next_pending.value = false
}
console.log('表单验证失败:', errorInfo); // 添加调试信息
// 检查是否有错误信息
......@@ -1367,8 +1377,10 @@ const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发
// 通过name找到对应的label
let error_name = '';
// 兼容:有的组件 name 使用 key,有的使用 '_' + key 或单独的 name
formData.value.forEach(item => {
if (item.key === error_item.name) {
const n = error_item?.name
if (item.key === n || item.name === n || ('_' + item.key) === n) {
error_name = item.component_props.label
}
});
......@@ -1473,6 +1485,8 @@ const {
buildPages,
page_nav,
is_last_page,
navigate_next_pending,
afterValidatedNavigateNext,
handlePrev,
handleNext,
handleSubmit,
......