Files
EmailBill/Web/src/App.vue
SunCheng 36e0a933f6
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 25s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
fix
2026-02-11 16:09:04 +08:00

467 lines
13 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>
<van-config-provider
:theme="theme"
:theme-vars="themeVars"
class="app-provider"
>
<div class="app-root">
<router-view v-slot="{ Component }">
<keep-alive
:include="cachedViews"
:max="8"
>
<component
:is="Component"
:key="route.name"
/>
</keep-alive>
</router-view>
<GlobalAddBill
v-if="isShowAddBill"
@success="handleAddTransactionSuccess"
/>
<div
v-if="needRefresh"
class="update-toast"
@click="updateServiceWorker"
>
<van-icon
name="upgrade"
class="update-icon"
/>
<span>新版本可用点击刷新</span>
</div>
<!-- 调试信息面板检测 GlassBottomNav -->
<div
v-if="showDebug"
class="debug-panel"
>
<div class="debug-title">
GlassBottomNav 调试信息
</div>
<div class="debug-section">
<div class="debug-subtitle">
容器检查 (.glass-nav-container)
</div>
<div
class="debug-item"
:style="{ color: debugInfo.containerExists ? '#81c784' : '#ff4d4f', fontWeight: 'bold' }"
>
元素存在: {{ debugInfo.containerExists ? '✓ 是' : '✗ 否' }}
</div>
<div class="debug-item">
position: {{ debugInfo.containerPosition }}
</div>
<div class="debug-item">
bottom (CSS): {{ debugInfo.containerBottomStyle }}
</div>
<div
class="debug-item"
:style="{ fontWeight: 'bold', color: '#ff9800' }"
>
bottom (实际): {{ debugInfo.containerRectBottom }}px
</div>
<div class="debug-item">
width: {{ debugInfo.containerWidth }}px
</div>
<div class="debug-item">
height: {{ debugInfo.containerHeight }}px
</div>
<div class="debug-item">
z-index: {{ debugInfo.containerZIndex }}
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
药丸导航 (.nav-pill)
</div>
<div
class="debug-item"
:style="{ color: debugInfo.pillExists ? '#81c784' : '#ff4d4f', fontWeight: 'bold' }"
>
元素存在: {{ debugInfo.pillExists ? '✓ 是' : '✗ 否' }}
</div>
<div class="debug-item">
width: {{ debugInfo.pillWidth }}px
</div>
<div class="debug-item">
height: {{ debugInfo.pillHeight }}px
</div>
<div
class="debug-item"
:style="{ fontWeight: 'bold', color: '#ff9800' }"
>
bottom (实际): {{ debugInfo.pillRectBottom }}px
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
父容器 (.app-root)
</div>
<div class="debug-item">
overflow: {{ debugInfo.appRootOverflow }}
</div>
<div class="debug-item">
height: {{ debugInfo.appRootHeight }}px
</div>
<div class="debug-item">
position: {{ debugInfo.appRootPosition }}
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
视口信息
</div>
<div class="debug-item">
window.innerHeight: {{ debugInfo.windowHeight }}px
</div>
<div class="debug-item">
--vh: {{ debugInfo.vhVar }}
</div>
<div class="debug-item">
safe-area-bottom: {{ debugInfo.safeAreaBottom }}
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
结果
</div>
<div
class="debug-item"
:style="{ color: parseFloat(debugInfo.distanceFromBottom) < 2 ? '#81c784' : '#ff4d4f', fontWeight: 'bold', fontSize: '12px' }"
>
距离底部: {{ debugInfo.distanceFromBottom }}px
{{ parseFloat(debugInfo.distanceFromBottom) < 2 ? '✓ 已贴底' : '✗ 未贴底' }}
</div>
</div>
<button
class="debug-close"
@click="showDebug = false"
>
关闭
</button>
</div>
</div>
</van-config-provider>
</template>
<script setup>
import { RouterView, useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useMessageStore } from '@/stores/message'
import GlobalAddBill from '@/components/Global/GlobalAddBill.vue'
import '@/styles/common.css'
import { needRefresh, updateServiceWorker } from './registerServiceWorker'
const messageStore = useMessageStore()
// 定义需要缓存的页面组件名称
const cachedViews = ref([
'CalendarV2', // 日历V2页面
'CalendarView', // 日历V1页面
'StatisticsView', // 统计页面
'StatisticsV2View', // 统计V2页面
'BalanceView', // 账单页面
'BudgetView' // 预算页面
])
const updateVh = () => {
const vh = window.innerHeight
document.documentElement.style.setProperty('--vh', `${vh}px`)
}
// 修复 PWA 模式下键盘收起页面不回弹的问题
const handleFocusOut = () => {
if (/(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent)) {
// 延迟一小段时间执行,确保键盘收起动作已开始
setTimeout(() => {
// 强制回到顶部
window.scrollTo(0, 0)
// 同时也触发一次高度更新
updateVh()
}, 100)
}
}
onMounted(() => {
updateVh()
window.addEventListener('resize', updateVh)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', updateVh)
}
// 注册全局失去焦点监听
document.addEventListener('focusout', handleFocusOut)
})
onUnmounted(() => {
window.removeEventListener('resize', updateVh)
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', updateVh)
}
// 销毁监听
document.removeEventListener('focusout', handleFocusOut)
})
const route = useRoute()
const theme = ref('light')
// Vant UI 主题变量映射
const themeVars = computed(() => {
const vars = {
navBarBackground: 'var(--bg-primary)',
navBarTextColor: 'var(--text-primary)',
cardBackground: 'var(--bg-secondary)',
cellBackground: 'var(--bg-secondary)',
background: 'var(--bg-primary)',
background2: 'var(--bg-secondary)',
textColor: 'var(--text-primary)',
textColor2: 'var(--text-secondary)',
borderColor: 'var(--bg-tertiary)',
tabbarBackground: 'var(--bg-primary)'
}
return vars
})
// 检测系统深色模式
const updateTheme = () => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
theme.value = isDark ? 'dark' : 'light'
// 在文档根元素上设置 data-theme 属性,使 CSS 变量生效
document.documentElement.setAttribute('data-theme', theme.value)
}
// 监听系统主题变化
let mediaQuery
onMounted(() => {
updateTheme()
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', updateTheme)
})
setInterval(() => {
messageStore.updateUnreadCount()
}, 60 * 1000) // 每60秒更新一次未读消息数
// 监听路由变化调整
watch(
() => route.path,
() => {
messageStore.updateUnreadCount()
}
)
const isShowAddBill = computed(() => {
return route.path === '/' || route.path === '/balance' || route.path === '/message' || route.path === '/calendar' || route.path === '/calendar-v2'
})
onUnmounted(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', updateTheme)
}
})
const handleAddTransactionSuccess = () => {
// 当添加交易成功时,通知当前页面刷新数据
const event = new Event('transactions-changed')
window.dispatchEvent(event)
}
// ===== 调试信息 =====
const showDebug = ref(true)
const debugInfo = ref({
containerExists: false,
containerPosition: '',
containerBottomStyle: '',
containerRectBottom: 0,
containerWidth: 0,
containerHeight: 0,
containerZIndex: '',
pillExists: false,
pillWidth: 0,
pillHeight: 0,
pillRectBottom: 0,
appRootOverflow: '',
appRootHeight: 0,
appRootPosition: '',
windowHeight: 0,
vhVar: '',
safeAreaBottom: '',
distanceFromBottom: '0'
})
const updateDebugInfo = () => {
debugInfo.value.windowHeight = window.innerHeight
debugInfo.value.vhVar = getComputedStyle(document.documentElement).getPropertyValue('--vh') || 'unset'
// 安全区域
const testDiv = document.createElement('div')
testDiv.style.paddingBottom = 'env(safe-area-inset-bottom, 0px)'
document.body.appendChild(testDiv)
debugInfo.value.safeAreaBottom = getComputedStyle(testDiv).paddingBottom
document.body.removeChild(testDiv)
// .glass-nav-container
const container = document.querySelector('.glass-nav-container')
debugInfo.value.containerExists = !!container
if (container) {
const rect = container.getBoundingClientRect()
const styles = getComputedStyle(container)
debugInfo.value.containerPosition = styles.position
debugInfo.value.containerBottomStyle = styles.bottom
debugInfo.value.containerRectBottom = rect.bottom.toFixed(2)
debugInfo.value.containerWidth = rect.width.toFixed(2)
debugInfo.value.containerHeight = rect.height.toFixed(2)
debugInfo.value.containerZIndex = styles.zIndex
}
// .nav-pill
const pill = document.querySelector('.nav-pill')
debugInfo.value.pillExists = !!pill
if (pill) {
const rect = pill.getBoundingClientRect()
debugInfo.value.pillWidth = rect.width.toFixed(2)
debugInfo.value.pillHeight = rect.height.toFixed(2)
debugInfo.value.pillRectBottom = rect.bottom.toFixed(2)
}
// .app-root
const appRoot = document.querySelector('.app-root')
if (appRoot) {
const styles = getComputedStyle(appRoot)
const rect = appRoot.getBoundingClientRect()
debugInfo.value.appRootOverflow = styles.overflow
debugInfo.value.appRootHeight = rect.height.toFixed(2)
debugInfo.value.appRootPosition = styles.position
}
// 距底部距离(取 container 或 pill 中更大的 bottom 值)
const bottom = Math.max(
parseFloat(debugInfo.value.containerRectBottom || 0),
parseFloat(debugInfo.value.pillRectBottom || 0)
)
debugInfo.value.distanceFromBottom = (window.innerHeight - bottom).toFixed(2)
}
onMounted(() => {
setTimeout(updateDebugInfo, 500)
const debugInterval = setInterval(updateDebugInfo, 1000)
onUnmounted(() => clearInterval(debugInterval))
})
</script>
<style scoped>
.app-provider {
/* 使用准确的视口高度 CSS 变量 */
height: var(--vh, 100vh);
width: 100%;
background-color: var(--van-background);
}
.app-root {
height: 100%;
width: 100%;
position: relative;
padding-top: max(0px, calc(env(safe-area-inset-top, 0px) * 0.75));
box-sizing: border-box;
overflow: hidden;
background-color: var(--van-background);
}
.update-toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background-color: var(--van-primary-color);
color: white;
padding: 10px 20px;
border-radius: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 2000;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.update-toast:active {
transform: translateX(-50%) scale(0.95);
}
.update-icon {
font-size: 18px;
}
/* 调试面板样式 */
.debug-panel {
position: fixed;
top: 50%;
right: 10px;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 12px;
border-radius: 8px;
font-size: 10px;
z-index: 9999;
min-width: 240px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
font-family: 'Courier New', monospace;
}
.debug-title {
font-weight: bold;
margin-bottom: 10px;
border-bottom: 2px solid rgba(255, 255, 255, 0.5);
padding-bottom: 6px;
font-size: 11px;
color: #4fc3f7;
}
.debug-section {
margin: 8px 0;
padding: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
border-left: 3px solid #4fc3f7;
}
.debug-subtitle {
font-weight: bold;
margin-bottom: 4px;
font-size: 10px;
color: #81c784;
}
.debug-item {
margin: 3px 0;
line-height: 1.5;
padding-left: 8px;
}
.debug-close {
margin-top: 10px;
padding: 6px 12px;
background: linear-gradient(135deg, #ff4d4f, #ff7875);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
width: 100%;
font-weight: bold;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.3);
}
.debug-close:active {
opacity: 0.8;
transform: scale(0.98);
}
</style>