deotalandAi/apps/FrontendDesigner/src/App.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>