hookehuyr

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

将下一页校验逻辑改为通过触发表单的 submit 事件统一处理
新增 navigate_next_pending 标记和 afterValidatedNavigateNext 方法
在 index.vue 的 onSubmit 中拦截下一页意图并处理翻页与状态重置
统一组件 name 属性赋值逻辑
1 ### 1 ###
2 # @Date: 2023-02-13 14:56:34 2 # @Date: 2023-02-13 14:56:34
3 # @LastEditors: hookehuyr hookehuyr@gmail.com 3 # @LastEditors: hookehuyr hookehuyr@gmail.com
4 - # @LastEditTime: 2025-09-09 17:56:45 4 + # @LastEditTime: 2025-11-27 16:16:59
5 # @FilePath: /data-table/.env.development 5 # @FilePath: /data-table/.env.development
6 # @Description: 文件描述 6 # @Description: 文件描述
7 ### 7 ###
......
1 自定义表单 1 自定义表单
2 +
3 + 项目目标
4 + - 支持多页表单的分页、校验与提交流程。
5 +
6 + 技术栈
7 + - 构建:vite;包管理:pnpm;框架:vue3(setup);移动端组件库:vant。
8 + - 路由:router;状态:pinia;样式:less + tailwindcss(若已引入)。
9 +
10 + 功能模块
11 + - 表单分页:通过 `usePagination` 管理页签与可视字段。
12 + - 表单校验:使用 `van-form` 的 `@submit` 与 `@failed` 事件统一处理。
13 +
14 + 迭代变更(2025-11-27)
15 + - 下一页校验逻辑改为触发表单的 `onSubmit`,不再直接调用 `validateCurrentPage`。
16 + - 新增 `navigate_next_pending` 标记与 `afterValidatedNavigateNext` 方法:
17 + - 当点击“下一页”时置标记,`onSubmit` 校验通过后执行翻页并调用 `resetValidation()` 重置后续页的错误显示状态。
18 + - `onFailed` 时清除该标记,避免误判后续提交动作。
19 + - `index.vue` 在 `onSubmit` 顶部拦截“下一页意图”,短路真实提交,仅做翻页与状态重置。
20 + - 保持分页最后一页提示与按钮颜色逻辑同步。
......
...@@ -18,6 +18,8 @@ export function usePagination(formDataRef, options = {}) { ...@@ -18,6 +18,8 @@ export function usePagination(formDataRef, options = {}) {
18 const current_page_index = ref(0) 18 const current_page_index = ref(0)
19 // 每一页的导航配置(由相邻的 paginator 控件决定) 19 // 每一页的导航配置(由相邻的 paginator 控件决定)
20 const page_nav_props_by_index = ref([]) 20 const page_nav_props_by_index = ref([])
21 + // 下一页意图标记:通过 onSubmit 校验通过后再真正切页
22 + const navigate_next_pending = ref(false)
21 23
22 /** 24 /**
23 * 过滤后的分页 25 * 过滤后的分页
...@@ -219,11 +221,27 @@ export function usePagination(formDataRef, options = {}) { ...@@ -219,11 +221,27 @@ export function usePagination(formDataRef, options = {}) {
219 * 下一页 221 * 下一页
220 * @description 验证当前页,通过后切换到下一页并执行页面切换后的回调 222 * @description 验证当前页,通过后切换到下一页并执行页面切换后的回调
221 */ 223 */
224 + /**
225 + * 下一页
226 + * @description 不直接调用 validate,而是触发表单 submit,让视图层的 onSubmit 统一处理校验
227 + */
222 const handleNext = async () => { 228 const handleNext = async () => {
223 - const ok = await validateCurrentPage() 229 + // 标记为“下一页校验意图”,供 onSubmit 判定
224 - if (!ok) return 230 + navigate_next_pending.value = true
231 + // 触发表单校验(验证失败将触发 onFailed,成功将触发 onSubmit)
232 + await myFormRef?.value?.submit?.()
233 + }
234 +
235 + /**
236 + * 校验通过后的翻页动作
237 + * @description onSubmit 检验通过且为“下一页意图”时调用;同时重置后续页面的错误状态
238 + */
239 + const afterValidatedNavigateNext = async () => {
240 + navigate_next_pending.value = false
225 if (current_page_index.value < filtered_pages.value.length - 1) { 241 if (current_page_index.value < filtered_pages.value.length - 1) {
226 current_page_index.value += 1 242 current_page_index.value += 1
243 + // 重置所有校验状态,避免新页显示历史错误
244 + try { myFormRef?.value?.resetValidation?.() } catch (e) {}
227 if (typeof afterSwitch === 'function') await afterSwitch() 245 if (typeof afterSwitch === 'function') await afterSwitch()
228 await nextTick() 246 await nextTick()
229 window.scrollTo({ top: 0 }) 247 window.scrollTo({ top: 0 })
...@@ -248,6 +266,8 @@ export function usePagination(formDataRef, options = {}) { ...@@ -248,6 +266,8 @@ export function usePagination(formDataRef, options = {}) {
248 buildPages, 266 buildPages,
249 page_nav, 267 page_nav,
250 is_last_page, 268 is_last_page,
269 + navigate_next_pending,
270 + afterValidatedNavigateNext,
251 validateCurrentPage, 271 validateCurrentPage,
252 handlePrev, 272 handlePrev,
253 handleNext, 273 handleNext,
......
...@@ -76,20 +76,19 @@ export function createComponentType(data) { ...@@ -76,20 +76,19 @@ export function createComponentType(data) {
76 if (item.component_props.required) { 76 if (item.component_props.required) {
77 item.rules = [{ required: true, message: item.placeholder ? item.placeholder : '必填项不能为空' }] 77 item.rules = [{ required: true, message: item.placeholder ? item.placeholder : '必填项不能为空' }]
78 } 78 }
79 + // 统一名称:确保所有控件都有稳定的 name,用于 van-form 错误项定位
80 + item.name = item.key
79 if (item.component_props.tag === 'input') { 81 if (item.component_props.tag === 'input') {
80 item.type = 'text'; 82 item.type = 'text';
81 - item.name = item.key;
82 item.component = TextField; 83 item.component = TextField;
83 } 84 }
84 if (item.component_props.tag === 'textarea') { 85 if (item.component_props.tag === 'textarea') {
85 item.type = 'textarea'; 86 item.type = 'textarea';
86 - item.name = item.key;
87 // item.rows = 10; 87 // item.rows = 10;
88 item.autosize = true; 88 item.autosize = true;
89 item.component = TextareaField; 89 item.component = TextareaField;
90 } 90 }
91 if (item.component_props.tag === 'number') { 91 if (item.component_props.tag === 'number') {
92 - item.name = item.key;
93 item.component = NumberField; 92 item.component = NumberField;
94 } 93 }
95 if (item.component_props.tag === 'radio') { 94 if (item.component_props.tag === 'radio') {
...@@ -120,35 +119,27 @@ export function createComponentType(data) { ...@@ -120,35 +119,27 @@ export function createComponentType(data) {
120 item.component = FileUploaderField; 119 item.component = FileUploaderField;
121 } 120 }
122 if (item.component_props.tag === 'phone') { 121 if (item.component_props.tag === 'phone') {
123 - item.name = item.key;
124 item.component = PhoneField; 122 item.component = PhoneField;
125 } 123 }
126 if (item.component_props.tag === 'email') { 124 if (item.component_props.tag === 'email') {
127 - item.name = item.key;
128 item.component = EmailField; 125 item.component = EmailField;
129 } 126 }
130 if (item.component_props.tag === 'sign') { 127 if (item.component_props.tag === 'sign') {
131 - item.name = item.key;
132 item.component = SignField; 128 item.component = SignField;
133 } 129 }
134 if (item.component_props.tag === 'rate') { 130 if (item.component_props.tag === 'rate') {
135 - item.name = item.key;
136 item.component = RatePickerField; 131 item.component = RatePickerField;
137 } 132 }
138 if (item.component_props.tag === 'calendar') { 133 if (item.component_props.tag === 'calendar') {
139 - item.name = item.key;
140 item.component = CalendarField; 134 item.component = CalendarField;
141 } 135 }
142 if (item.component_props.tag === 'id_card') { 136 if (item.component_props.tag === 'id_card') {
143 - item.name = item.key;
144 item.component = IdentityField; 137 item.component = IdentityField;
145 } 138 }
146 if (item.component_props.tag === 'desc') { 139 if (item.component_props.tag === 'desc') {
147 - item.name = item.key;
148 item.component = DesField; 140 item.component = DesField;
149 } 141 }
150 if (item.component_props.tag === 'divider') { 142 if (item.component_props.tag === 'divider') {
151 - item.name = item.key;
152 item.component = DividerField; 143 item.component = DividerField;
153 } 144 }
154 // if (item.component_props.tag === 'video') { 145 // if (item.component_props.tag === 'video') {
...@@ -156,52 +147,40 @@ export function createComponentType(data) { ...@@ -156,52 +147,40 @@ export function createComponentType(data) {
156 // item.component = VideoField; 147 // item.component = VideoField;
157 // } 148 // }
158 if (item.component_props.tag === 'marquee') { 149 if (item.component_props.tag === 'marquee') {
159 - item.name = item.key;
160 item.component = MarqueeField; 150 item.component = MarqueeField;
161 } 151 }
162 if (item.component_props.tag === 'contact') { 152 if (item.component_props.tag === 'contact') {
163 - item.name = item.key;
164 item.component = ContactField; 153 item.component = ContactField;
165 } 154 }
166 if (item.component_props.tag === 'rule') { 155 if (item.component_props.tag === 'rule') {
167 - item.name = item.key;
168 item.component = RuleField; 156 item.component = RuleField;
169 } 157 }
170 if (item.component_props.tag === 'button') { 158 if (item.component_props.tag === 'button') {
171 - item.name = item.key;
172 item.component = ButtonField; 159 item.component = ButtonField;
173 } 160 }
174 if (item.component_props.tag === 'multi_rule') { 161 if (item.component_props.tag === 'multi_rule') {
175 - item.name = item.key;
176 item.value = []; 162 item.value = [];
177 item.component = MultiRuleField; 163 item.component = MultiRuleField;
178 } 164 }
179 if (item.component_props.tag === 'note') { 165 if (item.component_props.tag === 'note') {
180 - item.name = item.key;
181 item.component = NoteField; 166 item.component = NoteField;
182 } 167 }
183 if (item.component_props.tag === 'name') { 168 if (item.component_props.tag === 'name') {
184 - item.name = item.key;
185 item.component = NameField; 169 item.component = NameField;
186 } 170 }
187 if (item.component_props.tag === 'gender') { 171 if (item.component_props.tag === 'gender') {
188 - item.name = item.key;
189 item.component = GenderField; 172 item.component = GenderField;
190 } 173 }
191 if (item.component_props.tag === 'appointment') { 174 if (item.component_props.tag === 'appointment') {
192 - item.name = item.key;
193 item.component = AppointmentField; 175 item.component = AppointmentField;
194 } 176 }
195 if (item.component_props.tag === 'custom') { 177 if (item.component_props.tag === 'custom') {
196 - item.name = item.key;
197 item.component = CustomField; 178 item.component = CustomField;
198 } 179 }
199 if (item.component_props.tag === 'group') { 180 if (item.component_props.tag === 'group') {
200 - item.name = item.key;
201 item.component = GroupField; 181 item.component = GroupField;
202 } 182 }
203 if (item.component_props.tag === 'org_picker') { 183 if (item.component_props.tag === 'org_picker') {
204 - item.name = item.key;
205 item.component = OrgPickerField; 184 item.component = OrgPickerField;
206 } 185 }
207 if (item.component_props.tag === 'volunteer_group') { 186 if (item.component_props.tag === 'volunteer_group') {
......
1 <!-- 1 <!--
2 * @Date: 2022-07-18 10:22:22 2 * @Date: 2022-07-18 10:22:22
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-27 15:33:12 4 + * @LastEditTime: 2025-11-27 16:25:10
5 * @FilePath: /data-table/src/views/index.vue 5 * @FilePath: /data-table/src/views/index.vue
6 * @Description: 首页 6 * @Description: 首页
7 --> 7 -->
...@@ -1191,6 +1191,12 @@ const successHandle = () => { // 表单成功提交后续操作 ...@@ -1191,6 +1191,12 @@ const successHandle = () => { // 表单成功提交后续操作
1191 } 1191 }
1192 1192
1193 const onSubmit = async (values) => { // 表单提交回调 1193 const onSubmit = async (values) => { // 表单提交回调
1194 + // TAG:下一页校验入口
1195 + // 当为“下一页意图”时,不进行真正提交,仅执行翻页并重置校验状态
1196 + if (navigate_next_pending?.value) {
1197 + await afterValidatedNavigateNext()
1198 + return false
1199 + }
1194 // 表单数据处理 1200 // 表单数据处理
1195 postData.value = preValidData(values); 1201 postData.value = preValidData(values);
1196 // 合并扩展字段 1202 // 合并扩展字段
...@@ -1353,6 +1359,10 @@ const onSubmit = async (values) => { // 表单提交回调 ...@@ -1353,6 +1359,10 @@ const onSubmit = async (values) => { // 表单提交回调
1353 }; 1359 };
1354 1360
1355 const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发 1361 const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发
1362 + // TAG:当为“下一页意图”时,验证失败需清除意图标记,避免误判
1363 + if (navigate_next_pending?.value) {
1364 + navigate_next_pending.value = false
1365 + }
1356 console.log('表单验证失败:', errorInfo); // 添加调试信息 1366 console.log('表单验证失败:', errorInfo); // 添加调试信息
1357 1367
1358 // 检查是否有错误信息 1368 // 检查是否有错误信息
...@@ -1367,8 +1377,10 @@ const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发 ...@@ -1367,8 +1377,10 @@ const onFailed = (errorInfo) => { // 提交表单且验证不通过后触发
1367 1377
1368 // 通过name找到对应的label 1378 // 通过name找到对应的label
1369 let error_name = ''; 1379 let error_name = '';
1380 + // 兼容:有的组件 name 使用 key,有的使用 '_' + key 或单独的 name
1370 formData.value.forEach(item => { 1381 formData.value.forEach(item => {
1371 - if (item.key === error_item.name) { 1382 + const n = error_item?.name
1383 + if (item.key === n || item.name === n || ('_' + item.key) === n) {
1372 error_name = item.component_props.label 1384 error_name = item.component_props.label
1373 } 1385 }
1374 }); 1386 });
...@@ -1473,6 +1485,8 @@ const { ...@@ -1473,6 +1485,8 @@ const {
1473 buildPages, 1485 buildPages,
1474 page_nav, 1486 page_nav,
1475 is_last_page, 1487 is_last_page,
1488 + navigate_next_pending,
1489 + afterValidatedNavigateNext,
1476 handlePrev, 1490 handlePrev,
1477 handleNext, 1491 handleNext,
1478 handleSubmit, 1492 handleSubmit,
......