mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 15:47:23 +08:00
✨ feat: 国际化 (i18n) 功能实现
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
quaternary
|
||||
class="inline-flex items-center gap-2 px-4 py-2 transition-all duration-300 hover:-translate-y-0.5"
|
||||
>
|
||||
请我喝咖啡
|
||||
{{ t('comp.coffee.title') }}
|
||||
</n-button>
|
||||
</slot>
|
||||
</template>
|
||||
@@ -17,20 +17,24 @@
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image
|
||||
:src="alipayQR"
|
||||
alt="支付宝收款码"
|
||||
:alt="t('comp.coffee.alipayQR')"
|
||||
class="w-32 h-32 rounded-lg cursor-none"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{
|
||||
t('comp.coffee.alipay')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image
|
||||
:src="wechatQR"
|
||||
alt="微信收款码"
|
||||
:alt="t('comp.coffee.wechatQR')"
|
||||
class="w-32 h-32 rounded-lg cursor-none"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{
|
||||
t('comp.coffee.wechat')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +43,7 @@
|
||||
class="text-sm text-gray-700 dark:text-gray-200 text-center cursor-pointer hover:text-green-500"
|
||||
@click="copyQQ"
|
||||
>
|
||||
QQ群:789288579
|
||||
{{ t('comp.coffee.qqGroup') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,11 +52,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||
import { NButton, NImage, NPopover, useMessage } from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const message = useMessage();
|
||||
const copyQQ = () => {
|
||||
navigator.clipboard.writeText('789288579');
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { isElectron } from '@/utils';
|
||||
|
||||
const store = useStore();
|
||||
const { locale } = useI18n();
|
||||
|
||||
const languages = [
|
||||
@@ -9,28 +13,44 @@ const languages = [
|
||||
{ label: 'English', value: 'en-US' }
|
||||
];
|
||||
|
||||
// 从配置中读取语言设置
|
||||
onMounted(() => {
|
||||
const savedLanguage = window.electron.ipcRenderer.sendSync('get-store-value', 'set.language');
|
||||
if (savedLanguage) {
|
||||
locale.value = savedLanguage;
|
||||
// 使用计算属性来获取当前语言
|
||||
const currentLanguage = computed({
|
||||
get: () => store.state.setData.language || 'zh-CN',
|
||||
set: (value: string) => {
|
||||
handleLanguageChange(value);
|
||||
}
|
||||
});
|
||||
|
||||
// 当语言改变时的处理函数
|
||||
const handleLanguageChange = (value: string) => {
|
||||
// 更新 i18n locale
|
||||
locale.value = value;
|
||||
// 保存语言设置到配置中
|
||||
window.electron.ipcRenderer.send('set-store-value', 'set.language', value);
|
||||
// 通过 mutation 更新 store
|
||||
store.commit('setLanguage', value);
|
||||
// 通知主进程语言已更改
|
||||
window.electron.ipcRenderer.send('change-language', value);
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send('change-language', value);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听来自主进程的语言切换事件
|
||||
const handleSetLanguage = (_: any, value: string) => {
|
||||
handleLanguageChange(value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('set-language', handleSetLanguage);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.removeListener('set-language', handleSetLanguage);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-select
|
||||
v-model:value="locale"
|
||||
:options="languages"
|
||||
size="small"
|
||||
@update:value="handleLanguageChange"
|
||||
/>
|
||||
<n-select v-model:value="currentLanguage" :options="languages" size="small" />
|
||||
</template>
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
@remove-song="(id) => emit('remove-song', id)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||
<div v-if="isLoadingMore" class="loading-more">
|
||||
{{ t('common.loadingMore') }}
|
||||
</div>
|
||||
<play-bottom />
|
||||
</div>
|
||||
</n-spin>
|
||||
@@ -80,6 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
@@ -88,6 +91,7 @@ import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/uti
|
||||
|
||||
import PlayBottom from './common/PlayBottom.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const props = withDefaults(
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
上一个
|
||||
{{ t('player.previous') }}
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="top">
|
||||
@@ -68,7 +68,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ isPlaying ? '暂停' : '播放' }}
|
||||
{{ isPlaying ? t('player.pause') : t('player.play') }}
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip v-if="!props.noList" placement="top">
|
||||
@@ -82,7 +82,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
下一个
|
||||
{{ t('player.next') }}
|
||||
</n-tooltip>
|
||||
|
||||
<div class="time-display">
|
||||
@@ -104,7 +104,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ volume === 0 ? '取消静音' : '静音' }}
|
||||
{{ volume === 0 ? t('player.unmute') : t('player.mute') }}
|
||||
</n-tooltip>
|
||||
<n-slider
|
||||
v-model:value="volume"
|
||||
@@ -129,7 +129,9 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ playMode === 'single' ? '单曲循环' : '列表循环' }}
|
||||
{{
|
||||
playMode === 'single' ? t('player.modeHint.single') : t('player.modeHint.list')
|
||||
}}
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="top">
|
||||
@@ -144,7 +146,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
{{ isFullscreen ? t('player.fullscreen.exit') : t('player.fullscreen.enter') }}
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="top">
|
||||
@@ -157,7 +159,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
关闭
|
||||
{{ t('player.close') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,7 +172,7 @@
|
||||
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
|
||||
</n-icon>
|
||||
<div class="mode-text">
|
||||
{{ playMode === 'single' ? '单曲循环' : '自动播放下一个' }}
|
||||
{{ playMode === 'single' ? t('player.modeHint.single') : t('player.modeHint.list') }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -188,11 +190,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NIcon, NSlider, NTooltip } from 'naive-ui';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMvUrl } from '@/api/mv';
|
||||
import { IMvItem } from '@/type/mv';
|
||||
|
||||
const { t } = useI18n();
|
||||
type PlayMode = 'single' | 'auto';
|
||||
const PLAY_MODE = {
|
||||
Single: 'single' as PlayMode,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<!-- 歌单分类列表 -->
|
||||
<div class="play-list-type">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
{{ t('comp.playlistType.title') }}
|
||||
</div>
|
||||
<div>
|
||||
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
|
||||
<span
|
||||
@@ -34,7 +36,9 @@
|
||||
"
|
||||
@click="handleToggleShowAllPlaylistCategory"
|
||||
>
|
||||
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
||||
{{
|
||||
!isShowAllPlaylistCategory ? t('comp.playlistType.showAll') : t('comp.playlistType.hide')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,11 +46,14 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { getPlaylistCategory } from '@/api/home';
|
||||
import type { IPlayListSort } from '@/type/playlist';
|
||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
// 歌单分类
|
||||
const playlistCategory = ref<IPlayListSort>();
|
||||
// 是否显示全部歌单分类
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="recommend-album">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">最新专辑</div>
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">
|
||||
{{ t('comp.recommendAlbum.title') }}
|
||||
</div>
|
||||
<div class="recommend-album-list">
|
||||
<template v-for="(item, index) in albumData?.albums" :key="item.id">
|
||||
<div
|
||||
@@ -33,6 +35,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { getNewAlbum } from '@/api/home';
|
||||
import { getAlbum } from '@/api/list';
|
||||
@@ -40,6 +43,7 @@ import MusicList from '@/components/MusicList.vue';
|
||||
import type { IAlbumNew } from '@/type/album';
|
||||
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const albumData = ref<IAlbumNew>();
|
||||
const loadAlbumList = async () => {
|
||||
const { data } = await getNewAlbum();
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
|
||||
@click="showMusic = true"
|
||||
>
|
||||
<div class="font-bold text-xl">每日推荐</div>
|
||||
<div class="font-bold text-lg">
|
||||
{{ t('comp.recommendSinger.title') }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<p
|
||||
@@ -45,7 +47,7 @@
|
||||
class="recommend-singer-item-bg"
|
||||
></div>
|
||||
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">
|
||||
{{ item.musicSize }}首
|
||||
{{ t('common.songCount', { count: item.musicSize }) }}
|
||||
</div>
|
||||
<div class="recommend-singer-item-info z-10">
|
||||
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
||||
@@ -61,7 +63,7 @@
|
||||
<music-list
|
||||
v-if="dayRecommendData?.dailySongs.length"
|
||||
v-model:show="showMusic"
|
||||
name="每日推荐列表"
|
||||
:name="t('comp.recommendSinger.songlist')"
|
||||
:song-list="dayRecommendData?.dailySongs"
|
||||
:cover="false"
|
||||
/>
|
||||
@@ -71,6 +73,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getDayRecommend, getHotSinger } from '@/api/home';
|
||||
@@ -81,6 +84,7 @@ import type { IHotSinger } from '@/type/singer';
|
||||
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// 歌手信息
|
||||
const hotSingerData = ref<IHotSinger>();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="recommend-music">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">本周最热音乐</div>
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
{{ t('comp.recommendSonglist.title') }}
|
||||
</div>
|
||||
<div
|
||||
v-show="recommendMusic?.result"
|
||||
v-loading="loading"
|
||||
@@ -21,6 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getRecommendMusic } from '@/api/home';
|
||||
@@ -29,6 +32,7 @@ import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
import SongItem from './common/SongItem.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
// 推荐歌曲
|
||||
const recommendMusic = ref<IRecommendMusic>();
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<!-- 标签页切换 -->
|
||||
<n-tabs v-model:value="activeTab" class="flex-1" type="line" animated>
|
||||
<n-tab-pane name="songs" :tab="$t('artist.songs')">
|
||||
<n-tab-pane name="songs" :tab="t('artist.hotSongs')">
|
||||
<div ref="songListRef" class="songs-list">
|
||||
<n-scrollbar style="max-height: 61vh" :size="5" @scroll="handleSongScroll">
|
||||
<div class="song-list-content">
|
||||
@@ -44,14 +44,14 @@
|
||||
:list="true"
|
||||
@play="handlePlay"
|
||||
/>
|
||||
<div v-if="songLoading" class="loading-more">{{ $t('common.loading') }}</div>
|
||||
<div v-if="songLoading" class="loading-more">{{ t('common.loading') }}</div>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="albums" :tab="$t('artist.albums')">
|
||||
<n-tab-pane name="albums" :tab="t('artist.albums')">
|
||||
<div ref="albumListRef" class="albums-list">
|
||||
<n-scrollbar style="max-height: 61vh" :size="5" @scroll="handleAlbumScroll">
|
||||
<div class="albums-grid">
|
||||
@@ -69,14 +69,14 @@
|
||||
type: '专辑'
|
||||
}"
|
||||
/>
|
||||
<div v-if="albumLoading" class="loading-more">{{ $t('common.loading') }}</div>
|
||||
<div v-if="albumLoading" class="loading-more">{{ t('common.loading') }}</div>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="about" :tab="$t('artist.description')">
|
||||
<n-tab-pane name="about" :tab="t('artist.description')">
|
||||
<div class="artist-description">
|
||||
<n-scrollbar style="max-height: 60vh">
|
||||
<div class="description-content" v-html="artistInfo?.briefDesc"></div>
|
||||
@@ -91,6 +91,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useDateFormat } from '@vueuse/core';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getArtistAlbums, getArtistDetail, getArtistTopSongs } from '@/api/artist';
|
||||
@@ -102,6 +103,8 @@ import { getImgUrl } from '@/utils';
|
||||
|
||||
import PlayBottom from './PlayBottom.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const modelValue = defineModel<boolean>('show', { required: true });
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<template #icon>
|
||||
<i class="ri-refresh-line"></i>
|
||||
</template>
|
||||
{{ $t('donation.refresh') }}
|
||||
{{ t('donation.refresh') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="donation-grid" :class="{ 'grid-expanded': isExpanded }">
|
||||
@@ -72,33 +72,33 @@
|
||||
<template #icon>
|
||||
<i :class="isExpanded ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"></i>
|
||||
</template>
|
||||
{{ isExpanded ? '收起' : '展开更多' }}
|
||||
{{ isExpanded ? t('common.collapse') : t('common.expand') }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
|
||||
<div class="description text-center text-sm text-gray-700 dark:text-gray-200">
|
||||
<p>{{ $t('donation.description') }}</p>
|
||||
<p>{{ $t('donation.message') }}</p>
|
||||
<p>{{ t('donation.description') }}</p>
|
||||
<p>{{ t('donation.message') }}</p>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image
|
||||
:src="alipay"
|
||||
:alt="$t('common.alipay')"
|
||||
:alt="t('common.alipay')"
|
||||
class="w-60 h-60 rounded-lg cursor-none"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{ $t('common.alipay') }}</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.alipay') }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image
|
||||
:src="wechat"
|
||||
:alt="$t('common.wechat')"
|
||||
:alt="t('common.wechat')"
|
||||
class="w-60 h-60 rounded-lg cursor-none"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{ $t('common.wechat') }}</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.wechat') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,12 +107,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import type { Donor } from '@/api/donation';
|
||||
import { getDonationList } from '@/api/donation';
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = 'https://avatars.githubusercontent.com/u/0?v=4';
|
||||
|
||||
|
||||
@@ -15,18 +15,20 @@
|
||||
placement="bottom"
|
||||
@after-leave="handleDrawerClose"
|
||||
>
|
||||
<n-drawer-content title="下载管理" closable :native-scrollbar="false">
|
||||
<n-drawer-content :title="t('download.title')" closable :native-scrollbar="false">
|
||||
<div class="drawer-container">
|
||||
<n-tabs type="line" animated class="h-full">
|
||||
<!-- 下载列表 -->
|
||||
<n-tab-pane name="downloading" tab="下载中" class="h-full">
|
||||
<n-tab-pane name="downloading" :tab="t('download.tabs.downloading')" class="h-full">
|
||||
<div class="download-list">
|
||||
<div v-if="downloadList.length === 0" class="empty-tip">
|
||||
<n-empty description="暂无下载任务" />
|
||||
<n-empty :description="t('download.empty.noTasks')" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="total-progress">
|
||||
<div class="total-progress-text">总进度: {{ totalProgress.toFixed(1) }}%</div>
|
||||
<div class="total-progress-text">
|
||||
{{ t('download.progress.total', { progress: totalProgress.toFixed(1) }) }}
|
||||
</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="Number(totalProgress.toFixed(1))"
|
||||
@@ -52,7 +54,10 @@
|
||||
{{ item.filename }}
|
||||
</div>
|
||||
<div class="download-item-artist">
|
||||
{{ item.songInfo?.ar?.map((a) => a.name).join(', ') || '未知歌手' }}
|
||||
{{
|
||||
item.songInfo?.ar?.map((a) => a.name).join(', ') ||
|
||||
t('download.artist.unknown')
|
||||
}}
|
||||
</div>
|
||||
<div class="download-item-progress">
|
||||
<n-progress
|
||||
@@ -83,10 +88,10 @@
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 已下载列表 -->
|
||||
<n-tab-pane name="downloaded" tab="已下载" class="h-full">
|
||||
<n-tab-pane name="downloaded" :tab="t('download.tabs.downloaded')" class="h-full">
|
||||
<div class="downloaded-list">
|
||||
<div v-if="downloadedList.length === 0" class="empty-tip">
|
||||
<n-empty description="暂无已下载歌曲" />
|
||||
<n-empty :description="t('download.empty.noDownloaded')" />
|
||||
</div>
|
||||
<div v-else class="downloaded-content">
|
||||
<div class="downloaded-items">
|
||||
@@ -143,19 +148,28 @@
|
||||
</n-drawer>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<n-modal v-model:show="showDeleteConfirm" preset="dialog" type="warning" title="删除确认">
|
||||
<n-modal
|
||||
v-model:show="showDeleteConfirm"
|
||||
preset="dialog"
|
||||
type="warning"
|
||||
:title="t('download.delete.title')"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<i class="iconfont ri-error-warning-line mr-2 text-xl"></i>
|
||||
<span>删除确认</span>
|
||||
<span>{{ t('download.delete.title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="delete-confirm-content">
|
||||
确定要删除歌曲 "{{ itemToDelete?.filename }}" 吗?此操作不可恢复。
|
||||
{{ t('download.delete.message', { filename: itemToDelete?.filename }) }}
|
||||
</div>
|
||||
<template #action>
|
||||
<n-button size="small" @click="showDeleteConfirm = false">取消</n-button>
|
||||
<n-button size="small" type="warning" @click="confirmDelete">确定删除</n-button>
|
||||
<n-button size="small" @click="showDeleteConfirm = false">{{
|
||||
t('download.delete.cancel')
|
||||
}}</n-button>
|
||||
<n-button size="small" type="warning" @click="confirmDelete">{{
|
||||
t('download.delete.confirm')
|
||||
}}</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -164,12 +178,15 @@
|
||||
import type { ProgressStatus } from 'naive-ui';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
// import { audioService } from '@/services/audioService';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface DownloadItem {
|
||||
filename: string;
|
||||
progress: number;
|
||||
@@ -247,13 +264,13 @@ const getStatusType = (item: DownloadItem) => {
|
||||
const getStatusText = (item: DownloadItem) => {
|
||||
switch (item.status) {
|
||||
case 'downloading':
|
||||
return '下载中';
|
||||
return t('download.status.downloading');
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
return t('download.status.completed');
|
||||
case 'error':
|
||||
return '失败';
|
||||
return t('download.status.failed');
|
||||
default:
|
||||
return '未知';
|
||||
return t('download.status.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -312,13 +329,13 @@ const confirmDelete = async () => {
|
||||
)
|
||||
);
|
||||
await refreshDownloadedList();
|
||||
message.success('删除成功');
|
||||
message.success(t('download.delete.success'));
|
||||
} else {
|
||||
message.error('删除失败');
|
||||
message.error(t('download.delete.failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete music:', error);
|
||||
message.error('删除失败');
|
||||
message.error(t('download.delete.failed'));
|
||||
} finally {
|
||||
showDeleteConfirm.value = false;
|
||||
itemToDelete.value = null;
|
||||
@@ -398,7 +415,7 @@ const refreshDownloadedList = async () => {
|
||||
return {
|
||||
...item,
|
||||
picUrl: songDetail?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
||||
ar: songDetail?.ar || item.ar || [{ name: '本地音乐' }]
|
||||
ar: songDetail?.ar || item.ar || [{ name: t('download.localMusic') }]
|
||||
};
|
||||
});
|
||||
} catch (detailError) {
|
||||
@@ -468,7 +485,7 @@ onMounted(() => {
|
||||
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||
// 刷新已下载列表
|
||||
refreshDownloadedList();
|
||||
message.success(`${data.filename} 下载完成`);
|
||||
message.success(t('download.message.downloadComplete', { filename: data.filename }));
|
||||
} else {
|
||||
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||
if (existingItem) {
|
||||
@@ -481,7 +498,9 @@ onMounted(() => {
|
||||
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||
}, 3000);
|
||||
}
|
||||
message.error(`${data.filename} 下载失败: ${data.error}`);
|
||||
message.error(
|
||||
t('download.message.downloadFailed', { filename: data.filename, error: data.error })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,24 +13,28 @@
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">Alger Music Player {{ config.version }}</h2>
|
||||
<p class="app-desc mb-2">在桌面安装应用,获得更好的体验</p>
|
||||
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||
<p class="app-desc mb-2">{{ t('comp.installApp.description') }}</p>
|
||||
<n-checkbox v-model:checked="noPrompt">{{ t('comp.installApp.noPrompt') }}</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<n-button class="cancel-btn" @click="closeModal">暂不安装</n-button>
|
||||
<n-button type="primary" class="install-btn" @click="handleInstall">立即安装</n-button>
|
||||
<n-button class="cancel-btn" @click="closeModal">{{
|
||||
t('comp.installApp.cancel')
|
||||
}}</n-button>
|
||||
<n-button type="primary" class="install-btn" @click="handleInstall">{{
|
||||
t('comp.installApp.install')
|
||||
}}</n-button>
|
||||
</div>
|
||||
<div class="modal-desc mt-4 text-center">
|
||||
<p class="text-xs text-gray-400">
|
||||
下载遇到问题?去
|
||||
{{ t('comp.installApp.downloadProblem') }}
|
||||
<a
|
||||
class="text-green-500"
|
||||
target="_blank"
|
||||
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||
>GitHub</a
|
||||
>
|
||||
下载最新版本
|
||||
{{ t('comp.installApp.downloadProblemLinkText') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,12 +43,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
import { getLatestReleaseInfo } from '@/utils/update';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showModal = ref(false);
|
||||
const noPrompt = ref(false);
|
||||
const releaseInfo = ref<any>(null);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
placement="right"
|
||||
@update:show="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<n-drawer-content title="添加到歌单" class="mac-style-drawer">
|
||||
<n-drawer-content :title="t('comp.playlistDrawer.title')" class="mac-style-drawer">
|
||||
<n-scrollbar class="h-full">
|
||||
<div class="playlist-drawer">
|
||||
<!-- 创建新歌单按钮和表单 -->
|
||||
@@ -18,14 +18,20 @@
|
||||
<div class="create-playlist-icon">
|
||||
<i class="iconfont" :class="isCreating ? 'ri-close-line' : 'ri-add-line'"></i>
|
||||
</div>
|
||||
<div class="create-playlist-text">{{ isCreating ? '取消创建' : '创建新歌单' }}</div>
|
||||
<div class="create-playlist-text">
|
||||
{{
|
||||
isCreating
|
||||
? t('comp.playlistDrawer.cancelCreate')
|
||||
: t('comp.playlistDrawer.createPlaylist')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建歌单表单 -->
|
||||
<div class="create-playlist-form" :class="{ 'is-visible': isCreating }">
|
||||
<n-input
|
||||
v-model:value="formValue.name"
|
||||
placeholder="歌单名称"
|
||||
:placeholder="t('comp.playlistDrawer.playlistName')"
|
||||
maxlength="40"
|
||||
class="mac-style-input"
|
||||
:status="inputError ? 'error' : undefined"
|
||||
@@ -40,11 +46,15 @@
|
||||
class="iconfont"
|
||||
:class="formValue.privacy ? 'ri-lock-line' : 'ri-earth-line'"
|
||||
></i>
|
||||
<span>{{ formValue.privacy ? '私密歌单' : '公开歌单' }}</span>
|
||||
<span>{{
|
||||
formValue.privacy
|
||||
? t('comp.playlistDrawer.privatePlaylist')
|
||||
: t('comp.playlistDrawer.publicPlaylist')
|
||||
}}</span>
|
||||
</div>
|
||||
<n-switch v-model:value="formValue.privacy" class="mac-style-switch">
|
||||
<template #checked>私密</template>
|
||||
<template #unchecked>公开</template>
|
||||
<template #checked>{{ t('comp.playlistDrawer.private') }}</template>
|
||||
<template #unchecked>{{ t('comp.playlistDrawer.public') }}</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
@@ -56,7 +66,7 @@
|
||||
:disabled="!formValue.name"
|
||||
@click="handleCreatePlaylist"
|
||||
>
|
||||
创建歌单
|
||||
{{ t('comp.playlistDrawer.create') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +90,10 @@
|
||||
/>
|
||||
<div class="playlist-item-info">
|
||||
<div class="playlist-item-name">{{ playlist.name }}</div>
|
||||
<div class="playlist-item-count">{{ playlist.trackCount }}首歌曲</div>
|
||||
<div class="playlist-item-count">
|
||||
{{ playlist.trackCount }}
|
||||
{{ t('comp.playlistDrawer.count') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-item-action">
|
||||
<i class="iconfont ri-add-line"></i>
|
||||
@@ -96,12 +109,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { createPlaylist, updatePlaylistTracks } from '@/api/music';
|
||||
import { getUserPlaylist } from '@/api/user';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
songId?: number;
|
||||
@@ -138,7 +153,7 @@ const fetchUserPlaylists = async () => {
|
||||
try {
|
||||
const { user } = store.state;
|
||||
if (!user?.userId) {
|
||||
message.error('请先登录');
|
||||
message.error(t('comp.playlistDrawer.loginFirst'));
|
||||
emit('update:modelValue', false);
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +164,7 @@ const fetchUserPlaylists = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌单失败:', error);
|
||||
message.error('获取歌单失败');
|
||||
message.error(t('comp.playlistDrawer.getPlaylistFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,21 +180,21 @@ const handleAddToPlaylist = async (playlist: any) => {
|
||||
console.log('res.data', res.data);
|
||||
|
||||
if (res.status === 200) {
|
||||
message.success('添加成功');
|
||||
message.success(t('comp.playlistDrawer.addSuccess'));
|
||||
emit('update:modelValue', false);
|
||||
} else {
|
||||
throw new Error(res.data?.msg || '添加失败');
|
||||
throw new Error(res.data?.msg || t('comp.playlistDrawer.addFailed'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('添加到歌单失败:', error);
|
||||
message.error(error.message || '添加到歌单失败');
|
||||
message.error(error.message || t('comp.playlistDrawer.addFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建歌单
|
||||
const handleCreatePlaylist = async () => {
|
||||
if (!formValue.value.name) {
|
||||
message.error('请输入歌单名称');
|
||||
message.error(t('comp.playlistDrawer.inputPlaylistName'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,7 +207,7 @@ const handleCreatePlaylist = async () => {
|
||||
});
|
||||
|
||||
if (res.data?.id) {
|
||||
message.success('创建成功');
|
||||
message.success(t('comp.playlistDrawer.createSuccess'));
|
||||
isCreating.value = false;
|
||||
formValue.value.name = '';
|
||||
formValue.value.privacy = false;
|
||||
@@ -200,7 +215,7 @@ const handleCreatePlaylist = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建歌单失败:', error);
|
||||
message.error('创建歌单失败');
|
||||
message.error(t('comp.playlistDrawer.createFailed'));
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ import { cloneDeep } from 'lodash';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { NImage, NText, useMessage } from 'naive-ui';
|
||||
import { computed, h, inject, ref, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getSongUrl } from '@/hooks/MusicListHook';
|
||||
@@ -97,6 +98,8 @@ import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, isElectron } from '@/utils';
|
||||
import { getImageBackground } from '@/utils/linearColor';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: SongResult;
|
||||
@@ -194,12 +197,12 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '播放',
|
||||
label: t('songItem.menu.play'),
|
||||
key: 'play',
|
||||
icon: () => h('i', { class: 'iconfont ri-play-circle-line' })
|
||||
},
|
||||
{
|
||||
label: '下一首播放',
|
||||
label: t('songItem.menu.playNext'),
|
||||
key: 'playNext',
|
||||
icon: () => h('i', { class: 'iconfont ri-play-list-2-line' })
|
||||
},
|
||||
@@ -208,17 +211,17 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
||||
key: 'd1'
|
||||
},
|
||||
{
|
||||
label: '下载歌曲',
|
||||
label: t('songItem.menu.download'),
|
||||
key: 'download',
|
||||
icon: () => h('i', { class: 'iconfont ri-download-line' })
|
||||
},
|
||||
{
|
||||
label: '添加到歌单',
|
||||
label: t('songItem.menu.addToPlaylist'),
|
||||
key: 'addToPlaylist',
|
||||
icon: () => h('i', { class: 'iconfont ri-folder-add-line' })
|
||||
},
|
||||
{
|
||||
label: isFavorite.value ? '取消喜欢' : '喜欢',
|
||||
label: isFavorite.value ? t('songItem.menu.unfavorite') : t('songItem.menu.favorite'),
|
||||
key: 'favorite',
|
||||
icon: () =>
|
||||
h('i', {
|
||||
@@ -234,7 +237,7 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
||||
key: 'd2'
|
||||
},
|
||||
{
|
||||
label: '从歌单中删除',
|
||||
label: t('songItem.menu.removeFromPlaylist'),
|
||||
key: 'remove',
|
||||
icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })
|
||||
}
|
||||
@@ -271,7 +274,7 @@ const handleSelect = (key: string | number) => {
|
||||
// 下载音乐
|
||||
const downloadMusic = async () => {
|
||||
if (isDownloading.value) {
|
||||
message.warning('正在下载中,请稍候...');
|
||||
message.warning(t('songItem.message.downloading'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -280,7 +283,7 @@ const downloadMusic = async () => {
|
||||
|
||||
const data = (await getSongUrl(props.item.id, cloneDeep(props.item), true)) as any;
|
||||
if (!data || !data.url) {
|
||||
throw new Error('获取音乐下载地址失败');
|
||||
throw new Error(t('songItem.message.getUrlFailed'));
|
||||
}
|
||||
|
||||
// 构建文件名
|
||||
@@ -298,7 +301,7 @@ const downloadMusic = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
message.success('已加入下载队列');
|
||||
message.success(t('songItem.message.downloadQueued'));
|
||||
|
||||
// 监听下载完成事件
|
||||
const handleDownloadComplete = (_, result) => {
|
||||
@@ -331,7 +334,7 @@ const downloadMusic = async () => {
|
||||
} catch (error: any) {
|
||||
console.error('Download error:', error);
|
||||
isDownloading.value = false;
|
||||
message.error(error.message || '下载失败');
|
||||
message.error(error.message || t('songItem.message.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -398,7 +401,7 @@ const artists = computed(() => {
|
||||
// 添加到下一首播放
|
||||
const handlePlayNext = () => {
|
||||
store.commit('addToNextPlay', props.item);
|
||||
message.success('已添加到下一首播放');
|
||||
message.success(t('songItem.message.addedToNextPlay'));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
<img src="@/assets/logo.png" alt="App Icon" />
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">发现新版本 {{ updateInfo.latestVersion }}</h2>
|
||||
<p class="app-desc mb-2">当前版本 {{ updateInfo.currentVersion }}</p>
|
||||
<h2 class="app-name">{{ t('comp.update.title') }} {{ updateInfo.latestVersion }}</h2>
|
||||
<p class="app-desc mb-2">
|
||||
{{ t('comp.update.currentVersion') }} {{ updateInfo.currentVersion }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="update-info">
|
||||
@@ -39,7 +41,7 @@
|
||||
:loading="downloading"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ '暂不更新' }}
|
||||
{{ t('comp.update.cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@@ -53,14 +55,14 @@
|
||||
</div>
|
||||
<div v-if="!downloading" class="modal-desc mt-4 text-center">
|
||||
<p class="text-xs text-gray-400">
|
||||
下载遇到问题?去
|
||||
{{ t('comp.installApp.downloadProblem') }}
|
||||
<a
|
||||
class="text-green-500"
|
||||
target="_blank"
|
||||
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||
>GitHub</a
|
||||
>
|
||||
下载最新版本
|
||||
{{ t('comp.installApp.downloadProblemLinkText') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,12 +72,15 @@
|
||||
<script setup lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
breaks: true, // 支持 GitHub 风格的换行
|
||||
@@ -141,10 +146,10 @@ const checkForUpdates = async () => {
|
||||
|
||||
const downloading = ref(false);
|
||||
const downloadProgress = ref(0);
|
||||
const downloadStatus = ref('准备下载...');
|
||||
const downloadStatus = ref(t('comp.update.prepareDownload'));
|
||||
const downloadBtnText = computed(() => {
|
||||
if (downloading.value) return '下载中...';
|
||||
return '立即更新';
|
||||
if (downloading.value) return t('comp.update.downloading');
|
||||
return t('comp.update.nowUpdate');
|
||||
});
|
||||
|
||||
// 处理下载状态更新
|
||||
@@ -159,7 +164,7 @@ const handleDownloadComplete = (_event: any, success: boolean, filePath: string)
|
||||
if (success) {
|
||||
window.electron.ipcRenderer.send('install-update', filePath);
|
||||
} else {
|
||||
window.$message.error('下载失败,请重试或手动下载');
|
||||
window.$message.error(t('comp.update.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,7 +230,7 @@ const handleUpdate = async () => {
|
||||
if (downloadUrl) {
|
||||
try {
|
||||
downloading.value = true;
|
||||
downloadStatus.value = '准备下载...';
|
||||
downloadStatus.value = t('comp.update.prepareDownload');
|
||||
|
||||
// 获取代理节点列表
|
||||
const proxyHosts = await getProxyNodes();
|
||||
@@ -235,11 +240,11 @@ const handleUpdate = async () => {
|
||||
window.electron.ipcRenderer.send('start-download', proxyDownloadUrl);
|
||||
} catch (error) {
|
||||
downloading.value = false;
|
||||
window.$message.error('启动下载失败,请重试或手动下载');
|
||||
window.$message.error(t('comp.update.startFailed'));
|
||||
console.error('下载失败:', error);
|
||||
}
|
||||
} else {
|
||||
window.$message.error('未找到适合当前系统的安装包,请手动下载');
|
||||
window.$message.error(t('comp.update.noDownloadUrl'));
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
<template>
|
||||
<div class="settings-panel transparent-popover">
|
||||
<div class="settings-title">页面设置</div>
|
||||
<div class="settings-title">{{ t('lyricSettings.title') }}</div>
|
||||
<div class="settings-content">
|
||||
<div class="settings-item">
|
||||
<span>纯净模式</span>
|
||||
<span>{{ t('lyricSettings.pureMode') }}</span>
|
||||
<n-switch v-model:value="config.pureModeEnabled" />
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<span>隐藏封面</span>
|
||||
<span>{{ t('lyricSettings.hideCover') }}</span>
|
||||
<n-switch v-model:value="config.hideCover" />
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<span>居中显示</span>
|
||||
<span>{{ t('lyricSettings.centerDisplay') }}</span>
|
||||
<n-switch v-model:value="config.centerLyrics" />
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<span>显示翻译</span>
|
||||
<span>{{ t('lyricSettings.showTranslation') }}</span>
|
||||
<n-switch v-model:value="config.showTranslation" />
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<span>隐藏播放栏</span>
|
||||
<span>{{ t('lyricSettings.hidePlayBar') }}</span>
|
||||
<n-switch v-model:value="config.hidePlayBar" />
|
||||
</div>
|
||||
|
||||
<div class="settings-slider">
|
||||
<span>字体大小</span>
|
||||
<span>{{ t('lyricSettings.fontSize') }}</span>
|
||||
<n-slider
|
||||
v-model:value="config.fontSize"
|
||||
:step="1"
|
||||
:min="12"
|
||||
:max="32"
|
||||
:marks="{
|
||||
12: '小',
|
||||
22: '中',
|
||||
32: '大'
|
||||
12: t('lyricSettings.fontSizeMarks.small'),
|
||||
22: t('lyricSettings.fontSizeMarks.medium'),
|
||||
32: t('lyricSettings.fontSizeMarks.large')
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="settings-slider">
|
||||
<span>文字间距</span>
|
||||
<span>{{ t('lyricSettings.letterSpacing') }}</span>
|
||||
<n-slider
|
||||
v-model:value="config.letterSpacing"
|
||||
:step="0.2"
|
||||
:min="-2"
|
||||
:max="10"
|
||||
:marks="{
|
||||
'-2': '紧凑',
|
||||
0: '默认',
|
||||
10: '宽松'
|
||||
'-2': t('lyricSettings.letterSpacingMarks.compact'),
|
||||
0: t('lyricSettings.letterSpacingMarks.default'),
|
||||
10: t('lyricSettings.letterSpacingMarks.loose')
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="settings-slider">
|
||||
<span>行高</span>
|
||||
<span>{{ t('lyricSettings.lineHeight') }}</span>
|
||||
<n-slider
|
||||
v-model:value="config.lineHeight"
|
||||
:step="0.1"
|
||||
:min="1"
|
||||
:max="3"
|
||||
:marks="{
|
||||
1: '紧凑',
|
||||
1.5: '默认',
|
||||
3: '宽松'
|
||||
1: t('lyricSettings.lineHeightMarks.compact'),
|
||||
1.5: t('lyricSettings.lineHeightMarks.default'),
|
||||
3: t('lyricSettings.lineHeightMarks.loose')
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<span>背景主题</span>
|
||||
<span>{{ t('lyricSettings.backgroundTheme') }}</span>
|
||||
<n-radio-group v-model:value="config.theme" name="theme">
|
||||
<n-radio value="default">默认</n-radio>
|
||||
<n-radio value="light">亮色</n-radio>
|
||||
<n-radio value="dark">暗色</n-radio>
|
||||
<n-radio value="default">{{ t('lyricSettings.themeOptions.default') }}</n-radio>
|
||||
<n-radio value="light">{{ t('lyricSettings.themeOptions.light') }}</n-radio>
|
||||
<n-radio value="dark">{{ t('lyricSettings.themeOptions.dark') }}</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +86,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface LyricConfig {
|
||||
hideCover: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
preset="dialog"
|
||||
title="快捷键设置"
|
||||
:title="t('settings.shortcutSettings.title')"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
@after-leave="handleAfterLeave"
|
||||
@@ -20,7 +20,7 @@
|
||||
<n-input
|
||||
:value="formatShortcut(shortcut)"
|
||||
:status="duplicateKeys[key] ? 'error' : undefined"
|
||||
placeholder="点击输入快捷键"
|
||||
:placeholder="t('settings.shortcutSettings.inputPlaceholder')"
|
||||
readonly
|
||||
@keydown="(e) => handleKeyDown(e, key)"
|
||||
@focus="() => startRecording(key)"
|
||||
@@ -32,7 +32,7 @@
|
||||
<i class="ri-error-warning-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
快捷键冲突
|
||||
{{ t('settings.shortcutSettings.shortcutConflict') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,10 +42,12 @@
|
||||
|
||||
<div class="shortcut-footer">
|
||||
<n-space justify="end">
|
||||
<n-button size="small" @click="handleCancel">取消</n-button>
|
||||
<n-button size="small" @click="resetShortcuts">恢复默认</n-button>
|
||||
<n-button size="small" @click="handleCancel">{{ t('common.cancel') }}</n-button>
|
||||
<n-button size="small" @click="resetShortcuts">{{
|
||||
t('settings.shortcutSettings.resetShortcuts')
|
||||
}}</n-button>
|
||||
<n-button type="primary" size="small" :disabled="hasConflict" @click="handleSave">
|
||||
保存
|
||||
{{ t('common.save') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
@@ -58,9 +60,12 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { isElectron } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Shortcuts {
|
||||
togglePlay: string;
|
||||
prevPlay: string;
|
||||
@@ -121,13 +126,13 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const shortcutLabels: Record<keyof Shortcuts, string> = {
|
||||
togglePlay: '播放/暂停',
|
||||
prevPlay: '上一首',
|
||||
nextPlay: '下一首',
|
||||
volumeUp: '音量增加',
|
||||
volumeDown: '音量减少',
|
||||
toggleFavorite: '收藏/取消收藏',
|
||||
toggleWindow: '显示/隐藏窗口'
|
||||
togglePlay: t('settings.shortcutSettings.togglePlay'),
|
||||
prevPlay: t('settings.shortcutSettings.prevPlay'),
|
||||
nextPlay: t('settings.shortcutSettings.nextPlay'),
|
||||
volumeUp: t('settings.shortcutSettings.volumeUp'),
|
||||
volumeDown: t('settings.shortcutSettings.volumeDown'),
|
||||
toggleFavorite: t('settings.shortcutSettings.toggleFavorite'),
|
||||
toggleWindow: t('settings.shortcutSettings.toggleWindow')
|
||||
};
|
||||
|
||||
const getShortcutLabel = (key: keyof Shortcuts) => shortcutLabels[key];
|
||||
@@ -221,12 +226,12 @@ const handleKeyDown = (e: KeyboardEvent, key: keyof Shortcuts) => {
|
||||
|
||||
const resetShortcuts = () => {
|
||||
tempShortcuts.value = { ...defaultShortcuts };
|
||||
message.success('已恢复默认快捷键,请记得保存');
|
||||
message.success(t('settings.shortcutSettings.messages.resetSuccess'));
|
||||
};
|
||||
|
||||
const saveShortcuts = () => {
|
||||
if (hasConflict.value) {
|
||||
message.error('存在冲突的快捷键,请重新设置');
|
||||
message.error(t('settings.shortcutSettings.messages.conflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,17 +246,17 @@ const saveShortcuts = () => {
|
||||
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', shortcutsToSave);
|
||||
// 然后更新快捷键
|
||||
window.electron.ipcRenderer.send('update-shortcuts');
|
||||
message.success('快捷键设置已保存');
|
||||
message.success(t('settings.shortcutSettings.messages.saveSuccess'));
|
||||
} catch (error) {
|
||||
console.error('保存快捷键失败:', error);
|
||||
message.error('保存快捷键失败,请重试');
|
||||
message.error(t('settings.shortcutSettings.messages.saveError'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
tempShortcuts.value = { ...shortcuts.value };
|
||||
message.info('已取消修改');
|
||||
message.info(t('settings.shortcutSettings.messages.cancelEdit'));
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user