guoyu/fronted_uniapp/components/custom-tabbar/custom-tabbar.vue
2025-12-03 18:58:36 +08:00

506 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view
class="custom-tabbar"
v-show="shouldShowTabbar"
>
<view
v-for="(item, index) in tabList"
:key="index"
class="tabbar-item"
:class="{ active: currentIndex === index }"
@click="switchTab(item, index)"
>
<view class="tabbar-icon">
<image
v-if="item.iconPath"
:src="item.iconPath"
class="tabbar-icon-image"
mode="aspectFit"
></image>
<u-icon
v-else
:name="item.icon"
:color="currentIndex === index ? '#378CE0' : '#7A7E83'"
size="44"
></u-icon>
</view>
<text
class="tabbar-text"
:style="{ color: currentIndex === index ? '#378CE0' : '#7A7E83' }"
>{{ item.text }}</text>
</view>
</view>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'CustomTabbar',
computed: {
...mapGetters('auth', ['userRole']),
tabList() {
// 定义 tabBar 页面列表(在 pages.json 的 tabBar.list 中配置的页面)
const tabBarPages = [
'pages/index/index',
'pages/course/list',
'pages/exam/index',
'pages/profile/profile'
]
// 根据用户角色返回不同的导航项
if (this.userRole === 'teacher') {
// 教师端导航
return [
{
pagePath: 'pages/index/index',
icon: 'home',
text: '首页',
isTabBar: true
},
{
pagePath: 'pages/exam/teacher-list',
icon: 'file-text',
text: '考试',
isTabBar: false
},
{
pagePath: 'pages/student/list',
icon: 'account',
text: '学生',
isTabBar: false
},
{
pagePath: 'pages/profile/profile',
icon: 'account',
text: '我的',
isTabBar: true
}
]
} else {
// 学生端导航(默认)
return [
{
pagePath: 'pages/index/index',
icon: 'home',
text: '首页',
isTabBar: true
},
{
pagePath: 'pages/course/list',
icon: 'list',
text: '课程',
isTabBar: true
},
{
pagePath: 'pages/exam/index',
icon: 'file-text',
iconPath: '/static/icon/kaoshi.png',
text: '考核',
isTabBar: true
},
{
pagePath: 'pages/profile/profile',
icon: 'account',
text: '我的',
isTabBar: true
}
]
}
}
},
data() {
return {
currentIndex: 0,
pageWatcherTimer: null,
lastPath: '',
updateTimer: null, // 防抖定时器
shouldShowTabbar: true // 是否显示 tabBar
}
},
mounted() {
// 初始化时立即更新一次(不使用防抖)
this.updateCurrentIndex()
this.checkShouldShowTabbar()
// 监听全局页面切换事件
uni.$on('tabbar-update', this.handleTabbarUpdate)
// 使用定时器定期检查页面状态(作为备用方案)
this.startPageWatcher()
},
watch: {
// 监听tabList变化确保路径匹配正确
tabList: {
handler() {
// tabList变化时使用统一更新方法
this.$nextTick(() => {
this.handleTabbarUpdate()
})
},
immediate: true
}
},
beforeDestroy() {
// 清理监听器
if (this.pageListener) {
uni.$off('navigateTo', this.pageListener)
uni.$off('switchTab', this.pageListener)
}
// 移除全局事件监听
uni.$off('tabbar-update', this.handleTabbarUpdate)
// 清除定时器
if (this.pageWatcherTimer) {
clearInterval(this.pageWatcherTimer)
}
// 清除防抖定时器
if (this.updateTimer) {
clearTimeout(this.updateTimer)
}
},
methods: {
handleTabbarUpdate() {
// 统一的更新入口,使用防抖避免频繁更新
if (this.updateTimer) {
clearTimeout(this.updateTimer)
}
this.updateTimer = setTimeout(() => {
this.updateCurrentIndex()
this.checkShouldShowTabbar()
}, 100) // 100ms防抖确保更稳定
},
checkShouldShowTabbar() {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) {
this.shouldShowTabbar = true
return
}
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) {
this.shouldShowTabbar = true
return
}
const currentPath = currentPage.route
// 检查当前页面是否是 tabBar 页面
const isTabBarPage = this.tabList.some(item =>
item.pagePath === currentPath && item.isTabBar
)
// 只在 tabBar 页面显示自定义 tabBar
this.shouldShowTabbar = isTabBarPage
} catch (e) {
// 出错时默认显示
this.shouldShowTabbar = true
}
},
startPageWatcher() {
// 定期检查页面路径变化(作为备用方案,频率降低)
// 只在事件机制失效时作为兜底方案
this.pageWatcherTimer = setInterval(() => {
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route) {
const currentPath = currentPage.route
// 如果路径发生变化,且与上次记录的路径不同,才更新
if (currentPath !== this.lastPath) {
this.lastPath = currentPath
// 使用统一的更新方法
this.handleTabbarUpdate()
}
}
}
} catch (e) {
// 静默处理错误
}
}, 800) // 进一步降低频率到800ms减少不必要的检查
},
setupPageListener() {
// 监听页面切换事件,实时更新导航栏状态
this.pageListener = () => {
// 延迟更新,确保页面已经切换完成(使用统一更新方法)
setTimeout(() => {
this.handleTabbarUpdate()
}, 100)
}
// 注意uniapp可能不支持这些事件这里作为备用方案
// 主要依赖全局事件和定时器
},
updateCurrentIndex() {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) {
if (this.currentIndex !== 0) {
this.currentIndex = 0
}
return
}
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) {
if (this.currentIndex !== 0) {
this.currentIndex = 0
}
return
}
// 获取当前页面路径(不带前导斜杠)
const currentPath = currentPage.route
// 如果路径没有变化,且索引已经正确,直接返回
if (currentPath === this.lastPath && this.currentIndex !== undefined) {
// 验证当前索引是否正确
const expectedIndex = this.tabList.findIndex(item => item.pagePath === currentPath)
if (expectedIndex !== -1 && this.currentIndex === expectedIndex) {
return // 路径和索引都正确,无需更新
}
}
// 更新路径记录
this.lastPath = currentPath
// 检查是否应该显示 tabBar
this.checkShouldShowTabbar()
// 精确匹配路径,优先匹配 tabBar 页面
let index = this.tabList.findIndex(item => {
return item.pagePath === currentPath && item.isTabBar
})
// 如果没找到 tabBar 页面,再查找所有页面(包括非 tabBar
if (index === -1) {
index = this.tabList.findIndex(item => {
return item.pagePath === currentPath
})
}
// 如果找到的是非 tabBar 页面,找到对应的 tabBar 页面索引
if (index !== -1 && !this.tabList[index].isTabBar) {
// 对于教师端,如果当前在考试管理页面,高亮"考试"项索引1
if (currentPath === 'pages/exam/teacher-list') {
index = 1 // 考试项的索引
}
// 对于教师端,如果当前在学生管理页面,高亮"学生"项索引2
else if (currentPath === 'pages/student/list') {
index = 2 // 学生项的索引
}
} else if (index === -1) {
// 如果完全找不到匹配的页面,尝试匹配部分路径
for (let i = 0; i < this.tabList.length; i++) {
const tab = this.tabList[i]
// 检查当前路径是否包含tab路径或者tab路径是否包含当前路径
if (currentPath.includes(tab.pagePath) || tab.pagePath.includes(currentPath)) {
index = i
break
}
}
}
const finalIndex = index !== -1 ? index : 0
// 只有当索引真正改变时才更新,避免不必要的重新渲染和跳动
if (this.currentIndex !== finalIndex) {
this.currentIndex = finalIndex
}
} catch (e) {
console.error('获取当前页面索引失败:', e)
if (this.currentIndex !== 0) {
this.currentIndex = 0
}
}
},
switchTab(item, index) {
// 如果点击的是当前页面,不执行切换
if (this.currentIndex === index) {
return
}
const url = '/' + item.pagePath
// 判断是否为 tabBar 页面
if (item.isTabBar) {
// 先更新索引,避免延迟导致的视觉跳动
this.currentIndex = index
// tabBar 页面使用 switchTab
// switchTab会自动处理从非tabBar页面切换到tabBar页面的情况
uni.switchTab({
url: url,
success: () => {
// 更新路径记录
this.lastPath = item.pagePath
// 延迟触发更新事件,确保页面已经完全切换
// 使用统一的事件机制,避免直接调用
setTimeout(() => {
uni.$emit('tabbar-update')
}, 150)
},
fail: (err) => {
console.error('切换tab失败:', err)
// 切换失败,恢复之前的索引(使用统一更新方法)
this.handleTabbarUpdate()
uni.showToast({
title: '切换失败',
icon: 'none'
})
}
})
} else {
// 非 tabBar 页面
// 先检查当前是否在tabBar页面
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = currentPage.route
const currentTab = this.tabList.find(tab => tab.pagePath === currentPath)
// 如果当前在非tabBar页面且要跳转到另一个非tabBar页面
// 使用reLaunch清空页面栈reLaunch不能用于tabBar页面所以这里安全
if (currentTab && !currentTab.isTabBar) {
uni.reLaunch({
url: url,
success: () => {
this.currentIndex = index
},
fail: (err) => {
console.error('导航失败:', err)
uni.showToast({
title: '导航失败',
icon: 'none'
})
}
})
} else {
// 从tabBar页面跳转到非tabBar页面使用navigateTo
uni.navigateTo({
url: url,
success: () => {
// 非 tabBar 页面也更新 currentIndex以高亮对应的导航项
this.currentIndex = index
},
fail: (err) => {
console.error('导航失败:', err)
// 如果navigateTo失败可能是页面栈已满尝试使用reLaunch
if (err.errMsg && err.errMsg.includes('navigateTo')) {
uni.reLaunch({
url: url,
success: () => {
this.currentIndex = index
},
fail: (reLaunchErr) => {
console.error('reLaunch也失败:', reLaunchErr)
uni.showToast({
title: '导航失败',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '导航失败',
icon: 'none'
})
}
}
})
}
}
}
}
}
</script>
<style lang="scss" scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-around;
background: #fff;
border-top: 1rpx solid #f0f0f0;
padding: 10rpx 0;
padding-bottom: calc(10rpx + env(safe-area-inset-bottom));
z-index: 999;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
/* 防止布局跳动 */
contain: layout style paint;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
@media (min-width: 768px) {
max-width: 1200rpx;
left: 50%;
transform: translateX(-50%);
border-left: 1rpx solid #f0f0f0;
border-right: 1rpx solid #f0f0f0;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8rpx 0;
transition: color 0.2s ease;
/* 防止布局跳动 */
min-height: 0;
contain: layout style;
@media (min-width: 768px) {
padding: 10rpx 0;
}
.tabbar-icon {
margin-bottom: 4rpx;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, color 0.2s ease;
transform-origin: center center;
/* 使用GPU加速避免重排 */
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
@media (min-width: 768px) {
margin-bottom: 6rpx;
}
.tabbar-icon-image {
width: 44rpx;
height: 44rpx;
@media (min-width: 768px) {
width: 48rpx;
height: 48rpx;
}
}
}
.tabbar-text {
font-size: 22rpx;
font-weight: 500;
transition: color 0.2s ease;
line-height: 1.2;
/* 防止文字重排 */
white-space: nowrap;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
&.active {
.tabbar-icon {
transform: scale(1.05);
}
}
}
}
</style>