feat: 国际化 (i18n) 功能实现

This commit is contained in:
alger
2025-02-19 01:01:43 +08:00
parent da2a32e420
commit ead017e4b1
64 changed files with 1870 additions and 510 deletions
@@ -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;
}
+14 -11
View File
@@ -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>
+17 -12
View File
@@ -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');
}
};