Files
EmailBill/Web/src/App.vue
SunCheng fe5de8bbcd
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 1m16s
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: 优化 TabBar 样式和调试信息展示,确保在 PWA 中正确贴底
2026-02-11 15:19:30 +08:00

634 lines
17 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>
<van-tabbar
v-show="showTabbar"
v-model="active"
style="position: fixed !important; bottom: 0 !important; left: 0; right: 0;"
>
<van-tabbar-item
name="ccalendar"
icon="notes"
to="/calendar"
>
日历
</van-tabbar-item>
<van-tabbar-item
name="statistics"
icon="chart-trending-o"
to="/"
@click="handleTabClick('/statistics')"
>
统计
</van-tabbar-item>
<van-tabbar-item
name="balance"
icon="balance-list"
:to="messageStore.unreadCount > 0 ? '/balance?tab=message' : '/balance'"
:badge="messageStore.unreadCount || null"
@click="handleTabClick('/balance')"
>
账单
</van-tabbar-item>
<van-tabbar-item
name="budget"
icon="bill-o"
to="/budget"
@click="handleTabClick('/budget')"
>
预算
</van-tabbar-item>
<van-tabbar-item
name="setting"
icon="setting"
to="/setting"
>
设置
</van-tabbar-item>
</van-tabbar>
<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>
<!-- 调试信息面板 -->
<div
v-if="showDebug"
class="debug-panel"
>
<div class="debug-title">
底部导航栏调试信息
</div>
<div class="debug-section">
<div class="debug-subtitle">
TabBar 存在性检查
</div>
<div
class="debug-item"
:style="{ color: debugInfo.tabbarExists ? '#81c784' : '#ff4d4f', fontWeight: 'bold' }"
>
TabBar 元素存在: {{ debugInfo.tabbarExists ? '✓ 是' : '✗ 否' }}
</div>
<div
class="debug-item"
:style="{ color: debugInfo.tabbarVisible ? '#81c784' : '#ff4d4f', fontWeight: 'bold' }"
>
TabBar 可见: {{ debugInfo.tabbarVisible ? '✓ 是' : '✗ 否' }}
</div>
<div class="debug-item">
display: {{ debugInfo.tabbarDisplay }}
</div>
<div class="debug-item">
opacity: {{ debugInfo.tabbarOpacity }}
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
TabBar 位置样式
</div>
<div class="debug-item">
position: {{ debugInfo.tabbarPosition }}
</div>
<div class="debug-item">
top (CSS): {{ debugInfo.tabbarTopStyle }}
</div>
<div class="debug-item">
bottom (CSS): {{ debugInfo.tabbarBottomStyle }}
</div>
<div class="debug-item">
left (实际): {{ debugInfo.tabbarLeft }}px
</div>
<div class="debug-item">
right (实际): {{ debugInfo.tabbarRight }}px
</div>
<div
class="debug-item"
:style="{ fontWeight: 'bold', color: '#ff9800' }"
>
top (实际): {{ debugInfo.tabbarTop }}px
</div>
<div
class="debug-item"
:style="{ fontWeight: 'bold', color: '#ff9800' }"
>
bottom (实际): {{ debugInfo.tabbarBottom }}px
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
TabBar 尺寸
</div>
<div class="debug-item">
width: {{ debugInfo.tabbarWidth }}px
</div>
<div class="debug-item">
height: {{ debugInfo.tabbarHeight }}px
</div>
<div class="debug-item">
padding-bottom: {{ debugInfo.tabbarPaddingBottom }}
</div>
<div class="debug-item">
box-sizing: {{ debugInfo.tabbarBoxSizing }}
</div>
</div>
<div class="debug-section">
<div class="debug-subtitle">
结果
</div>
<div
class="debug-item"
:style="{ color: debugInfo.distanceFromBottom === '0.00' ? '#81c784' : '#ff4d4f', fontWeight: 'bold', fontSize: '12px' }"
>
距离底部: {{ debugInfo.distanceFromBottom }}px {{ debugInfo.distanceFromBottom === '0.00' ? '✓ 已贴底' : '✗ 未贴底' }}
</div>
</div>
<button
class="debug-close"
@click="showDebug = false"
>
关闭
</button>
</div>
<!-- 视觉调试辅助线 - 标记屏幕底部 -->
<div
v-if="showDebug"
class="debug-bottom-line"
/>
<!-- 视觉调试辅助线 - 标记 TabBar 顶部 -->
<div
v-if="showDebug"
class="debug-tabbar-top-line"
:style="{ top: debugInfo.tabbarTop + 'px' }"
/>
</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 { needRefresh, updateServiceWorker } from './registerServiceWorker'
import '@/styles/common.css'
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()
// 根据路由判断是否显示Tabbar
const showTabbar = computed(() => {
return (
route.path === '/' ||
route.path === '/calendar' ||
route.path === '/calendar-v2' ||
route.path === '/message' ||
route.path === '/setting' ||
route.path === '/balance' ||
route.path === '/budget'
)
})
const active = ref('')
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)
setActive(route.path)
})
setInterval(() => {
messageStore.updateUnreadCount()
}, 60 * 1000) // 每60秒更新一次未读消息数
// 监听路由变化调整
watch(
() => route.path,
(newPath) => {
setActive(newPath)
messageStore.updateUnreadCount()
}
)
const setActive = (path) => {
active.value = (() => {
switch (path) {
case '/calendar':
case '/calendar-v2':
return 'ccalendar'
case '/balance':
case '/message':
return 'balance'
case '/setting':
return 'setting'
case '/budget':
return 'budget'
default:
return 'statistics'
}
})()
}
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)
}
})
// 处理tab点击如果点击当前页面则滚动到顶部
const handleTabClick = (path) => {
if (route.path === path) {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
const handleAddTransactionSuccess = () => {
// 当添加交易成功时,通知当前页面刷新数据
const event = new Event('transactions-changed')
window.dispatchEvent(event)
}
// 调试信息
const showDebug = ref(true) // 默认显示调试面板
const debugInfo = ref({
windowHeight: 0,
viewportHeight: 0,
safeAreaBottom: '0',
tabbarExists: false,
tabbarVisible: false,
tabbarHeight: 0,
tabbarWidth: 0,
tabbarBottom: 0,
tabbarTop: 0,
tabbarLeft: 0,
tabbarRight: 0,
tabbarPaddingBottom: '0',
tabbarPosition: '',
tabbarBottomStyle: '',
tabbarTopStyle: '',
tabbarMinHeight: '',
tabbarBoxSizing: '',
tabbarDisplay: '',
tabbarOpacity: '',
appRootHeight: 0,
appRootBottom: 0,
bodyHeight: 0,
htmlHeight: 0,
tabbarTransform: '',
tabbarZIndex: '',
distanceFromBottom: 0
})
// 更新调试信息
const updateDebugInfo = () => {
debugInfo.value.windowHeight = window.innerHeight
debugInfo.value.viewportHeight = window.visualViewport?.height || window.innerHeight
// 获取安全区域底部值
const testDiv = document.createElement('div')
testDiv.style.paddingBottom = 'env(safe-area-inset-bottom, 0px)'
document.body.appendChild(testDiv)
const computed = window.getComputedStyle(testDiv)
debugInfo.value.safeAreaBottom = computed.paddingBottom
document.body.removeChild(testDiv)
// 获取 TabBar 的实际尺寸和位置
const tabbar = document.querySelector('.van-tabbar')
debugInfo.value.tabbarExists = !!tabbar
debugInfo.value.tabbarVisible = tabbar ? window.getComputedStyle(tabbar).display !== 'none' : false
if (tabbar) {
const rect = tabbar.getBoundingClientRect()
const styles = window.getComputedStyle(tabbar)
debugInfo.value.tabbarHeight = rect.height.toFixed(2)
debugInfo.value.tabbarWidth = rect.width.toFixed(2)
debugInfo.value.tabbarBottom = rect.bottom.toFixed(2)
debugInfo.value.tabbarPaddingBottom = styles.paddingBottom
debugInfo.value.tabbarTop = rect.top.toFixed(2)
debugInfo.value.tabbarLeft = rect.left.toFixed(2)
debugInfo.value.tabbarRight = rect.right.toFixed(2)
debugInfo.value.tabbarPosition = styles.position
debugInfo.value.tabbarBottomStyle = styles.bottom
debugInfo.value.tabbarTopStyle = styles.top
debugInfo.value.tabbarMinHeight = styles.minHeight
debugInfo.value.tabbarBoxSizing = styles.boxSizing
debugInfo.value.tabbarDisplay = styles.display
debugInfo.value.tabbarOpacity = styles.opacity
// 检查是否有 transform 或其他可能影响定位的样式
debugInfo.value.tabbarTransform = styles.transform
debugInfo.value.tabbarZIndex = styles.zIndex
}
// 获取 app-root 的高度
const appRoot = document.querySelector('.app-root')
if (appRoot) {
const rect = appRoot.getBoundingClientRect()
debugInfo.value.appRootHeight = rect.height.toFixed(2)
debugInfo.value.appRootBottom = rect.bottom.toFixed(2)
}
// 获取 body 和 html 的高度
debugInfo.value.bodyHeight = document.body.getBoundingClientRect().height.toFixed(2)
debugInfo.value.htmlHeight = document.documentElement.getBoundingClientRect().height.toFixed(2)
// 计算 TabBar 距离屏幕底部的实际距离
debugInfo.value.distanceFromBottom = (window.innerHeight - parseFloat(debugInfo.value.tabbarBottom || 0)).toFixed(2)
}
onMounted(() => {
// 延迟获取调试信息,确保 DOM 已渲染
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);
}
/* TabBar 固定在底部 */
:deep(.van-tabbar) {
position: fixed !important;
bottom: 0 !important;
left: 0;
right: 0;
/* 重置所有 padding然后只添加安全区域 */
padding: 0 !important;
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
height: calc(50px + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box !important;
background: var(--bg-primary) !important;
border-top: 1px solid var(--bg-tertiary);
}
/* 确保 TabBar 项目居中 */
:deep(.van-tabbar-item) {
padding: 0 !important;
height: 50px !important;
color: var(--text-secondary);
}
:deep(.van-tabbar-item--active) {
color: var(--color-primary);
}
.debug-overlay {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
font-size: 12px;
pointer-events: auto;
}
.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);
}
/* 调试辅助线 - 屏幕底部红线 */
.debug-bottom-line {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: red;
z-index: 10000;
pointer-events: none;
}
/* 调试辅助线 - TabBar 顶部蓝线 */
.debug-tabbar-top-line {
position: fixed;
left: 0;
right: 0;
height: 2px;
background: blue;
z-index: 10000;
pointer-events: none;
}
</style>