mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
✨ feat: 国际化 (i18n) 功能实现
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
<div v-if="isComponent ? favoriteSongs.length : true" class="favorite-page">
|
||||
<div class="favorite-header" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
<div class="favorite-header-left">
|
||||
<h2>我的收藏</h2>
|
||||
<div class="favorite-count">共 {{ favoriteList.length }} 首</div>
|
||||
<h2>{{ t('favorite.title') }}</h2>
|
||||
<div class="favorite-count">{{ t('favorite.count', { count: favoriteList.length }) }}</div>
|
||||
</div>
|
||||
<div v-if="!isComponent && isElectron" class="favorite-header-right">
|
||||
<n-button
|
||||
@@ -17,7 +17,7 @@
|
||||
<template #icon>
|
||||
<i class="iconfont ri-checkbox-multiple-line"></i>
|
||||
</template>
|
||||
批量下载
|
||||
{{ t('favorite.batchDownload') }}
|
||||
</n-button>
|
||||
<div v-else class="select-controls">
|
||||
<n-checkbox
|
||||
@@ -26,7 +26,7 @@
|
||||
:indeterminate="isIndeterminate"
|
||||
@update:checked="handleSelectAll"
|
||||
>
|
||||
全选
|
||||
{{ t('common.selectAll') }}
|
||||
</n-checkbox>
|
||||
<n-button-group class="operation-btns">
|
||||
<n-button
|
||||
@@ -40,9 +40,11 @@
|
||||
<template #icon>
|
||||
<i class="iconfont ri-download-line"></i>
|
||||
</template>
|
||||
下载 ({{ selectedSongs.length }})
|
||||
{{ t('favorite.download', { count: selectedSongs.length }) }}
|
||||
</n-button>
|
||||
<n-button size="small" class="cancel-btn" @click="cancelSelect">
|
||||
{{ t('common.cancel') }}
|
||||
</n-button>
|
||||
<n-button size="small" class="cancel-btn" @click="cancelSelect"> 取消 </n-button>
|
||||
</n-button-group>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +52,7 @@
|
||||
<div class="favorite-main" :class="setAnimationClass('animate__bounceInRight')">
|
||||
<n-scrollbar ref="scrollbarRef" class="favorite-content" @scroll="handleScroll">
|
||||
<div v-if="favoriteList.length === 0" class="empty-tip">
|
||||
<n-empty description="还没有收藏歌曲" />
|
||||
<n-empty :description="t('favorite.emptyTip')" />
|
||||
</div>
|
||||
<div v-else class="favorite-list">
|
||||
<song-item
|
||||
@@ -66,14 +68,14 @@
|
||||
@select="handleSelect"
|
||||
/>
|
||||
<div v-if="isComponent" class="favorite-list-more text-center">
|
||||
<n-button text type="primary" @click="handleMore">查看更多</n-button>
|
||||
<n-button text type="primary" @click="handleMore">{{ t('common.viewMore') }}</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-if="noMore" class="no-more-tip">没有更多了</div>
|
||||
<div v-if="noMore" class="no-more-tip">{{ t('common.noMore') }}</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
@@ -84,6 +86,7 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
@@ -93,6 +96,7 @@ import { getSongUrl } from '@/hooks/MusicListHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { isElectron, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const message = useMessage();
|
||||
const favoriteList = computed(() => store.state.favoriteList);
|
||||
@@ -130,18 +134,18 @@ const handleSelect = (songId: number, selected: boolean) => {
|
||||
// 批量下载
|
||||
const handleBatchDownload = async () => {
|
||||
if (isDownloading.value) {
|
||||
message.warning('正在下载中,请稍候...');
|
||||
message.warning(t('favorite.downloading'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSongs.value.length === 0) {
|
||||
message.warning('请先选择要下载的歌曲');
|
||||
message.warning(t('favorite.selectSongsFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
message.success('开始下载...');
|
||||
message.success(t('favorite.downloading'));
|
||||
|
||||
// 移除旧的监听器
|
||||
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
||||
@@ -160,7 +164,7 @@ const handleBatchDownload = async () => {
|
||||
// 当所有下载完成时
|
||||
if (successCount + failCount === selectedSongs.value.length) {
|
||||
isDownloading.value = false;
|
||||
message.success(`下载完成`);
|
||||
message.success(t('favorite.downloadSuccess'));
|
||||
cancelSelect();
|
||||
}
|
||||
});
|
||||
@@ -201,7 +205,7 @@ const handleBatchDownload = async () => {
|
||||
console.error('下载失败:', error);
|
||||
isDownloading.value = false;
|
||||
message.destroyAll();
|
||||
message.error('下载失败');
|
||||
message.error(t('favorite.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="history-page">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">播放历史</div>
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">
|
||||
{{ t('history.title') }}
|
||||
</div>
|
||||
<n-scrollbar ref="scrollbarRef" :size="100" @scroll="handleScroll">
|
||||
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div
|
||||
@@ -12,7 +14,7 @@
|
||||
>
|
||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||
<div class="history-item-count min-w-[60px]">
|
||||
{{ item.count }}
|
||||
{{ t('history.playCount', { count: item.count }) }}
|
||||
</div>
|
||||
<div class="history-item-delete">
|
||||
<i class="iconfont icon-close" @click="handleDelMusic(item)"></i>
|
||||
@@ -23,7 +25,7 @@
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-if="noMore" class="no-more-tip">没有更多了</div>
|
||||
<div v-if="noMore" class="no-more-tip">{{ t('common.noMore') }}</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
@@ -31,6 +33,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
@@ -43,6 +46,7 @@ defineOptions({
|
||||
name: 'History'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { delMusic, musicList } = useMusicHistory();
|
||||
const scrollbarRef = ref();
|
||||
@@ -89,7 +93,7 @@ const getHistorySongs = async () => {
|
||||
noMore.value = displayList.value.length >= musicList.value.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史记录失败:', error);
|
||||
console.error(t('history.getHistoryFailed'), error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
@@ -11,6 +12,7 @@ defineOptions({
|
||||
name: 'Login'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
@@ -36,15 +38,12 @@ const loadLogin = async () => {
|
||||
qrUrl.value = data.data.qrimg;
|
||||
|
||||
const timer = timerIsQr(key);
|
||||
// 添加对定时器的引用,以便在出现错误时可以清除
|
||||
timerRef.value = timer as any;
|
||||
} catch (error) {
|
||||
console.error('加载登录信息时出错:', error);
|
||||
console.error(t('login.message.loadError'), error);
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 ref 来保存定时器,便于在任何地方清除它
|
||||
|
||||
const timerIsQr = (key: string) => {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
@@ -59,15 +58,14 @@ const timerIsQr = (key: string) => {
|
||||
const user = await getUserDetail();
|
||||
store.state.user = user.data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(user.data.profile));
|
||||
message.success('登录成功');
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
router.push('/user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查二维码状态时出错:', error);
|
||||
// 在出现错误时清除定时器
|
||||
console.error(t('login.message.qrCheckError'), error);
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
}
|
||||
@@ -96,7 +94,7 @@ const password = ref('');
|
||||
const loginPhone = async () => {
|
||||
const { data } = await loginByCellphone(phone.value, password.value);
|
||||
if (data.code === 200) {
|
||||
message.success('登录成功');
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
store.state.user = data.profile;
|
||||
localStorage.setItem('token', data.cookie);
|
||||
setTimeout(() => {
|
||||
@@ -112,22 +110,34 @@ const loginPhone = async () => {
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div class="login-title">扫码登陆</div>
|
||||
<div class="login-title">{{ t('login.title.qr') }}</div>
|
||||
<img class="qr-img" :src="qrUrl" />
|
||||
<div class="text">使用网易云APP扫码登录</div>
|
||||
<div class="text">{{ t('login.qrTip') }}</div>
|
||||
</div>
|
||||
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div class="login-title">手机号登录</div>
|
||||
<div class="login-title">{{ t('login.title.phone') }}</div>
|
||||
<div class="phone-page">
|
||||
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
|
||||
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="phone-input"
|
||||
type="text"
|
||||
:placeholder="t('login.placeholder.phone')"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
class="phone-input"
|
||||
type="password"
|
||||
:placeholder="t('login.placeholder.password')"
|
||||
/>
|
||||
</div>
|
||||
<div class="text">使用网易云账号登录</div>
|
||||
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
|
||||
<div class="text">{{ t('login.phoneTip') }}</div>
|
||||
<n-button class="btn-login" @click="loginPhone()">{{ t('login.button.login') }}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
|
||||
<div class="title" @click="chooseQr()">
|
||||
{{ isQr ? t('login.button.switchToPhone') : t('login.button.switchToQr') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:class="setAnimationClass('animate__fadeInDown')"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<div class="title">热搜列表</div>
|
||||
<div class="title">{{ t('search.title.hotSearch') }}</div>
|
||||
<div class="hot-search-list">
|
||||
<template v-for="(item, index) in hotSearchData?.data" :key="index">
|
||||
<div
|
||||
@@ -64,20 +64,20 @@
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoadingMore" class="loading-more">
|
||||
<n-spin size="small" />
|
||||
<span class="ml-2">加载中...</span>
|
||||
<span class="ml-2">{{ t('search.loading.more') }}</span>
|
||||
</div>
|
||||
<div v-if="!hasMore && searchDetail" class="no-more">没有更多了</div>
|
||||
<div v-if="!hasMore && searchDetail" class="no-more">{{ t('search.noMore') }}</div>
|
||||
</template>
|
||||
<!-- 搜索历史 -->
|
||||
<template v-else>
|
||||
<div class="search-history">
|
||||
<div class="search-history-header title">
|
||||
<span>搜索历史</span>
|
||||
<span>{{ t('search.title.searchHistory') }}</span>
|
||||
<n-button text type="error" @click="clearSearchHistory">
|
||||
<template #icon>
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</template>
|
||||
清空
|
||||
{{ t('search.button.clear') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="search-history-list">
|
||||
@@ -105,6 +105,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDateFormat } from '@vueuse/core';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
@@ -119,6 +120,7 @@ defineOptions({
|
||||
name: 'Search'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
@@ -184,7 +186,7 @@ onMounted(() => {
|
||||
loadSearch(route.query.keyword);
|
||||
});
|
||||
|
||||
const hotKeyword = ref(route.query.keyword || '搜索列表');
|
||||
const hotKeyword = ref(route.query.keyword || t('search.title.searchList'));
|
||||
|
||||
watch(
|
||||
() => store.state.searchValue,
|
||||
@@ -286,7 +288,7 @@ const loadSearch = async (keywords: any, type: any = null, isLoadMore = false) =
|
||||
|
||||
page.value++;
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
console.error(t('search.error.searchFailed'), error);
|
||||
} finally {
|
||||
searchDetailLoading.value = false;
|
||||
isLoadingMore.value = false;
|
||||
|
||||
+194
-141
@@ -9,7 +9,7 @@
|
||||
:class="{ active: currentSection === section.id }"
|
||||
@click="scrollToSection(section.id)"
|
||||
>
|
||||
{{ section.title }}
|
||||
{{ t(`settings.sections.${section.id}`) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
<div class="set-page">
|
||||
<!-- 基础设置 -->
|
||||
<div id="basic" ref="basicRef" class="settings-section">
|
||||
<div class="settings-section-title">基础设置</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.basic') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">主题模式</div>
|
||||
<div class="set-item-content">切换日间/夜间主题</div>
|
||||
<div class="set-item-title">{{ t('settings.basic.themeMode') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.basic.themeModeDesc') }}</div>
|
||||
</div>
|
||||
<n-switch v-model:value="isDarkTheme">
|
||||
<template #checked><i class="ri-moon-line"></i></template>
|
||||
@@ -31,15 +31,28 @@
|
||||
</n-switch>
|
||||
</div>
|
||||
|
||||
<!-- 语言设置 -->
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">字体设置</div>
|
||||
<div class="set-item-content">选择字体,优先使用排在前面的字体</div>
|
||||
<div class="set-item-title">{{ t('settings.basic.language') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.basic.languageDesc') }}</div>
|
||||
</div>
|
||||
<language-switcher />
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">{{ t('settings.basic.font') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.basic.fontDesc') }}</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<n-radio-group v-model:value="setData.fontScope" class="mt-2">
|
||||
<n-radio key="global" value="global">全局</n-radio>
|
||||
<n-radio key="lyric" value="lyric">仅歌词</n-radio>
|
||||
<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"
|
||||
@@ -55,52 +68,52 @@
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFonts.length > 0" class="font-preview-container">
|
||||
<div class="font-preview-title">字体预览</div>
|
||||
<div class="font-preview-title">{{ t('settings.basic.fontPreview.title') }}</div>
|
||||
<div class="font-preview" :style="{ fontFamily: setData.fontFamily }">
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">中文</div>
|
||||
<div class="preview-text">静夜思 床前明月光 疑是地上霜</div>
|
||||
<div class="preview-label">{{ t('settings.basic.fontPreview.chinese') }}</div>
|
||||
<div class="preview-text">{{ t('settings.basic.fontPreview.chineseText') }}</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">English</div>
|
||||
<div class="preview-text">The quick brown fox jumps over the lazy dog</div>
|
||||
<div class="preview-label">{{ t('settings.basic.fontPreview.english') }}</div>
|
||||
<div class="preview-text">{{ t('settings.basic.fontPreview.englishText') }}</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">日本語</div>
|
||||
<div class="preview-text">あいうえお かきくけこ さしすせそ</div>
|
||||
<div class="preview-label">{{ t('settings.basic.fontPreview.japanese') }}</div>
|
||||
<div class="preview-text">{{ t('settings.basic.fontPreview.japaneseText') }}</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">한국어</div>
|
||||
<div class="preview-text">가나다라마 바사아자차 카타파하</div>
|
||||
<div class="preview-label">{{ t('settings.basic.fontPreview.korean') }}</div>
|
||||
<div class="preview-text">{{ t('settings.basic.fontPreview.koreanText') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">动画速度</div>
|
||||
<div class="set-item-title">{{ t('settings.basic.animation') }}</div>
|
||||
<div class="set-item-content">
|
||||
<div class="flex items-center gap-2">
|
||||
<n-switch v-model:value="setData.noAnimate">
|
||||
<template #checked>关闭</template>
|
||||
<template #unchecked>开启</template>
|
||||
<template #checked>{{ t('common.off') }}</template>
|
||||
<template #unchecked>{{ t('common.on') }}</template>
|
||||
</n-switch>
|
||||
<span>是否开启动画</span>
|
||||
<span>{{ t('settings.basic.animationDesc') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">{{ setData.animationSpeed }}x</span>
|
||||
<div class="w-40">
|
||||
<div class="w-60">
|
||||
<n-slider
|
||||
v-model:value="setData.animationSpeed"
|
||||
:min="0.1"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
:marks="{
|
||||
0.1: '极慢',
|
||||
1: '正常',
|
||||
3: '极快'
|
||||
0.1: t('settings.basic.animationSpeed.slow'),
|
||||
1: t('settings.basic.animationSpeed.normal'),
|
||||
3: t('settings.basic.animationSpeed.fast')
|
||||
}"
|
||||
:disabled="setData.noAnimate"
|
||||
class="w-40"
|
||||
@@ -113,25 +126,25 @@
|
||||
|
||||
<!-- 播放设置 -->
|
||||
<div id="playback" ref="playbackRef" class="settings-section">
|
||||
<div class="settings-section-title">播放设置</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.playback') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">音质设置</div>
|
||||
<div class="set-item-content">选择音乐播放音质(VIP)</div>
|
||||
<div class="set-item-title">{{ t('settings.playback.quality') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.playback.qualityDesc') }}</div>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="setData.musicQuality"
|
||||
:options="[
|
||||
{ label: '标准', value: 'standard' },
|
||||
{ label: '较高', value: 'higher' },
|
||||
{ label: '极高', value: 'exhigh' },
|
||||
{ label: '无损', value: 'lossless' },
|
||||
{ label: 'Hi-Res', value: 'hires' },
|
||||
{ label: '高清环绕声', value: 'jyeffect' },
|
||||
{ label: '沉浸环绕声', value: 'sky' },
|
||||
{ label: '杜比全景声', value: 'dolby' },
|
||||
{ label: '超清母带', value: 'jymaster' }
|
||||
{ label: t('settings.playback.qualityOptions.standard'), value: 'standard' },
|
||||
{ label: t('settings.playback.qualityOptions.higher'), value: 'higher' },
|
||||
{ label: t('settings.playback.qualityOptions.exhigh'), value: 'exhigh' },
|
||||
{ label: t('settings.playback.qualityOptions.lossless'), value: 'lossless' },
|
||||
{ label: t('settings.playback.qualityOptions.hires'), value: 'hires' },
|
||||
{ label: t('settings.playback.qualityOptions.jyeffect'), value: 'jyeffect' },
|
||||
{ label: t('settings.playback.qualityOptions.sky'), value: 'sky' },
|
||||
{ label: t('settings.playback.qualityOptions.dolby'), value: 'dolby' },
|
||||
{ label: t('settings.playback.qualityOptions.jymaster'), value: 'jymaster' }
|
||||
]"
|
||||
style="width: 160px"
|
||||
/>
|
||||
@@ -139,12 +152,12 @@
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">自动播放</div>
|
||||
<div class="set-item-content">重新打开应用时是否自动继续播放</div>
|
||||
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.playback.autoPlayDesc') }}</div>
|
||||
</div>
|
||||
<n-switch v-model:value="setData.autoPlay">
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
<template #checked>{{ t('common.on') }}</template>
|
||||
<template #unchecked>{{ t('common.off') }}</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,21 +165,19 @@
|
||||
|
||||
<!-- 应用设置 -->
|
||||
<div v-if="isElectron" id="application" ref="applicationRef" class="settings-section">
|
||||
<div class="settings-section-title">应用设置</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.application') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">关闭行为</div>
|
||||
<div class="set-item-content">
|
||||
{{ closeActionLabels[setData.closeAction] || '每次询问' }}
|
||||
</div>
|
||||
<div class="set-item-title">{{ t('settings.application.closeAction') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.application.closeActionDesc') }}</div>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="setData.closeAction"
|
||||
:options="[
|
||||
{ label: '每次询问', value: 'ask' },
|
||||
{ label: '最小化到托盘', value: 'minimize' },
|
||||
{ label: '直接退出', value: 'close' }
|
||||
{ label: t('settings.application.closeOptions.ask'), value: 'ask' },
|
||||
{ label: t('settings.application.closeOptions.minimize'), value: 'minimize' },
|
||||
{ label: t('settings.application.closeOptions.close'), value: 'close' }
|
||||
]"
|
||||
style="width: 160px"
|
||||
/>
|
||||
@@ -174,40 +185,44 @@
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">快捷键设置</div>
|
||||
<div class="set-item-content">自定义全局快捷键</div>
|
||||
<div class="set-item-title">{{ t('settings.application.shortcut') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.application.shortcutDesc') }}</div>
|
||||
</div>
|
||||
<n-button size="small" @click="showShortcutModal = true">配置</n-button>
|
||||
<n-button size="small" @click="showShortcutModal = true">{{
|
||||
t('common.configure')
|
||||
}}</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="isElectron" class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">下载管理</div>
|
||||
<div class="set-item-title">{{ t('settings.application.download') }}</div>
|
||||
<div class="set-item-content">
|
||||
<n-switch v-model:value="setData.alwaysShowDownloadButton" class="mr-2">
|
||||
<template #checked>显示</template>
|
||||
<template #unchecked>隐藏</template>
|
||||
<template #checked>{{ t('common.show') }}</template>
|
||||
<template #unchecked>{{ t('common.hide') }}</template>
|
||||
</n-switch>
|
||||
是否始终显示下载列表按钮
|
||||
{{ t('settings.application.downloadDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-button size="small" @click="store.commit('setShowDownloadDrawer', true)">
|
||||
打开下载管理
|
||||
{{ t('settings.application.download') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">下载目录</div>
|
||||
<div class="set-item-title">{{ t('settings.application.downloadPath') }}</div>
|
||||
<div class="set-item-content">
|
||||
{{ setData.downloadPath || '默认下载目录' }}
|
||||
{{ setData.downloadPath || t('settings.application.downloadPathDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-button size="small" @click="openDownloadPath">打开目录</n-button>
|
||||
<n-button size="small" @click="selectDownloadPath">修改目录</n-button>
|
||||
<n-button size="small" @click="openDownloadPath">{{ t('common.open') }}</n-button>
|
||||
<n-button size="small" @click="selectDownloadPath">{{
|
||||
t('common.modify')
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,42 +230,41 @@
|
||||
|
||||
<!-- 网络设置 -->
|
||||
<div v-if="isElectron" id="network" ref="networkRef" class="settings-section">
|
||||
<div class="settings-section-title">网络设置</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.network') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">音乐API端口</div>
|
||||
<div class="set-item-content">修改后需要重启应用</div>
|
||||
<div class="set-item-title">{{ t('settings.network.apiPort') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.network.apiPortDesc') }}</div>
|
||||
</div>
|
||||
<n-input-number v-model:value="setData.musicApiPort" />
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">代理设置</div>
|
||||
<div class="set-item-content">无法访问音乐时可以开启代理</div>
|
||||
<div class="set-item-title">{{ t('settings.network.proxy') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.network.proxyDesc') }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-switch v-model:value="setData.proxyConfig.enable">
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
<template #checked>{{ t('common.on') }}</template>
|
||||
<template #unchecked>{{ t('common.off') }}</template>
|
||||
</n-switch>
|
||||
<n-button size="small" @click="showProxyModal = true">配置</n-button>
|
||||
<n-button size="small" @click="showProxyModal = true">{{
|
||||
t('common.configure')
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">realIP</div>
|
||||
<div class="set-item-content">
|
||||
由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决,如:116.25.146.177
|
||||
即可解决
|
||||
</div>
|
||||
<div class="set-item-title">{{ t('settings.network.realIP') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.network.realIPDesc') }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-switch v-model:value="setData.enableRealIP">
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
<template #checked>{{ t('common.on') }}</template>
|
||||
<template #unchecked>{{ t('common.off') }}</template>
|
||||
</n-switch>
|
||||
<n-input
|
||||
v-if="setData.enableRealIP"
|
||||
@@ -266,48 +280,52 @@
|
||||
|
||||
<!-- 系统管理 -->
|
||||
<div v-if="isElectron" id="system" ref="systemRef" class="settings-section">
|
||||
<div class="settings-section-title">系统管理</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.system') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">缓存管理</div>
|
||||
<div class="set-item-content">清除缓存</div>
|
||||
<div class="set-item-title">{{ t('settings.system.cache') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.system.cacheDesc') }}</div>
|
||||
</div>
|
||||
<n-button size="small" @click="showClearCacheModal = true"> 清除缓存 </n-button>
|
||||
<n-button size="small" @click="showClearCacheModal = true">
|
||||
{{ t('settings.system.cacheDesc') }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">重启</div>
|
||||
<div class="set-item-content">重启应用</div>
|
||||
<div class="set-item-title">{{ t('settings.system.restart') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.system.restartDesc') }}</div>
|
||||
</div>
|
||||
<n-button size="small" @click="restartApp">重启</n-button>
|
||||
<n-button size="small" @click="restartApp">{{
|
||||
t('settings.system.restart')
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关于 -->
|
||||
<div id="about" ref="aboutRef" class="settings-section">
|
||||
<div class="settings-section-title">关于</div>
|
||||
<div class="settings-section-title">{{ t('settings.regard') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">版本</div>
|
||||
<div class="set-item-title">{{ t('settings.about.version') }}</div>
|
||||
<div class="set-item-content">
|
||||
{{ updateInfo.currentVersion }}
|
||||
<template v-if="updateInfo.hasUpdate">
|
||||
<n-tag type="success" class="ml-2"
|
||||
>发现新版本 {{ updateInfo.latestVersion }}</n-tag
|
||||
>
|
||||
<n-tag type="success" class="ml-2">
|
||||
{{ t('settings.about.hasUpdate') }} {{ updateInfo.latestVersion }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-button size="small" :loading="checking" @click="checkForUpdates(true)">
|
||||
{{ checking ? '检查中...' : '检查更新' }}
|
||||
{{ checking ? t('settings.about.checking') : t('settings.about.checkUpdate') }}
|
||||
</n-button>
|
||||
<n-button v-if="updateInfo.hasUpdate" size="small" @click="openReleasePage">
|
||||
前往更新
|
||||
{{ t('settings.about.gotoUpdate') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -318,13 +336,13 @@
|
||||
>
|
||||
<coffee>
|
||||
<div>
|
||||
<div class="set-item-title">作者</div>
|
||||
<div class="set-item-content">algerkong 点个star🌟呗</div>
|
||||
<div class="set-item-title">{{ t('settings.about.author') }}</div>
|
||||
<div class="set-item-content">{{ t('settings.about.authorDesc') }}</div>
|
||||
</div>
|
||||
</coffee>
|
||||
<div>
|
||||
<n-button size="small" @click="openAuthor">
|
||||
<i class="ri-github-line"></i>前往github
|
||||
<i class="ri-github-line"></i>{{ t('settings.about.gotoGithub') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,18 +351,18 @@
|
||||
|
||||
<!-- 捐赠支持 -->
|
||||
<div id="donation" ref="donationRef" class="settings-section">
|
||||
<div class="settings-section-title">捐赠支持</div>
|
||||
<div class="settings-section-title">{{ t('settings.sections.donation') }}</div>
|
||||
<div class="settings-section-content">
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">捐赠支持</div>
|
||||
<div class="set-item-content">感谢您的支持,让我有动力能够持续改进</div>
|
||||
<div class="set-item-title">{{ t('settings.sections.donation') }}</div>
|
||||
<div class="set-item-content">{{ t('donation.message') }}</div>
|
||||
</div>
|
||||
<n-button text @click="toggleDonationList">
|
||||
<template #icon>
|
||||
<i :class="isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'" />
|
||||
</template>
|
||||
{{ isDonationListVisible ? '隐藏列表' : '显示列表' }}
|
||||
{{ isDonationListVisible ? t('common.hide') : t('common.show') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<donation-list v-if="isDonationListVisible" />
|
||||
@@ -361,9 +379,9 @@
|
||||
<n-modal
|
||||
v-model:show="showProxyModal"
|
||||
preset="dialog"
|
||||
title="代理设置"
|
||||
positive-text="确认"
|
||||
negative-text="取消"
|
||||
:title="t('settings.network.proxy')"
|
||||
:positive-text="t('common.confirm')"
|
||||
:negative-text="t('common.cancel')"
|
||||
:show-icon="false"
|
||||
@positive-click="handleProxyConfirm"
|
||||
@negative-click="showProxyModal = false"
|
||||
@@ -376,7 +394,7 @@
|
||||
label-width="80"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<n-form-item label="代理协议" path="protocol">
|
||||
<n-form-item :label="t('settings.network.proxy')" path="protocol">
|
||||
<n-select
|
||||
v-model:value="proxyForm.protocol"
|
||||
:options="[
|
||||
@@ -386,13 +404,16 @@
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="代理地址" path="host">
|
||||
<n-input v-model:value="proxyForm.host" placeholder="请输入代理地址" />
|
||||
<n-form-item :label="t('settings.network.proxyHost')" path="host">
|
||||
<n-input
|
||||
v-model:value="proxyForm.host"
|
||||
:placeholder="t('settings.network.proxyHostPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="代理端口" path="port">
|
||||
<n-form-item :label="t('settings.network.proxyPort')" path="port">
|
||||
<n-input-number
|
||||
v-model:value="proxyForm.port"
|
||||
placeholder="请输入代理端口"
|
||||
:placeholder="t('settings.network.proxyPortPlaceholder')"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
/>
|
||||
@@ -403,9 +424,9 @@
|
||||
<n-modal
|
||||
v-model:show="showClearCacheModal"
|
||||
preset="dialog"
|
||||
title="清除缓存"
|
||||
positive-text="确认"
|
||||
negative-text="取消"
|
||||
:title="t('settings.system.cache')"
|
||||
:positive-text="t('common.confirm')"
|
||||
:negative-text="t('common.cancel')"
|
||||
@positive-click="clearCache"
|
||||
@negative-click="
|
||||
() => {
|
||||
@@ -414,7 +435,7 @@
|
||||
"
|
||||
>
|
||||
<n-space vertical>
|
||||
<p>请选择要清除的缓存类型:</p>
|
||||
<p>{{ t('settings.system.cacheClearTitle') }}</p>
|
||||
<n-checkbox-group v-model:value="selectedCacheTypes">
|
||||
<n-space vertical>
|
||||
<n-checkbox
|
||||
@@ -425,8 +446,10 @@
|
||||
>
|
||||
<template #default>
|
||||
<div>
|
||||
<div>{{ option.label }}</div>
|
||||
<div class="text-gray-400 text-sm">{{ option.description }}</div>
|
||||
<div>{{ t(`settings.system.cacheTypes.${option.key}.label`) }}</div>
|
||||
<div class="text-gray-400 text-sm">
|
||||
{{ t(`settings.system.cacheTypes.${option.key}.description`) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-checkbox>
|
||||
@@ -441,12 +464,14 @@
|
||||
import type { FormRules } from 'naive-ui';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, h, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import localData from '@/../main/set.json';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
import DonationList from '@/components/common/DonationList.vue';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
|
||||
import ShortcutSettings from '@/components/settings/ShortcutSettings.vue';
|
||||
import { isElectron } from '@/utils';
|
||||
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
||||
@@ -463,11 +488,7 @@ const updateInfo = ref<UpdateResult>({
|
||||
releaseInfo: null
|
||||
});
|
||||
|
||||
const closeActionLabels = {
|
||||
ask: '每次询问',
|
||||
minimize: '最小化到托盘',
|
||||
close: '直接退出'
|
||||
} as const;
|
||||
const { t } = useI18n();
|
||||
|
||||
const setData = computed(() => {
|
||||
const data = store.state.setData;
|
||||
@@ -515,15 +536,15 @@ const checkForUpdates = async (isClick = false) => {
|
||||
if (result) {
|
||||
updateInfo.value = result;
|
||||
if (!result.hasUpdate && isClick) {
|
||||
message.success('当前已是最新版本');
|
||||
message.success(t('settings.about.latest'));
|
||||
}
|
||||
} else if (isClick) {
|
||||
message.success('当前已是最新版本');
|
||||
message.success(t('settings.about.latest'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
if (isClick) {
|
||||
message.error('检查更新失败,请稍后重试');
|
||||
message.error(t('settings.about.messages.checkError'));
|
||||
}
|
||||
} finally {
|
||||
checking.value = false;
|
||||
@@ -559,12 +580,12 @@ const proxyForm = ref({
|
||||
const proxyRules: FormRules = {
|
||||
protocol: {
|
||||
required: true,
|
||||
message: '请选择代理协议',
|
||||
message: t('settings.validation.selectProxyProtocol'),
|
||||
trigger: ['blur', 'change']
|
||||
},
|
||||
host: {
|
||||
required: true,
|
||||
message: '请输入代理地址',
|
||||
message: t('settings.validation.proxyHost'),
|
||||
trigger: ['blur', 'change'],
|
||||
validator: (_rule, value) => {
|
||||
if (!value) return false;
|
||||
@@ -576,7 +597,7 @@ const proxyRules: FormRules = {
|
||||
},
|
||||
port: {
|
||||
required: true,
|
||||
message: '请输入有效的端口号(1-65535)',
|
||||
message: t('settings.validation.portNumber'),
|
||||
trigger: ['blur', 'change'],
|
||||
validator: (_rule, value) => {
|
||||
return value >= 1 && value <= 65535;
|
||||
@@ -675,9 +696,9 @@ const handleProxyConfirm = async () => {
|
||||
}
|
||||
});
|
||||
showProxyModal.value = false;
|
||||
message.success('代理设置已保存,重启应用后生效');
|
||||
message.success(t('settings.network.messages.proxySuccess'));
|
||||
} catch (err) {
|
||||
message.error('请检查输入是否正确');
|
||||
message.error(t('settings.network.messages.proxyError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -690,10 +711,10 @@ const validateAndSaveRealIP = () => {
|
||||
enableRealIP: true
|
||||
});
|
||||
if (setData.value.realIP) {
|
||||
message.success('真实IP设置已保存');
|
||||
message.success(t('settings.network.messages.realIPSuccess'));
|
||||
}
|
||||
} else {
|
||||
message.error('请输入有效的IP地址');
|
||||
message.error(t('settings.network.messages.realIPError'));
|
||||
store.commit('setSetData', {
|
||||
...setData.value,
|
||||
realIP: ''
|
||||
@@ -725,13 +746,41 @@ const toggleDonationList = () => {
|
||||
// 清除缓存相关
|
||||
const showClearCacheModal = ref(false);
|
||||
const clearCacheOptions = ref([
|
||||
{ label: '播放历史', key: 'history', description: '清除播放过的歌曲记录' },
|
||||
{ label: '收藏记录', key: 'favorite', description: '清除本地收藏的歌曲记录(不会影响云端收藏)' },
|
||||
{ label: '用户数据', key: 'user', description: '清除登录信息和用户相关数据' },
|
||||
{ label: '应用设置', key: 'settings', description: '清除应用的所有自定义设置' },
|
||||
{ label: '下载记录', key: 'downloads', description: '清除下载历史记录(不会删除已下载的文件)' },
|
||||
{ label: '音乐资源', key: 'resources', description: '清除已加载的音乐文件、歌词等资源缓存' },
|
||||
{ label: '歌词资源', key: 'lyrics', description: '清除已加载的歌词资源缓存' }
|
||||
{
|
||||
label: t('settings.system.cacheTypes.history.label'),
|
||||
key: 'history',
|
||||
description: t('settings.system.cacheTypes.history.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.favorite.label'),
|
||||
key: 'favorite',
|
||||
description: t('settings.system.cacheTypes.favorite.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.user.label'),
|
||||
key: 'user',
|
||||
description: t('settings.system.cacheTypes.user.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.settings.label'),
|
||||
key: 'settings',
|
||||
description: t('settings.system.cacheTypes.settings.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.downloads.label'),
|
||||
key: 'downloads',
|
||||
description: t('settings.system.cacheTypes.downloads.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.resources.label'),
|
||||
key: 'resources',
|
||||
description: t('settings.system.cacheTypes.resources.description')
|
||||
},
|
||||
{
|
||||
label: t('settings.system.cacheTypes.lyrics.label'),
|
||||
key: 'lyrics',
|
||||
description: t('settings.system.cacheTypes.lyrics.description')
|
||||
}
|
||||
]);
|
||||
|
||||
const selectedCacheTypes = ref<string[]>([]);
|
||||
@@ -795,7 +844,7 @@ const clearCache = async () => {
|
||||
});
|
||||
|
||||
await Promise.all(clearTasks);
|
||||
message.success('清除成功,部分设置在重启后生效');
|
||||
message.success(t('settings.system.messages.clearSuccess'));
|
||||
showClearCacheModal.value = false;
|
||||
selectedCacheTypes.value = [];
|
||||
};
|
||||
@@ -808,13 +857,13 @@ const handleShortcutsChange = (shortcuts: any) => {
|
||||
|
||||
// 定义设置分类
|
||||
const settingSections = [
|
||||
{ id: 'basic', title: '基础设置' },
|
||||
{ id: 'playback', title: '播放设置' },
|
||||
{ id: 'application', title: '应用设置', electron: true },
|
||||
{ id: 'network', title: '网络设置', electron: true },
|
||||
{ id: 'system', title: '系统管理', electron: true },
|
||||
{ id: 'about', title: '关于' },
|
||||
{ id: 'donation', title: '捐赠支持' }
|
||||
{ id: 'basic', title: t('settings.sections.basic') },
|
||||
{ id: 'playback', title: t('settings.sections.playback') },
|
||||
{ id: 'application', title: t('settings.sections.application'), electron: true },
|
||||
{ id: 'network', title: t('settings.sections.network'), electron: true },
|
||||
{ id: 'system', title: t('settings.sections.system'), electron: true },
|
||||
{ id: 'regard', title: t('settings.sections.regard') },
|
||||
{ id: 'donation', title: t('settings.sections.donation') }
|
||||
];
|
||||
|
||||
// 当前激活的分类
|
||||
@@ -992,4 +1041,8 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-select) {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,22 +13,22 @@
|
||||
<div class="user-info-list">
|
||||
<div class="user-info-item">
|
||||
<div class="label">{{ userDetail.profile.followeds }}</div>
|
||||
<div>粉丝</div>
|
||||
<div>{{ t('user.profile.followers') }}</div>
|
||||
</div>
|
||||
<div class="user-info-item">
|
||||
<div class="label">{{ userDetail.profile.follows }}</div>
|
||||
<div>关注</div>
|
||||
<div>{{ t('user.profile.following') }}</div>
|
||||
</div>
|
||||
<div class="user-info-item">
|
||||
<div class="label">{{ userDetail.level }}</div>
|
||||
<div>等级</div>
|
||||
<div>{{ t('user.profile.level') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
|
||||
|
||||
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
<div class="title">创建的歌单</div>
|
||||
<div class="title">{{ t('user.playlist.created') }}</div>
|
||||
<n-scrollbar>
|
||||
<div
|
||||
v-for="(item, index) in playList"
|
||||
@@ -45,7 +45,9 @@
|
||||
<div class="play-list-item-info">
|
||||
<div class="play-list-item-name">{{ item.name }}</div>
|
||||
<div class="play-list-item-count">
|
||||
{{ item.trackCount }}首,播放{{ item.playCount }}次
|
||||
{{ t('user.playlist.trackCount', { count: item.trackCount }) }},{{
|
||||
t('user.playlist.playCount', { count: item.playCount })
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +63,7 @@
|
||||
class="right"
|
||||
:class="setAnimationClass('animate__fadeInRight')"
|
||||
>
|
||||
<div class="title">听歌排行</div>
|
||||
<div class="title">{{ t('user.ranking.title') }}</div>
|
||||
<div class="record-list">
|
||||
<n-scrollbar>
|
||||
<div
|
||||
@@ -72,7 +74,9 @@
|
||||
:style="setAnimationDelay(index, 25)"
|
||||
>
|
||||
<song-item class="song-item" :item="item" @play="handlePlay" />
|
||||
<div class="play-count">{{ item.playCount }}次</div>
|
||||
<div class="play-count">
|
||||
{{ t('user.ranking.playCount', { count: item.playCount }) }}
|
||||
</div>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
@@ -93,6 +97,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
@@ -110,6 +115,7 @@ defineOptions({
|
||||
name: 'User'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const userDetail = ref<IUserDetail>();
|
||||
@@ -251,15 +257,15 @@ const handleRemoveFromPlaylist = async (songId: number) => {
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
message.success('删除成功');
|
||||
message.success(t('user.message.deleteSuccess'));
|
||||
// 重新加载歌单详情
|
||||
await loadPlaylistDetail(list.value.id);
|
||||
} else {
|
||||
throw new Error(res.data?.msg || '删除失败');
|
||||
throw new Error(res.data?.msg || t('user.message.deleteFailed'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('删除歌曲失败:', error);
|
||||
message.error(error.message || '删除失败');
|
||||
message.error(error.message || t('user.message.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user