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
- 黄色背景表示"生成中"状态
- 绿色背景表示"已完成"状态
- 使用条件类名动态切换样式
-**提交跳转体验** - 提交后先关闭并重置弹框,再无固定延迟跳转结果页
-**返回重置体验** - 结果页返回后表单不保留旧数据
### 视觉优化
-**首页网格导航** - 优化导航图标视觉体验
......
......@@ -5,6 +5,36 @@
---
## [2026-02-12] - 修复结果页返回后表单未重置
### 修复
- 关闭弹框时清理已选产品,确保返回后表单为空
---
**详细信息**
- **影响文件**: src/pages/index/index.vue, src/pages/search/index.vue
- **技术栈**: Vue 3, Taro 4
- **测试状态**: 未执行
- **备注**: 避免结果页返回后表单残留
---
## [2026-02-12] - 优化计划书提交跳转体验
### 优化
- 提交后关闭并重置弹框,跳转结果页去除固定延迟
---
**详细信息**
- **影响文件**: 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
- **技术栈**: Vue 3, Taro 4
- **测试状态**: 未执行
- **备注**: 避免关闭弹框后等待造成的困惑
---
## [2026-02-12] - 优化错误提示并关闭Mock数据
### 优化
......@@ -2988,4 +3018,3 @@ if (isReset) {
- **技术栈**: Vue 3, Taro 4, Composition API
- **测试状态**: 已通过 ESLint 检查
- **备注**: 修复计划书提交在所有页面的成功判断和错误处理逻辑
......
......@@ -178,6 +178,7 @@ const formData = ref({})
* 模版组件引用
*/
const templateRef = ref(null)
const isClosingFromChild = ref(false)
/**
* 监听显示状态变化,弹窗打开时确保表单是干净的
......@@ -185,12 +186,17 @@ const templateRef = ref(null)
*/
watch(
() => props.visible,
(newVal) => {
async (newVal, oldVal) => {
if (newVal && Object.keys(formData.value).length > 0) {
// 弹窗打开且表单有数据时,强制重置
console.log('[PlanFormContainer] 弹窗打开时检测到残留数据,强制重置')
resetForm()
}
if (!newVal && oldVal && !isClosingFromChild.value) {
await nextTick()
resetForm()
}
}
)
......@@ -215,6 +221,8 @@ watch(
const close = async () => {
console.log('[PlanFormContainer] 关闭弹窗,准备重置表单')
isClosingFromChild.value = true
// ⚠️ 关键:先触发关闭事件,让父组件更新 visible
emit('close')
......@@ -224,6 +232,8 @@ const close = async () => {
// 现在重置表单,确保不会被子组件保留的引用覆盖
resetForm()
isClosingFromChild.value = false
console.log('[PlanFormContainer] 弹窗已关闭,表单已重置')
}
......
......@@ -153,14 +153,14 @@
v-if="selectedProduct"
v-model:visible="showPlanPopup"
:product="selectedProduct"
@close="showPlanPopup = false"
@close="handlePlanClose"
@submit="handlePlanSubmit"
/>
</view>
</template>
<script setup>
import { ref, shallowRef } from 'vue';
import { ref, shallowRef, nextTick } from 'vue';
import Taro, { useShareAppMessage, useLoad, useDidShow } from '@tarojs/taro';
import { useGo } from '@/hooks/useGo';
import { useUserStore } from '@/stores/user';
......@@ -232,12 +232,16 @@ const openPlanPopup = (productId) => {
* @param {number} result.product_id - 产品 ID
* @param {string} result.form_sn - 表单标识
*/
const handlePlanSubmit = (result) => {
const handlePlanSubmit = async (result) => {
console.log('[Home Page] 计划书提交结果:', result);
// 关闭弹窗
showPlanPopup.value = false;
await nextTick();
selectedProduct.value = null;
// 构建结果页面参数
const params = {
success: result.success ? 'true' : 'false'
......@@ -248,10 +252,15 @@ const handlePlanSubmit = (result) => {
params.message = result.message;
}
// 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕
setTimeout(() => {
go('/pages/plan-submit-result/index', params);
}, 500);
go('/pages/plan-submit-result/index', params);
};
const handlePlanClose = async () => {
showPlanPopup.value = false;
await nextTick();
selectedProduct.value = null;
};
/**
......
......@@ -139,7 +139,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
......@@ -510,12 +510,14 @@ const openPlanPopup = (product) => {
* @param {number} result.product_id - 产品 ID
* @param {string} result.form_sn - 表单标识
*/
const handlePlanSubmit = (result) => {
const handlePlanSubmit = async (result) => {
console.log('[Product Center] 计划书提交结果:', result)
// 关闭弹窗
showPlanPopup.value = false
await nextTick()
// 构建结果页面参数
const params = {
success: result.success ? 'true' : 'false'
......@@ -528,12 +530,9 @@ const handlePlanSubmit = (result) => {
console.log('[Product Center] 跳转到结果页面,参数:', params)
// 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕
setTimeout(() => {
Taro.navigateTo({
url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
})
}, 500)
Taro.navigateTo({
url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
})
}
</script>
......
......@@ -125,7 +125,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, nextTick } from 'vue'
import NavHeader from '@/components/navigation/NavHeader.vue'
import IconFont from '@/components/icons/IconFont.vue'
import PlanFormContainer from '@/components/plan/PlanFormContainer.vue'
......@@ -228,12 +228,14 @@ const openPlanPopup = () => {
* @param {number} result.product_id - 产品 ID
* @param {string} result.form_sn - 表单标识
*/
const handlePlanSubmit = (result) => {
const handlePlanSubmit = async (result) => {
console.log('[Product Detail] 计划书提交结果:', result)
// 关闭弹窗
showPlanPopup.value = false
await nextTick()
// 构建结果页面参数
const params = {
success: result.success ? 'true' : 'false'
......@@ -246,12 +248,9 @@ const handlePlanSubmit = (result) => {
console.log('[Product Detail] 跳转到结果页面,参数:', params)
// 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕
setTimeout(() => {
Taro.navigateTo({
url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
})
}, 500)
Taro.navigateTo({
url: `/pages/plan-submit-result/index?${new URLSearchParams(params).toString()}`
})
}
useLoad((options) => {
......
......@@ -118,14 +118,14 @@
v-if="selectedProduct"
v-model:visible="showPlanPopup"
:product="selectedProduct"
@close="showPlanPopup = false"
@close="handlePlanClose"
@submit="handlePlanSubmit"
/>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import LoadMoreList from '@/components/list/LoadMoreList'
......@@ -469,12 +469,16 @@ const openPlanPopup = (productId) => {
* @param {number} result.product_id - 产品 ID
* @param {string} result.form_sn - 表单标识
*/
const handlePlanSubmit = (result) => {
const handlePlanSubmit = async (result) => {
console.log('[Search Page] 计划书提交结果:', result)
// 关闭弹窗
showPlanPopup.value = false
await nextTick()
selectedProduct.value = null
// 构建结果页面参数
const params = {
success: result.success ? 'true' : 'false'
......@@ -487,10 +491,15 @@ const handlePlanSubmit = (result) => {
console.log('[Search Page] 跳转到结果页面,参数:', params)
// 延迟跳转,确保 PlanFormContainer 的 toast 显示完毕
setTimeout(() => {
go('/pages/plan-submit-result/index', params)
}, 500)
go('/pages/plan-submit-result/index', params)
}
const handlePlanClose = async () => {
showPlanPopup.value = false
await nextTick()
selectedProduct.value = null
}
/**
......