Files
ruoyi-vue/src/components/MobileBottomNav/index.vue
2026-01-05 22:39:32 +08:00

312 lines
7.5 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>
<div class="mobile-bottom-nav" :class="{ 'scrollable': navItems.length > 5 }" v-if="isMobile && show">
<div
v-for="item in navItems"
:key="item.path"
class="nav-item"
:class="{ 'active': isActive(item.path) }"
@click="handleNavClick(item)"
>
<div class="nav-icon">
<i :class="item.icon" v-if="item.icon"></i>
<svg-icon :icon-class="item.iconClass" v-else-if="item.iconClass" />
<el-badge :value="item.badge" :hidden="!item.badge" v-if="item.badge">
<div class="icon-placeholder"></div>
</el-badge>
</div>
<div class="nav-label">{{ item.label }}</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'MobileBottomNav',
props: {
items: {
type: Array,
default: () => []
},
show: {
type: Boolean,
default: true
}
},
data() {
return {
navItemsCache: null
}
},
computed: {
...mapGetters(['device', 'sidebarRouters']),
isMobile() {
return this.device === 'mobile' || window.innerWidth < 768
},
navItems() {
// 如果提供了自定义items直接使用
if (this.items && this.items.length > 0) {
return this.items
}
// 使用缓存,避免重复计算
if (this.navItemsCache) {
return this.navItemsCache
}
// 从侧边栏路由中获取可用的路由
const routes = this.sidebarRouters || []
// 扁平化路由,获取所有叶子节点路由
const flattenRoutes = (routes, parentPath = '') => {
let result = []
if (!routes || !Array.isArray(routes)) {
return result
}
routes.forEach(route => {
if (route.hidden) return
// 处理路径 - 确保路径正确
let fullPath = route.path || ''
if (parentPath) {
if (fullPath.startsWith('/')) {
fullPath = fullPath
} else {
// 合并路径
const basePath = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath
fullPath = `${basePath}/${fullPath}`.replace(/\/+/g, '/')
}
}
// 确保路径以/开头
if (fullPath && !fullPath.startsWith('/')) {
fullPath = '/' + fullPath
}
// 如果有子路由,递归处理
if (route.children && route.children.length > 0) {
result = result.concat(flattenRoutes(route.children, fullPath))
} else {
// 叶子节点路由且有meta信息
if (route.meta && route.meta.title && fullPath) {
result.push({
path: fullPath,
label: route.meta.title,
icon: route.meta.icon || 'el-icon-menu',
iconClass: route.meta.icon,
route: route
})
}
}
})
return result
}
const flatRoutes = flattenRoutes(routes)
// 过滤并获取所有主要路由(不限制数量,显示所有)
const mainRoutes = flatRoutes
.filter(route => {
// 过滤掉一些特殊路由
const excludePaths = ['/redirect', '/login', '/register', '/404', '/401', '/user/profile']
const path = route.path || ''
return path &&
path !== '/' &&
!excludePaths.some(exclude => path.includes(exclude)) &&
!path.startsWith('/user/')
})
// 不限制数量,显示所有可用路由
// 缓存结果
if (mainRoutes.length > 0) {
this.navItemsCache = mainRoutes
return mainRoutes
}
// 如果没有找到路由,返回默认导航
const defaultRoutes = [
{
path: '/sloworder/index',
label: '首页',
icon: 'el-icon-s-home'
}
]
this.navItemsCache = defaultRoutes
return defaultRoutes
}
},
watch: {
sidebarRouters: {
handler() {
// 路由变化时清除缓存
this.navItemsCache = null
},
deep: true
}
},
mounted() {
// 等待路由加载完成
this.$nextTick(() => {
// 延迟一下,确保路由已经加载
setTimeout(() => {
this.navItemsCache = null
this.$forceUpdate()
}, 500)
})
},
methods: {
isActive(path) {
if (!path) return false
const currentPath = this.$route.path
return currentPath === path || currentPath.startsWith(path + '/')
},
handleNavClick(item) {
if (item.handler) {
item.handler()
this.$emit('nav-click', item)
return
}
if (item.path) {
// 确保路径正确
let path = item.path
if (!path.startsWith('/')) {
path = `/${path}`
}
// 移除末尾的斜杠(除了根路径)
if (path !== '/' && path.endsWith('/')) {
path = path.slice(0, -1)
}
// 尝试导航
this.$router.push(path).catch(err => {
// 如果push失败尝试replace
if (err.name !== 'NavigationDuplicated') {
this.$router.replace(path).catch(() => {
console.error('Navigation to', path, 'failed')
// 不显示错误消息,避免打扰用户
})
}
})
}
this.$emit('nav-click', item)
}
}
}
</script>
<style lang="scss" scoped>
.mobile-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: #fff;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-start;
align-items: center;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
padding-bottom: env(safe-area-inset-bottom);
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
padding: 0 4px;
// 隐藏滚动条但保持滚动功能
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
.nav-item {
flex: 0 0 auto;
min-width: 70px;
max-width: 90px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 4px;
cursor: pointer;
transition: all 0.3s;
-webkit-tap-highlight-color: transparent;
.nav-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
position: relative;
i, .svg-icon {
font-size: 22px;
color: #909399;
transition: all 0.3s;
}
.icon-placeholder {
width: 22px;
height: 22px;
}
}
.nav-label {
font-size: 11px;
color: #909399;
transition: all 0.3s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
text-align: center;
line-height: 1.2;
}
&.active {
.nav-icon {
i, .svg-icon {
color: #409eff;
font-size: 24px;
}
}
.nav-label {
color: #409eff;
font-weight: 600;
}
}
&:active {
transform: scale(0.95);
opacity: 0.8;
}
}
}
// 桌面端隐藏
@media (min-width: 769px) {
.mobile-bottom-nav {
display: none;
}
}
// 为底部导航预留空间
@media (max-width: 768px) {
.app-main {
padding-bottom: 60px !important;
}
}
</style>