mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 23:57:22 +08:00
✨ feat: 增强语言切换功能和用户播放列表显示
This commit is contained in:
+2
-1
@@ -16,6 +16,7 @@ dist.zip
|
||||
.vscode
|
||||
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
.env.*.local
|
||||
|
||||
@@ -23,4 +24,4 @@ out
|
||||
|
||||
.cursorrules
|
||||
|
||||
.github/deploy_keys
|
||||
.github/deploy_keys
|
||||
|
||||
@@ -87,5 +87,8 @@ export default {
|
||||
exitApp: 'Exit App',
|
||||
rememberChoice: 'Remember my choice',
|
||||
closeApp: 'Close App'
|
||||
},
|
||||
userPlayList: {
|
||||
title: "{name}'s Playlist"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,5 +86,8 @@ export default {
|
||||
exitApp: '退出应用',
|
||||
rememberChoice: '记住我的选择',
|
||||
closeApp: '关闭应用'
|
||||
},
|
||||
userPlayList: {
|
||||
title: '{name}的常听'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +106,7 @@ if (!isSingleInstance) {
|
||||
|
||||
// 监听语言切换
|
||||
ipcMain.on('change-language', (_, locale: Language) => {
|
||||
console.log('locale',locale)
|
||||
// 更新主进程的语言设置
|
||||
i18n.global.locale = locale;
|
||||
// 更新托盘菜单
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
|
||||
import { app, BrowserWindow, Menu, nativeImage, Tray, ipcMain } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import type { Language } from '../../i18n/main';
|
||||
@@ -35,9 +35,12 @@ export function updateTrayMenu() {
|
||||
i18n.global.locale = value;
|
||||
// 更新托盘菜单
|
||||
updateTrayMenu();
|
||||
// 通知渲染进程
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('set-language', value);
|
||||
// 直接通知主窗口
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
for (const win of windows) {
|
||||
win.webContents.send('language-changed', value);
|
||||
console.log('向窗口发送语言变更事件:', value);
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
Vendored
+1
@@ -18,6 +18,7 @@ declare global {
|
||||
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
||||
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
|
||||
removeDownloadListeners: () => void;
|
||||
onLanguageChanged: (callback: (locale: string) => void) => void;
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
};
|
||||
$message: any;
|
||||
|
||||
@@ -24,6 +24,14 @@ const api = {
|
||||
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => {
|
||||
ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath));
|
||||
},
|
||||
// 语言相关
|
||||
onLanguageChanged: (callback: (locale: string) => void) => {
|
||||
console.log('注册语言变更监听器');
|
||||
ipcRenderer.on('language-changed', (_event, locale) => {
|
||||
console.log('收到语言变更事件:', locale);
|
||||
callback(locale);
|
||||
});
|
||||
},
|
||||
removeDownloadListeners: () => {
|
||||
ipcRenderer.removeAllListeners('download-progress');
|
||||
ipcRenderer.removeAllListeners('download-complete');
|
||||
|
||||
@@ -57,13 +57,15 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const handleSetLanguage = (_: any, value: string) => {
|
||||
locale.value = value;
|
||||
// settingsStore.setLanguage(value);
|
||||
const handleSetLanguage = (value: string) => {
|
||||
console.log('应用语言变更:', value);
|
||||
if (value) {
|
||||
locale.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
settingsStore.initializeSettings();
|
||||
handleSetLanguage(null, settingsStore.setData.language);
|
||||
handleSetLanguage(settingsStore.setData.language);
|
||||
settingsStore.initializeTheme();
|
||||
settingsStore.initializeSystemFonts();
|
||||
if (isMobile.value) {
|
||||
@@ -71,13 +73,12 @@ if (isMobile.value) {
|
||||
}
|
||||
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('language-changed', handleSetLanguage);
|
||||
window.api.onLanguageChanged(handleSetLanguage);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 先初始化播放状态
|
||||
await playerStore.initializePlayState();
|
||||
|
||||
// 如果有正在播放的音乐,则初始化音频监听器
|
||||
if (playerStore.playMusic && playerStore.playMusic.id) {
|
||||
// 使用 nextTick 确保 DOM 更新后再初始化
|
||||
|
||||
@@ -144,11 +144,14 @@ watch(modelValue, (newVal) => {
|
||||
});
|
||||
const loading = ref(false);
|
||||
// 加载歌手信息
|
||||
|
||||
const previousArtistId = ref<number>();
|
||||
const loadArtistInfo = async (id: number) => {
|
||||
if (currentArtistId.value === id) return;
|
||||
// if (currentArtistId.value === id) return;
|
||||
if (previousArtistId.value === id) return;
|
||||
activeTab.value = 'songs';
|
||||
loading.value = true;
|
||||
currentArtistId.value = id;
|
||||
previousArtistId.value = id;
|
||||
try {
|
||||
const info = await getArtistDetail(id);
|
||||
if (info.data?.data?.artist) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { computed } from 'vue';
|
||||
import { usePlayerStore } from '@/store/modules/player';
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
const isPlay = computed(() => playerStore.isPlay);
|
||||
const isPlay = computed(() => playerStore.playMusicUrl);
|
||||
|
||||
defineProps({
|
||||
height: {
|
||||
|
||||
@@ -392,7 +392,8 @@ const toggleSelect = () => {
|
||||
};
|
||||
|
||||
const handleArtistClick = (id: number) => {
|
||||
settingsStore.currentArtistId = id;
|
||||
settingsStore.setCurrentArtistId(id);
|
||||
settingsStore.setShowArtistDrawer(true);
|
||||
};
|
||||
|
||||
// 获取歌手列表(最多显示5个)
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
:space-between="20"
|
||||
draggable
|
||||
show-arrow
|
||||
autoplay
|
||||
:autoplay="false"
|
||||
>
|
||||
<n-carousel-item
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(0, 100)"
|
||||
style="width: calc((100% / 5) - 16px)"
|
||||
:style="getCarouselItemStyle(0, 100, 6 )"
|
||||
>
|
||||
<div v-if="dayRecommendData" class="recommend-singer-item relative">
|
||||
<div
|
||||
@@ -46,14 +45,13 @@
|
||||
<n-carousel-item
|
||||
v-if="userStore.user && userPlaylist.length"
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(1, 100) + '; width: calc(100% / 2); max-width: 460px;'"
|
||||
:style="getCarouselItemStyleForPlaylist(userPlaylist.length)"
|
||||
>
|
||||
<div class="user-play">
|
||||
<div class="user-play-title flex items-center mb-3">
|
||||
<n-avatar size="small" round :src="userStore.user?.avatarUrl" class="mr-2" />
|
||||
{{ userStore.user?.nickname }}的常听
|
||||
<div class="user-play-title mb-3">
|
||||
{{ t('comp.userPlayList.title', { name: userStore.user?.nickname }) }}
|
||||
</div>
|
||||
<div class="user-play-list">
|
||||
<div class="user-play-list" :class="getPlaylistGridClass(userPlaylist.length)">
|
||||
<div
|
||||
v-for="item in userPlaylist"
|
||||
:key="item.id"
|
||||
@@ -62,12 +60,16 @@
|
||||
>
|
||||
<div class="user-play-item-img">
|
||||
<img :src="getImgUrl(item.coverImgUrl, '200y200')" alt="" />
|
||||
<div class="user-play-item-overlay"></div>
|
||||
</div>
|
||||
<div class="user-play-item-info">
|
||||
<div class="user-play-item-info-name">{{ item.name }}</div>
|
||||
<div class="user-play-item-info-count text-xs opacity-70">
|
||||
{{ t('common.songCount', { count: item.trackCount }) }}
|
||||
<div class="user-play-item-overlay">
|
||||
<div class="user-play-item-play-btn">
|
||||
<i class="iconfont icon-playfill text-3xl text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-play-item-title">
|
||||
<div class="user-play-item-title-name">{{ item.name }}</div>
|
||||
</div>
|
||||
<div class="user-play-item-count">
|
||||
<div class="user-play-item-count-tag">{{ t('common.songCount', { count: item.trackCount }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,13 +80,13 @@
|
||||
v-for="(item, index) in hotSingerData?.artists"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(index + 1, 100)"
|
||||
style="width: calc((100% / 5) - 16px)"
|
||||
:style="getCarouselItemStyle(index + 1, 100, 6)"
|
||||
>
|
||||
<div
|
||||
class="recommend-singer-item relative"
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(index + 2, 100)"
|
||||
@click="handleOpenSinger(item.id)"
|
||||
>
|
||||
<div
|
||||
:style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))"
|
||||
@@ -94,11 +96,12 @@
|
||||
{{ 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)">
|
||||
<i class="iconfont icon-playfill text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
|
||||
<div class="recommend-singer-item-info-name text-el text-right line-clamp-1">{{ item.name }}</div>
|
||||
</div>
|
||||
<!-- 播放按钮(hover时显示) -->
|
||||
<div class="recommend-singer-item-play-overlay" @click.stop="handleOpenSinger(item.id)">
|
||||
<div class="recommend-singer-item-play-btn">
|
||||
<i class="iconfont icon-playfill text-4xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,8 +136,7 @@ import { getDayRecommend, getHotSinger } from '@/api/home';
|
||||
import { getListDetail } from '@/api/list';
|
||||
import { getUserPlaylist } from '@/api/user';
|
||||
import MusicList from '@/components/MusicList.vue';
|
||||
import router from '@/router';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useSettingsStore, useUserStore } from '@/store';
|
||||
import { IDayRecommend } from '@/type/day_recommend';
|
||||
import { Playlist } from '@/type/list';
|
||||
import type { IListDetail } from '@/type/listDetail';
|
||||
@@ -157,6 +159,58 @@ const playlistLoading = ref(false);
|
||||
const playlistItem = ref<Playlist | null>(null);
|
||||
const playlistDetail = ref<IListDetail | null>(null);
|
||||
|
||||
/**
|
||||
* 获取轮播项的样式
|
||||
* @param index 项目索引(用于动画延迟)
|
||||
* @param delayStep 动画延迟的步长(毫秒)
|
||||
* @param totalItems 总共分成几等分(默认为5)
|
||||
* @param maxWidth 最大宽度(可选,单位为px)
|
||||
* @returns 样式字符串
|
||||
*/
|
||||
const getCarouselItemStyle = (
|
||||
index: number,
|
||||
delayStep: number,
|
||||
totalItems: number = 5,
|
||||
maxWidth?: number
|
||||
) => {
|
||||
const animationDelay = setAnimationDelay(index, delayStep);
|
||||
const width = `calc((100% / ${totalItems}) - 16px)`;
|
||||
const maxWidthStyle = maxWidth ? `max-width: ${maxWidth}px;` : '';
|
||||
|
||||
return `${animationDelay}; width: ${width}; ${maxWidthStyle}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据歌单数量获取轮播项的样式
|
||||
* @param playlistCount 歌单数量
|
||||
* @returns 样式字符串
|
||||
*/
|
||||
const getCarouselItemStyleForPlaylist = (playlistCount: number) => {
|
||||
const animationDelay = setAnimationDelay(1, 100);
|
||||
let width = '';
|
||||
let maxWidth = '';
|
||||
|
||||
switch(playlistCount) {
|
||||
case 1:
|
||||
width = 'calc(100% / 4 - 16px)';
|
||||
maxWidth = 'max-width: 180px;';
|
||||
break;
|
||||
case 2:
|
||||
width = 'calc(100% / 3 - 16px)';
|
||||
maxWidth = 'max-width: 380px;';
|
||||
break;
|
||||
case 3:
|
||||
width = 'calc(100% / 2 - 16px)';
|
||||
maxWidth = 'max-width: 520px;';
|
||||
break;
|
||||
default:
|
||||
width = 'calc(100% / 1 - 16px)';
|
||||
maxWidth = 'max-width: 656px;';
|
||||
}
|
||||
|
||||
return `${animationDelay}; width: ${width}; ${maxWidth}`;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
});
|
||||
@@ -179,22 +233,21 @@ const loadData = async () => {
|
||||
hotSingerData.value = singerData;
|
||||
if (userStore.user) {
|
||||
const { data: playlistData } = await getUserPlaylist(userStore.user?.userId);
|
||||
// 确保最多只显示4个歌单,并按播放次数排序
|
||||
userPlaylist.value = (playlistData.playlist as Playlist[])
|
||||
.sort((a, b) => b.playCount - a.playCount)
|
||||
.slice(0, 3);
|
||||
.slice(0, 4);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toSearchSinger = (keyword: string) => {
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
keyword
|
||||
}
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const handleOpenSinger = (id: number) => {
|
||||
settingsStore.setCurrentArtistId(id);
|
||||
settingsStore.setShowArtistDrawer(true);
|
||||
};
|
||||
|
||||
const toPlaylist = async (id: number) => {
|
||||
@@ -226,63 +279,127 @@ watchEffect(() => {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
|
||||
const getPlaylistGridClass = (length: number) => {
|
||||
switch(length) {
|
||||
case 1:
|
||||
return 'one-column';
|
||||
case 2:
|
||||
return 'two-columns';
|
||||
case 3:
|
||||
return 'three-columns';
|
||||
default:
|
||||
return 'four-columns';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recommend-singer {
|
||||
&-list {
|
||||
@apply flex;
|
||||
height: 280px;
|
||||
height: 220px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
&-item {
|
||||
@apply flex-1 h-full rounded-3xl p-5 flex flex-col justify-between overflow-hidden;
|
||||
@apply flex-1 h-full rounded-3xl p-5 flex flex-col justify-between overflow-hidden relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
&-bg {
|
||||
@apply bg-gray-900 dark:bg-gray-800 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
|
||||
filter: brightness(60%);
|
||||
}
|
||||
|
||||
&-info {
|
||||
@apply flex items-center p-2;
|
||||
&-play {
|
||||
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer text-white;
|
||||
}
|
||||
@apply flex flex-col p-2;
|
||||
&-name {
|
||||
@apply text-gray-100 dark:text-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
&-count {
|
||||
@apply text-gray-100 dark:text-gray-100;
|
||||
}
|
||||
|
||||
&-play {
|
||||
&-overlay {
|
||||
@apply absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-black/20 z-20 opacity-0 transition-all duration-300 flex items-center justify-center;
|
||||
backdrop-filter: blur(1px);
|
||||
|
||||
.recommend-singer-item:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
@apply w-20 h-20 bg-transparent flex justify-center items-center text-white;
|
||||
transform: translateY(50px) scale(0.8);
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
.recommend-singer-item:hover & {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-play {
|
||||
@apply bg-light-200 dark:bg-dark-100 rounded-3xl p-5 h-full flex flex-col;
|
||||
@apply bg-light-300 dark:bg-dark-300 rounded-3xl px-4 py-3 h-full;
|
||||
backdrop-filter: blur(20px);
|
||||
&-title {
|
||||
@apply text-gray-900 dark:text-gray-100 font-bold text-lg;
|
||||
@apply text-gray-900 dark:text-gray-100 font-bold text-lg line-clamp-1;
|
||||
}
|
||||
&-list {
|
||||
@apply grid grid-cols-3 gap-5 h-full;
|
||||
@apply grid gap-3 h-full;
|
||||
&.one-column {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
.user-play-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
&.two-columns {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.user-play-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
&.three-columns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
.user-play-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
&.four-columns {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
.user-play-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-item {
|
||||
@apply bg-light dark:bg-dark-200 rounded-2xl overflow-hidden flex flex-col cursor-pointer transition-all duration-300;
|
||||
height: 190px;
|
||||
&:hover {
|
||||
@apply rounded-2xl overflow-hidden flex flex-col;
|
||||
height: 176px;
|
||||
|
||||
&-img {
|
||||
@apply relative cursor-pointer transition-all duration-300;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 100%; /* 确保宽高比为1:1,即正方形 */
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
.user-play-item-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&-img {
|
||||
@apply relative;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 100%; /* 确保宽高比为1:1,即正方形 */
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
img {
|
||||
@apply absolute inset-0 w-full h-full object-cover;
|
||||
@@ -291,13 +408,25 @@ watchEffect(() => {
|
||||
&-overlay {
|
||||
@apply absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center opacity-0 transition-opacity duration-300;
|
||||
}
|
||||
&-info {
|
||||
@apply px-1 py-1;
|
||||
&-title {
|
||||
@apply absolute top-0 left-0 right-0 p-2 bg-gradient-to-b from-black/70 to-transparent z-10;
|
||||
&-name {
|
||||
@apply text-gray-900 dark:text-gray-100 font-medium text-sm line-clamp-1;
|
||||
@apply text-white font-medium text-sm line-clamp-3;
|
||||
}
|
||||
&-count {
|
||||
@apply text-gray-700 dark:text-gray-300 mt-1;
|
||||
}
|
||||
&-count {
|
||||
@apply absolute bottom-2 right-2 z-10;
|
||||
&-tag {
|
||||
@apply px-2 py-0.5 text-xs text-white bg-black/50 backdrop-blur-sm rounded-full;
|
||||
}
|
||||
}
|
||||
&-play-btn {
|
||||
@apply flex items-center justify-center;
|
||||
transform: scale(0.8);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.user-play-item:hover & {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ watch(
|
||||
() => settingsStore.currentArtistId,
|
||||
(newId) => {
|
||||
if (newId) {
|
||||
console.log('newId',newId)
|
||||
artistDrawerShow.value = true;
|
||||
nextTick(() => {
|
||||
artistDrawerRef.value?.loadArtistInfo(newId);
|
||||
|
||||
Vendored
+1
@@ -8,6 +8,7 @@ export interface IElectronAPI {
|
||||
openLyric: () => void;
|
||||
sendLyric: (data: string) => void;
|
||||
unblockMusic: (id: number) => Promise<string>;
|
||||
onLanguageChanged: (callback: (locale: string) => void) => void;
|
||||
store: {
|
||||
get: (key: string) => Promise<any>;
|
||||
set: (key: string, value: any) => Promise<boolean>;
|
||||
|
||||
Reference in New Issue
Block a user