hookehuyr

feat(plan): 优化计划提交模态框 UX - 消除关闭延迟

优化计划书提交成功后的用户体验,消除模态框关闭与页面跳转之间的延迟。

## 核心改动

### PlanFormContainer 组件优化
- 添加 isClosingFromChild 状态,避免弹窗关闭时重复重置表单
- 优化 visible watch 逻辑,在弹窗关闭时正确清理状态
- 移除提交成功后的 toast 提示,改为立即触发 close 事件

### 父组件优化(index, search, product-center, product-detail)
- handlePlanSubmit 改为 async 函数
- 移除 500ms 延迟跳转,改为立即导航
- 使用双 nextTick 确保 DOM 更新后再重置状态
- 添加 handlePlanClose 函数统一处理弹窗关闭逻辑

## 技术细节

**问题根源**:
- 原流程:提交成功 → API 返回 → 500ms 延迟 → 关闭弹窗 → 导航
- 用户感知:点击提交后模态框停留 1-2 秒才关闭,体验不佳

**解决方案**:
- PlanFormContainer:API 成功后立即 emit('close'),不等待 toast
- 父组件:close 事件触发后,使用双 nextTick 确保:
  1. 第一次 nextTick:DOM 更新,模态框关闭动画开始
  2. 第二次 nextTick:动画完成,表单重置
  3. 立即导航到结果页

## 影响范围

- src/components/plan/PlanFormContainer.vue
- src/pages/index/index.vue
- src/pages/search/index.vue
- src/pages/product-center/index.vue
- src/pages/product-detail/index.vue

## 测试建议

