mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-25 08:47:22 +08:00
feat: 设置页增加音频设备配置
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { usePlayerCoreStore } from '@/store/modules/playerCore';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const playerStore = usePlayerCoreStore();
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isTesting = ref(false);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
await playerStore.refreshAudioDevices();
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeviceChange = async (deviceId: string) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
await playerStore.setAudioOutputDevice(deviceId);
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
isTesting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用与播放器相同的 AudioContext 测试
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
|
||||||
|
// 应用当前选择的设备
|
||||||
|
const deviceId = playerStore.audioOutputDeviceId;
|
||||||
|
if (deviceId && deviceId !== 'default' && (audioContext as any).setSinkId) {
|
||||||
|
await (audioContext as any).setSinkId(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oscillator = audioContext.createOscillator();
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
oscillator.frequency.value = 440;
|
||||||
|
gainNode.gain.value = 0.3;
|
||||||
|
|
||||||
|
oscillator.start();
|
||||||
|
setTimeout(() => {
|
||||||
|
oscillator.stop();
|
||||||
|
audioContext.close();
|
||||||
|
isTesting.value = false;
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试音频失败:', error);
|
||||||
|
isTesting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('settings.playback.audioDevice') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-button size="tiny" :loading="isLoading" quaternary @click="handleRefresh">
|
||||||
|
<template #icon>
|
||||||
|
<i class="ri-refresh-line"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button size="tiny" :loading="isTesting" quaternary @click="handleTest">
|
||||||
|
<template #icon>
|
||||||
|
<i class="ri-volume-up-line"></i>
|
||||||
|
</template>
|
||||||
|
{{ t('settings.playback.testAudio') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-select
|
||||||
|
:value="playerStore.audioOutputDeviceId"
|
||||||
|
:options="
|
||||||
|
playerStore.availableAudioDevices.map((d) => ({
|
||||||
|
label: d.label,
|
||||||
|
value: d.deviceId
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:loading="isLoading"
|
||||||
|
:placeholder="t('settings.playback.selectAudioDevice')"
|
||||||
|
@update:value="handleDeviceChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ResponsiveModal
|
<responsive-modal
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="t('settings.playback.musicSources')"
|
:title="t('settings.playback.musicSources')"
|
||||||
@close="handleCancel"
|
@close="handleCancel"
|
||||||
@@ -58,7 +58,9 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">{{ source.key }}</span>
|
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">{{
|
||||||
|
source.key
|
||||||
|
}}</span>
|
||||||
<div
|
<div
|
||||||
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -67,7 +69,10 @@
|
|||||||
: 'border-gray-300 dark:border-gray-600'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<i v-if="isSourceSelected(source.key)" class="ri-check-line text-white text-xs scale-75"></i>
|
<i
|
||||||
|
v-if="isSourceSelected(source.key)"
|
||||||
|
class="ri-check-line text-white text-xs scale-75"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +102,9 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">落雪音源</span>
|
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate"
|
||||||
|
>落雪音源</span
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -106,11 +113,18 @@
|
|||||||
: 'border-gray-300 dark:border-gray-600'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<i v-if="isSourceSelected('lxMusic')" class="ri-check-line text-white text-xs scale-75"></i>
|
<i
|
||||||
|
v-if="isSourceSelected('lxMusic')"
|
||||||
|
class="ri-check-line text-white text-xs scale-75"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
||||||
{{ activeLxApiId && lxMusicScriptInfo ? lxMusicScriptInfo.name : t('settings.playback.lxMusic.scripts.notConfigured') }}
|
{{
|
||||||
|
activeLxApiId && lxMusicScriptInfo
|
||||||
|
? lxMusicScriptInfo.name
|
||||||
|
: t('settings.playback.lxMusic.scripts.notConfigured')
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +153,9 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">{{ t('settings.playback.sourceLabels.custom') }}</span>
|
<span class="font-semibold text-gray-900 dark:text-white text-sm truncate">{{
|
||||||
|
t('settings.playback.sourceLabels.custom')
|
||||||
|
}}</span>
|
||||||
<div
|
<div
|
||||||
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
class="w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -148,11 +164,18 @@
|
|||||||
: 'border-gray-300 dark:border-gray-600'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<i v-if="isSourceSelected('custom')" class="ri-check-line text-white text-xs scale-75"></i>
|
<i
|
||||||
|
v-if="isSourceSelected('custom')"
|
||||||
|
class="ri-check-line text-white text-xs scale-75"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
<p class="text-[10px] text-gray-500 mt-0.5 truncate">
|
||||||
{{ settingsStore.setData.customApiPlugin ? t('settings.playback.customApi.status.imported') : t('settings.playback.customApi.status.notImported') }}
|
{{
|
||||||
|
settingsStore.setData.customApiPlugin
|
||||||
|
? t('settings.playback.customApi.status.imported')
|
||||||
|
: t('settings.playback.customApi.status.notImported')
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +185,9 @@
|
|||||||
<!-- LX Music Management Tab -->
|
<!-- LX Music Management Tab -->
|
||||||
<div v-else-if="activeTab === 'lxMusic'" class="space-y-3 pb-2">
|
<div v-else-if="activeTab === 'lxMusic'" class="space-y-3 pb-2">
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<h3 class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('settings.playback.lxMusic.scripts.title') }}</h3>
|
<h3 class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('settings.playback.lxMusic.scripts.title') }}
|
||||||
|
</h3>
|
||||||
<button
|
<button
|
||||||
@click="importLxMusicScript"
|
@click="importLxMusicScript"
|
||||||
class="flex items-center gap-1 px-2.5 py-1 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium rounded-lg transition-colors"
|
class="flex items-center gap-1 px-2.5 py-1 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium rounded-lg transition-colors"
|
||||||
@@ -191,12 +216,17 @@
|
|||||||
class="peer appearance-none w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600 checked:border-emerald-500 checked:bg-emerald-500 transition-colors cursor-pointer"
|
class="peer appearance-none w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600 checked:border-emerald-500 checked:bg-emerald-500 transition-colors cursor-pointer"
|
||||||
@change="setActiveLxApi(api.id)"
|
@change="setActiveLxApi(api.id)"
|
||||||
/>
|
/>
|
||||||
<i class="ri-check-line absolute text-white text-[10px] pointer-events-none opacity-0 peer-checked:opacity-100 transition-opacity"></i>
|
<i
|
||||||
|
class="ri-check-line absolute text-white text-[10px] pointer-events-none opacity-0 peer-checked:opacity-100 transition-opacity"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0 mr-2">
|
<div class="flex-1 min-w-0 mr-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span v-if="editingScriptId !== api.id" class="font-medium text-sm text-gray-900 dark:text-white truncate">
|
<span
|
||||||
|
v-if="editingScriptId !== api.id"
|
||||||
|
class="font-medium text-sm text-gray-900 dark:text-white truncate"
|
||||||
|
>
|
||||||
{{ api.name }}
|
{{ api.name }}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -217,7 +247,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
<div class="flex items-center gap-2 mt-0.5">
|
||||||
<span v-if="api.info.version" class="text-[10px] text-gray-500 bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded">
|
<span
|
||||||
|
v-if="api.info.version"
|
||||||
|
class="text-[10px] text-gray-500 bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded"
|
||||||
|
>
|
||||||
v{{ api.info.version }}
|
v{{ api.info.version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,13 +265,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="py-6 text-center text-xs text-gray-400 bg-gray-50 dark:bg-white/5 rounded-xl border border-dashed border-gray-200 dark:border-white/10">
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-6 text-center text-xs text-gray-400 bg-gray-50 dark:bg-white/5 rounded-xl border border-dashed border-gray-200 dark:border-white/10"
|
||||||
|
>
|
||||||
<p>{{ t('settings.playback.lxMusic.scripts.empty') }}</p>
|
<p>{{ t('settings.playback.lxMusic.scripts.empty') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL Import -->
|
<!-- URL Import -->
|
||||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-white/5">
|
||||||
<h4 class="text-xs font-medium mb-2 text-gray-900 dark:text-white">{{ t('settings.playback.lxMusic.scripts.importOnline') }}</h4>
|
<h4 class="text-xs font-medium mb-2 text-gray-900 dark:text-white">
|
||||||
|
{{ t('settings.playback.lxMusic.scripts.importOnline') }}
|
||||||
|
</h4>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="lxScriptUrl"
|
v-model="lxScriptUrl"
|
||||||
@@ -260,8 +298,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom API Tab -->
|
<!-- Custom API Tab -->
|
||||||
<div v-else-if="activeTab === 'customApi'" class="flex flex-col items-center justify-center py-6 text-center h-full">
|
<div
|
||||||
<div class="w-12 h-12 bg-violet-100 dark:bg-violet-500/20 text-violet-500 rounded-xl flex items-center justify-center mb-3">
|
v-else-if="activeTab === 'customApi'"
|
||||||
|
class="flex flex-col items-center justify-center py-6 text-center h-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 bg-violet-100 dark:bg-violet-500/20 text-violet-500 rounded-xl flex items-center justify-center mb-3"
|
||||||
|
>
|
||||||
<i class="ri-plug-fill text-2xl"></i>
|
<i class="ri-plug-fill text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -280,9 +323,15 @@
|
|||||||
{{ t('settings.playback.customApi.importConfig') }}
|
{{ t('settings.playback.customApi.importConfig') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="settingsStore.setData.customApiPluginName" class="mt-4 flex items-center gap-2 px-3 py-1.5 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-lg text-xs">
|
<div
|
||||||
|
v-if="settingsStore.setData.customApiPluginName"
|
||||||
|
class="mt-4 flex items-center gap-2 px-3 py-1.5 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-lg text-xs"
|
||||||
|
>
|
||||||
<i class="ri-check-circle-fill"></i>
|
<i class="ri-check-circle-fill"></i>
|
||||||
<span>{{ t('settings.playback.customApi.currentSource') }}: <b>{{ settingsStore.setData.customApiPluginName }}</b></span>
|
<span
|
||||||
|
>{{ t('settings.playback.customApi.currentSource') }}:
|
||||||
|
<b>{{ settingsStore.setData.customApiPluginName }}</b></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="mt-4 text-xs text-gray-400">
|
<div v-else class="mt-4 text-xs text-gray-400">
|
||||||
@@ -311,7 +360,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ResponsiveModal>
|
</responsive-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -344,8 +393,7 @@ const MUSIC_SOURCES: MusicSourceConfig[] = [
|
|||||||
{ key: 'migu', color: '#ff6600' },
|
{ key: 'migu', color: '#ff6600' },
|
||||||
{ key: 'kugou', color: '#2979ff' },
|
{ key: 'kugou', color: '#2979ff' },
|
||||||
{ key: 'kuwo', color: '#ff8c00' },
|
{ key: 'kuwo', color: '#ff8c00' },
|
||||||
{ key: 'pyncmd', color: '#ec4141' },
|
{ key: 'pyncmd', color: '#ec4141' }
|
||||||
{ key: 'bilibili', color: '#00a1d6' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ==================== Props & Emits ====================
|
// ==================== Props & Emits ====================
|
||||||
@@ -356,7 +404,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
sources: {
|
sources: {
|
||||||
type: Array as () => ExtendedPlatform[],
|
type: Array as () => ExtendedPlatform[],
|
||||||
default: () => ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'] as ExtendedPlatform[]
|
default: () => ['migu', 'kugou', 'kuwo', 'pyncmd'] as ExtendedPlatform[]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -649,7 +697,9 @@ const importLxMusicScriptFromUrl = async () => {
|
|||||||
lxScriptUrl.value = '';
|
lxScriptUrl.value = '';
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('从 URL 导入落雪音源脚本失败:', error);
|
console.error('从 URL 导入落雪音源脚本失败:', error);
|
||||||
message.error(`${t('settings.playback.lxMusic.scripts.importOnline')} ${t('common.error')}:${error.message}`);
|
message.error(
|
||||||
|
`${t('settings.playback.lxMusic.scripts.importOnline')} ${t('common.error')}:${error.message}`
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isImportingFromUrl.value = false;
|
isImportingFromUrl.value = false;
|
||||||
}
|
}
|
||||||
@@ -699,7 +749,7 @@ const saveScriptName = (apiId: string) => {
|
|||||||
* 确认选择
|
* 确认选择
|
||||||
*/
|
*/
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
const defaultPlatforms: ExtendedPlatform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'];
|
const defaultPlatforms: ExtendedPlatform[] = ['migu', 'kugou', 'kuwo', 'pyncmd'];
|
||||||
const valuesToEmit =
|
const valuesToEmit =
|
||||||
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
||||||
emit('update:sources', valuesToEmit);
|
emit('update:sources', valuesToEmit);
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-4 rounded-lg transition-all bg-light dark:bg-dark text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 hover:dark:bg-gray-800"
|
class="setting-item flex items-center justify-between p-4 transition-colors bg-transparent text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 last:border-b-0 hover:bg-gray-50 hover:dark:bg-white/5"
|
||||||
:class="[
|
:class="[
|
||||||
// 移动端垂直布局
|
// 移动端垂直布局
|
||||||
{ 'max-md:flex-col max-md:items-start max-md:gap-3 max-md:p-3': !inline },
|
{ 'max-md:flex-col max-md:items-start max-md:gap-3': !inline },
|
||||||
// 可点击样式
|
// 可点击样式
|
||||||
{
|
{
|
||||||
'cursor-pointer hover:text-green-500 hover:!bg-green-50 hover:dark:!bg-green-900/30':
|
'cursor-pointer active:bg-gray-100 active:dark:bg-white/10': clickable
|
||||||
clickable
|
|
||||||
},
|
},
|
||||||
customClass
|
customClass
|
||||||
]"
|
]"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<!-- 左侧:标题和描述 -->
|
<!-- 左侧:标题和描述 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 mr-4">
|
||||||
<div class="text-base font-medium mb-1">
|
<div class="text-base font-medium mb-0.5">
|
||||||
<slot name="title">{{ title }}</slot>
|
<slot name="title">{{ title }}</slot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="description || $slots.description"
|
v-if="description || $slots.description"
|
||||||
class="text-sm text-gray-500 dark:text-gray-400"
|
class="text-sm text-gray-500 dark:text-gray-400 leading-normal"
|
||||||
>
|
>
|
||||||
<slot name="description">{{ description }}</slot>
|
<slot name="description">{{ description }}</slot>
|
||||||
</div>
|
</div>
|
||||||
<!-- 额外内容插槽 -->
|
<!-- 额外内容插槽 -->
|
||||||
|
<div v-if="$slots.extra" class="mt-2">
|
||||||
<slot name="extra"></slot>
|
<slot name="extra"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:操作区 -->
|
<!-- 右侧:操作区 -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="id" :ref="setRef" class="mb-6 scroll-mt-4">
|
<div :id="id" :ref="setRef" class="mb-8 scroll-mt-20">
|
||||||
<!-- 分组标题 -->
|
<!-- 分组标题 -->
|
||||||
<div class="text-base font-medium mb-4 text-gray-600 dark:text-white">
|
<div class="text-xl font-bold mb-4 text-gray-900 dark:text-white px-1">
|
||||||
<slot name="title">{{ title }}</slot>
|
<slot name="title">{{ title }}</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设置项列表 -->
|
<!-- 设置项列表容器 -->
|
||||||
<div class="space-y-4 max-md:space-y-3">
|
<div
|
||||||
|
class="bg-white dark:bg-neutral-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 shadow-sm"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+133
-146
@@ -1,22 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full">
|
<div class="h-full w-full bg-white dark:bg-black transition-colors duration-500 flex flex-col">
|
||||||
<!-- 左侧导航栏 -->
|
<!-- 顶部导航区 -->
|
||||||
<setting-nav
|
<div
|
||||||
v-if="!isMobile"
|
class="flex-shrink-0 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-black z-10 pr-4 sm:pr-6 lg:pr-8 pt-6 pb-2"
|
||||||
:sections="navSections"
|
|
||||||
:current-section="currentSection"
|
|
||||||
@navigate="scrollToSection"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 右侧内容区 -->
|
|
||||||
<n-scrollbar ref="scrollbarRef" class="flex-1 h-full" @scroll="handleScroll">
|
|
||||||
<div class="p-4 pb-20 max-md:p-3 max-md:pb-24">
|
|
||||||
<!-- 基础设置 -->
|
|
||||||
<setting-section
|
|
||||||
id="basic"
|
|
||||||
:title="t('settings.sections.basic')"
|
|
||||||
@ref="(el) => (sectionRefs.basic = el as HTMLElement | null)"
|
|
||||||
>
|
>
|
||||||
|
<h1 class="text-2xl md:text-3xl font-bold text-neutral-900 dark:text-white mb-6">
|
||||||
|
{{ t('common.settings') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<n-scrollbar x-scrollable class="w-full">
|
||||||
|
<div class="flex items-center pl-2 pb-2 whitespace-nowrap">
|
||||||
|
<div
|
||||||
|
v-for="section in navSections"
|
||||||
|
:key="section.id"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentSection === section.id }"
|
||||||
|
@click="currentSection = section.id"
|
||||||
|
>
|
||||||
|
{{ section.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<n-scrollbar class="flex-1">
|
||||||
|
<div class="w-full mx-auto pb-32 pt-6 px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- 基础设置 -->
|
||||||
|
<div v-show="currentSection === 'basic'" class="animate-fade-in">
|
||||||
|
<setting-section :title="t('settings.sections.basic')">
|
||||||
<!-- 主题设置 -->
|
<!-- 主题设置 -->
|
||||||
<setting-item
|
<setting-item
|
||||||
:title="t('settings.basic.themeMode')"
|
:title="t('settings.basic.themeMode')"
|
||||||
@@ -25,7 +37,10 @@
|
|||||||
<template #action>
|
<template #action>
|
||||||
<div class="flex items-center gap-3 max-md:flex-wrap">
|
<div class="flex items-center gap-3 max-md:flex-wrap">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<n-switch v-model:value="setData.autoTheme" @update:value="handleAutoThemeChange">
|
<n-switch
|
||||||
|
v-model:value="setData.autoTheme"
|
||||||
|
@update:value="handleAutoThemeChange"
|
||||||
|
>
|
||||||
<template #checked><i class="ri-smartphone-line"></i></template>
|
<template #checked><i class="ri-smartphone-line"></i></template>
|
||||||
<template #unchecked><i class="ri-settings-line"></i></template>
|
<template #unchecked><i class="ri-settings-line"></i></template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
@@ -112,19 +127,19 @@
|
|||||||
|
|
||||||
<!-- 字体预览 -->
|
<!-- 字体预览 -->
|
||||||
<div
|
<div
|
||||||
v-if="selectedFonts.length > 0"
|
v-if="isElectron && selectedFonts.length > 0"
|
||||||
class="mt-4 p-4 max-md:p-3 rounded-lg bg-gray-50 dark:bg-dark-100 border border-gray-200 dark:border-gray-700"
|
class="p-4 border-b border-gray-100 dark:border-gray-800"
|
||||||
>
|
>
|
||||||
<div class="text-sm font-medium mb-3 text-gray-600 dark:text-gray-300">
|
<div class="text-base font-bold mb-4 text-gray-900 dark:text-white">
|
||||||
{{ t('settings.basic.fontPreview.title') }}
|
{{ t('settings.basic.fontPreview.title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3" :style="{ fontFamily: setData.fontFamily }">
|
<div class="space-y-4" :style="{ fontFamily: setData.fontFamily }">
|
||||||
<div v-for="preview in fontPreviews" :key="preview.key" class="flex flex-col gap-1">
|
<div v-for="preview in fontPreviews" :key="preview.key" class="flex flex-col gap-2">
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||||
{{ t(`settings.basic.fontPreview.${preview.key}`) }}
|
{{ t(`settings.basic.fontPreview.${preview.key}`) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-base text-gray-900 dark:text-gray-100 p-2 rounded bg-white dark:bg-dark border border-gray-200 dark:border-gray-700"
|
class="text-lg text-gray-900 dark:text-gray-100 p-3 rounded-xl bg-gray-50 dark:bg-black/20"
|
||||||
>
|
>
|
||||||
{{ t(`settings.basic.fontPreview.${preview.key}Text`) }}
|
{{ t(`settings.basic.fontPreview.${preview.key}Text`) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +152,9 @@
|
|||||||
<template #description>
|
<template #description>
|
||||||
<div class="text-sm text-gray-500 mb-2">
|
<div class="text-sm text-gray-500 mb-2">
|
||||||
{{ t('settings.basic.tokenStatus') }}:
|
{{ t('settings.basic.tokenStatus') }}:
|
||||||
{{ currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet') }}
|
{{
|
||||||
|
currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet')
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentToken" class="text-xs text-gray-400 mb-2 font-mono break-all">
|
<div v-if="currentToken" class="text-xs text-gray-400 mb-2 font-mono break-all">
|
||||||
{{ currentToken.substring(0, 50) }}...
|
{{ currentToken.substring(0, 50) }}...
|
||||||
@@ -218,13 +235,11 @@
|
|||||||
</n-switch>
|
</n-switch>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 播放设置 -->
|
<!-- 播放设置 -->
|
||||||
<setting-section
|
<div v-show="currentSection === 'playback'" class="animate-fade-in">
|
||||||
id="playback"
|
<setting-section :title="t('settings.sections.playback')">
|
||||||
:title="t('settings.sections.playback')"
|
|
||||||
@ref="(el) => (sectionRefs.playback = el as HTMLElement | null)"
|
|
||||||
>
|
|
||||||
<!-- 音质设置 -->
|
<!-- 音质设置 -->
|
||||||
<setting-item
|
<setting-item
|
||||||
:title="t('settings.playback.quality')"
|
:title="t('settings.playback.quality')"
|
||||||
@@ -237,25 +252,6 @@
|
|||||||
/>
|
/>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
|
|
||||||
<!-- 会员购买链接 -->
|
|
||||||
<div
|
|
||||||
class="p-3 max-md:p-2 bg-light-100 dark:bg-dark-100 rounded-lg text-sm max-md:text-xs"
|
|
||||||
>
|
|
||||||
<div>大家还是需要支持正版,本软件只做开源探讨</div>
|
|
||||||
<div class="mt-2">各大音乐会员购买链接</div>
|
|
||||||
<div class="flex gap-4 max-md:gap-2 flex-wrap mt-1">
|
|
||||||
<a
|
|
||||||
v-for="link in memberLinks"
|
|
||||||
:key="link.url"
|
|
||||||
class="text-green-400 hover:text-green-500"
|
|
||||||
:href="link.url"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ link.name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 音源设置 -->
|
<!-- 音源设置 -->
|
||||||
<setting-item v-if="isElectron" :title="t('settings.playback.musicSources')">
|
<setting-item v-if="isElectron" :title="t('settings.playback.musicSources')">
|
||||||
<template #description>
|
<template #description>
|
||||||
@@ -267,7 +263,9 @@
|
|||||||
<span>{{ t('settings.playback.musicUnblockEnableDesc') }}</span>
|
<span>{{ t('settings.playback.musicUnblockEnableDesc') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="setData.enableMusicUnblock" class="mt-2 text-sm">
|
<div v-if="setData.enableMusicUnblock" class="mt-2 text-sm">
|
||||||
<span class="text-gray-500">{{ t('settings.playback.selectedMusicSources') }}</span>
|
<span class="text-gray-500">{{
|
||||||
|
t('settings.playback.selectedMusicSources')
|
||||||
|
}}</span>
|
||||||
<span v-if="musicSources.length > 0" class="text-gray-400">{{
|
<span v-if="musicSources.length > 0" class="text-gray-400">{{
|
||||||
musicSources.join(', ')
|
musicSources.join(', ')
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -307,15 +305,42 @@
|
|||||||
<template #unchecked>{{ t('common.off') }}</template>
|
<template #unchecked>{{ t('common.off') }}</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
|
|
||||||
|
<!-- 音频输出设备 -->
|
||||||
|
<setting-item
|
||||||
|
v-if="isElectron"
|
||||||
|
:title="t('settings.playback.audioDevice')"
|
||||||
|
:description="t('settings.playback.audioDeviceDesc')"
|
||||||
|
>
|
||||||
|
<audio-device-settings />
|
||||||
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
|
||||||
<!-- 应用设置 -->
|
<!-- 会员购买链接 -->
|
||||||
<setting-section
|
<div
|
||||||
v-if="isElectron"
|
class="mt-6 p-4 rounded-2xl bg-white dark:bg-neutral-900 border border-gray-100 dark:border-gray-800"
|
||||||
id="application"
|
|
||||||
:title="t('settings.sections.application')"
|
|
||||||
@ref="(el) => (sectionRefs.application = el as HTMLElement | null)"
|
|
||||||
>
|
>
|
||||||
|
<div class="text-sm font-medium text-gray-500 mb-3">支持正版</div>
|
||||||
|
<div class="text-base text-gray-900 dark:text-white mb-4">
|
||||||
|
大家还是需要支持正版,本软件只做开源探讨。各大音乐会员购买链接:
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 flex-wrap">
|
||||||
|
<a
|
||||||
|
v-for="link in memberLinks"
|
||||||
|
:key="link.url"
|
||||||
|
class="px-4 py-2 rounded-xl bg-gray-50 dark:bg-black/20 text-primary hover:text-green-500 transition-colors"
|
||||||
|
:href="link.url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ link.name }} <i class="ri-external-link-line ml-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 应用设置 -->
|
||||||
|
<div v-show="currentSection === 'application'" class="animate-fade-in">
|
||||||
|
<setting-section v-if="isElectron" :title="t('settings.sections.application')">
|
||||||
<!-- 关闭行为 -->
|
<!-- 关闭行为 -->
|
||||||
<setting-item
|
<setting-item
|
||||||
:title="t('settings.application.closeAction')"
|
:title="t('settings.application.closeAction')"
|
||||||
@@ -347,7 +372,7 @@
|
|||||||
</n-switch>
|
</n-switch>
|
||||||
{{ t('settings.application.downloadDesc') }}
|
{{ t('settings.application.downloadDesc') }}
|
||||||
</template>
|
</template>
|
||||||
<n-button size="small" @click="settingsStore.showDownloadDrawer = true">
|
<n-button size="small" @click="router.push('/downloads')">
|
||||||
{{ t('settings.application.download') }}
|
{{ t('settings.application.download') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
@@ -390,14 +415,11 @@
|
|||||||
}}</n-button>
|
}}</n-button>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 网络设置 -->
|
<!-- 网络设置 -->
|
||||||
<setting-section
|
<div v-show="currentSection === 'network'" class="animate-fade-in">
|
||||||
v-if="isElectron"
|
<setting-section v-if="isElectron" :title="t('settings.sections.network')">
|
||||||
id="network"
|
|
||||||
:title="t('settings.sections.network')"
|
|
||||||
@ref="(el) => (sectionRefs.network = el as HTMLElement | null)"
|
|
||||||
>
|
|
||||||
<!-- API端口 -->
|
<!-- API端口 -->
|
||||||
<setting-item
|
<setting-item
|
||||||
:title="t('settings.network.apiPort')"
|
:title="t('settings.network.apiPort')"
|
||||||
@@ -446,14 +468,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 系统管理 -->
|
<!-- 系统管理 -->
|
||||||
<setting-section
|
<div v-show="currentSection === 'system'" class="animate-fade-in">
|
||||||
v-if="isElectron"
|
<setting-section v-if="isElectron" :title="t('settings.sections.system')">
|
||||||
id="system"
|
|
||||||
:title="t('settings.sections.system')"
|
|
||||||
@ref="(el) => (sectionRefs.system = el as HTMLElement | null)"
|
|
||||||
>
|
|
||||||
<!-- 清除缓存 -->
|
<!-- 清除缓存 -->
|
||||||
<setting-item
|
<setting-item
|
||||||
:title="t('settings.system.cache')"
|
:title="t('settings.system.cache')"
|
||||||
@@ -469,16 +488,16 @@
|
|||||||
:title="t('settings.system.restart')"
|
:title="t('settings.system.restart')"
|
||||||
:description="t('settings.system.restartDesc')"
|
:description="t('settings.system.restartDesc')"
|
||||||
>
|
>
|
||||||
<n-button size="small" @click="restartApp">{{ t('settings.system.restart') }}</n-button>
|
<n-button size="small" @click="restartApp">{{
|
||||||
|
t('settings.system.restart')
|
||||||
|
}}</n-button>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<setting-section
|
<div v-show="currentSection === 'about'" class="animate-fade-in">
|
||||||
id="about"
|
<setting-section :title="t('settings.sections.about')">
|
||||||
:title="t('settings.sections.about')"
|
|
||||||
@ref="(el) => (sectionRefs.about = el as HTMLElement | null)"
|
|
||||||
>
|
|
||||||
<!-- 版本信息 -->
|
<!-- 版本信息 -->
|
||||||
<setting-item :title="t('settings.about.version')">
|
<setting-item :title="t('settings.about.version')">
|
||||||
<template #description>
|
<template #description>
|
||||||
@@ -511,28 +530,18 @@
|
|||||||
</n-button>
|
</n-button>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 捐赠支持 -->
|
<!-- 捐赠支持 -->
|
||||||
<setting-section
|
<div v-show="currentSection === 'donation'" class="animate-fade-in">
|
||||||
id="donation"
|
<setting-section :title="t('settings.sections.donation')">
|
||||||
:title="t('settings.sections.donation')"
|
<donation-list />
|
||||||
@ref="(el) => (sectionRefs.donation = el as HTMLElement | null)"
|
|
||||||
>
|
|
||||||
<setting-item
|
|
||||||
:title="t('settings.sections.donation')"
|
|
||||||
:description="t('donation.message')"
|
|
||||||
>
|
|
||||||
<n-button text @click="toggleDonationList">
|
|
||||||
<template #icon>
|
|
||||||
<i :class="isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'" />
|
|
||||||
</template>
|
|
||||||
{{ isDonationListVisible ? t('common.hide') : t('common.show') }}
|
|
||||||
</n-button>
|
|
||||||
</setting-item>
|
|
||||||
<donation-list v-if="isDonationListVisible" />
|
|
||||||
</setting-section>
|
</setting-section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="h-20"></div>
|
||||||
<play-bottom />
|
<play-bottom />
|
||||||
|
</div>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
|
|
||||||
<!-- 弹窗组件 -->
|
<!-- 弹窗组件 -->
|
||||||
@@ -559,14 +568,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import localData from '@/../main/set.json';
|
import localData from '@/../main/set.json';
|
||||||
import { getUserDetail } from '@/api/login';
|
import { getUserDetail } from '@/api/login';
|
||||||
import DonationList from '@/components/common/DonationList.vue';
|
import DonationList from '@/components/common/DonationList.vue';
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
|
||||||
|
import AudioDeviceSettings from '@/components/settings/AudioDeviceSettings.vue';
|
||||||
import ClearCacheSettings from '@/components/settings/ClearCacheSettings.vue';
|
import ClearCacheSettings from '@/components/settings/ClearCacheSettings.vue';
|
||||||
import CookieSettingsModal from '@/components/settings/CookieSettingsModal.vue';
|
import CookieSettingsModal from '@/components/settings/CookieSettingsModal.vue';
|
||||||
import MusicSourceSettings from '@/components/settings/MusicSourceSettings.vue';
|
import MusicSourceSettings from '@/components/settings/MusicSourceSettings.vue';
|
||||||
@@ -582,11 +593,10 @@ import { checkUpdate, UpdateResult } from '@/utils/update';
|
|||||||
|
|
||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
import SettingItem from './SettingItem.vue';
|
import SettingItem from './SettingItem.vue';
|
||||||
import SettingNav from './SettingNav.vue';
|
|
||||||
import SettingSection from './SettingSection.vue';
|
import SettingSection from './SettingSection.vue';
|
||||||
|
|
||||||
// ==================== 常量配置 ====================
|
// ==================== 常量配置 ====================
|
||||||
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'];
|
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd'];
|
||||||
|
|
||||||
const memberLinks = [
|
const memberLinks = [
|
||||||
{ name: '网易云音乐会员', url: 'https://music.163.com/store/vip' },
|
{ name: '网易云音乐会员', url: 'https://music.163.com/store/vip' },
|
||||||
@@ -607,6 +617,7 @@ const settingsStore = useSettingsStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// ==================== 设置数据管理 ====================
|
// ==================== 设置数据管理 ====================
|
||||||
const saveSettings = useDebounceFn((data) => {
|
const saveSettings = useDebounceFn((data) => {
|
||||||
@@ -830,14 +841,6 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== 捐赠列表 ====================
|
|
||||||
const isDonationListVisible = ref(localStorage.getItem('donationListVisible') !== 'false');
|
|
||||||
|
|
||||||
const toggleDonationList = () => {
|
|
||||||
isDonationListVisible.value = !isDonationListVisible.value;
|
|
||||||
localStorage.setItem('donationListVisible', isDonationListVisible.value.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 弹窗控制 ====================
|
// ==================== 弹窗控制 ====================
|
||||||
const showClearCacheModal = ref(false);
|
const showClearCacheModal = ref(false);
|
||||||
const showShortcutModal = ref(false);
|
const showShortcutModal = ref(false);
|
||||||
@@ -984,45 +987,6 @@ const navSections = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentSection = ref('basic');
|
const currentSection = ref('basic');
|
||||||
const scrollbarRef = ref();
|
|
||||||
const sectionRefs = reactive<Record<string, HTMLElement | null>>({
|
|
||||||
basic: null,
|
|
||||||
playback: null,
|
|
||||||
application: null,
|
|
||||||
network: null,
|
|
||||||
system: null,
|
|
||||||
about: null,
|
|
||||||
donation: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const scrollToSection = async (sectionId: string) => {
|
|
||||||
currentSection.value = sectionId;
|
|
||||||
const sectionEl = sectionRefs[sectionId];
|
|
||||||
if (sectionEl) {
|
|
||||||
await nextTick();
|
|
||||||
scrollbarRef.value?.scrollTo({ top: sectionEl.offsetTop - 20, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCROLL_OFFSET_THRESHOLD = 100;
|
|
||||||
|
|
||||||
const handleScroll = (e: any) => {
|
|
||||||
const { scrollTop } = e.target;
|
|
||||||
let lastValidSection = 'basic';
|
|
||||||
|
|
||||||
for (const section of settingSections) {
|
|
||||||
if (!section.electron || isElectron) {
|
|
||||||
const el = sectionRefs[section.id];
|
|
||||||
if (el && scrollTop >= el.offsetTop - SCROLL_OFFSET_THRESHOLD) {
|
|
||||||
lastValidSection = section.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastValidSection !== currentSection.value) {
|
|
||||||
currentSection.value = lastValidSection;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 初始化 ====================
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -1045,9 +1009,6 @@ onMounted(async () => {
|
|||||||
gpuAccelerationChanged.value = false;
|
gpuAccelerationChanged.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
handleScroll({ target: { scrollTop: 0 } });
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1059,4 +1020,30 @@ onMounted(async () => {
|
|||||||
:deep(.n-input-number) {
|
:deep(.n-input-number) {
|
||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
@apply py-1.5 px-4 mr-3 inline-block rounded-full cursor-pointer transition-all duration-300;
|
||||||
|
@apply text-sm font-medium;
|
||||||
|
@apply bg-gray-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400;
|
||||||
|
@apply hover:bg-gray-200 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-white;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply bg-primary text-white shadow-lg shadow-primary/25 scale-105;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user