feat(project): 移动端 App 内项目列表与详情切换增加右侧滑入/滑出动画

列表与详情为同一路由页面,路由级动画不会触发,改在项目页内部用 transition 处理:
- 滑入期间保持列表与底部选项卡,结束后再隐藏,避免下层页面高度跳变
- 动画期间详情固定覆盖于列表及选项卡之上,按实际位置换算 top
- 返回时等滑出动画结束再清空当前项目数据
This commit is contained in:
kuaifan 2026-10-28 13:22:25 +00:00
parent 9d07f88e8c
commit 093a1e9afb
3 changed files with 105 additions and 3 deletions

View File

@ -1119,6 +1119,8 @@ export default {
this.releaseViewCover()
this.viewLeavePath = from.path
const isSubPage = route => /^manage-(setting|calendar|file)/.test(route.name || '')
const isProjectList = route => route.name === 'manage-project' && !/^\d+$/.test(route.params.projectId)
const isProjectPanel = route => route.name === 'manage-project' && /^\d+$/.test(route.params.projectId)
if (this.$isEEUIApp && this.windowPortrait) {
if (from.name === 'manage-application' && isSubPage(to)) {
// 划入动画结束后再隐藏底部选项卡
@ -1132,6 +1134,13 @@ export default {
this.viewTransition = {enter: 'manage-view-stay', leave: 'mobile-dialog-leave-active manage-view-top'}
return
}
if (isProjectList(from) && isProjectPanel(to)) {
// 项目列表与详情为同一页面组件,划入动画由项目页处理并负责释放 tabbarHold
this.tabbarHold = true
this.tabbarTransition = 'none'
this.viewTransition = {enter: '', leave: ''}
return
}
}
this.tabbarHold = false
this.tabbarTransition = 'mobile-slide'

View File

@ -1,7 +1,17 @@
<template>
<div class="page-project">
<ProjectPanel v-if="projectId > 0" ref="panel"/>
<ProjectList v-if="windowPortrait" v-show="projectId === 0"/>
<transition
:enter-active-class="panelTransition.enter"
:leave-active-class="panelLeaveClass"
@enter="onPanelCover"
@leave="onPanelCover"
@after-enter="onPanelEntered"
@enter-cancelled="onPanelEntered"
@after-leave="onPanelLeft"
@leave-cancelled="onPanelLeft">
<ProjectPanel v-if="projectId > 0" ref="panel"/>
</transition>
<ProjectList v-if="windowPortrait" v-show="projectId === 0 || listStay"/>
</div>
</template>
@ -12,6 +22,13 @@ import ProjectList from "./components/ProjectList";
export default {
components: {ProjectList, ProjectPanel},
data() {
return {
panelTransition: {enter: '', leave: ''},
listStay: false, // 详情划入期间保持列表显示
}
},
deactivated() {
this.$store.dispatch("forgetTaskCompleteTemp", true);
},
@ -19,6 +36,15 @@ export default {
computed: {
...mapState(['cacheProjects', 'wsOpenNum']),
/**
* 划出类名须在详情渲染时即确定(Vue 离开动画读取的是元素上一次渲染时的类名)
* 非返回列表的离开(切到其它页面)此时页面已被 keep-alive 移出,动画不可见
* @returns {string}
*/
panelLeaveClass() {
return this.$isEEUIApp && this.windowPortrait ? 'mobile-dialog-leave-active' : ''
},
projectId() {
const {projectId} = this.$route.params;
return parseInt(/^\d+$/.test(projectId) ? projectId : 0);
@ -26,6 +52,11 @@ export default {
},
watch: {
// 须在 projectId 之前定义:先确定动画再处理项目数据
'$route'(to, from) {
this.updatePanelTransition(to, from)
},
projectId: {
handler() {
this.getProjectData();
@ -43,9 +74,59 @@ export default {
},
methods: {
getProjectData() {
/**
* 移动端项目列表与详情切换动画(同一页面组件,路由动画不会触发)
* @param to
* @param from
*/
updatePanelTransition(to, from) {
const isList = route => route.name === 'manage-project' && !/^\d+$/.test(route.params.projectId)
const isPanel = route => route.name === 'manage-project' && /^\d+$/.test(route.params.projectId)
this.listStay = false
this.panelTransition = {enter: '', leave: ''}
if (this.$isEEUIApp && this.windowPortrait) {
if (isList(from) && isPanel(to)) {
this.listStay = true
this.panelTransition.enter = 'mobile-dialog-enter-active'
} else if (isPanel(from) && isList(to)) {
this.panelTransition.leave = 'mobile-dialog-leave-active'
}
}
},
onPanelCover(el) {
if (this.panelTransition.enter || this.panelTransition.leave) {
// 动画期间详情固定覆盖于列表及底部选项卡之上
// fixed 的参照可能不是视口(祖先 will-change: transform),按实际位置换算 top
el.setAttribute('data-panel-cover', '')
el.style.top = `${this.$el.getBoundingClientRect().top - el.getBoundingClientRect().top}px`
}
},
releasePanelCover(el) {
el.style.top = ''
el.removeAttribute('data-panel-cover')
},
onPanelEntered(el) {
this.releasePanelCover(el)
this.listStay = false
this.$store.state.mobileTabbarHold = false
},
onPanelLeft(el) {
this.releasePanelCover(el)
if (this.projectId <= 0) {
this.$store.state.projectId = 0;
}
},
getProjectData() {
if (this.projectId <= 0) {
// 详情划出期间保留项目数据,动画结束后再清除
if (!this.panelTransition.leave) {
this.$store.state.projectId = 0;
}
return;
}
const projectId = this.projectId;

View File

@ -7,6 +7,18 @@
width: 0;
height: 100%;
background-color: #fafafa;
// 移动端列表与详情切换动画期间(top 由脚本按页面位置换算)
&[data-panel-cover] {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
min-height: 0;
z-index: 100;
}
}
}