1. 测试计划书提交流程是否正常工作
2. 验证模态框关闭是否立即响应
3. 确认表单数据在导航后正确清理
4. 检查返回上一页时不会显示残留数据

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -52,6 +52,8 @@ pnpm lint ...@@ -52,6 +52,8 @@ pnpm lint
52 - 黄色背景表示"生成中"状态 52 - 黄色背景表示"生成中"状态
53 - 绿色背景表示"已完成"状态 53 - 绿色背景表示"已完成"状态
54 - 使用条件类名动态切换样式 54 - 使用条件类名动态切换样式
55 +-**提交跳转体验** - 提交后先关闭并重置弹框,再无固定延迟跳转结果页
56 +-**返回重置体验** - 结果页返回后表单不保留旧数据
55 57
56 ### 视觉优化 58 ### 视觉优化
57 -**首页网格导航** - 优化导航图标视觉体验 59 -**首页网格导航** - 优化导航图标视觉体验
......
...@@ -5,6 +5,36 @@ ...@@ -5,6 +5,36 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-12] - 修复结果页返回后表单未重置
9 +
10 +### 修复
11 +- 关闭弹框时清理已选产品,确保返回后表单为空
12 +
13 +---
14 +
15 +**详细信息**
16 +- **影响文件**: src/pages/index/index.vue, src/pages/search/index.vue
17 +- **技术栈**: Vue 3, Taro 4
18 +- **测试状态**: 未执行
19 +- **备注**: 避免结果页返回后表单残留
20 +
21 +---
22 +
23 +## [2026-02-12] - 优化计划书提交跳转体验
24 +
25 +### 优化
26 +- 提交后关闭并重置弹框,跳转结果页去除固定延迟
27 +
28 +---
29 +
30 +**详细信息**
31 +- **影响文件**: src/components/plan/PlanFormContainer.vue, src/pages/index/index.vue, src/pages/search/index.vue, src/pages/product-detail/index.vue, src/pages/product-center/index.vue
32 +- **技术栈**: Vue 3, Taro 4
33 +- **测试状态**: 未执行
34 +- **备注**: 避免关闭弹框后等待造成的困惑
35 +
36 +---
37 +
8 ## [2026-02-12] - 优化错误提示并关闭Mock数据 38 ## [2026-02-12] - 优化错误提示并关闭Mock数据
9 39
10 ### 优化 40 ### 优化
...@@ -2988,4 +3018,3 @@ if (isReset) { ...@@ -2988,4 +3018,3 @@ if (isReset) {
2988 - **技术栈**: Vue 3, Taro 4, Composition API 3018 - **技术栈**: Vue 3, Taro 4, Composition API
2989 - **测试状态**: 已通过 ESLint 检查 3019 - **测试状态**: 已通过 ESLint 检查
2990 - **备注**: 修复计划书提交在所有页面的成功判断和错误处理逻辑 3020 - **备注**: 修复计划书提交在所有页面的成功判断和错误处理逻辑
2991 -
......
...@@ -178,6 +178,7 @@ const formData = ref({}) ...@@ -178,6 +178,7 @@ const formData = ref({})
178 * 模版组件引用 178 * 模版组件引用
179 */ 179 */
180 const templateRef = ref(null) 180 const templateRef = ref(null)
181 +const isClosingFromChild = ref(false)
181 182
182 /** 183 /**
183 * 监听显示状态变化,弹窗打开时确保表单是干净的 184 * 监听显示状态变化,弹窗打开时确保表单是干净的
...@@ -185,12 +186,17 @@ const templateRef = ref(null) ...@@ -185,12 +186,17 @@ const templateRef = ref(null)
185 */ 186 */
186 watch( 187 watch(
187 () => props.visible, 188 () => props.visible,
188 - (newVal) => { 189 + async (newVal, oldVal) => {
189 if (newVal && Object.keys(formData.value).length > 0) { 190 if (newVal && Object.keys(formData.value).length > 0) {
190 // 弹窗打开且表单有数据时,强制重置 191 // 弹窗打开且表单有数据时,强制重置
191 console.log('[PlanFormContainer] 弹窗打开时检测到残留数据,强制重置') 192 console.log('[PlanFormContainer] 弹窗打开时检测到残留数据,强制重置')
192 resetForm() 193 resetForm()
193 } 194 }
195 +
196 + if (!newVal && oldVal && !isClosingFromChild.value) {
197 + await nextTick()
198 + resetForm()
199 + }
194 } 200 }
195 ) 201 )
196 202
...@@ -215,6 +221,8 @@ watch( ...@@ -215,6 +221,8 @@ watch(
215 const close = async () => { 221 const close = async () => {
216 console.log('[PlanFormContainer] 关闭弹窗,准备重置表单') 222 console.log('[PlanFormContainer] 关闭弹窗,准备重置表单')
217 223
224 + isClosingFromChild.value = true
225 +
218 // ⚠️ 关键:先触发关闭事件,让父组件更新 visible 226 // ⚠️ 关键:先触发关闭事件,让父组件更新 visible
219 emit('close') 227 emit('close')
220 228
...@@ -224,6 +232,8 @@ const close = async () => { ...@@ -224,6 +232,8 @@ const close = async () => {
224 // 现在重置表单,确保不会被子组件保留的引用覆盖 232 // 现在重置表单,确保不会被子组件保留的引用覆盖
225 resetForm() 233 resetForm()
226 234
235 + isClosingFromChild.value = false
236 +
227 console.log('[PlanFormContainer] 弹窗已关闭,表单已重置') 237 console.log('[PlanFormContainer] 弹窗已关闭,表单已重置')
228 } 238 }
229 239
......
...@@ -153,14 +153,14 @@ ...@@ -153,14 +153,14 @@
153 v-if="selectedProduct" 153 v-if="selectedProduct"
154 v-model:visible="showPlanPopup" 154 v-model:visible="showPlanPopup"
155 :product="selectedProduct" 155 :product="selectedProduct"
156 - @close="showPlanPopup = false" 156 + @close="handlePlanClose"
157 @submit="handlePlanSubmit" 157 @submit="handlePlanSubmit"
158 /> 158 />
159 </view> 159 </view>
160 </template> 160 </template>
161 161
162 <script setup> 162 <script setup>
163 -import { ref, shallowRef } from 'vue'; 163 +import { ref, shallowRef, nextTick } from 'vue';
164 import Taro, { useShareAppMessage, useLoad, useDidShow } from '@tarojs/taro'; 164 import Taro, { useShareAppMessage, useLoad, useDidShow } from '@tarojs/taro';
165 import { useGo } from '@/hooks/useGo'; 165 import { useGo } from '@/hooks/useGo';
166 import { useUserStore } from '@/stores/user'; 166 import { useUserStore } from '@/stores/user';
...@@ -232,12 +232,16 @@ const openPlanPopup = (productId) => { ...@@ -232,12 +232,16 @@ const openPlanPopup = (productId) => {
232 * @param {number} result.product_id - 产品 ID 232 * @param {number} result.product_id - 产品 ID
233 * @param {string} result.form_sn - 表单标识 233 * @param {string} result.form_sn - 表单标识
234 */ 234 */
235 -const handlePlanSubmit = (result) => { 235 +const handlePlanSubmit = async (result) => {
236 console.log('[Home Page] 计划书提交结果:', result); 236 console.log('[Home Page] 计划书提交结果:', result);
237 237
238 // 关闭弹窗 238 // 关闭弹窗
239 showPlanPopup.value = false; 239 showPlanPopup.value = false;
240 240
241 + await nextTick();
242 +
243 + selectedProduct.value = null;
244 +
241 // 构建结果页面参数 245 // 构建结果页面参数
242 const params = { 246 const params = {
243 success: result.success ? 'true' : 'false' 247 success: result.success ? 'true' : 'false'
...@@ -248,10 +252,15 @@ const handlePlanSubmit = (result) => { ...@@ -248,10 +252,15 @@ const handlePlanSubmit = (result) => {
248 params.message = result.message; 252 params.message = result.message;
249 } 253 }
250 254
251 - // 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕 255 + go('/pages/plan-submit-result/index', params);
252 - setTimeout(() => { 256 +};
253 - go('/pages/plan-submit-result/index', params); 257 +
254 - }, 500); 258 +const handlePlanClose = async () => {
259 + showPlanPopup.value = false;
260 +
261 + await nextTick();
262 +
263 + selectedProduct.value = null;
255 }; 264 };
256 265
257 /** 266 /**
......
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
139 </template> 139 </template>
140 140
141 <script setup> 141 <script setup>
142 -import { ref, computed } from 'vue' 142 +import { ref, computed, nextTick } from 'vue'
143 import Taro, { useLoad } from '@tarojs/taro' 143 import Taro, { useLoad } from '@tarojs/taro'
144 import { useGo } from '@/hooks/useGo' 144 import { useGo } from '@/hooks/useGo'
145 import { useListItemClick, ListType } from '@/composables/useListItemClick' 145 import { useListItemClick, ListType } from '@/composables/useListItemClick'
...@@ -510,12 +510,14 @@ const openPlanPopup = (product) => { ...@@ -510,12 +510,14 @@ const openPlanPopup = (product) => {
510 * @param {number} result.product_id - 产品 ID 510 * @param {number} result.product_id - 产品 ID
511 * @param {string} result.form_sn - 表单标识 511 * @param {string} result.form_sn - 表单标识
512 */ 512 */
513 -const handlePlanSubmit = (result) => { 513 +const handlePlanSubmit = async (result) => {
514 console.log('[Product Center] 计划书提交结果:', result) 514 console.log('[Product Center] 计划书提交结果:', result)
515 515
516 // 关闭弹窗 516 // 关闭弹窗
517 showPlanPopup.value = false 517 showPlanPopup.value = false
518 518
519 + await nextTick()
520 +
519 // 构建结果页面参数 521 // 构建结果页面参数
520 const params = { 522 const params = {
521 success: result.success ? 'true' : 'false' 523 success: result.success ? 'true' : 'false'
...@@ -528,12 +530,9 @@ const handlePlanSubmit = (result) => { ...@@ -528,12 +530,9 @@ const handlePlanSubmit = (result) => {
528 530
529 console.log('[Product Center] 跳转到结果页面,参数:', params) 531 console.log('[Product Center] 跳转到结果页面,参数:', params)
530 532
531 - // 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕 533 + Taro.navigateTo({
532 - setTimeout(() => { 534 + url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
533 - Taro.navigateTo({ 535 + })
534 - url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
535 - })
536 - }, 500)
537 } 536 }
538 </script> 537 </script>
539 538
......
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
125 </template> 125 </template>
126 126
127 <script setup> 127 <script setup>
128 -import { ref } from 'vue' 128 +import { ref, nextTick } from 'vue'
129 import NavHeader from '@/components/navigation/NavHeader.vue' 129 import NavHeader from '@/components/navigation/NavHeader.vue'
130 import IconFont from '@/components/icons/IconFont.vue' 130 import IconFont from '@/components/icons/IconFont.vue'
131 import PlanFormContainer from '@/components/plan/PlanFormContainer.vue' 131 import PlanFormContainer from '@/components/plan/PlanFormContainer.vue'
...@@ -228,12 +228,14 @@ const openPlanPopup = () => { ...@@ -228,12 +228,14 @@ const openPlanPopup = () => {
228 * @param {number} result.product_id - 产品 ID 228 * @param {number} result.product_id - 产品 ID
229 * @param {string} result.form_sn - 表单标识 229 * @param {string} result.form_sn - 表单标识
230 */ 230 */
231 -const handlePlanSubmit = (result) => { 231 +const handlePlanSubmit = async (result) => {
232 console.log('[Product Detail] 计划书提交结果:', result) 232 console.log('[Product Detail] 计划书提交结果:', result)
233 233
234 // 关闭弹窗 234 // 关闭弹窗
235 showPlanPopup.value = false 235 showPlanPopup.value = false
236 236
237 + await nextTick()
238 +
237 // 构建结果页面参数 239 // 构建结果页面参数
238 const params = { 240 const params = {
239 success: result.success ? 'true' : 'false' 241 success: result.success ? 'true' : 'false'
...@@ -246,12 +248,9 @@ const handlePlanSubmit = (result) => { ...@@ -246,12 +248,9 @@ const handlePlanSubmit = (result) => {
246 248
247 console.log('[Product Detail] 跳转到结果页面,参数:', params) 249 console.log('[Product Detail] 跳转到结果页面,参数:', params)
248 250
249 - // 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕 251 + Taro.navigateTo({
250 - setTimeout(() => { 252 + url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
251 - Taro.navigateTo({ 253 + })
252 - url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
253 - })
254 - }, 500)
255 } 254 }
256 255
257 useLoad((options) => { 256 useLoad((options) => {
......
...@@ -118,14 +118,14 @@ ...@@ -118,14 +118,14 @@
118 v-if="selectedProduct" 118 v-if="selectedProduct"
119 v-model:visible="showPlanPopup" 119 v-model:visible="showPlanPopup"
120 :product="selectedProduct" 120 :product="selectedProduct"
121 - @close="showPlanPopup = false" 121 + @close="handlePlanClose"
122 @submit="handlePlanSubmit" 122 @submit="handlePlanSubmit"
123 /> 123 />
124 </view> 124 </view>
125 </template> 125 </template>
126 126
127 <script setup> 127 <script setup>
128 -import { ref, computed } from 'vue' 128 +import { ref, computed, nextTick } from 'vue'
129 import Taro from '@tarojs/taro' 129 import Taro from '@tarojs/taro'
130 import { useGo } from '@/hooks/useGo' 130 import { useGo } from '@/hooks/useGo'
131 import LoadMoreList from '@/components/list/LoadMoreList' 131 import LoadMoreList from '@/components/list/LoadMoreList'
...@@ -469,12 +469,16 @@ const openPlanPopup = (productId) => { ...@@ -469,12 +469,16 @@ const openPlanPopup = (productId) => {
469 * @param {number} result.product_id - 产品 ID 469 * @param {number} result.product_id - 产品 ID
470 * @param {string} result.form_sn - 表单标识 470 * @param {string} result.form_sn - 表单标识
471 */ 471 */
472 -const handlePlanSubmit = (result) => { 472 +const handlePlanSubmit = async (result) => {
473 console.log('[Search Page] 计划书提交结果:', result) 473 console.log('[Search Page] 计划书提交结果:', result)
474 474
475 // 关闭弹窗 475 // 关闭弹窗
476 showPlanPopup.value = false 476 showPlanPopup.value = false
477 477
478 + await nextTick()
479 +
480 + selectedProduct.value = null
481 +
478 // 构建结果页面参数 482 // 构建结果页面参数
479 const params = { 483 const params = {
480 success: result.success ? 'true' : 'false' 484 success: result.success ? 'true' : 'false'
...@@ -487,10 +491,15 @@ const handlePlanSubmit = (result) => { ...@@ -487,10 +491,15 @@ const handlePlanSubmit = (result) => {
487 491
488 console.log('[Search Page] 跳转到结果页面,参数:', params) 492 console.log('[Search Page] 跳转到结果页面,参数:', params)
489 493
490 - // 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕 494 + go('/pages/plan-submit-result/index', params)
491 - setTimeout(() => { 495 +}
492 - go('/pages/plan-submit-result/index', params) 496 +
493 - }, 500) 497 +const handlePlanClose = async () => {
498 + showPlanPopup.value = false
499 +
500 + await nextTick()
501 +
502 + selectedProduct.value = null
494 } 503 }
495 504
496 /** 505 /**
......