Files
EmailBill/Web/src/App.vue
SunCheng 338bac20ce
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
todo
2026-02-02 16:59:24 +08:00

304 lines
7.1 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"
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"
>
<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>
</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', // 统计页面
'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')
// 检测系统深色模式
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)
}
</script>
<style scoped>
.app-provider {
/* 使用准确的视口高度 CSS 变量 */
height: var(--vh, 100vh);
width: 100%;
}
.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;
}
/* 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: auto !important;
min-height: 50px !important;
box-sizing: content-box !important;
}
/* 确保 TabBar 项目居中 */
:deep(.van-tabbar-item) {
padding: 0 !important;
height: 50px !important;
}
.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;
}
</style>