mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 06:30:49 +08:00
- 将设置页面拆分为7个独立Tab组件(Basic/Playback/Application/Network/System/About/Donation) - 抽取自定义SBtn/SSelect/SInput组件替代naive-ui原生组件 - 使用provide/inject共享setData/message/dialog - 捐赠列表:去除dicebear外部头像改用首字母头像,去除n-popover改用title属性 - 捐赠列表:IntersectionObserver自动分页加载,首字母跳过*号等符号字符 - SInput:有suffix时增大右侧padding防止数值遮挡单位
354 lines
12 KiB
Vue
354 lines
12 KiB
Vue
<template>
|
|
<setting-section :title="t('settings.sections.basic')">
|
|
<setting-item
|
|
:title="t('settings.basic.themeMode')"
|
|
:description="t('settings.basic.themeModeDesc')"
|
|
>
|
|
<template #action>
|
|
<div class="flex items-center gap-3 max-md:flex-wrap">
|
|
<div class="flex items-center gap-2">
|
|
<n-switch v-model:value="setData.autoTheme" @update:value="handleAutoThemeChange">
|
|
<template #checked><i class="ri-smartphone-line"></i></template>
|
|
<template #unchecked><i class="ri-settings-line"></i></template>
|
|
</n-switch>
|
|
<span class="text-sm text-gray-500 max-md:hidden">
|
|
{{
|
|
setData.autoTheme ? t('settings.basic.autoTheme') : t('settings.basic.manualTheme')
|
|
}}
|
|
</span>
|
|
</div>
|
|
<n-switch
|
|
v-model:value="isDarkTheme"
|
|
:disabled="setData.autoTheme"
|
|
:class="{ 'opacity-50': setData.autoTheme }"
|
|
>
|
|
<template #checked><i class="ri-moon-line"></i></template>
|
|
<template #unchecked><i class="ri-sun-line"></i></template>
|
|
</n-switch>
|
|
</div>
|
|
</template>
|
|
</setting-item>
|
|
|
|
<setting-item
|
|
:title="t('settings.basic.language')"
|
|
:description="t('settings.basic.languageDesc')"
|
|
>
|
|
<language-switcher />
|
|
</setting-item>
|
|
|
|
<setting-item
|
|
v-if="!isElectron"
|
|
:title="t('settings.basic.tabletMode')"
|
|
:description="t('settings.basic.tabletModeDesc')"
|
|
>
|
|
<n-switch v-model:value="setData.tabletMode">
|
|
<template #checked><i class="ri-tablet-line"></i></template>
|
|
<template #unchecked><i class="ri-smartphone-line"></i></template>
|
|
</n-switch>
|
|
</setting-item>
|
|
|
|
<setting-item
|
|
:title="t('settings.translationEngine')"
|
|
:description="t('settings.translationEngine')"
|
|
>
|
|
<s-select
|
|
v-model="setData.lyricTranslationEngine"
|
|
:options="translationEngineOptions"
|
|
width="w-40 max-md:w-full"
|
|
/>
|
|
</setting-item>
|
|
|
|
<setting-item
|
|
v-if="isElectron"
|
|
:title="t('settings.basic.font')"
|
|
:description="t('settings.basic.fontDesc')"
|
|
>
|
|
<template #action>
|
|
<div class="flex gap-2 max-md:flex-col max-md:w-full">
|
|
<n-radio-group v-model:value="setData.fontScope" class="mt-2">
|
|
<n-radio key="global" value="global">{{
|
|
t('settings.basic.fontScope.global')
|
|
}}</n-radio>
|
|
<n-radio key="lyric" value="lyric">{{ t('settings.basic.fontScope.lyric') }}</n-radio>
|
|
</n-radio-group>
|
|
<n-select
|
|
v-model:value="selectedFonts"
|
|
:options="systemFonts"
|
|
filterable
|
|
multiple
|
|
placeholder="选择字体"
|
|
class="w-[300px] max-md:w-full"
|
|
:render-label="renderFontLabel"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</setting-item>
|
|
|
|
<div
|
|
v-if="isElectron && selectedFonts.length > 0"
|
|
class="p-4 border-b border-gray-100 dark:border-gray-800"
|
|
>
|
|
<div class="text-base font-bold mb-4 text-gray-900 dark:text-white">
|
|
{{ t('settings.basic.fontPreview.title') }}
|
|
</div>
|
|
<div class="space-y-4" :style="{ fontFamily: setData.fontFamily }">
|
|
<div v-for="preview in fontPreviews" :key="preview.key" class="flex flex-col gap-2">
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
|
{{ t(`settings.basic.fontPreview.${preview.key}`) }}
|
|
</div>
|
|
<div
|
|
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`) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<setting-item :title="t('settings.basic.tokenManagement')">
|
|
<template #description>
|
|
<div class="text-sm text-gray-500 mb-2">
|
|
{{ t('settings.basic.tokenStatus') }}:
|
|
{{ currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet') }}
|
|
</div>
|
|
<div v-if="currentToken" class="text-xs text-gray-400 mb-2 font-mono break-all">
|
|
{{ currentToken.substring(0, 50) }}...
|
|
</div>
|
|
</template>
|
|
<template #action>
|
|
<div class="flex gap-2">
|
|
<s-btn @click="showTokenModal = true">
|
|
{{ currentToken ? t('settings.basic.modifyToken') : t('settings.basic.setToken') }}
|
|
</s-btn>
|
|
<s-btn v-if="currentToken" variant="danger" @click="clearToken">
|
|
{{ t('settings.basic.clearToken') }}
|
|
</s-btn>
|
|
</div>
|
|
</template>
|
|
</setting-item>
|
|
|
|
<setting-item :title="t('settings.basic.animation')">
|
|
<template #description>
|
|
<div class="flex items-center gap-2">
|
|
<n-switch v-model:value="setData.noAnimate">
|
|
<template #checked>{{ t('common.off') }}</template>
|
|
<template #unchecked>{{ t('common.on') }}</template>
|
|
</n-switch>
|
|
<span>{{ t('settings.basic.animationDesc') }}</span>
|
|
</div>
|
|
</template>
|
|
<template #action>
|
|
<div class="flex items-center gap-2">
|
|
<span v-if="!isMobile" class="text-sm text-gray-400">{{ setData.animationSpeed }}x</span>
|
|
<div class="w-40 max-md:w-auto flex justify-end">
|
|
<n-slider
|
|
v-if="!isMobile"
|
|
v-model:value="setData.animationSpeed"
|
|
:min="0.1"
|
|
:max="3"
|
|
:step="0.1"
|
|
:marks="animationSpeedMarks"
|
|
:disabled="setData.noAnimate"
|
|
/>
|
|
<s-input
|
|
v-else
|
|
v-model="setData.animationSpeed"
|
|
type="number"
|
|
:min="0.1"
|
|
:max="3"
|
|
:step="0.1"
|
|
:disabled="setData.noAnimate"
|
|
width="w-[120px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</setting-item>
|
|
|
|
<setting-item v-if="isElectron" :title="t('settings.basic.gpuAcceleration')">
|
|
<template #description>
|
|
<div class="text-sm text-gray-500 mb-2">
|
|
{{ t('settings.basic.gpuAccelerationDesc') }}
|
|
</div>
|
|
<div v-if="gpuAccelerationChanged" class="text-xs text-amber-500">
|
|
<i class="ri-information-line mr-1"></i>
|
|
{{ t('settings.basic.gpuAccelerationRestart') }}
|
|
</div>
|
|
</template>
|
|
<n-switch
|
|
v-model:value="setData.enableGpuAcceleration"
|
|
@update:value="handleGpuAccelerationChange"
|
|
>
|
|
<template #checked><i class="ri-cpu-line"></i></template>
|
|
<template #unchecked><i class="ri-cpu-line"></i></template>
|
|
</n-switch>
|
|
</setting-item>
|
|
|
|
<cookie-settings-modal
|
|
v-model:show="showTokenModal"
|
|
:initial-value="currentToken"
|
|
@save="handleTokenSave"
|
|
/>
|
|
</setting-section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, h, inject, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import { getUserDetail } from '@/api/login';
|
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
|
|
import CookieSettingsModal from '@/components/settings/CookieSettingsModal.vue';
|
|
import { useSettingsStore } from '@/store/modules/settings';
|
|
import { useUserStore } from '@/store/modules/user';
|
|
import { isElectron, isMobile } from '@/utils';
|
|
|
|
import { SETTINGS_DATA_KEY, SETTINGS_MESSAGE_KEY } from '../keys';
|
|
import SBtn from '../SBtn.vue';
|
|
import SettingItem from '../SettingItem.vue';
|
|
import SettingSection from '../SettingSection.vue';
|
|
import SInput from '../SInput.vue';
|
|
import SSelect from '../SSelect.vue';
|
|
|
|
const fontPreviews = [
|
|
{ key: 'chinese' },
|
|
{ key: 'english' },
|
|
{ key: 'japanese' },
|
|
{ key: 'korean' }
|
|
];
|
|
|
|
const { t } = useI18n();
|
|
const settingsStore = useSettingsStore();
|
|
const userStore = useUserStore();
|
|
const setData = inject(SETTINGS_DATA_KEY)!;
|
|
const message = inject(SETTINGS_MESSAGE_KEY)!;
|
|
|
|
const isDarkTheme = computed({
|
|
get: () => settingsStore.theme === 'dark',
|
|
set: () => settingsStore.toggleTheme()
|
|
});
|
|
|
|
const handleAutoThemeChange = (value: boolean) => {
|
|
settingsStore.setAutoTheme(value);
|
|
};
|
|
|
|
const gpuAccelerationChanged = ref(false);
|
|
|
|
const handleGpuAccelerationChange = (enabled: boolean) => {
|
|
try {
|
|
if (window.electron) {
|
|
window.electron.ipcRenderer.send('update-gpu-acceleration', enabled);
|
|
gpuAccelerationChanged.value = true;
|
|
message.info(t('settings.basic.gpuAccelerationChangeSuccess'));
|
|
}
|
|
} catch (error) {
|
|
console.error('GPU加速设置更新失败:', error);
|
|
message.error(t('settings.basic.gpuAccelerationChangeError'));
|
|
}
|
|
};
|
|
|
|
const translationEngineOptions = computed(() => [
|
|
{ label: t('settings.translationEngineOptions.none'), value: 'none' },
|
|
{ label: t('settings.translationEngineOptions.opencc'), value: 'opencc' }
|
|
]);
|
|
|
|
const animationSpeedMarks = computed(() => ({
|
|
0.1: t('settings.basic.animationSpeed.slow'),
|
|
1: t('settings.basic.animationSpeed.normal'),
|
|
3: t('settings.basic.animationSpeed.fast')
|
|
}));
|
|
|
|
const systemFonts = computed(() => settingsStore.systemFonts);
|
|
const selectedFonts = ref<string[]>([]);
|
|
|
|
const renderFontLabel = (option: { label: string; value: string }) => {
|
|
return h('span', { style: { fontFamily: option.value } }, option.label);
|
|
};
|
|
|
|
watch(
|
|
selectedFonts,
|
|
(newFonts) => {
|
|
setData.value = {
|
|
...setData.value,
|
|
fontFamily: newFonts.length === 0 ? 'system-ui' : newFonts.join(',')
|
|
};
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
watch(
|
|
() => setData.value.fontFamily,
|
|
(newFont) => {
|
|
if (newFont) {
|
|
selectedFonts.value = newFont === 'system-ui' ? [] : newFont.split(',');
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const showTokenModal = ref(false);
|
|
const currentToken = ref(localStorage.getItem('token') || '');
|
|
|
|
const handleTokenSave = async (token: string) => {
|
|
try {
|
|
const originalToken = localStorage.getItem('token');
|
|
localStorage.setItem('token', token);
|
|
|
|
const user = await getUserDetail();
|
|
if (user.data && user.data.profile) {
|
|
userStore.setUser(user.data.profile);
|
|
currentToken.value = token;
|
|
message.success(t('settings.cookie.message.saveSuccess'));
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
if (originalToken) localStorage.setItem('token', originalToken);
|
|
else localStorage.removeItem('token');
|
|
message.error(t('settings.cookie.message.saveError'));
|
|
}
|
|
} catch {
|
|
const originalToken = localStorage.getItem('token');
|
|
if (originalToken) localStorage.setItem('token', originalToken);
|
|
else localStorage.removeItem('token');
|
|
message.error(t('settings.cookie.message.saveError'));
|
|
}
|
|
};
|
|
|
|
const clearToken = () => {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
currentToken.value = '';
|
|
userStore.user = null;
|
|
message.success(t('settings.basic.clearToken') + '成功');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
};
|
|
|
|
watch(
|
|
() => localStorage.getItem('token'),
|
|
(newToken) => {
|
|
currentToken.value = newToken || '';
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
onMounted(() => {
|
|
if (window.electron) {
|
|
window.electron.ipcRenderer.on('gpu-acceleration-updated', (_, enabled: boolean) => {
|
|
console.log('GPU加速设置已更新:', enabled);
|
|
gpuAccelerationChanged.value = true;
|
|
});
|
|
|
|
window.electron.ipcRenderer.on('gpu-acceleration-update-error', (_, errorMessage: string) => {
|
|
console.error('GPU加速设置更新错误:', errorMessage);
|
|
gpuAccelerationChanged.value = false;
|
|
});
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (window.electron) {
|
|
window.electron.ipcRenderer.removeAllListeners?.('gpu-acceleration-updated');
|
|
window.electron.ipcRenderer.removeAllListeners?.('gpu-acceleration-update-error');
|
|
}
|
|
});
|
|
</script>
|