refactor: 整理组件目录结构
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
- TransactionDetail, CategoryBillPopup 移入 Transaction/ - BudgetTypeTabs 移入 Budget/ - GlassBottomNav, ModernEmpty 移入 Global/ - Icon, IconSelector, ClassifySelector 等 8 个通用组件移入 Common/ - 更新所有相关引用路径
This commit is contained in:
323
Web/src/components/Global/GlassBottomNav.vue
Normal file
323
Web/src/components/Global/GlassBottomNav.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user