2026-02-09 19:25:51 +08:00
|
|
|
|
<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' },
|
|
|
|
|
|
{ name: 'statistics', label: '统计', icon: 'chart-trending-o', path: '/' },
|
|
|
|
|
|
{ name: 'balance', label: '账单', icon: 'balance-list', path: '/balance' },
|
|
|
|
|
|
{ name: 'budget', label: '预算', icon: 'bill-o', path: '/budget' },
|
|
|
|
|
|
{ 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: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
width: 100%;
|
2026-02-11 13:00:01 +08:00
|
|
|
|
// 基础高度 + 安全区域,确保在PWA中正确贴底
|
|
|
|
|
|
height: calc(95px + env(safe-area-inset-bottom, 0px));
|
2026-02-09 19:25:51 +08:00
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 亮色模式渐变(默认) */
|
|
|
|
|
|
.gradient-fade {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: linear-gradient(180deg, rgba(246, 247, 248, 0) 0%, rgba(246, 247, 248, 1) 50%);
|
|
|
|
|
|
padding: 12px 21px 21px 21px;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 适配安全区域 */
|
|
|
|
|
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
|
|
|
|
|
.gradient-fade {
|
|
|
|
|
|
padding-bottom: calc(21px + env(safe-area-inset-bottom));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式适配 */
|
|
|
|
|
|
@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>
|