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.
This commit is contained in:
AdingApkgg
2026-01-20 10:08:08 +08:00
parent 8d4d5a2fa9
commit c323ea1fd9
5 changed files with 163 additions and 118 deletions

View File

@@ -62,7 +62,7 @@
</template>
<script setup lang="ts">
import { defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
import { defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue'
import AnimatedBackground from '@/components/AnimatedBackground.vue'
import { useSearchStore } from '@/stores/search'
import { useUIStore } from '@/stores/ui'
@@ -75,6 +75,7 @@ import {
} 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'
@@ -137,6 +138,9 @@ onMounted(async () => {
// 初始化 UI Store恢复持久化状态 + 会话状态)
uiStore.init()
// 初始化音效设置
initSoundFromSettings(settingsStore.settings.enableSound)
// URL hash 优先级最高 - 覆盖会话状态
const hash = window.location.hash
if (hash.startsWith('#atk-comment-')) {
@@ -174,6 +178,14 @@ onUnmounted(() => {
destroyBackground()
})
// 监听音效设置变化
watch(
() => settingsStore.settings.enableSound,
(enabled) => {
syncSoundWithSettings(enabled)
},
)
// 设置相关函数
function saveSettings(customApi: string, newCustomCSS: string) {
// 保存自定义 API 到 search store

View File

@@ -56,56 +56,37 @@
<!-- 内容区域 -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<div class="max-w-3xl mx-auto px-4 sm:px-6 py-6 sm:py-8 space-y-6">
<!-- 主题设置卡片 -->
<div
class="settings-card"
>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-500 flex items-center justify-center shadow-lg shadow-violet-500/30">
<Palette :size="20" class="text-white" />
</div>
<div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white">外观主题</h2>
<p class="text-sm text-gray-500 dark:text-slate-400">选择亮色暗色或跟随系统</p>
</div>
</div>
<!-- 主题选项 -->
<div class="grid grid-cols-3 gap-3">
<button
v-for="option in themeOptions"
:key="option.value"
type="button"
:class="[
'flex flex-col items-center gap-2 p-4 rounded-xl transition-all duration-200',
uiStore.themeMode === option.value
? 'bg-[#ff1493]/10 border-2 border-[#ff1493] dark:border-[#ff69b4]'
: 'bg-slate-50 dark:bg-slate-800/60 border-2 border-transparent hover:border-pink-200 dark:hover:border-pink-900'
]"
@click="handleThemeChange(option.value)"
>
<!-- 图标 -->
<div
:class="[
'w-10 h-10 rounded-xl flex items-center justify-center transition-colors',
uiStore.themeMode === option.value
? 'bg-[#ff1493] text-white'
: 'bg-gray-200 dark:bg-slate-700 text-gray-600 dark:text-slate-400'
]"
>
<component :is="option.icon" :size="20" />
<!-- 音效设置卡片 -->
<div class="settings-card">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-pink-500 to-rose-500 flex items-center justify-center shadow-lg shadow-pink-500/30">
<Volume2 :size="20" class="text-white" />
</div>
<!-- 标签 -->
<div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white">音效</h2>
<p class="text-sm text-gray-500 dark:text-slate-400">界面交互音效</p>
</div>
</div>
<!-- 开关 -->
<button
type="button"
role="switch"
:aria-checked="localEnableSound"
:class="[
'relative inline-flex h-7 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
localEnableSound
? 'bg-[#ff1493]'
: 'bg-gray-300 dark:bg-slate-600'
]"
@click="toggleSound"
>
<span
:class="[
'text-sm font-medium',
uiStore.themeMode === option.value
? 'text-[#ff1493] dark:text-[#ff69b4]'
: 'text-gray-700 dark:text-slate-300'
'pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out',
localEnableSound ? 'translate-x-5' : 'translate-x-0'
]"
>
{{ option.label }}
</span>
/>
</button>
</div>
</div>
@@ -114,13 +95,40 @@
<div
class="settings-card"
>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-500 flex items-center justify-center shadow-lg shadow-cyan-500/30">
<Server :size="20" class="text-white" />
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-500 flex items-center justify-center shadow-lg shadow-cyan-500/30">
<Server :size="20" class="text-white" />
</div>
<div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white">聚搜 API 后端</h2>
<p class="text-sm text-gray-500 dark:text-slate-400">选择或自定义 URL 地址</p>
</div>
</div>
<div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white">聚搜 API 后端</h2>
<p class="text-sm text-gray-500 dark:text-slate-400">选择或自定义 URL 地址</p>
<!-- 部署后端 & 贡献 API 按钮 -->
<div class="flex items-center gap-2">
<a
:href="deployUrl"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/40 border border-cyan-200 dark:border-cyan-800/50 hover:bg-cyan-100 dark:hover:bg-cyan-950/60 active:scale-95 transition-all"
@click="playTap"
>
<Github :size="14" />
<span class="hidden sm:inline">部署后端</span>
<span class="sm:hidden">部署</span>
</a>
<a
:href="contributeUrl"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/40 border border-cyan-200 dark:border-cyan-800/50 hover:bg-cyan-100 dark:hover:bg-cyan-950/60 active:scale-95 transition-all"
@click="playTap"
>
<Plus :size="14" />
<span class="hidden sm:inline">贡献 API</span>
<span class="sm:hidden">贡献</span>
</a>
</div>
</div>
@@ -191,7 +199,7 @@
v-if="selectedApiOption === 'custom'"
class="overflow-hidden"
>
<div class="mt-4 space-y-3">
<div class="mt-4">
<div class="relative">
<LinkIcon :size="18" class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
@@ -202,18 +210,6 @@
@input="handleTyping"
/>
</div>
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-slate-400">
<Github :size="14" />
<span>部署后端:</span>
<a
href="https://github.com/Moe-Sakura/Wrangler-API"
target="_blank"
rel="noopener noreferrer"
class="text-[#ff1493] dark:text-[#ff69b4] hover:underline"
>
Moe-Sakura/Wrangler-API
</a>
</div>
</div>
</div>
</Transition>
@@ -467,7 +463,7 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { playTap, playCelebration, playSelect, playType } from '@/composables/useSound'
import { playTap, playCelebration, playSelect, playType, playToggleOn, playToggleOff } from '@/composables/useSound'
// Prism Editor
import { PrismEditor } from 'vue-prism-editor'
@@ -529,29 +525,14 @@ import {
Check,
Github,
X,
Palette,
Sun,
Moon,
Monitor,
Plus,
Volume2,
} from 'lucide-vue-next'
import { useUIStore, type ThemeMode } from '@/stores/ui'
import { useSettingsStore, DEFAULT_API_CONFIG } from '@/stores/settings'
import apiData from '@/data/api.json'
const uiStore = useUIStore()
const settingsStore = useSettingsStore()
// 主题选项
const themeOptions = [
{ value: 'light' as ThemeMode, label: '亮色', icon: Sun },
{ value: 'system' as ThemeMode, label: '系统', icon: Monitor },
{ value: 'dark' as ThemeMode, label: '暗色', icon: Moon },
]
function handleThemeChange(mode: ThemeMode) {
playSelect()
uiStore.setThemeMode(mode)
}
const props = defineProps<{
isOpen: boolean
customApi: string
@@ -563,46 +544,33 @@ const emit = defineEmits<{
save: [customApi: string, customCSS: string]
}>()
// API 服务器选项
// API 服务器选项 - 从 JSON 读取
const apiOptions = [
{ value: 'cfapi', label: 'Cloudflare' },
{ value: 'api', label: '香港 雨云' },
{ value: 'gzapi', label: '广州 腾讯云' },
{ value: 'usapi', label: '洛杉矶 CloudCone' },
{ value: 'jpapi', label: '东京 ClawCloud' },
{ value: 'deapi', label: '法兰克福 ClawCloud' },
...apiData.servers.map(server => ({ value: server.key, label: server.label })),
{ value: 'custom', label: '自定义' },
]
// API URL 映射
const apiUrls: Record<string, string> = {
cfapi: 'https://cf.api.searchgal.homes',
api: 'https://api.searchgal.homes',
gzapi: 'https://gz.api.searchgal.homes',
usapi: 'https://us.api.searchgal.homes',
jpapi: 'https://jp.api.searchgal.homes',
deapi: 'https://de.api.searchgal.homes',
}
// API URL 映射 - 从 JSON 读取
const apiUrls: Record<string, string> = Object.fromEntries(
apiData.servers.map(server => [server.key, server.url]),
)
// 部署后端 & 贡献 API 的 URL
const deployUrl = apiData.deployUrl
const contributeUrl = apiData.contributeUrl
// 根据 URL 判断选中的选项
function getOptionFromUrl(url: string): string {
if (!url || url === apiUrls.cfapi) {
return 'cfapi'
// 空 URL 或匹配第一个服务器(默认)
const defaultKey = apiData.servers[0]?.key || 'cfapi'
if (!url || url === apiUrls[defaultKey]) {
return defaultKey
}
if (url === apiUrls.api) {
return 'api'
}
if (url === apiUrls.gzapi) {
return 'gzapi'
}
if (url === apiUrls.usapi) {
return 'usapi'
}
if (url === apiUrls.jpapi) {
return 'jpapi'
}
if (url === apiUrls.deapi) {
return 'deapi'
// 遍历查找匹配的服务器
for (const [key, serverUrl] of Object.entries(apiUrls)) {
if (url === serverUrl) {
return key
}
}
return 'custom'
}
@@ -700,6 +668,26 @@ const localAiTranslateModel = ref(settingsStore.settings.aiTranslateModel)
const localBackgroundImageApiUrl = ref(settingsStore.settings.backgroundImageApiUrl)
const localVideoParseApiUrl = ref(settingsStore.settings.videoParseApiUrl)
// 音效设置
const localEnableSound = ref(settingsStore.settings.enableSound)
// 切换音效
function toggleSound() {
localEnableSound.value = !localEnableSound.value
// 播放对应的开关音效
if (localEnableSound.value) {
// 临时启用音效来播放开启音
settingsStore.updateSetting('enableSound', true)
playToggleOn()
} else {
playToggleOff()
// 延迟关闭,让关闭音效播放完
setTimeout(() => {
settingsStore.updateSetting('enableSound', false)
}, 150)
}
}
// 计算最终的 API 地址
const localCustomApi = computed(() => {
if (selectedApiOption.value === 'custom') {
@@ -750,6 +738,8 @@ watch(() => props.isOpen, (isOpen) => {
localAiTranslateModel.value = settingsStore.settings.aiTranslateModel
localBackgroundImageApiUrl.value = settingsStore.settings.backgroundImageApiUrl
localVideoParseApiUrl.value = settingsStore.settings.videoParseApiUrl
// 同步音效设置
localEnableSound.value = settingsStore.settings.enableSound
}
}, { immediate: true })

View File

@@ -5,9 +5,19 @@
import { ref } from 'vue'
// 音效是否启用
// 音效是否启用(初始值,会被 settings store 覆盖)
const soundEnabled = ref(true)
// 初始化音效设置(从 settings store 同步)
export function initSoundFromSettings(enabled: boolean): void {
soundEnabled.value = enabled
}
// 监听 settings 变化的函数(供外部调用)
export function syncSoundWithSettings(enabled: boolean): void {
soundEnabled.value = enabled
}
// AudioContext 单例
let audioContext: AudioContext | null = null

31
src/data/api.json Normal file
View File

@@ -0,0 +1,31 @@
{
"servers": [
{
"key": "cfapi",
"label": "Cloudflare",
"url": "https://cf.api.searchgal.homes"
},
{
"key": "api",
"label": "香港 雨云",
"url": "https://api.searchgal.homes"
},
{
"key": "gzapi",
"label": "广州 腾讯云",
"url": "https://gz.api.searchgal.homes"
},
{
"key": "jpapi",
"label": "东京 ClawCloud",
"url": "https://jp.api.searchgal.homes"
},
{
"key": "deapi",
"label": "法兰克福 ClawCloud",
"url": "https://de.api.searchgal.homes"
}
],
"deployUrl": "https://github.com/Moe-Sakura/Wrangler-API",
"contributeUrl": "https://github.com/Moe-Sakura/frontend/edit/dev/src/data/api.json"
}

View File

@@ -12,6 +12,7 @@ export interface UserSettings {
showPlatformIcons: boolean
compactMode: boolean
enableNotifications: boolean
enableSound: boolean
// API 高级配置
vndbApiBaseUrl: string
vndbImageProxyUrl: string
@@ -44,6 +45,7 @@ const DEFAULT_SETTINGS: UserSettings = {
showPlatformIcons: true,
compactMode: false,
enableNotifications: true,
enableSound: true,
// API 高级配置
vndbApiBaseUrl: DEFAULT_API_CONFIG.vndbApiBaseUrl,
vndbImageProxyUrl: DEFAULT_API_CONFIG.vndbImageProxyUrl,