324 lines
7.7 KiB
Vue
324 lines
7.7 KiB
Vue
<template>
|
||
<div class="glass-nav-container">
|
||
<!-- 渐变淡化效果 -->
|
||
<div class="gradient-fade">
|
||
<!-- 药丸形导航栏 -->
|
||
<div class="nav-pill">
|
||
<div
|
||
v-for="(item, index) in navItems"
|
||
:key="item.name"
|
||
class="nav-item"
|
||
:class="{ 'nav-item-active': activeTab === item.name }"
|
||
@click="handleTabClick(item, index)"
|
||
>
|
||
<van-icon
|
||
:name="item.icon"
|
||
size="24"
|
||
:color="getIconColor(activeTab === item.name)"
|
||
/>
|
||
<span
|
||
class="nav-label"
|
||
:class="{ 'nav-label-active': activeTab === item.name }"
|
||
>
|
||
{{ item.label }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: String,
|
||
default: 'statistics'
|
||
},
|
||
items: {
|
||
type: Array,
|
||
default () {
|
||
return [
|
||
{ name: 'calendar', label: '日历', icon: 'notes', path: '/calendar-v2' },
|
||
{ name: 'statistics', label: '统计', icon: 'chart-trending-o', path: '/statistics-v2' },
|
||
{ name: 'balance', label: '账单', icon: 'balance-list', path: '/balance' },
|
||
{ name: 'budget', label: '预算', icon: 'bill-o', path: '/budget-v2' },
|
||
{ name: 'setting', label: '设置', icon: 'setting', path: '/setting' }
|
||
]
|
||
}
|
||
}
|
||
})
|
||
|
||
const emit = defineEmits(['update:modelValue', 'tab-click'])
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
// 使用计算属性来获取导航项,优先使用传入的 props
|
||
const navItems = computed(() => props.items)
|
||
|
||
// 响应式的活动标签状态
|
||
const activeTab = ref(props.modelValue)
|
||
|
||
// 检测当前主题(暗色或亮色)
|
||
const isDarkMode = ref(false)
|
||
|
||
const updateTheme = () => {
|
||
isDarkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||
}
|
||
|
||
// 根据主题和激活状态计算图标颜色
|
||
const getIconColor = (isActive) => {
|
||
if (isActive) {
|
||
// 激活状态:暗色模式用浅色,亮色模式用深色
|
||
return isDarkMode.value ? '#FAFAF9' : '#1A1A1A'
|
||
} else {
|
||
// 非激活状态:暗色模式用灰色,亮色模式用浅灰色
|
||
return isDarkMode.value ? '#6B6B6F' : '#9CA3AF'
|
||
}
|
||
}
|
||
|
||
// 根据当前路由路径匹配对应的导航项
|
||
const getActiveTabFromRoute = (currentPath) => {
|
||
// 规范化路径: 去掉 -v2 后缀以支持版本切换
|
||
const normalizedPath = currentPath.replace(/-v2$/, '')
|
||
|
||
const matchedItem = navItems.value.find((item) => {
|
||
if (!item.path) {
|
||
return false
|
||
}
|
||
|
||
// 完全匹配
|
||
if (item.path === currentPath || item.path === normalizedPath) {
|
||
return true
|
||
}
|
||
|
||
// 首页特殊处理: '/' 应该匹配 '/' 和 '/statistics*'
|
||
if (item.path === '/' && (currentPath === '/' || normalizedPath === '/statistics')) {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
})
|
||
|
||
return matchedItem?.name || props.modelValue
|
||
}
|
||
|
||
// 更新激活状态的通用方法
|
||
const updateActiveTab = (newTab) => {
|
||
if (newTab && newTab !== activeTab.value) {
|
||
activeTab.value = newTab
|
||
emit('update:modelValue', newTab)
|
||
}
|
||
}
|
||
|
||
// 监听外部 modelValue 的变化
|
||
watch(
|
||
() => props.modelValue,
|
||
(newValue) => {
|
||
updateActiveTab(newValue)
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
// 监听路由变化,自动同步底部导航高亮状态
|
||
watch(
|
||
() => route.path,
|
||
(newPath) => {
|
||
const matchedTab = getActiveTabFromRoute(newPath)
|
||
updateActiveTab(matchedTab)
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
const handleTabClick = (item, index) => {
|
||
activeTab.value = item.name
|
||
emit('update:modelValue', item.name)
|
||
emit('tab-click', item, index)
|
||
|
||
// 如果有路径定义,则进行路由跳转
|
||
if (item.path) {
|
||
router.push(item.path).catch((err) => {
|
||
// 忽略相同路由导航错误
|
||
if (err.name !== 'NavigationDuplicated') {
|
||
console.warn('Navigation error:', err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 组件挂载时确保状态正确
|
||
onMounted(() => {
|
||
const matchedTab = getActiveTabFromRoute(route.path)
|
||
updateActiveTab(matchedTab)
|
||
|
||
// 初始化主题检测
|
||
updateTheme()
|
||
|
||
// 监听系统主题变化
|
||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||
mediaQuery.addEventListener('change', updateTheme)
|
||
|
||
// 组件卸载时清理监听器
|
||
const cleanup = () => {
|
||
mediaQuery.removeEventListener('change', updateTheme)
|
||
}
|
||
|
||
// Vue 3 中可以直接在 onMounted 中返回清理函数
|
||
return cleanup
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.glass-nav-container {
|
||
position: fixed;
|
||
bottom: 0px; /* iOS PWA安全区域适配:容器整体抬高 */ /*不抬高*/
|
||
left: 0;
|
||
right: 0;
|
||
width: 100%;
|
||
z-index: 1000;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 亮色模式渐变(默认) */
|
||
.gradient-fade {
|
||
width: 100%;
|
||
background: linear-gradient(180deg, rgba(246, 247, 248, 0) 0%, rgba(246, 247, 248, 1) 50%);
|
||
padding: 12px 16px;
|
||
padding-bottom: 16px; /* 简化padding,安全区域已由container的bottom处理 */
|
||
pointer-events: none;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
/* 亮色模式导航栏(默认) - 增强透明和毛玻璃效果 */
|
||
.nav-pill {
|
||
width: 100%;
|
||
height: 62px;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
backdrop-filter: blur(40px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||
border: 1px solid rgba(229, 231, 235, 0.6);
|
||
border-radius: 31px;
|
||
padding: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
box-shadow:
|
||
0 -4px 24px rgba(0, 0, 0, 0.08),
|
||
0 0 0 0.5px rgba(255, 255, 255, 0.5) inset;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.nav-item {
|
||
width: 56px;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border-radius: 27px;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
/* 亮色模式文字颜色(默认) */
|
||
.nav-label {
|
||
font-family:
|
||
'Inter',
|
||
-apple-system,
|
||
BlinkMacSystemFont,
|
||
sans-serif;
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
transition: all 0.2s ease;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.nav-label-active {
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
}
|
||
|
||
/* 响应式适配 */
|
||
@media (max-width: 375px) {
|
||
.gradient-fade {
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
}
|
||
|
||
.nav-item {
|
||
width: 52px;
|
||
}
|
||
}
|
||
|
||
/* 深色模式适配 - 增强透明和毛玻璃效果 */
|
||
@media (prefers-color-scheme: dark) {
|
||
.gradient-fade {
|
||
background: linear-gradient(180deg, rgba(11, 11, 14, 0) 0%, rgba(11, 11, 14, 1) 50%);
|
||
}
|
||
|
||
.nav-pill {
|
||
background: rgba(26, 26, 30, 0.75);
|
||
backdrop-filter: blur(40px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||
border-color: rgba(42, 42, 46, 0.6);
|
||
box-shadow:
|
||
0 -4px 24px rgba(0, 0, 0, 0.25),
|
||
0 0 0 0.5px rgba(255, 255, 255, 0.1) inset;
|
||
}
|
||
|
||
.nav-label {
|
||
color: #6b6b6f;
|
||
}
|
||
|
||
.nav-label-active {
|
||
color: #fafaf9;
|
||
}
|
||
}
|
||
|
||
/* iOS 样式优化 */
|
||
.nav-pill {
|
||
-webkit-touch-callout: none;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
|
||
/* 毛玻璃效果增强 - 根据浏览器支持调整透明度 */
|
||
@supports (backdrop-filter: blur(40px)) {
|
||
.nav-pill {
|
||
background: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.nav-pill {
|
||
background: rgba(26, 26, 30, 0.65);
|
||
}
|
||
}
|
||
}
|
||
|
||
@supports not (backdrop-filter: blur(40px)) {
|
||
/* 浏览器不支持毛玻璃效果时,增加不透明度以确保可读性 */
|
||
.nav-pill {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.nav-pill {
|
||
background: rgba(26, 26, 30, 0.95);
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
}
|
||
}
|
||
</style>
|