Files
SearcjGal-frontend/src/App.vue
AdingApkgg c323ea1fd9 feat: implement sound settings management in App and SettingsModal components
- Added sound settings functionality, allowing users to enable or disable sound effects.
- Integrated sound initialization and synchronization with user settings in App.vue.
- Updated SettingsModal.vue to include a dedicated sound settings card with toggle functionality.
- Enhanced sound management in useSound.ts to support initialization and synchronization with settings store.
2026-01-20 10:08:08 +08:00

286 lines
8.0 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>
<!--
#app 样式说明
- min-h-screen: 最小高度 100vh
- relative: 相对定位
- 字体行高换行等样式由 Tailwind @layer base 处理
-->
<div
id="app"
class="min-h-screen relative"
>
<!-- 背景层容器 - GPU 加速 -->
<div
id="background-container"
class="fixed inset-0 z-[-2] overflow-hidden gpu-layer"
>
<!-- 默认背景纹理无图片时显示 -->
<div
id="background-pattern"
class="absolute inset-0 transition-opacity duration-800"
:class="{ 'opacity-0': hasBackgroundImage }"
/>
<!-- 动画背景图层 - 使用 anime.js + CSS Ken Burns 效果 -->
<AnimatedBackground
:image-url="backgroundImageUrl"
:image-key="currentBgKey"
:ken-burns-class="kenBurnsClass"
/>
<!-- 半透明遮罩层提升内容可读性 -->
<div class="absolute inset-0 bg-white/15 dark:bg-slate-900/30 z-[1]" />
</div>
<main class="flex-1 flex flex-col min-h-screen">
<StatsCorner />
<TopToolbar :current-background-url="randomImageUrl" @open-settings="openSettings" />
<SearchHeader ref="searchHeaderRef" />
<SearchResults />
<FloatingButtons />
<CommentsModal />
<VndbPanel />
<SearchHistoryModal @select="handleHistorySelect" />
<SettingsModal
:is-open="uiStore.isSettingsModalOpen"
:custom-api="searchStore.customApi"
:custom-c-s-s="uiStore.customCSS"
@close="uiStore.isSettingsModalOpen = false"
@save="saveSettings"
/>
<!-- 键盘快捷键帮助 -->
<KeyboardHelpPanel />
<!-- 图片预览器 -->
<ImageViewer />
<!-- SW 更新提示 -->
<UpdateToast />
</main>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue'
import AnimatedBackground from '@/components/AnimatedBackground.vue'
import { useSearchStore } from '@/stores/search'
import { useUIStore } from '@/stores/ui'
import { useSettingsStore } from '@/stores/settings'
import {
saveCustomCSS,
applyCustomCSS,
applyCustomJS,
applyCustomHTML,
} from '@/utils/theme'
import { scheduleIdleTask } from '@/composables/usePerformance'
import { useBackgroundImage } from '@/composables/useBackgroundImage'
import { initSoundFromSettings, syncSoundWithSettings } from '@/composables/useSound'
// 关键组件 - 同步加载
import StatsCorner from '@/components/StatsCorner.vue'
import TopToolbar from '@/components/TopToolbar.vue'
import SearchHeader from '@/components/SearchHeader.vue'
import SearchResults from '@/components/SearchResults.vue'
import FloatingButtons from '@/components/FloatingButtons.vue'
// 非关键组件 - 异步懒加载(用户交互时才加载)
const CommentsModal = defineAsyncComponent(() => import('@/components/CommentsModal.vue'))
const VndbPanel = defineAsyncComponent(() => import('@/components/VndbPanel.vue'))
const SettingsModal = defineAsyncComponent(() => import('@/components/SettingsModal.vue'))
const SearchHistoryModal = defineAsyncComponent(() => import('@/components/SearchHistoryModal.vue'))
const KeyboardHelpPanel = defineAsyncComponent(() => import('@/components/KeyboardHelpPanel.vue'))
const ImageViewer = defineAsyncComponent(() => import('@/components/ImageViewer.vue'))
const UpdateToast = defineAsyncComponent(() => import('@/components/UpdateToast.vue'))
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
// 启用全局快捷键
useKeyboardShortcuts()
const searchStore = useSearchStore()
const uiStore = useUIStore()
const settingsStore = useSettingsStore()
const searchHeaderRef = ref<InstanceType<typeof SearchHeader> | null>(null)
// 背景图片管理
const {
currentImageUrl: randomImageUrl,
currentBgKey,
hasBackgroundImage,
backgroundImageUrl,
kenBurnsClass,
init: initBackground,
destroy: destroyBackground,
} = useBackgroundImage()
// 切换设置面板(互斥)
function openSettings() {
uiStore.toggleSettingsModal()
}
// 处理历史记录选择
function handleHistorySelect(item: { query: string; mode: 'game' | 'patch' }) {
// 同步设置 store用于其他地方读取
searchStore.setSearchQuery(item.query)
searchStore.setSearchMode(item.mode)
// 直接调用 SearchHeader 的搜索方法(会更新 URL 参数)
searchHeaderRef.value?.searchWithParams(item.query, item.mode)
// 关闭历史模态框
uiStore.isHistoryModalOpen = false
}
onMounted(async () => {
// === 关键任务:立即执行 ===
// 初始化 UI Store恢复持久化状态 + 会话状态)
uiStore.init()
// 初始化音效设置
initSoundFromSettings(settingsStore.settings.enableSound)
// URL hash 优先级最高 - 覆盖会话状态
const hash = window.location.hash
if (hash.startsWith('#atk-comment-')) {
// 评论链接:打开评论面板(互斥)
uiStore.openCommentsModal()
}
// 恢复保存的搜索状态
searchStore.restoreState()
// === 非关键任务:空闲时执行 ===
scheduleIdleTask(() => {
// 应用自定义 CSS
if (uiStore.customCSS) {
applyCustomCSS(uiStore.customCSS)
}
// 应用自定义 JS
if (settingsStore.settings.customJS) {
applyCustomJS(settingsStore.settings.customJS)
}
// 应用自定义 HTML
if (settingsStore.settings.customHTML) {
applyCustomHTML(settingsStore.settings.customHTML)
}
}, { timeout: 2000 })
// === 背景图片初始化 ===
await initBackground()
})
onUnmounted(() => {
destroyBackground()
})
// 监听音效设置变化
watch(
() => settingsStore.settings.enableSound,
(enabled) => {
syncSoundWithSettings(enabled)
},
)
// 设置相关函数
function saveSettings(customApi: string, newCustomCSS: string) {
// 保存自定义 API 到 search store
searchStore.setCustomApi(customApi)
// 保存自定义 CSS 到 UI store会自动持久化
uiStore.setCustomCSS(newCustomCSS)
// 同时保存到旧的 localStorage key兼容性
saveCustomCSS(newCustomCSS)
// 应用到页面
applyCustomCSS(newCustomCSS)
// 应用自定义 JSsettingsStore 已在 SettingsModal 中更新)
applyCustomJS(settingsStore.settings.customJS)
// 应用自定义 HTML
applyCustomHTML(settingsStore.settings.customHTML)
}
</script>
<style>
@import "tailwindcss";
/* Tailwind v4: 配置 dark 变体使用 .dark 类 */
@custom-variant dark (&:where(.dark, .dark *));
/* Ken Burns 动画效果 - 使用 CSS 动画实现更流畅的背景切换 */
.ken-burns {
/* 初始状态 - 稍微放大以便动画有空间 */
transform-origin: center center;
}
/* 缩放进入 - 从 100% 缓慢放大到 115% */
.kb-zoom-in {
animation: kb-zoom-in 12s ease-out forwards;
}
@keyframes kb-zoom-in {
0% { transform: scale(1); }
100% { transform: scale(1.15); }
}
/* 缩放退出 - 从 115% 缓慢缩小到 100% */
.kb-zoom-out {
animation: kb-zoom-out 12s ease-out forwards;
}
@keyframes kb-zoom-out {
0% { transform: scale(1.15); }
100% { transform: scale(1); }
}
/* 向左平移 */
.kb-pan-left {
animation: kb-pan-left 12s ease-out forwards;
}
@keyframes kb-pan-left {
0% { transform: scale(1.1) translateX(3%); }
100% { transform: scale(1.1) translateX(-3%); }
}
/* 向右平移 */
.kb-pan-right {
animation: kb-pan-right 12s ease-out forwards;
}
@keyframes kb-pan-right {
0% { transform: scale(1.1) translateX(-3%); }
100% { transform: scale(1.1) translateX(3%); }
}
/* 向上平移 */
.kb-pan-up {
animation: kb-pan-up 12s ease-out forwards;
}
@keyframes kb-pan-up {
0% { transform: scale(1.1) translateY(3%); }
100% { transform: scale(1.1) translateY(-3%); }
}
/* 向下平移 */
.kb-pan-down {
animation: kb-pan-down 12s ease-out forwards;
}
@keyframes kb-pan-down {
0% { transform: scale(1.1) translateY(-3%); }
100% { transform: scale(1.1) translateY(3%); }
}
/* 减少动画偏好 - 禁用 Ken Burns 效果 */
@media (prefers-reduced-motion: reduce) {
.ken-burns {
animation: none !important;
transform: scale(1.05) !important;
}
}
</style>