Vue3关闭PrimeVue Dialog后路由跳转无法滚动到指定ID问题
现有一个通过<router-view/>加载页面文件的视图,视图内包含PrimeVue Dialog弹窗组件,以及用于搜索功能的vue3-simple-typeahead组件,相关模板代码如下:
<Dialog header="Search" v-model:visible="displayBasic" > <vue3-simple-typeahead id="typeahead_id" ref="typeahead_ref" placeholder="Search" :items="baritems" :minInputLength="1" :itemProjection="itemProjectionFunction" @onBlur="typeheadblur" @selectItem="selectItemEventHandler" > </vue3-simple-typeahead> <template #footer> <Button label="Close" @click="closeBasic" /> </template> </Dialog>
vue3-simple-typeahead绑定的数据源baritems结构如下:
const baritems = ref([ { href:"hello.html#F1", title:"hello" }, // 其余数据项 ])
关闭Dialog弹窗的方法逻辑如下:
const closeBasic = () => { displayBasic.value = false; }
项目中定义selectItemEventHandler方法,会在用户选中vue3-simple-typeahead的选项时触发:预期逻辑为先关闭Dialog弹窗,再提取选中项的href属性值,执行router.push在<router-view/>中加载对应页面文件,方法代码如下:
const selectItemEventHandler = (item)=>{ displayBasic.value = false; let part = item.href.split('.')[0] let term = item.href.split('#')[1] router.push({ name: part, force: true ,params: { term } }) }
在router.push加载的目标页面中,通过watchEffect监听路由参数,根据路由参数获取对应id的DOM元素,执行平滑滚动到对应元素位置,逻辑代码如下:
watchEffect(()=>{ id = route.params.term; let el = document.getElementById(id); el.scrollIntoView({behavior: "smooth"}); })
问题现象
上述逻辑执行时控制台无任何报错:用户在vue3-simple-typeahead输入内容选中选项后,Dialog正常关闭、URL正常更新、目标页面文件正常加载,但无法滚动到对应id的元素位置。如果移除selectItemEventHandler中关闭弹窗的displayBasic.value = false;代码,滚动功能可正常生效,但Dialog弹窗无法自动关闭。
问题核心是执行时序冲突:
- 设置
displayBasic.value = false触发Dialog关闭流程时,PrimeVue Dialog会同步执行焦点重置、body滚动锁移除、关闭动画等一系列DOM操作,这些操作和同步执行的路由跳转、scrollIntoView逻辑在同一个更新周期内触发 - 滚动操作执行时,页面还处于弹窗关闭的过渡状态,body滚动锁尚未完全释放、焦点还在弹窗组件上,最终平滑滚动行为被打断,没有实际生效
- 当注释掉关闭弹窗的代码时,没有弹窗的DOM更新和滚动锁干扰,滚动逻辑自然可以正常执行
两种常用方案可按需选择:
方案1:等Dialog完全关闭后再执行路由跳转
利用Dialog提供的@hide事件(弹窗关闭动画完成、状态完全稳定后触发),把路由跳转逻辑挪到关闭完成回调中,从根源上避免时序冲突:
- 新增响应式变量缓存当前选中的搜索项,修改选中事件逻辑:
const currentSelectedItem = ref(null) // 选中项时仅触发弹窗关闭 const selectItemEventHandler = (item)=>{ currentSelectedItem.value = item displayBasic.value = false; } // 弹窗完全关闭后再执行路由跳转 const onDialogHide = () => { if (!currentSelectedItem.value) return let part = currentSelectedItem.value.href.split('.')[0] let term = currentSelectedItem.value.href.split('#')[1] router.push({ name: part, force: true ,params: { term } }) currentSelectedItem.value = null }
- 给Dialog组件绑定关闭完成事件:
<Dialog header="Search" v-model:visible="displayBasic" @hide="onDialogHide"> <!-- 其余原有内容保持不变 --> </Dialog>
方案2:延迟执行滚动逻辑,等待页面状态稳定
不需要改动原有跳转逻辑,只需要修改目标页面的滚动监听代码,等DOM更新完成、弹窗滚动锁完全释放后再执行滚动:
// 记得先从vue中引入nextTick import { nextTick } from 'vue' watchEffect(()=>{ const targetId = route.params.term; nextTick(() => { // 延迟时长和Dialog默认关闭动画时长匹配即可,一般100-200ms足够 setTimeout(() => { const el = document.getElementById(targetId); el?.scrollIntoView({behavior: "smooth"}); }, 150) }) })
方案对比:方案1逻辑更严谨,不会受动画时长调整影响;方案2改动代码量最小,适配成本更低。
内容的提问来源于stack exchange,提问作者slevin

