528 lines
10 KiB
Vue
528 lines
10 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useTheme } from './composables/useTheme'
|
|
import { useI18n } from './composables/useI18n'
|
|
import { useResponsive } from './composables/useResponsive'
|
|
import { useAppStore } from './stores/index.js'
|
|
|
|
// 响应式数据
|
|
const showMobileMenu = ref(false)
|
|
const currentYear = ref(new Date().getFullYear())
|
|
|
|
// 状态管理
|
|
const appStore = useAppStore()
|
|
|
|
// 主题系统
|
|
const {
|
|
currentTheme,
|
|
toggleTheme,
|
|
isDarkMode,
|
|
themes
|
|
} = useTheme()
|
|
|
|
// 国际化
|
|
const {
|
|
currentLocale,
|
|
locales,
|
|
t,
|
|
setLocale
|
|
} = useI18n()
|
|
|
|
// 响应式检测
|
|
const {
|
|
isMobile,
|
|
isTablet,
|
|
isDesktop,
|
|
screenSize
|
|
} = useResponsive()
|
|
|
|
// 路由
|
|
const router = useRouter()
|
|
|
|
// 计算属性
|
|
const isSmallScreen = computed(() => isMobile.value || isTablet.value)
|
|
const appTitle = computed(() => t('app.title'))
|
|
|
|
// 移动端菜单切换
|
|
const toggleMobileMenu = () => {
|
|
showMobileMenu.value = !showMobileMenu.value
|
|
}
|
|
|
|
// 语言切换处理
|
|
const handleLanguageChange = (locale) => {
|
|
setLocale(locale)
|
|
// 保存语言偏好到localStorage
|
|
localStorage.setItem('preferred-language', locale)
|
|
}
|
|
|
|
// 监听stores语言切换事件
|
|
const handleStoreLocaleChange = (event) => {
|
|
// 已移除自定义事件,改用响应式监听
|
|
}
|
|
|
|
// 主题切换处理
|
|
const handleThemeChange = () => {
|
|
toggleTheme()
|
|
// 保存主题偏好到localStorage
|
|
localStorage.setItem('preferred-theme', currentTheme.value)
|
|
}
|
|
|
|
// 初始化应用设置
|
|
const initializeApp = () => {
|
|
// 设置页面标题
|
|
document.title = appTitle.value
|
|
|
|
// 设置页面语言
|
|
document.documentElement.lang = currentLocale.value
|
|
|
|
// 设置Meta主题色 (移动端状态栏)
|
|
const metaThemeColor = document.querySelector('meta[name=theme-color]')
|
|
if (metaThemeColor) {
|
|
const bgColor = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--bg-primary')?.trim() || '#ffffff'
|
|
metaThemeColor.setAttribute('content', bgColor)
|
|
}
|
|
}
|
|
|
|
// 监听响应式变化
|
|
const handleResize = () => {
|
|
if (!isSmallScreen.value && showMobileMenu.value) {
|
|
showMobileMenu.value = false
|
|
}
|
|
}
|
|
|
|
// 监听语言变化
|
|
const unwatchLocale = watch(() => currentLocale.value, () => {
|
|
initializeApp()
|
|
})
|
|
|
|
// 监听主题变化
|
|
const unwatchTheme = watch(() => currentTheme.value, () => {
|
|
initializeApp()
|
|
})
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
initializeApp()
|
|
window.addEventListener('resize', handleResize)
|
|
window.addEventListener('locale-changed', handleStoreLocaleChange)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize)
|
|
window.removeEventListener('locale-changed', handleStoreLocaleChange)
|
|
unwatchLocale()
|
|
unwatchTheme()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
id="app"
|
|
class="app"
|
|
:class="{
|
|
'app--mobile': isMobile,
|
|
'app--tablet': isTablet,
|
|
'app--desktop': isDesktop,
|
|
'app--dark': isDarkMode,
|
|
'menu-open': showMobileMenu
|
|
}"
|
|
>
|
|
<!-- 主要内容区域 -->
|
|
<main class="app-main">
|
|
<div class="main-container">
|
|
<router-view v-slot="{ Component, route }">
|
|
<component :is="Component" :key="route.path" />
|
|
</router-view>
|
|
</div>
|
|
</main>
|
|
<!-- 全局加载指示器 -->
|
|
<div
|
|
v-if="appStore.isLoading"
|
|
class="loading-overlay"
|
|
>
|
|
<div class="loading-spinner">
|
|
<div class="spinner"></div>
|
|
<p class="loading-text">{{ t('common.loading') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* 应用主容器 */
|
|
.app {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
/* 头部导航 */
|
|
.app-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: var(--z-header);
|
|
background-color: var(--bg-primary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
backdrop-filter: blur(10px);
|
|
-webkit-backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.header-container {
|
|
max-width: var(--container-max-width);
|
|
margin: 0 auto;
|
|
padding: 0 var(--spacing-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: var(--header-height);
|
|
}
|
|
|
|
.header-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.header-brand:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.brand-logo {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 700;
|
|
margin: 0;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.header-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.nav-link {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--border-radius-md);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.nav-link:hover,
|
|
.nav-link.router-link-active {
|
|
color: var(--text-primary);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.nav-link.router-link-active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px;
|
|
left: var(--spacing-md);
|
|
right: var(--spacing-md);
|
|
height: 2px;
|
|
background: var(--primary-color);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.header-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.control-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border: none;
|
|
border-radius: var(--border-radius-md);
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.lang-text {
|
|
margin-left: var(--spacing-xs);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* 移动端菜单 */
|
|
.mobile-menu-btn {
|
|
display: none;
|
|
}
|
|
|
|
.hamburger {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
position: relative;
|
|
}
|
|
|
|
.hamburger span {
|
|
display: block;
|
|
height: 2px;
|
|
width: 100%;
|
|
background-color: currentColor;
|
|
border-radius: 1px;
|
|
transition: all 0.3s ease;
|
|
transform-origin: center;
|
|
}
|
|
|
|
.hamburger span:nth-child(1) {
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.hamburger span:nth-child(2) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.hamburger span:nth-child(3) {
|
|
transform: translateY(4px);
|
|
}
|
|
|
|
.hamburger.active span:nth-child(1) {
|
|
transform: translateY(0) rotate(45deg);
|
|
}
|
|
|
|
.hamburger.active span:nth-child(2) {
|
|
opacity: 0;
|
|
transform: scaleX(0);
|
|
}
|
|
|
|
.hamburger.active span:nth-child(3) {
|
|
transform: translateY(0) rotate(-45deg);
|
|
}
|
|
|
|
.mobile-nav {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: var(--bg-primary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
transform: translateY(-100%);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.mobile-nav--open {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.mobile-nav-menu {
|
|
padding: var(--spacing-md);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.mobile-nav-link {
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--border-radius-md);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
display: block;
|
|
}
|
|
|
|
.mobile-nav-link:hover,
|
|
.mobile-nav-link.router-link-active {
|
|
color: var(--text-primary);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
/* 主要内容 */
|
|
.app-main {
|
|
flex: 1;
|
|
padding: var(--spacing-lg) 0;
|
|
}
|
|
|
|
.main-container {
|
|
max-width: var(--container-max-width);
|
|
margin: 0 auto;
|
|
padding: 0 var(--spacing-md);
|
|
width: 100%;
|
|
}
|
|
|
|
/* 底部 */
|
|
.app-footer {
|
|
background-color: var(--bg-secondary);
|
|
border-top: 1px solid var(--border-color);
|
|
padding: var(--spacing-lg) 0;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.footer-container {
|
|
max-width: var(--container-max-width);
|
|
margin: 0 auto;
|
|
padding: 0 var(--spacing-md);
|
|
text-align: center;
|
|
}
|
|
|
|
.footer-text {
|
|
margin: 0 0 var(--spacing-sm) 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.footer-tech {
|
|
margin: 0;
|
|
color: var(--text-muted);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.heart {
|
|
color: var(--primary-color);
|
|
animation: heartbeat 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes heartbeat {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
|
|
/* 加载指示器 */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: var(--z-overlay);
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.loading-spinner {
|
|
background-color: var(--bg-primary);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--border-radius-lg);
|
|
text-align: center;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.spinner {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border: 2px solid var(--border-color);
|
|
border-top-color: var(--primary-color);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto var(--spacing-md);
|
|
}
|
|
|
|
.loading-text {
|
|
margin: 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* 页面过渡动画 */
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* 响应式断点 */
|
|
@media (max-width: 768px) {
|
|
.header-nav--desktop {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-menu-btn {
|
|
display: flex;
|
|
}
|
|
|
|
.header-container {
|
|
padding: 0 var(--spacing-sm);
|
|
}
|
|
|
|
.main-container {
|
|
padding: 0 var(--spacing-sm);
|
|
}
|
|
|
|
.footer-container {
|
|
padding: 0 var(--spacing-sm);
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: var(--font-size-md);
|
|
}
|
|
|
|
.app-main {
|
|
padding: var(--spacing-md) 0;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 769px) {
|
|
.mobile-nav {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* 平板端优化 */
|
|
@media (min-width: 768px) and (max-width: 1024px) {
|
|
.header-container {
|
|
padding: 0 var(--spacing-lg);
|
|
}
|
|
|
|
.main-container {
|
|
padding: 0 var(--spacing-lg);
|
|
}
|
|
|
|
.footer-container {
|
|
padding: 0 var(--spacing-lg);
|
|
}
|
|
}
|
|
</style>
|