Compare commits

..

2 Commits

Author SHA1 Message Date
algerkong
f9c920008c feat: changelog 2025-05-12 08:59:42 +08:00
algerkong
ef778a172b 🔧 chore: 更新版本号至 4.6.0 2025-05-12 08:55:28 +08:00
74 changed files with 1707 additions and 4317 deletions

View File

@@ -3,7 +3,7 @@ name: Deploy Web
on: on:
push: push:
branches: branches:
- main # 或者您的主分支名称 - dev_electron # 或者您的主分支名称
workflow_dispatch: # 允许手动触发 workflow_dispatch: # 允许手动触发
jobs: jobs:

View File

@@ -1,55 +1,34 @@
# Alger Music Player
<h2 align="center">🎵 Alger Music Player</h2>
<div align="center">
<div align="center">
<a href="https://github.com/algerkong/AlgerMusicPlayer/stargazers">
<img src="https://img.shields.io/github/stars/algerkong/AlgerMusicPlayer?style=for-the-badge&logo=github&label=Stars&logoColor=white&color=22c55e" alt="GitHub stars">
</a>
<a href="https://github.com/algerkong/AlgerMusicPlayer/releases">
<img src="https://img.shields.io/github/v/release/algerkong/AlgerMusicPlayer?style=for-the-badge&logo=github&label=Release&logoColor=white&color=1a67af" alt="GitHub release">
</a>
<a href="https://pd.qq.com/s/cs056n33q?b=5">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-algermusic-blue?style=for-the-badge" alt="QQ频道">
</a>
<a href="https://t.me/+9efsKRuvKBk2NWVl">
<img src="https://img.shields.io/badge/AlgerMusic-blue?style=for-the-badge&logo=telegram&logoColor=white&label=Telegram" alt="Telegram">
</a>
</div>
</div>
<div align="center">
<a href="https://hellogithub.com/repository/607b849c598d48e08fe38789d156ebdc" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=607b849c598d48e08fe38789d156ebdc&claim_uid=ObuMXUfeHBmk9TI&theme=neutral" alt="FeaturedHelloGitHub" width="160" height="32" /></a>
</div>
主要功能如下 主要功能如下
- 🎵 音乐推荐 - 🎵 音乐推荐
- 🔐 网易云账号登录与同步 - 🔐 网易云账号登录与同步
- 📝 功能 - 📝 功能
- 播放历史记录 - 播放历史记录
- 歌曲收藏管理 - 歌曲收藏管理
- 自定义快捷键配置(全局或应用内) - 自定义快捷键配置
- 🎨 界面与交互 - 🎨 界面与交互
- 沉浸式歌词显示(点击左下角封面进入) - 沉浸式歌词显示(点击左下角封面进入)
- 独立桌面歌词窗口 - 独立桌面歌词窗口
- 明暗主题切换 - 明暗主题切换
- 可远程控制播放
- 🎼 音乐功能 - 🎼 音乐功能
- 支持歌单、MV、专辑等完整音乐服务 - 支持歌单、MV、专辑等完整音乐服务
- 灰色音乐资源解析(基于 @unblockneteasemusic/server - 灰色音乐资源解析(基于 @unblockneteasemusic/server
- 音乐单独解析
- EQ均衡器
- 定时播放
- 高品质音乐试听需网易云VIP - 高品质音乐试听需网易云VIP
- 音乐文件下载(支持右键下载和批量下载, 附带歌词封面等信息) - 音乐文件下载(支持右键下载和批量下载, 附带歌词封面等信息)
- 🚀 技术特性 - 🚀 技术特性
- 本地化服务无需依赖在线API (基于 netease-cloud-music-api) - 本地化服务无需依赖在线API (基于 netease-cloud-music-api)
- 自动更新检测 - 自动更新检测
- 全平台适配Desktop & Web & Mobile Web & Android<测试> & ios<后续> - 全平台适配Desktop & Web & Mobile Web & Android<后续> & ios<后续>
## 项目简介 ## 项目简介
一个第三方音乐播放器、本地服务、桌面歌词、音乐下载、最高音质 一个第三方音乐播放器、本地服务、桌面歌词、音乐下载、最高音质
## 预览地址 ## 预览地址
[http://music.alger.fun/](http://music.alger.fun/) [http://mc.alger.fun/](http://mc.alger.fun/)
QQ群:789288579
tg群:[AlgerMusic tg](https://t.me/+9efsKRuvKBk2NWVl)
## 软件截图 ## 软件截图
![首页白](./docs/image.png) ![首页白](./docs/image.png)

View File

@@ -56,7 +56,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.7.7", "axios": "^1.7.7",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"electron": "^36.2.0", "electron": "^36.1.0",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"electron-vite": "^3.1.0", "electron-vite": "^3.1.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
@@ -130,14 +130,13 @@
] ]
}, },
"win": { "win": {
"icon": "resources/icon.ico", "icon": "resources/favicon.ico",
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
"arch": [ "arch": [
"x64", "x64",
"ia32", "ia32"
"arm64"
] ]
} }
], ],
@@ -150,15 +149,13 @@
{ {
"target": "AppImage", "target": "AppImage",
"arch": [ "arch": [
"x64", "x64"
"arm64"
] ]
}, },
{ {
"target": "deb", "target": "deb",
"arch": [ "arch": [
"x64", "x64"
"arm64"
] ]
} }
], ],
@@ -169,8 +166,8 @@
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
"allowToChangeInstallationDirectory": true, "allowToChangeInstallationDirectory": true,
"installerIcon": "resources/icon.ico", "installerIcon": "resources/favicon.ico",
"uninstallerIcon": "resources/icon.ico", "uninstallerIcon": "resources/favicon.ico",
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "AlgerMusicPlayer", "shortcutName": "AlgerMusicPlayer",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -27,8 +27,6 @@ export default {
refresh: 'Refresh', refresh: 'Refresh',
retry: 'Retry', retry: 'Retry',
reset: 'Reset', reset: 'Reset',
copySuccess: 'Copied to clipboard',
copyFailed: 'Copy failed',
validation: { validation: {
required: 'This field is required', required: 'This field is required',
invalidInput: 'Invalid input', invalidInput: 'Invalid input',
@@ -49,8 +47,7 @@ export default {
prev: 'Previous', prev: 'Previous',
next: 'Next', next: 'Next',
pause: 'Pause', pause: 'Pause',
play: 'Play', play: 'Play'
favorite: 'Favorite'
}, },
language: 'Language' language: 'Language'
}; };

View File

@@ -1,6 +1,6 @@
export default { export default {
installApp: { installApp: {
description: 'Install the application for a better experience', description: 'Install the application on the desktop for a better experience',
noPrompt: 'Do not prompt again', noPrompt: 'Do not prompt again',
install: 'Install now', install: 'Install now',
cancel: 'Cancel', cancel: 'Cancel',
@@ -60,7 +60,7 @@ export default {
wechatQR: 'Wechat QR code', wechatQR: 'Wechat QR code',
coffeeDesc: 'A cup of coffee, a support', coffeeDesc: 'A cup of coffee, a support',
coffeeDescLinkText: 'View more', coffeeDescLinkText: 'View more',
qqGroup: 'QQ group: algermusic', qqGroup: 'QQ group: 789288579',
messages: { messages: {
copySuccess: 'Copied to clipboard' copySuccess: 'Copied to clipboard'
}, },
@@ -104,17 +104,6 @@ export default {
}, },
musicList: { musicList: {
searchSongs: 'Search Songs', searchSongs: 'Search Songs',
noSearchResults: 'No search results', noSearchResults: 'No search results'
switchToNormal: 'Switch to normal layout',
switchToCompact: 'Switch to compact layout',
playAll: 'Play All',
collect: 'Collect',
collectSuccess: 'Collect Success',
cancelCollectSuccess: 'Cancel Collect Success',
cancelCollect: 'Cancel Collect',
addToPlaylist: 'Add to Playlist',
addToPlaylistSuccess: 'Add to Playlist Success',
operationFailed: 'Operation Failed',
songsAlreadyInPlaylist: 'Songs already in playlist'
} }
}; };

View File

@@ -3,7 +3,5 @@ export default {
'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.', 'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.',
message: 'You can leave your email or github name when leaving a message.', message: 'You can leave your email or github name when leaving a message.',
refresh: 'Refresh List', refresh: 'Refresh List',
toDonateList: 'Buy me a coffee', toDonateList: 'Buy me a coffee'
title: 'Donation List',
noMessage: 'No Message'
}; };

View File

@@ -11,7 +11,5 @@ export default {
downloadSuccess: 'Download completed', downloadSuccess: 'Download completed',
downloadFailed: 'Download failed', downloadFailed: 'Download failed',
downloading: 'Downloading, please wait...', downloading: 'Downloading, please wait...',
selectSongsFirst: 'Please select songs to download first', selectSongsFirst: 'Please select songs to download first'
descending: 'Descending',
ascending: 'Ascending'
}; };

View File

@@ -29,15 +29,6 @@ export default {
lrc: { lrc: {
noLrc: 'No lyrics, please enjoy' noLrc: 'No lyrics, please enjoy'
}, },
reparse: {
title: 'Select Music Source',
desc: 'Click a source to directly reparse the current song. This source will be used next time this song plays.',
success: 'Reparse successful',
failed: 'Reparse failed',
warning: 'Please select a music source',
bilibiliNotSupported: 'Bilibili videos do not support reparsing',
processing: 'Processing...'
},
playBar: { playBar: {
expand: 'Expand Lyrics', expand: 'Expand Lyrics',
collapse: 'Collapse Lyrics', collapse: 'Collapse Lyrics',
@@ -46,7 +37,6 @@ export default {
noSongPlaying: 'No song playing', noSongPlaying: 'No song playing',
eq: 'Equalizer', eq: 'Equalizer',
playList: 'Play List', playList: 'Play List',
reparse: 'Reparse',
playMode: { playMode: {
sequence: 'Sequence', sequence: 'Sequence',
loop: 'Loop', loop: 'Loop',
@@ -105,13 +95,5 @@ export default {
playbackStopped: 'Music playback stopped', playbackStopped: 'Music playback stopped',
minutesRemaining: '{minutes} min remaining', minutesRemaining: '{minutes} min remaining',
songsRemaining: '{count} songs remaining' songsRemaining: '{count} songs remaining'
},
playList: {
clearAll: 'Clear Playlist',
alreadyEmpty: 'Playlist is already empty',
cleared: 'Playlist cleared',
empty: 'Playlist is empty',
clearConfirmTitle: 'Clear Playlist',
clearConfirmContent: 'This will clear all songs in the playlist and stop the current playback. Continue?'
} }
}; };

View File

@@ -6,8 +6,7 @@ export default {
}, },
button: { button: {
clear: 'Clear', clear: 'Clear',
back: 'Back', back: 'Back'
playAll: 'Play All'
}, },
loading: { loading: {
more: 'Loading...', more: 'Loading...',

View File

@@ -66,9 +66,7 @@ export default {
noMusicSources: 'No sources selected', noMusicSources: 'No sources selected',
gdmusicInfo: 'GD Music Station intelligently resolves music from multiple platforms automatically', gdmusicInfo: 'GD Music Station intelligently resolves music from multiple platforms automatically',
autoPlay: 'Auto Play', autoPlay: 'Auto Play',
autoPlayDesc: 'Auto resume playback when reopening the app', autoPlayDesc: 'Auto resume playback when reopening the app'
showStatusBar: "Show Status Bar",
showStatusBarContent: "You can display the music control function in your mac status bar (effective after a restart)"
}, },
application: { application: {
closeAction: 'Close Action', closeAction: 'Close Action',

View File

@@ -27,8 +27,6 @@ export default {
refresh: '刷新', refresh: '刷新',
retry: '重试', retry: '重试',
reset: '重置', reset: '重置',
copySuccess: '已复制到剪贴板',
copyFailed: '复制失败',
validation: { validation: {
required: '此项是必填的', required: '此项是必填的',
invalidInput: '输入无效', invalidInput: '输入无效',
@@ -49,7 +47,6 @@ export default {
prev: '上一首', prev: '上一首',
next: '下一首', next: '下一首',
pause: '暂停', pause: '暂停',
play: '播放', play: '播放'
favorite: '收藏'
} }
}; };

View File

@@ -1,6 +1,6 @@
export default { export default {
installApp: { installApp: {
description: '安装应用程序,获得更好的体验', description: '在桌面安装应用,获得更好的体验',
noPrompt: '不再提示', noPrompt: '不再提示',
install: '立即安装', install: '立即安装',
cancel: '暂不安装', cancel: '暂不安装',
@@ -58,7 +58,7 @@ export default {
wechatQR: '微信收款码', wechatQR: '微信收款码',
coffeeDesc: '一杯咖啡,一份支持', coffeeDesc: '一杯咖啡,一份支持',
coffeeDescLinkText: '查看更多', coffeeDescLinkText: '查看更多',
qqGroup: 'QQ频道algermusic', qqGroup: 'QQ789288579',
messages: { messages: {
copySuccess: '已复制到剪贴板' copySuccess: '已复制到剪贴板'
}, },
@@ -102,17 +102,6 @@ export default {
}, },
musicList: { musicList: {
searchSongs: '搜索歌曲', searchSongs: '搜索歌曲',
noSearchResults: '没有找到相关歌曲', noSearchResults: '没有找到相关歌曲'
switchToNormal: '切换到默认布局',
switchToCompact: '切换到紧凑布局',
playAll: '播放全部',
collect: '收藏',
collectSuccess: '收藏成功',
cancelCollectSuccess: '取消收藏成功',
operationFailed: '操作失败',
cancelCollect: '取消收藏',
addToPlaylist: '添加到播放列表',
addToPlaylistSuccess: '添加到播放列表成功',
songsAlreadyInPlaylist: '歌曲已存在于播放列表中'
} }
}; };

View File

@@ -2,7 +2,5 @@ export default {
description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。', description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。',
message: '留言时可留下您的邮箱或 github名称。', message: '留言时可留下您的邮箱或 github名称。',
refresh: '刷新列表', refresh: '刷新列表',
toDonateList: '请我喝咖啡', toDonateList: '请我喝咖啡'
noMessage: '暂无留言',
title: '捐赠列表'
}; };

View File

@@ -7,7 +7,5 @@ export default {
downloadSuccess: '下载完成', downloadSuccess: '下载完成',
downloadFailed: '下载失败', downloadFailed: '下载失败',
downloading: '正在下载中,请稍候...', downloading: '正在下载中,请稍候...',
selectSongsFirst: '请先选择要下载的歌曲', selectSongsFirst: '请先选择要下载的歌曲'
descending: '降',
ascending: '升'
}; };

View File

@@ -29,15 +29,6 @@ export default {
lrc: { lrc: {
noLrc: '暂无歌词, 请欣赏' noLrc: '暂无歌词, 请欣赏'
}, },
reparse: {
title: '选择解析音源',
desc: '点击音源直接进行解析,下次播放此歌曲时将使用所选音源',
success: '重新解析成功',
failed: '重新解析失败',
warning: '请选择一个音源',
bilibiliNotSupported: 'B站视频不支持重新解析',
processing: '解析中...'
},
playBar: { playBar: {
expand: '展开歌词', expand: '展开歌词',
collapse: '收起歌词', collapse: '收起歌词',
@@ -46,7 +37,6 @@ export default {
noSongPlaying: '没有正在播放的歌曲', noSongPlaying: '没有正在播放的歌曲',
eq: '均衡器', eq: '均衡器',
playList: '播放列表', playList: '播放列表',
reparse: '重新解析',
playMode: { playMode: {
sequence: '顺序播放', sequence: '顺序播放',
loop: '循环播放', loop: '循环播放',
@@ -106,13 +96,5 @@ export default {
playbackStopped: '音乐播放已停止', playbackStopped: '音乐播放已停止',
minutesRemaining: '剩余{minutes}分钟', minutesRemaining: '剩余{minutes}分钟',
songsRemaining: '剩余{count}首歌' songsRemaining: '剩余{count}首歌'
},
playList: {
clearAll: '清空播放列表',
alreadyEmpty: '播放列表已经为空',
cleared: '已清空播放列表',
empty: '播放列表为空',
clearConfirmTitle: '清空播放列表',
clearConfirmContent: '这将清空所有播放列表中的歌曲并停止当前播放。是否继续?'
} }
}; };

View File

@@ -6,8 +6,7 @@ export default {
}, },
button: { button: {
clear: '清空', clear: '清空',
back: '返回', back: '返回'
playAll: '播放列表'
}, },
loading: { loading: {
more: '加载中...', more: '加载中...',

View File

@@ -44,7 +44,7 @@ export default {
}, },
playback: { playback: {
quality: '音质设置', quality: '音质设置',
qualityDesc: '选择音乐播放音质(网易云VIP', qualityDesc: '选择音乐播放音质VIP',
qualityOptions: { qualityOptions: {
standard: '标准', standard: '标准',
higher: '较高', higher: '较高',
@@ -66,9 +66,7 @@ export default {
noMusicSources: '未选择音源', noMusicSources: '未选择音源',
gdmusicInfo: 'GD音乐台可自动解析多个平台音源自动选择最佳结果', gdmusicInfo: 'GD音乐台可自动解析多个平台音源自动选择最佳结果',
autoPlay: '自动播放', autoPlay: '自动播放',
autoPlayDesc: '重新打开应用时是否自动继续播放', autoPlayDesc: '重新打开应用时是否自动继续播放'
showStatusBar: '是否显示状态栏控制功能',
showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)',
}, },
application: { application: {
closeAction: '关闭行为', closeAction: '关闭行为',

View File

@@ -89,8 +89,6 @@ const createWin = () => {
alwaysOnTop: true, alwaysOnTop: true,
resizable: true, resizable: true,
roundedCorners: false, roundedCorners: false,
titleBarStyle: 'hidden',
titleBarOverlay: false,
// 添加跨屏幕支持选项 // 添加跨屏幕支持选项
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
@@ -122,8 +120,6 @@ const createWin = () => {
} }
}); });
lyricWindow.on('blur', () => lyricWindow && lyricWindow.setMaximizable(false))
return lyricWindow; return lyricWindow;
}; };

View File

@@ -24,7 +24,6 @@ type SetConfig = {
fontFamily: string; fontFamily: string;
fontScope: 'global' | 'lyric'; fontScope: 'global' | 'lyric';
language: string; language: string;
showTopAction: boolean;
}; };
interface StoreType { interface StoreType {
set: SetConfig; set: SetConfig;

View File

@@ -11,7 +11,6 @@ import { join } from 'path';
import type { Language } from '../../i18n/main'; import type { Language } from '../../i18n/main';
import i18n from '../../i18n/main'; import i18n from '../../i18n/main';
import { getStore } from './config';
// 歌曲信息接口定义 // 歌曲信息接口定义
interface SongInfo { interface SongInfo {
@@ -175,18 +174,6 @@ export function updateTrayMenu(mainWindow: BrowserWindow) {
}) })
); );
// 收藏
menu.append(
new MenuItem({
label: i18n.global.t('common.tray.favorite'),
type: 'normal',
click: () => {
console.log('[Tray] 发送收藏命令 - macOS菜单');
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
}
})
);
menu.append( menu.append(
new MenuItem({ new MenuItem({
label: i18n.global.t('common.tray.next'), label: i18n.global.t('common.tray.next'),
@@ -266,14 +253,6 @@ export function updateTrayMenu(mainWindow: BrowserWindow) {
mainWindow.show(); mainWindow.show();
} }
}, },
{
label: i18n.global.t('common.tray.favorite'),
type: 'normal',
click: () => {
console.log('[Tray] 发送收藏命令 - Windows/Linux菜单');
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
}
},
{ type: 'separator' }, { type: 'separator' },
{ {
label: i18n.global.t('common.tray.prev'), label: i18n.global.t('common.tray.prev'),
@@ -328,8 +307,7 @@ export function updateTrayMenu(mainWindow: BrowserWindow) {
// 初始化状态栏Tray // 初始化状态栏Tray
function initializeStatusBarTray(mainWindow: BrowserWindow) { function initializeStatusBarTray(mainWindow: BrowserWindow) {
const store = getStore() if (process.platform !== 'darwin') return;
if (process.platform !== 'darwin' || !store.get('set.showTopAction')) return;
const iconSize = getProperIconSize(); const iconSize = getProperIconSize();

View File

@@ -23,6 +23,5 @@
"alwaysShowDownloadButton": false, "alwaysShowDownloadButton": false,
"unlimitedDownload": false, "unlimitedDownload": false,
"enableMusicUnblock": true, "enableMusicUnblock": true,
"enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "kuwo"], "enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "youtube"]
"showTopAction": false
} }

View File

@@ -1,6 +1,6 @@
import match from '@unblockneteasemusic/server'; import match from '@unblockneteasemusic/server';
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili'; type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube';
interface SongData { interface SongData {
name: string; name: string;
@@ -30,7 +30,7 @@ interface UnblockResult {
} }
// 所有可用平台 // 所有可用平台
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili']; export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
/** /**
* 音乐解析函数 * 音乐解析函数
@@ -46,16 +46,12 @@ const unblockMusic = async (
retryCount = 1, retryCount = 1,
enabledPlatforms?: Platform[] enabledPlatforms?: Platform[]
): Promise<UnblockResult> => { ): Promise<UnblockResult> => {
// 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台 const platforms = enabledPlatforms || ALL_PLATFORMS;
const filteredPlatforms = enabledPlatforms
? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform))
: ALL_PLATFORMS;
songData.album = songData.album || songData.al; songData.album = songData.album || songData.al;
songData.artists = songData.artists || songData.ar; songData.artists = songData.artists || songData.ar;
const retry = async (attempt: number): Promise<UnblockResult> => { const retry = async (attempt: number): Promise<UnblockResult> => {
try { try {
const data = await match(parseInt(String(id), 10), filteredPlatforms, songData); const data = await match(parseInt(String(id), 10), platforms,songData);
const result: UnblockResult = { const result: UnblockResult = {
data: { data: {
data, data,

View File

@@ -26,7 +26,6 @@ import { isElectron, isLyricWindow } from '@/utils';
import { initAudioListeners } from './hooks/MusicHook'; import { initAudioListeners } from './hooks/MusicHook';
import { isMobile } from './utils'; import { isMobile } from './utils';
import { useAppShortcuts } from './utils/appShortcuts'; import { useAppShortcuts } from './utils/appShortcuts';
import { audioService } from './services/audioService';
const { locale } = useI18n(); const { locale } = useI18n();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -120,8 +119,6 @@ onMounted(async () => {
window.api.sendSong(cloneDeep(playerStore.playMusic)); window.api.sendSong(cloneDeep(playerStore.playMusic));
} }
} }
audioService.releaseOperationLock();
}); });
</script> </script>

View File

@@ -152,32 +152,3 @@ export const getBilibiliAudioUrl = async (bvid: string, cid: number): Promise<st
throw error; throw error;
} }
}; };
// 根据音乐名称搜索并直接返回音频URL
export const searchAndGetBilibiliAudioUrl = async (
keyword: string
): Promise<string> => {
try {
// 搜索B站视频取第一页第一个结果
const res = await searchBilibili({ keyword, page: 1, pagesize: 1 });
const result = res.data?.data?.result;
if (!result || result.length === 0) {
throw new Error('未找到相关B站视频');
}
const first = result[0];
const bvid = first.bvid;
// 需要获取视频详情以获得cid
const detailRes = await getBilibiliVideoDetail(bvid);
const pages = detailRes.data.pages;
if (!pages || pages.length === 0) {
throw new Error('未找到视频分P信息');
}
const cid = pages[0].cid;
// 获取音频URL
return await getBilibiliAudioUrl(bvid, cid);
} catch (error) {
console.error('根据名称搜索B站音频URL失败:', error);
throw error;
}
}

View File

@@ -28,104 +28,106 @@ export interface ParsedMusicResult {
* @param id 音乐ID * @param id 音乐ID
* @param data 音乐数据,包含名称和艺术家信息 * @param data 音乐数据,包含名称和艺术家信息
* @param quality 音质设置 * @param quality 音质设置
* @param timeout 超时时间(毫秒)默认15000ms
* @returns 解析后的音乐URL及相关信息 * @returns 解析后的音乐URL及相关信息
*/ */
export const parseFromGDMusic = async ( export const parseFromGDMusic = async (
id: number, id: number,
data: any, data: any,
quality: string = '999', quality: string = '320'
timeout: number = 15000
): Promise<ParsedMusicResult | null> => { ): Promise<ParsedMusicResult | null> => {
// 创建一个超时Promise
const timeoutPromise = new Promise<null>((_, reject) => {
setTimeout(() => {
reject(new Error('GD音乐台解析超时'));
}, timeout);
});
try { try {
// 使用Promise.race竞争主解析流程和超时 // 处理不同数据结构
return await Promise.race([ if (!data) {
(async () => { console.error('GD音乐台解析歌曲数据为空');
// 处理不同数据结构 throw new Error('歌曲数据为空');
if (!data) {
console.error('GD音乐台解析歌曲数据为空');
throw new Error('歌曲数据为空');
}
const songName = data.name || '';
let artistNames = '';
// 处理不同的艺术家字段结构
if (data.artists && Array.isArray(data.artists)) {
artistNames = data.artists.map(artist => artist.name).join(' ');
} else if (data.ar && Array.isArray(data.ar)) {
artistNames = data.ar.map(artist => artist.name).join(' ');
} else if (data.artist) {
artistNames = typeof data.artist === 'string' ? data.artist : '';
}
const searchQuery = `${songName} ${artistNames}`.trim();
if (!searchQuery || searchQuery.length < 2) {
console.error('GD音乐台解析搜索查询过短', { name: songName, artists: artistNames });
throw new Error('搜索查询过短');
}
// 所有可用的音乐源 netease、kuwo、joox、tidal
const allSources = [
'kuwo', 'joox', 'tidal', 'netease'
] as MusicSourceType[];
console.log('GD音乐台开始搜索:', searchQuery);
// 依次尝试所有音源
for (const source of allSources) {
try {
const result = await searchAndGetUrl(source, searchQuery, quality);
if (result) {
console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`);
// 返回符合原API格式的数据
return {
data: {
data: {
url: result.url.replace(/\\/g, ''),
br: parseInt(result.br, 10) * 1000 || 320000,
size: result.size || 0,
md5: '',
platform: 'gdmusic',
gain: 0
},
params: {
id: parseInt(String(id), 10),
type: 'song'
}
}
};
}
} catch (error) {
console.error(`GD音乐台 ${source} 音源解析失败:`, error);
// 该音源失败,继续尝试下一个音源
continue;
}
}
console.log('GD音乐台所有音源均解析失败');
return null;
})(),
timeoutPromise
]);
} catch (error: any) {
if (error.message === 'GD音乐台解析超时') {
console.error('GD音乐台解析超时(15秒):', error);
} else {
console.error('GD音乐台解析完全失败:', error);
} }
const songName = data.name || '';
let artistNames = '';
// 处理不同的艺术家字段结构
if (data.artists && Array.isArray(data.artists)) {
artistNames = data.artists.map(artist => artist.name).join(' ');
} else if (data.ar && Array.isArray(data.ar)) {
artistNames = data.ar.map(artist => artist.name).join(' ');
} else if (data.artist) {
artistNames = typeof data.artist === 'string' ? data.artist : '';
}
const searchQuery = `${songName} ${artistNames}`.trim();
if (!searchQuery || searchQuery.length < 2) {
console.error('GD音乐台解析搜索查询过短', { name: songName, artists: artistNames });
throw new Error('搜索查询过短');
}
// 所有可用的音乐源
const allSources = [
'tencent', 'kugou', 'kuwo', 'migu', 'netease',
'joox', 'ytmusic', 'spotify', 'qobuz', 'deezer'
] as MusicSourceType[];
console.log('GD音乐台开始搜索:', searchQuery);
// 依次尝试所有音源
for (const source of allSources) {
try {
const result = await searchAndGetUrl(source, searchQuery, quality);
if (result) {
console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`);
// 返回符合原API格式的数据
return {
data: {
data: {
url: result.url.replace(/\\/g, ''),
br: parseInt(result.br, 10) * 1000 || 320000,
size: result.size || 0,
md5: '',
platform: 'gdmusic',
gain: 0
},
params: {
id: parseInt(String(id), 10),
type: 'song'
}
}
};
}
} catch (error) {
console.error(`GD音乐台 ${source} 音源解析失败:`, error);
// 该音源失败,继续尝试下一个音源
continue;
}
}
console.log('GD音乐台所有音源均解析失败');
return null;
} catch (error) {
console.error('GD音乐台解析完全失败:', error);
return null; return null;
} }
}; };
/**
* 获取音质映射
* @param qualitySetting 设置中的音质选项
* @returns 映射到GD音乐台的音质参数
*/
export const getQualityMapping = (qualitySetting: string): string => {
const qualityMap: Record<string, string> = {
standard: '128',
higher: '320',
exhigh: '320',
lossless: '740',
hires: '999',
jyeffect: '999',
sky: '999',
dolby: '999',
jymaster: '999'
};
return qualityMap[qualitySetting] || '320';
};
interface GDMusicUrlResult { interface GDMusicUrlResult {
url: string; url: string;
br: string; br: string;

View File

@@ -40,8 +40,3 @@ export function getListDetail(id: number | string) {
export function getAlbum(id: number | string) { export function getAlbum(id: number | string) {
return request.get('/album', { params: { id } }); return request.get('/album', { params: { id } });
} }
// 获取排行榜列表
export function getToplist() {
return request.get('/toplist');
}

View File

@@ -5,9 +5,7 @@ import { isElectron } from '@/utils';
import request from '@/utils/request'; import request from '@/utils/request';
import requestMusic from '@/utils/request_music'; import requestMusic from '@/utils/request_music';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { parseFromGDMusic } from './gdmusic'; import { parseFromGDMusic, getQualityMapping } from './gdmusic';
import type { SongResult } from '@/type/music';
import { searchAndGetBilibiliAudioUrl } from './bilibili';
const { addData, getData, deleteData } = musicDB; const { addData, getData, deleteData } = musicDB;
@@ -82,7 +80,7 @@ export const getMusicLrc = async (id: number) => {
} }
}; };
export const getParsingMusicUrl = async (id: number, data: SongResult) => { export const getParsingMusicUrl = async (id: number, data: any) => {
const settingStore = useSettingsStore(); const settingStore = useSettingsStore();
// 如果禁用了音乐解析功能,则直接返回空结果 // 如果禁用了音乐解析功能,则直接返回空结果
@@ -90,49 +88,15 @@ export const getParsingMusicUrl = async (id: number, data: SongResult) => {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } }); return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
} }
// 获取音源设置,优先使用歌曲自定义音源
const songId = String(id);
const savedSource = localStorage.getItem(`song_source_${songId}`);
let enabledSources: any[] = [];
// 如果有歌曲自定义音源,使用自定义音源
if (savedSource) {
try {
enabledSources = JSON.parse(savedSource);
console.log(`使用歌曲 ${id} 自定义音源:`, enabledSources);
if(enabledSources.includes('bilibili')){
// 构建搜索关键词,依次判断歌曲名称、歌手名称和专辑名称是否存在
const songName = data?.name || '';
const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
const name = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
console.log('开始搜索bilibili音频', name);
return {
data: {
code: 200,
message: 'success',
data: {
url: await searchAndGetBilibiliAudioUrl(name)
}
}
}
}
} catch (e) {
console.error('e',e)
console.error('解析自定义音源失败, 使用全局设置', e);
enabledSources = settingStore.setData.enabledMusicSources || [];
}
} else {
// 没有自定义音源,使用全局音源设置
enabledSources = settingStore.setData.enabledMusicSources || [];
}
// 检查是否选择了GD音乐台解析 // 检查是否选择了GD音乐台解析
const enabledSources = settingStore.setData.enabledMusicSources || [];
if (enabledSources.includes('gdmusic')) { if (enabledSources.includes('gdmusic')) {
// 获取音质设置并转换为GD音乐台格式 // 获取音质设置并转换为GD音乐台格式
try { try {
const gdResult = await parseFromGDMusic(id, data, '999'); const quality = getQualityMapping(settingStore.setData.musicQuality || 'higher');
// 调用封装的GD音乐台解析服务
const gdResult = await parseFromGDMusic(id, data, quality);
if (gdResult) { if (gdResult) {
return gdResult; return gdResult;
} }
@@ -175,55 +139,5 @@ export const updatePlaylistTracks = (params: {
pid: number; pid: number;
tracks: string; tracks: string;
}) => { }) => {
return request.post('/playlist/tracks', params); return request.get('/playlist/tracks', { params });
}; };
/**
* 根据类型获取列表数据
* @param type 列表类型 album/playlist
* @param id 列表ID
*/
export function getMusicListByType(type: string, id: string) {
if (type === 'album') {
return getAlbumDetail(id);
} else if (type === 'playlist') {
return getPlaylistDetail(id);
}
return Promise.reject(new Error('未知列表类型'));
}
/**
* 获取专辑详情
* @param id 专辑ID
*/
export function getAlbumDetail(id: string) {
return request({
url: '/album',
method: 'get',
params: {
id
}
});
}
/**
* 获取歌单详情
* @param id 歌单ID
*/
export function getPlaylistDetail(id: string) {
return request({
url: '/playlist/detail',
method: 'get',
params: {
id
}
});
}
export function subscribePlaylist(params: { t: number; id: number }) {
return request({
url: '/playlist/subscribe',
method: 'post',
params
});
}

View File

@@ -71,12 +71,12 @@ const { t } = useI18n();
const message = useMessage(); const message = useMessage();
const copyQQ = () => { const copyQQ = () => {
navigator.clipboard.writeText('algermusic'); navigator.clipboard.writeText('789288579');
message.success(t('common.copySuccess')); message.success('已复制到剪贴板');
}; };
const toDonateList = () => { const toDonateList = () => {
window.open('http://donate.alger.fun/download', '_blank'); window.open('http://donate.alger.fun', '_blank');
}; };
defineProps({ defineProps({

View File

@@ -193,6 +193,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { getMvUrl } from '@/api/mv'; import { getMvUrl } from '@/api/mv';
import { usePlayerStore } from '@/store/modules/player';
import { IMvItem } from '@/type/mv'; import { IMvItem } from '@/type/mv';
const { t } = useI18n(); const { t } = useI18n();
@@ -221,6 +222,7 @@ const emit = defineEmits<{
(e: 'prev', loading: (value: boolean) => void): void; (e: 'prev', loading: (value: boolean) => void): void;
}>(); }>();
const playerStore = usePlayerStore();
const mvUrl = ref<string>(); const mvUrl = ref<string>();
const playMode = ref<PlayMode>(PLAY_MODE.Auto); const playMode = ref<PlayMode>(PLAY_MODE.Auto);
@@ -357,6 +359,9 @@ const loadMvUrl = async (mv: IMvItem) => {
const handleClose = () => { const handleClose = () => {
emit('update:show', false); emit('update:show', false);
if (playerStore.playMusicUrl) {
playerStore.setIsPlay(true);
}
}; };
const handleEnded = () => { const handleEnded = () => {

View File

@@ -1,41 +1,6 @@
<template> <template>
<div class="donation-container"> <div class="donation-container">
<div class="qrcode-container"> <div class="refresh-container">
<div class="description">
<p>{{ t('donation.description') }}</p>
<p>{{ t('donation.message') }}</p>
<n-button type="primary" @click="toDonateList">
<template #icon>
<i class="ri-cup-line"></i>
</template>
{{ t('donation.toDonateList') }}
</n-button>
</div>
<div class="qrcode-grid">
<div class="qrcode-item">
<n-image
:src="alipay"
:alt="t('common.alipay')"
class="qrcode-image"
preview-disabled
/>
<span class="qrcode-label">{{ t('common.alipay') }}</span>
</div>
<div class="qrcode-item">
<n-image
:src="wechat"
:alt="t('common.wechat')"
class="qrcode-image"
preview-disabled
/>
<span class="qrcode-label">{{ t('common.wechat') }}</span>
</div>
</div>
</div>
<div class="header-container">
<h3 class="section-title">{{ t('donation.title') }}</h3>
<n-button secondary round size="small" :loading="isLoading" @click="fetchDonors"> <n-button secondary round size="small" :loading="isLoading" @click="fetchDonors">
<template #icon> <template #icon>
<i class="ri-refresh-line"></i> <i class="ri-refresh-line"></i>
@@ -43,13 +8,15 @@
{{ t('donation.refresh') }} {{ t('donation.refresh') }}
</n-button> </n-button>
</div> </div>
<div class="donation-grid" :class="{ 'grid-expanded': isExpanded }"> <div class="donation-grid" :class="{ 'grid-expanded': isExpanded }">
<div <div
v-for="donor in displayDonors" v-for="(donor, index) in displayDonors"
:key="donor.id" :key="donor.id"
class="donation-card" class="donation-card animate__animated"
:class="{ 'no-message': !donor.message }" :class="getAnimationClass(index)"
:style="{ animationDelay: `${index * 0.1}s` }"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
> >
<div class="card-content"> <div class="card-content">
<div class="donor-avatar"> <div class="donor-avatar">
@@ -57,45 +24,46 @@
:src="donor.avatar" :src="donor.avatar"
:fallback-src="defaultAvatar" :fallback-src="defaultAvatar"
round round
class="avatar-img" size="large"
class="animate__animated animate__pulse animate__infinite avatar-img"
/> />
<div class="donor-badge" :class="getBadgeClass(donor.badge)">
{{ donor.badge }}
</div>
</div> </div>
<div class="donor-info"> <div class="donor-info">
<div class="donor-meta"> <div class="donor-name">{{ donor.name }}</div>
<div class="donor-name">{{ donor.name }}</div> <div class="donation-meta">
<!-- <div class="price-tag">{{ donor.amount }}</div> --> <n-tag
:type="getAmountTagType(donor.amount)"
size="small"
class="donation-amount animate__animated"
round
bordered
>
{{ donor.amount }}
</n-tag>
<span class="donation-date">{{ donor.date }}</span>
</div> </div>
<div class="donation-date">{{ donor.date }}</div>
</div> </div>
</div> </div>
<div v-if="donor.message" class="donation-message">
<!-- 有留言的情况 --> <n-popover trigger="hover" placement="bottom">
<n-popover <template #trigger>
v-if="donor.message" <div class="message-content">
trigger="hover" <i class="ri-double-quotes-l quote-icon"></i>
placement="bottom" <div class="message-text">{{ donor.message }}</div>
:show-arrow="true" <i class="ri-double-quotes-r quote-icon"></i>
:width="240" </div>
> </template>
<template #trigger> <div class="message-popup">
<div class="donation-message">
<i class="ri-double-quotes-l quote-icon"></i> <i class="ri-double-quotes-l quote-icon"></i>
<span class="message-text">{{ donor.message }}</span> {{ donor.message }}
<i class="ri-double-quotes-r quote-icon"></i> <i class="ri-double-quotes-r quote-icon"></i>
</div> </div>
</template> </n-popover>
<div class="message-popover">
<i class="ri-double-quotes-l quote-icon"></i>
<span>{{ donor.message }}</span>
<i class="ri-double-quotes-r quote-icon"></i>
</div>
</n-popover>
<!-- 没有留言的情况显示占位符 -->
<div v-else class="donation-message-placeholder">
<i class="ri-emotion-line"></i>
<span>{{ t('donation.noMessage') }}</span>
</div> </div>
<div class="card-sparkles"></div>
</div> </div>
</div> </div>
@@ -107,6 +75,40 @@
{{ isExpanded ? t('common.collapse') : t('common.expand') }} {{ isExpanded ? t('common.collapse') : t('common.expand') }}
</n-button> </n-button>
</div> </div>
<div class="p-6 rounded-lg shadow-lg">
<div class="description text-center text-sm text-gray-700 dark:text-gray-200">
<p>{{ t('donation.description') }}</p>
<p>{{ t('donation.message') }}</p>
</div>
<div class="flex justify-between mt-6">
<div class="flex flex-col items-center gap-2">
<n-image
:src="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>
</div>
<n-button type="primary" @click="toDonateList">
<template #icon>
<i class="ri-cup-line"></i>
</template>
{{ t('donation.toDonateList') }}
<i class="ri-arrow-right-s-line"></i>
</n-button>
<div class="flex flex-col items-center gap-2">
<n-image
:src="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>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -150,9 +152,72 @@ onActivated(() => {
fetchDonors(); fetchDonors();
}); });
// 只按金额排序的捐赠列表 // 动画类名列表
const animationClasses = [
'animate__fadeInUp',
'animate__fadeInLeft',
'animate__fadeInRight',
'animate__zoomIn'
];
// 获取随机动画类名
const getAnimationClass = (index: number) => {
return animationClasses[index % animationClasses.length];
};
// 根据金额获取标签类型
const getAmountTagType = (amount: number): 'success' | 'warning' | 'error' | 'info' => {
if (amount >= 5) return 'warning';
if (amount >= 2) return 'success';
return 'info';
};
// 获取徽章样式类名
const getBadgeClass = (badge: string): string => {
if (badge.includes('金牌')) return 'badge-gold';
if (badge.includes('银牌')) return 'badge-silver';
return 'badge-bronze';
};
// 鼠标悬停效果
const handleMouseEnter = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement;
card.style.transform = 'translateY(-2px)';
card.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.12)';
// 添加金额标签动画
const amountTag = card.querySelector('.donation-amount');
if (amountTag) {
amountTag.classList.add('animate__tada');
}
};
const handleMouseLeave = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement;
card.style.transform = 'translateY(0)';
card.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.08)';
// 移除金额标签动画
const amountTag = card.querySelector('.donation-amount');
if (amountTag) {
amountTag.classList.remove('animate__tada');
}
};
// 按金额和留言排序的捐赠列表
const sortedDonors = computed(() => { const sortedDonors = computed(() => {
return [...donors.value].sort((a, b) => b.amount - a.amount); return [...donors.value].sort((a, b) => {
// 如果一个有留言一个没有,有留言的排在前面
if (a.message && !b.message) return -1;
if (!a.message && b.message) return 1;
// 都有留言或都没有留言时,按金额从大到小排序
const amountDiff = b.amount - a.amount;
if (amountDiff !== 0) return amountDiff;
// 金额相同时,按日期从新到旧排序
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
}); });
const isExpanded = ref(false); const isExpanded = ref(false);
@@ -169,27 +234,19 @@ const toggleExpand = () => {
}; };
const toDonateList = () => { const toDonateList = () => {
window.open('http://donate.alger.fun/download', '_blank'); window.open('http://donate.alger.fun', '_blank');
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.donation-container { .donation-container {
@apply w-full overflow-hidden flex flex-col gap-4; @apply w-full overflow-hidden;
}
.header-container {
@apply flex justify-between items-center px-4 py-2;
.section-title {
@apply text-lg font-medium text-gray-700 dark:text-gray-200;
}
} }
.donation-grid { .donation-grid {
@apply grid gap-3 transition-all duration-300 overflow-hidden; @apply grid gap-3 px-2 py-3 transition-all duration-300 overflow-hidden;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
max-height: 320px; max-height: 280px;
@media (min-width: 768px) { @media (min-width: 768px) {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -205,138 +262,127 @@ const toDonateList = () => {
} }
.donation-card { .donation-card {
@apply rounded-lg p-2.5 transition-all duration-200 hover:shadow-md; @apply relative rounded-lg p-3 min-w-0 w-full transition-all duration-500 shadow-md backdrop-blur-md;
@apply bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm; @apply bg-gradient-to-br from-white/[0.03] to-white/[0.08] border border-white/[0.08];
@apply border border-gray-200 dark:border-gray-700/10; @apply hover:shadow-lg;
@apply flex flex-col;
min-height: 100px;
.card-content { .card-content {
@apply flex items-start gap-2 mb-2; @apply relative z-10 flex items-start gap-3;
} }
}
.donor-avatar { .donor-avatar {
@apply relative flex-shrink-0; @apply relative flex-shrink-0 w-10 h-9 transition-transform duration-300;
.avatar-img { .avatar-img {
@apply border border-gray-200 dark:border-gray-700/10 shadow-sm; @apply border border-white/20 dark:border-gray-800/50 shadow-sm;
@apply w-9 h-9; @apply w-10 h-9;
}
} }
}
.donor-info { .donor-badge {
@apply flex-1 min-w-0 flex flex-col justify-center; @apply absolute -bottom-2 -right-1 px-1.5 py-0.5 text-xs font-medium text-white/90 rounded-full whitespace-nowrap;
@apply bg-gradient-to-r from-pink-400 to-pink-500 shadow-sm opacity-90 scale-90;
@apply transition-all duration-300;
}
.donor-info {
@apply flex-1 min-w-0;
.donor-meta {
@apply flex justify-between items-center mb-0.5;
.donor-name { .donor-name {
@apply text-sm font-medium truncate flex-1 mr-1; @apply text-sm font-medium mb-0.5 truncate;
} }
.price-tag { .donation-meta {
@apply text-xs text-gray-400/80 dark:text-gray-500/80 whitespace-nowrap; @apply flex items-center gap-2 mb-1;
.donation-date {
@apply text-xs text-gray-400/80 dark:text-gray-500/80;
}
} }
} }
.donation-date { .donation-message {
@apply text-xs text-gray-400/60 dark:text-gray-500/60; @apply text-sm text-gray-600 dark:text-gray-300 leading-relaxed mt-3 w-full;
}
}
.donation-message { .message-content {
@apply text-xs text-gray-500 dark:text-gray-400 italic mt-1 px-2 py-1.5; @apply relative p-2 rounded-lg transition-all duration-300 cursor-pointer;
@apply bg-gray-100/10 dark:bg-dark-300 rounded; @apply bg-white/[0.02] hover:bg-[var(--n-color)];
@apply flex items-start;
@apply cursor-pointer transition-all duration-200; .message-text {
@apply px-6 italic line-clamp-2;
.quote-icon { }
@apply text-gray-300 dark:text-gray-600 flex-shrink-0 opacity-60;
.quote-icon {
&:first-child { @apply absolute text-gray-400/60 dark:text-gray-500/60 text-sm;
@apply mr-1 self-start;
&:first-child {
@apply left-0.5 top-2;
}
&:last-child {
@apply right-0.5 bottom-2;
}
}
} }
&:last-child {
@apply ml-1 self-end;
}
}
.message-text {
@apply flex-1 line-clamp-2;
} }
&:hover { &:hover {
@apply bg-gray-100/40 dark:bg-dark-200; .donor-avatar {
@apply scale-105 rotate-3;
}
.donor-badge {
@apply scale-95 -translate-y-0.5;
}
.card-sparkles {
@apply opacity-60 scale-110;
}
} }
} }
.donation-message-placeholder { .card-sparkles {
@apply text-xs text-gray-400 dark:text-gray-500 mt-1 px-2 py-1.5; @apply absolute inset-0 pointer-events-none opacity-0 transition-all duration-500;
@apply bg-gray-100/5 dark:bg-dark-300 rounded; background-image: radial-gradient(2px 2px at 20px 30px, rgba(255, 255, 255, 0.95), transparent),
@apply flex items-center justify-center gap-1 italic; radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.95), transparent),
@apply border border-transparent; radial-gradient(2.5px 2.5px at 50px 160px, rgba(255, 255, 255, 0.95), transparent),
radial-gradient(2px 2px at 90px 40px, rgba(255, 255, 255, 0.95), transparent),
i { radial-gradient(2.5px 2.5px at 130px 80px, rgba(255, 255, 255, 0.95), transparent);
@apply text-gray-300 dark:text-gray-600; background-size: 200% 200%;
animation: sparkle 8s ease infinite;
}
@keyframes sparkle {
0%,
100% {
@apply bg-[0%_0%] opacity-40 scale-100;
}
50% {
@apply bg-[100%_100%] opacity-80 scale-110;
} }
} }
.message-popover { .refresh-container {
@apply text-sm text-gray-700 dark:text-gray-200 italic p-2; @apply flex justify-end px-2 py-2;
@apply flex items-start;
.quote-icon {
@apply text-gray-400 dark:text-gray-500 flex-shrink-0;
&:first-child {
@apply mr-1.5 self-start;
}
&:last-child {
@apply ml-1.5 self-end;
}
}
} }
.expand-button { .expand-button {
@apply flex justify-center items-center py-2; @apply flex justify-center items-center py-2;
:deep(.n-button) { :deep(.n-button) {
@apply transition-all duration-200 hover:-translate-y-0.5; @apply transition-all duration-300 hover:-translate-y-0.5;
} }
} }
.qrcode-container { .message-popup {
@apply p-5 rounded-lg shadow-sm bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm border border-gray-200 dark:border-gray-700/10; @apply relative px-4 py-2 text-sm;
max-width: 300px;
.description { line-height: 1.6;
@apply text-center text-sm text-gray-600 dark:text-gray-300 mb-4; font-style: italic;
p { .quote-icon {
@apply mb-2; @apply text-gray-400/60 dark:text-gray-500/60;
} font-size: 0.9rem;
}
.qrcode-grid {
@apply flex justify-between items-center gap-4 flex-wrap;
.qrcode-item {
@apply flex flex-col items-center gap-2;
.qrcode-image {
@apply w-36 h-36 rounded-lg border border-gray-200 dark:border-gray-700/10 shadow-sm transition-transform duration-200 hover:scale-105;
}
.qrcode-label {
@apply text-sm text-gray-600 dark:text-gray-300;
}
}
.donate-button {
@apply flex flex-col items-center justify-center;
}
} }
} }
</style> </style>

View File

@@ -45,7 +45,8 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { isElectron } from '@/utils'; import { isElectron, isMobile } from '@/utils';
import { getLatestReleaseInfo, getProxyNodes } from '@/utils/update';
import config from '../../../../package.json'; import config from '../../../../package.json';
@@ -53,6 +54,7 @@ const { t } = useI18n();
const showModal = ref(false); const showModal = ref(false);
const noPrompt = ref(false); const noPrompt = ref(false);
const releaseInfo = ref<any>(null);
const closeModal = () => { const closeModal = () => {
showModal.value = false; showModal.value = false;
@@ -61,9 +63,11 @@ const closeModal = () => {
} }
}; };
const proxyHosts = ref<string[]>([]);
onMounted(async () => { onMounted(async () => {
// 如果是 electron 环境,不显示安装提示 // 如果是 electron 环境,不显示安装提示
if (isElectron) { if (isElectron || isMobile.value) {
return; return;
} }
@@ -72,11 +76,58 @@ onMounted(async () => {
if (isDismissed) { if (isDismissed) {
return; return;
} }
// 获取最新版本信息
releaseInfo.value = await getLatestReleaseInfo();
showModal.value = true; showModal.value = true;
proxyHosts.value = await getProxyNodes();
}); });
const handleInstall = async (): Promise<void> => { const handleInstall = async (): Promise<void> => {
window.open('http://donate.alger.fun/download', '_blank'); const assets = releaseInfo.value?.assets || [];
const { userAgent } = navigator;
const isMac = userAgent.toLowerCase().includes('mac');
const isWindows = userAgent.toLowerCase().includes('win');
const isLinux = userAgent.toLowerCase().includes('linux');
const isX64 =
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
let downloadUrl = '';
// 根据平台和架构选择对应的安装包
if (isMac) {
// macOS
const macAsset = assets.find((asset) => asset.name.includes('mac'));
downloadUrl = macAsset?.browser_download_url || '';
} else if (isWindows) {
// Windows
let winAsset = assets.find(
(asset) =>
asset.name.includes('win') &&
(isX64 ? asset.name.includes('x64') : asset.name.includes('ia32'))
);
if (!winAsset) {
winAsset = assets.find((asset) => asset.name.includes('win.exe'));
}
downloadUrl = winAsset?.browser_download_url || '';
} else if (isLinux) {
// Linux
const linuxAsset = assets.find(
(asset) =>
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
asset.name.includes('x64')
);
downloadUrl = linuxAsset?.browser_download_url || '';
}
if (downloadUrl) {
const proxyDownloadUrl = `${proxyHosts.value[0]}/${downloadUrl}`;
window.open(proxyDownloadUrl, '_blank');
} else {
// 如果没有找到对应的安装包,跳转到 release 页面
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
}
closeModal();
}; };
</script> </script>

View File

@@ -1,38 +0,0 @@
import { Router } from 'vue-router';
import { useMusicStore } from '@/store/modules/music';
/**
* 导航到音乐列表页面的通用方法
* @param router Vue路由实例
* @param options 导航选项
*/
export function navigateToMusicList(
router: Router,
options: {
id?: string | number;
type?: 'album' | 'playlist' | 'dailyRecommend' | string;
name: string;
songList: any[];
listInfo?: any;
canRemove?: boolean;
}
) {
const musicStore = useMusicStore();
const { id, type, name, songList, listInfo, canRemove = false } = options;
// 保存数据到状态管理
musicStore.setCurrentMusicList(songList, name, listInfo, canRemove);
// 路由跳转
if (id) {
router.push({
name: 'musicList',
params: { id },
query: { type }
});
} else {
router.push({
name: 'musicList'
});
}
}

View File

@@ -1,12 +1,11 @@
<template> <template>
<div v-if="isPlay && !isMobile" class="bottom" :style="{ height }"></div> <div v-if="isPlay" class="bottom" :style="{ height }"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import { isMobile } from '@/utils';
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
const isPlay = computed(() => playerStore.playMusicUrl); const isPlay = computed(() => playerStore.playMusicUrl);

View File

@@ -254,7 +254,7 @@ watch(
} }
.playlist-drawer { .playlist-drawer {
@apply flex flex-col gap-6 py-6; @apply flex flex-col gap-6;
} }
.create-playlist-section { .create-playlist-section {
@@ -335,7 +335,7 @@ watch(
} }
.playlist-list { .playlist-list {
@apply flex flex-col gap-2 pb-40; @apply flex flex-col gap-2;
} }
.playlist-item { .playlist-item {
@@ -367,9 +367,4 @@ watch(
} }
} }
} }
:deep(.n-drawer-body-content-wrapper) {
padding-bottom: 0 !important;
padding-top: 0 !important;
}
</style> </style>

View File

@@ -21,6 +21,15 @@
<span>{{ item.size }}</span> <span>{{ item.size }}</span>
</div> </div>
<music-list
v-if="['专辑', 'playlist'].includes(item.type)"
v-model:show="showPop"
:name="item.name"
:song-list="songList"
:list-info="listInfo"
:cover="false"
:z-index="zIndex"
/>
<mv-player <mv-player
v-if="item.type === 'mv'" v-if="item.type === 'mv'"
v-model:show="showPop" v-model:show="showPop"
@@ -33,11 +42,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { getAlbum, getListDetail } from '@/api/list'; import { getAlbum, getListDetail } from '@/api/list';
import MvPlayer from '@/components/MvPlayer.vue'; import MvPlayer from '@/components/MvPlayer.vue';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import { IMvItem } from '@/type/mv'; import { IMvItem } from '@/type/mv';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
import { useRouter } from 'vue-router';
import { useMusicStore } from '@/store/modules/music'; import MusicList from '../MusicList.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -62,8 +72,6 @@ const showPop = ref(false);
const listInfo = ref<any>(null); const listInfo = ref<any>(null);
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
const router = useRouter();
const musicStore = useMusicStore();
const getCurrentMv = () => { const getCurrentMv = () => {
return { return {
@@ -75,6 +83,7 @@ const getCurrentMv = () => {
const handleClick = async () => { const handleClick = async () => {
listInfo.value = null; listInfo.value = null;
if (props.item.type === '专辑') { if (props.item.type === '专辑') {
showPop.value = true;
const res = await getAlbum(props.item.id); const res = await getAlbum(props.item.id);
songList.value = res.data.songs.map((song: any) => { songList.value = res.data.songs.map((song: any) => {
song.al.picUrl = song.al.picUrl || props.item.picUrl; song.al.picUrl = song.al.picUrl || props.item.picUrl;
@@ -88,47 +97,24 @@ const handleClick = async () => {
}, },
description: res.data.album.description description: res.data.album.description
}; };
}
// 保存数据到store
musicStore.setCurrentMusicList( if (props.item.type === 'playlist') {
songList.value, showPop.value = true;
props.item.name,
listInfo.value,
false
);
// 使用路由跳转
router.push({
name: 'musicList',
params: { id: props.item.id },
query: { type: 'album' }
});
} else if (props.item.type === 'playlist') {
const res = await getListDetail(props.item.id); const res = await getListDetail(props.item.id);
songList.value = res.data.playlist.tracks; songList.value = res.data.playlist.tracks;
listInfo.value = res.data.playlist; listInfo.value = res.data.playlist;
}
// 保存数据到store
musicStore.setCurrentMusicList( if (props.item.type === 'mv') {
songList.value,
props.item.name,
listInfo.value,
false
);
// 使用路由跳转
router.push({
name: 'musicList',
params: { id: props.item.id },
query: { type: 'playlist' }
});
} else if (props.item.type === 'mv') {
handleShowMv(); handleShowMv();
} }
}; };
const handleShowMv = async () => { const handleShowMv = async () => {
playerStore.handlePause(); playerStore.setIsPlay(false);
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
showPop.value = true; showPop.value = true;
}; };
</script> </script>

View File

@@ -1,19 +1,14 @@
<template> <template>
<div <div
class="song-item" class="song-item"
:class="{ 'song-mini': mini, 'song-list': list, 'song-compact': compact }" :class="{ 'song-mini': mini, 'song-list': list }"
@contextmenu.prevent="handleContextMenu" @contextmenu.prevent="handleContextMenu"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
> >
<div v-if="compact && index !== undefined" class="song-item-index" :class="{ 'text-green-500': isPlaying }">
{{ index + 1 }}
</div>
<div v-if="selectable" class="song-item-select" @click.stop="toggleSelect"> <div v-if="selectable" class="song-item-select" @click.stop="toggleSelect">
<n-checkbox :checked="selected" /> <n-checkbox :checked="selected" />
</div> </div>
<n-image <n-image
v-if="item.picUrl && !compact" v-if="item.picUrl"
ref="songImg" ref="songImg"
:src="getImgUrl(item.picUrl, '100y100')" :src="getImgUrl(item.picUrl, '100y100')"
class="song-item-img" class="song-item-img"
@@ -23,9 +18,9 @@
}" }"
@load="imageLoad" @load="imageLoad"
/> />
<div class="song-item-content" :class="{ 'song-item-content-compact': compact }"> <div class="song-item-content">
<div v-if="list" class="song-item-content-wrapper"> <div v-if="list" class="song-item-content-wrapper">
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">{{ <n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{
item.name item.name
}}</n-ellipsis> }}</n-ellipsis>
<div class="song-item-content-divider">-</div> <div class="song-item-content-divider">-</div>
@@ -40,36 +35,9 @@
</template> </template>
</n-ellipsis> </n-ellipsis>
</div> </div>
<template v-else-if="compact">
<div class="song-item-content-compact-wrapper">
<div class="w-60 flex-shrink-0 flex items-center" @dblclick="playMusicEvent(item)">
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">
{{ item.name }}
</n-ellipsis>
</div>
<div class="w-40 flex-shrink-0 song-item-content-compact-artist flex items-center">
<n-ellipsis line-clamp="1">
<template v-for="(artist, index) in artists" :key="index">
<span
class="cursor-pointer hover:text-green-500"
@click.stop="handleArtistClick(artist.id)"
>{{ artist.name }}</span
>
<span v-if="index < artists.length - 1"> / </span>
</template>
</n-ellipsis>
</div>
</div>
<div class="song-item-content-album flex items-center">
<n-ellipsis line-clamp="1">{{ item.al?.name || '-' }}</n-ellipsis>
</div>
<div class="song-item-content-duration flex items-center">
{{ formatDuration(getDuration(item)) }}
</div>
</template>
<template v-else> <template v-else>
<div class="song-item-content-title" @dblclick="playMusicEvent(item)"> <div class="song-item-content-title">
<n-ellipsis class="text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">{{ item.name }}</n-ellipsis> <n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
</div> </div>
<div class="song-item-content-name"> <div class="song-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1"> <n-ellipsis class="text-ellipsis" line-clamp="1">
@@ -85,36 +53,22 @@
</div> </div>
</template> </template>
</div> </div>
<div class="song-item-operating" :class="{ <div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
'song-item-operating-list': list, <div v-if="favorite" class="song-item-operating-like">
'song-item-operating-compact': compact
}">
<div v-if="favorite" class="song-item-operating-like" :class="{ 'opacity-0': compact && !isHovering && !isFavorite }">
<i <i
class="iconfont icon-likefill" class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }" :class="{ 'like-active': isFavorite }"
@click.stop="toggleFavorite" @click.stop="toggleFavorite"
></i> ></i>
</div> </div>
<n-tooltip v-if="isNext" trigger="hover" :z-index="9999999" :delay="400">
<template #trigger>
<div class="song-item-operating-next" @click.stop="handlePlayNext">
<i class="iconfont ri-skip-forward-fill"></i>
</div>
</template>
{{ t('songItem.menu.playNext') }}
</n-tooltip>
<div <div
class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated" class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated"
:class="{ 'bg-green-600': isPlaying, 'animate__flipInY': playLoading, 'opacity-0': compact && !isHovering && !isPlaying }" :class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
@click="playMusicEvent(item)" @click="playMusicEvent(item)"
> >
<i v-if="isPlaying && play" class="iconfont icon-stop"></i> <i v-if="isPlaying && play" class="iconfont icon-stop"></i>
<i v-else class="iconfont icon-playfill"></i> <i v-else class="iconfont icon-playfill"></i>
</div> </div>
<div v-if="compact" class="song-item-operating-menu" @click.stop="handleMenuClick" :class="{ 'opacity-0': compact && !isHovering && !isPlaying }">
<i class="iconfont ri-more-fill"></i>
</div>
</div> </div>
<n-dropdown <n-dropdown
v-if="isElectron" v-if="isElectron"
@@ -122,7 +76,7 @@
:x="dropdownX" :x="dropdownX"
:y="dropdownY" :y="dropdownY"
:options="dropdownOptions" :options="dropdownOptions"
:z-index="99999999" :z-index="99999"
placement="bottom-start" placement="bottom-start"
@clickoutside="showDropdown = false" @clickoutside="showDropdown = false"
@select="handleSelect" @select="handleSelect"
@@ -133,12 +87,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import type { MenuOption } from 'naive-ui'; import type { MenuOption } from 'naive-ui';
import { NEllipsis, NImage, useMessage } from 'naive-ui'; import { NImage, NText, useMessage } from 'naive-ui';
import { computed, h, inject, ref, useTemplateRef } from 'vue'; import { computed, h, inject, ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { getSongUrl } from '@/store/modules/player'; import { getSongUrl } from '@/hooks/MusicListHook';
import { useArtist } from '@/hooks/useArtist'; import { useArtist } from '@/hooks/useArtist';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store'; import { usePlayerStore } from '@/store';
import type { SongResult } from '@/type/music'; import type { SongResult } from '@/type/music';
import { getImgUrl, isElectron } from '@/utils'; import { getImgUrl, isElectron } from '@/utils';
@@ -151,24 +106,18 @@ const props = withDefaults(
item: SongResult; item: SongResult;
mini?: boolean; mini?: boolean;
list?: boolean; list?: boolean;
compact?: boolean;
favorite?: boolean; favorite?: boolean;
selectable?: boolean; selectable?: boolean;
selected?: boolean; selected?: boolean;
canRemove?: boolean; canRemove?: boolean;
isNext?: boolean;
index?: number;
}>(), }>(),
{ {
mini: false, mini: false,
list: false, list: false,
compact: false,
favorite: true, favorite: true,
selectable: false, selectable: false,
selected: false, selected: false,
canRemove: false, canRemove: false
isNext: false,
index: undefined
} }
); );
@@ -188,7 +137,6 @@ const isPlaying = computed(() => {
const showDropdown = ref(false); const showDropdown = ref(false);
const dropdownX = ref(0); const dropdownX = ref(0);
const dropdownY = ref(0); const dropdownY = ref(0);
const isHovering = ref(false);
const isDownloading = ref(false); const isDownloading = ref(false);
@@ -214,49 +162,26 @@ const renderSongPreview = () => {
h( h(
'div', 'div',
{ {
class: 'flex-1 min-w-0 py-1 overflow-hidden' class: 'flex-1 min-w-0 py-1'
}, },
[ [
h( h(
'div', 'div',
{ {
class: 'mb-1 overflow-hidden' class: 'mb-1'
}, },
[ [
h( h(
NEllipsis, NText,
{ {
lineClamp: 1,
depth: 1, depth: 1,
class: 'text-sm font-medium w-full', class: 'text-sm font-medium'
style: 'max-width: 150px; min-width: 120px;'
}, },
{ {
default: () => props.item.name default: () => props.item.name
} }
) )
] ]
),
h(
'div',
{
class: 'text-xs text-gray-500 dark:text-gray-400 overflow-hidden'
},
[
h(
NEllipsis,
{
lineClamp: 1,
style: 'max-width: 150px;'
},
{
default: () => {
const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(' / ');
return artistNames || '未知艺术家';
}
}
)
]
) )
] ]
) )
@@ -333,13 +258,6 @@ const handleContextMenu = (e: MouseEvent) => {
dropdownY.value = e.clientY; dropdownY.value = e.clientY;
}; };
const handleMenuClick = (e: MouseEvent) => {
e.preventDefault();
showDropdown.value = true;
dropdownX.value = e.clientX;
dropdownY.value = e.clientY;
};
const handleSelect = (key: string | number) => { const handleSelect = (key: string | number) => {
showDropdown.value = false; showDropdown.value = false;
if (key === 'download') { if (key === 'download') {
@@ -443,6 +361,18 @@ const imageLoad = async () => {
// 播放音乐 设置音乐详情 打开音乐底栏 // 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = async (item: SongResult) => { const playMusicEvent = async (item: SongResult) => {
// 如果是当前正在播放的音乐,则切换播放/暂停状态
if (playMusic.value.id === item.id) {
if (play.value) {
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
} else {
playerStore.setPlayMusic(true);
audioService.getCurrentSound()?.play();
}
return;
}
try { try {
// 使用store的setPlay方法该方法已经包含了B站视频URL处理逻辑 // 使用store的setPlay方法该方法已经包含了B站视频URL处理逻辑
const result = await playerStore.setPlay(item); const result = await playerStore.setPlay(item);
@@ -495,33 +425,6 @@ const handlePlayNext = () => {
playerStore.addToNextPlay(props.item); playerStore.addToNextPlay(props.item);
message.success(t('songItem.message.addedToNextPlay')); message.success(t('songItem.message.addedToNextPlay'));
}; };
// 获取歌曲时长
const getDuration = (item: SongResult): number => {
// 检查各种可能的时长属性路径
if (item.duration) return item.duration;
if (typeof item.dt === 'number') return item.dt;
// 遍历可能存在的其他时长属性路径
return 0;
};
// 格式化时长
const formatDuration = (ms: number): string => {
if (!ms) return '--:--';
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
// 鼠标悬停事件
const handleMouseEnter = () => {
isHovering.value = true;
};
const handleMouseLeave = () => {
isHovering.value = false;
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -538,24 +441,13 @@ const handleMouseLeave = () => {
@apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900; @apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;
&:hover { &:hover {
@apply bg-light-100 dark:bg-dark-100; @apply bg-gray-100 dark:bg-gray-800;
.song-item-operating-compact {
.song-item-operating-like,
.song-item-operating-play {
@apply opacity-100;
}
}
} }
&-img { &-img {
@apply w-12 h-12 rounded-2xl mr-4; @apply w-12 h-12 rounded-2xl mr-4;
} }
&-index {
@apply w-8 text-center text-gray-500 dark:text-gray-400 text-sm;
}
&-content { &-content {
@apply flex-1; @apply flex-1;
@@ -566,26 +458,6 @@ const handleMouseLeave = () => {
&-name { &-name {
@apply text-xs text-gray-500 dark:text-gray-400; @apply text-xs text-gray-500 dark:text-gray-400;
} }
&-compact {
@apply flex items-center gap-4;
&-wrapper {
@apply flex-1 min-w-0;
}
&-artist {
@apply text-sm text-gray-500 dark:text-gray-400 ml-2;
}
}
&-album {
@apply w-32 text-sm text-gray-500 dark:text-gray-400;
}
&-duration {
@apply w-16 text-sm text-gray-500 dark:text-gray-400 text-right;
}
} }
&-operating { &-operating {
@@ -603,14 +475,6 @@ const handleMouseLeave = () => {
@apply mr-2 cursor-pointer ml-4 transition-all; @apply mr-2 cursor-pointer ml-4 transition-all;
} }
&-next {
@apply mr-2 cursor-pointer transition-all;
.iconfont {
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
}
}
.like-active { .like-active {
@apply text-red-500 dark:text-red-500; @apply text-red-500 dark:text-red-500;
} }
@@ -632,14 +496,6 @@ const handleMouseLeave = () => {
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500; @apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
} }
} }
&-menu {
@apply cursor-pointer flex items-center justify-center px-2;
.iconfont {
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
}
}
} }
&-select { &-select {
@@ -647,61 +503,6 @@ const handleMouseLeave = () => {
} }
} }
.song-compact {
@apply rounded-lg p-2 h-12 mb-1 border-b dark:border-gray-800 border-gray-100;
&:hover {
@apply bg-gray-50 dark:bg-gray-700;
.opacity-0 {
opacity: 1;
}
}
.song-item-content {
&-title {
@apply text-sm cursor-pointer;
}
}
.song-item-content-compact-wrapper {
@apply flex items-center;
}
.song-item-content-compact-artist {
@apply w-40;
}
.song-item-operating-compact {
@apply border-none bg-transparent gap-2 flex items-center;
.song-item-operating-like,
.song-item-operating-play {
@apply transition-opacity duration-200;
}
.song-item-operating-play {
@apply w-7 h-7;
.iconfont {
@apply text-base;
}
}
.song-item-operating-like {
@apply mr-1 ml-0;
.iconfont {
@apply text-base;
}
}
.opacity-0 {
opacity: 0;
}
}
}
.song-mini { .song-mini {
@apply p-2 rounded-2xl; @apply p-2 rounded-2xl;

View File

@@ -22,19 +22,26 @@
</div> </div>
</template> </template>
</div> </div>
<music-list
v-model:show="showMusic"
:name="albumName"
:song-list="songList"
:cover="true"
:loading="loadingList"
:list-info="albumInfo"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getNewAlbum } from '@/api/home'; import { getNewAlbum } from '@/api/home';
import { getAlbum } from '@/api/list'; import { getAlbum } from '@/api/list';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils'; import MusicList from '@/components/MusicList.vue';
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
import type { IAlbumNew } from '@/type/album'; import type { IAlbumNew } from '@/type/album';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const { t } = useI18n(); const { t } = useI18n();
const albumData = ref<IAlbumNew>(); const albumData = ref<IAlbumNew>();
@@ -43,42 +50,33 @@ const loadAlbumList = async () => {
albumData.value = data; albumData.value = data;
}; };
const router = useRouter(); const showMusic = ref(false);
const songList = ref([]);
const albumName = ref('');
const loadingList = ref(false);
const albumInfo = ref<any>({});
const handleClick = async (item: any) => { const handleClick = async (item: any) => {
openAlbum(item); songList.value = [];
}; albumInfo.value = {};
albumName.value = item.name;
const openAlbum = async (album: any) => { loadingList.value = true;
if (!album) return; showMusic.value = true;
const res = await getAlbum(item.id);
try { const { songs, album } = res.data;
const res = await getAlbum(album.id); songList.value = songs.map((song: any) => {
const { songs, album: albumInfo } = res.data; song.al.picUrl = song.al.picUrl || album.picUrl;
song.picUrl = song.al.picUrl || album.picUrl || song.picUrl;
const formattedSongs = songs.map((song: any) => { return song;
song.al.picUrl = song.al.picUrl || albumInfo.picUrl; });
song.picUrl = song.al.picUrl || albumInfo.picUrl || song.picUrl; albumInfo.value = {
return song; ...album,
}); creator: {
avatarUrl: album.artist.img1v1Url,
navigateToMusicList(router, { nickname: `${album.artist.name} - ${album.company}`
id: album.id, },
type: 'album', description: album.description
name: album.name, };
songList: formattedSongs, loadingList.value = false;
listInfo: {
...albumInfo,
creator: {
avatarUrl: albumInfo.artist.img1v1Url,
nickname: `${albumInfo.artist.name} - ${albumInfo.company}`
},
description: albumInfo.description
}
});
} catch (error) {
console.error('获取专辑详情失败:', error);
}
}; };
onMounted(() => { onMounted(() => {

View File

@@ -23,7 +23,7 @@
></div> ></div>
<div <div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer" class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
@click="showDayRecommend" @click="showMusic = true"
> >
<div class="font-bold text-lg"> <div class="font-bold text-lg">
{{ t('comp.recommendSinger.title') }} {{ t('comp.recommendSinger.title') }}
@@ -57,7 +57,7 @@
v-for="item in userPlaylist" v-for="item in userPlaylist"
:key="item.id" :key="item.id"
class="user-play-item" class="user-play-item"
@click="openPlaylist(item)" @click="toPlaylist(item.id)"
> >
<div class="user-play-item-img"> <div class="user-play-item-img">
<img :src="getImgUrl(item.coverImgUrl, '200y200')" alt="" /> <img :src="getImgUrl(item.coverImgUrl, '200y200')" alt="" />
@@ -124,18 +124,35 @@
</n-carousel-item> </n-carousel-item>
</n-carousel> </n-carousel>
</div> </div>
<music-list
v-if="dayRecommendData?.dailySongs.length"
v-model:show="showMusic"
:name="t('comp.recommendSinger.songlist')"
:song-list="dayRecommendData?.dailySongs"
:cover="false"
/>
<!-- 添加用户歌单弹窗 -->
<music-list
v-model:show="showPlaylist"
v-model:loading="playlistLoading"
:name="playlistItem?.name || ''"
:song-list="playlistDetail?.playlist?.tracks || []"
:list-info="playlistDetail?.playlist"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, watchEffect } from 'vue'; import { onMounted, ref, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getDayRecommend, getHotSinger } from '@/api/home'; import { getDayRecommend, getHotSinger } from '@/api/home';
import { getListDetail } from '@/api/list'; import { getListDetail } from '@/api/list';
import { getMusicDetail } from '@/api/music'; import { getMusicDetail } from '@/api/music';
import { getUserPlaylist } from '@/api/user'; import { getUserPlaylist } from '@/api/user';
import MusicList from '@/components/MusicList.vue';
import { useArtist } from '@/hooks/useArtist'; import { useArtist } from '@/hooks/useArtist';
import { usePlayerStore, useUserStore } from '@/store'; import { usePlayerStore, useUserStore } from '@/store';
import { IDayRecommend } from '@/type/day_recommend'; import { IDayRecommend } from '@/type/day_recommend';
@@ -151,20 +168,20 @@ import {
setBackgroundImg setBackgroundImg
} from '@/utils'; } from '@/utils';
import { getArtistDetail } from '@/api/artist'; import { getArtistDetail } from '@/api/artist';
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
const userStore = useUserStore(); const userStore = useUserStore();
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
// 歌手信息 // 歌手信息
const hotSingerData = ref<IHotSinger>(); const hotSingerData = ref<IHotSinger>();
const dayRecommendData = ref<IDayRecommend>(); const dayRecommendData = ref<IDayRecommend>();
const showMusic = ref(false);
const userPlaylist = ref<Playlist[]>([]); const userPlaylist = ref<Playlist[]>([]);
// 为歌单弹窗添加的状态 // 为歌单弹窗添加的状态
const showPlaylist = ref(false);
const playlistLoading = ref(false); const playlistLoading = ref(false);
const playlistItem = ref<Playlist | null>(null); const playlistItem = ref<Playlist | null>(null);
const playlistDetail = ref<IListDetail | null>(null); const playlistDetail = ref<IListDetail | null>(null);
@@ -289,34 +306,27 @@ const handleArtistClick = (id: number) => {
navigateToArtist(id); navigateToArtist(id);
}; };
const showDayRecommend = () => { const toPlaylist = async (id: number) => {
if (!dayRecommendData.value?.dailySongs) return;
navigateToMusicList(router, {
type: 'dailyRecommend',
name: t('comp.recommendSinger.songlist'),
songList: dayRecommendData.value.dailySongs,
canRemove: false
});
};
const openPlaylist = (item: any) => {
playlistItem.value = item;
playlistLoading.value = true; playlistLoading.value = true;
playlistItem.value = null;
getListDetail(item.id).then(res => { playlistDetail.value = null;
playlistDetail.value = res.data; showPlaylist.value = true;
// 设置当前点击的歌单信息
const selectedPlaylist = userPlaylist.value.find((item) => item.id === id);
if (selectedPlaylist) {
playlistItem.value = selectedPlaylist;
}
try {
// 获取歌单详情
const { data } = await getListDetail(id);
playlistDetail.value = data;
} catch (error) {
console.error('获取歌单详情失败:', error);
} finally {
playlistLoading.value = false; playlistLoading.value = false;
}
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: false
});
});
}; };
// 添加直接播放歌单的方法 // 添加直接播放歌单的方法

View File

@@ -31,34 +31,34 @@
<!-- 控制按钮区域 --> <!-- 控制按钮区域 -->
<div class="control-buttons"> <div class="control-buttons">
<div class="control-button previous" @click="handlePrev"> <button class="control-button previous" @click="handlePrev">
<i class="iconfont icon-prev"></i> <i class="iconfont icon-prev"></i>
</div> </button>
<div class="control-button play" @click="playMusicEvent"> <button class="control-button play" @click="playMusicEvent">
<i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i> <i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div> </button>
<div class="control-button next" @click="handleNext"> <button class="control-button next" @click="handleNext">
<i class="iconfont icon-next"></i> <i class="iconfont icon-next"></i>
</div> </button>
</div> </div>
<!-- 右侧功能按钮 --> <!-- 右侧功能按钮 -->
<div class="function-buttons"> <div class="function-buttons">
<div class="function-button"> <button class="function-button">
<i <i
class="iconfont icon-likefill" class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }" :class="{ 'like-active': isFavorite }"
@click="toggleFavorite" @click="toggleFavorite"
></i> ></i>
</div> </button>
<n-popover v-if="component" trigger="hover" :z-index="99999999" placement="top" :show-arrow="false"> <n-popover trigger="click" :z-index="99999999" placement="top" :show-arrow="false">
<template #trigger> <template #trigger>
<div class="function-button" @click="mute"> <button class="function-button" @click="mute">
<i class="iconfont" :class="getVolumeIcon"></i> <i class="iconfont" :class="getVolumeIcon"></i>
</div> </button>
</template> </template>
<div class="volume-slider-wrapper transparent-popover"> <div class="volume-slider-wrapper">
<n-slider <n-slider
v-model:value="volumeSlider" v-model:value="volumeSlider"
:step="0.01" :step="0.01"
@@ -69,15 +69,15 @@
</n-popover> </n-popover>
<!-- 播放列表按钮 --> <!-- 播放列表按钮 -->
<div v-if="!component" class="function-button" @click="togglePlaylist"> <button v-if="!component" class="function-button" @click="togglePlaylist">
<i class="iconfont icon-list"></i> <i class="iconfont icon-list"></i>
</div> </button>
</div> </div>
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
<div v-if="!component" class="close-button" @click="handleClose"> <button v-if="!component" class="close-button" @click="handleClose">
<i class="iconfont ri-close-line"></i> <i class="iconfont ri-close-line"></i>
</div> </button>
</div> </div>
<!-- 进度条 --> <!-- 进度条 -->
@@ -312,7 +312,25 @@ const handleNext = () => playerStore.nextPlay();
const playMusicEvent = async () => { const playMusicEvent = async () => {
try { try {
playerStore.setPlay(playerStore.playMusic); if (!playerStore.playMusic?.id || !playerStore.playMusicUrl) {
console.warn('No valid music or URL available');
playerStore.setPlay(playerStore.playMusic);
return;
}
if (play.value) {
if (audioService.getCurrentSound()) {
audioService.pause();
playerStore.setPlayMusic(false);
}
} else {
if (audioService.getCurrentSound()) {
audioService.play();
} else {
await audioService.play(playerStore.playMusicUrl, playerStore.playMusic);
}
playerStore.setPlayMusic(true);
}
} catch (error) { } catch (error) {
console.error('播放出错:', error); console.error('播放出错:', error);
playerStore.nextPlay(); playerStore.nextPlay();
@@ -441,7 +459,7 @@ const setMusicFull = () => {
} }
.control-button { .control-button {
@apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200; @apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600;
width: 32px; width: 32px;
height: 32px; height: 32px;
@@ -466,9 +484,10 @@ const setMusicFull = () => {
} }
.function-button { .function-button {
@apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200; @apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer;
width: 32px; width: 32px;
height: 32px; height: 32px;
color: var(--text-color-2, #666);
&:hover { &:hover {
@apply bg-gray-100 dark:bg-dark-300; @apply bg-gray-100 dark:bg-dark-300;
@@ -527,7 +546,8 @@ const setMusicFull = () => {
} }
.volume-slider-wrapper { .volume-slider-wrapper {
@apply p-2 py-4 rounded-xl bg-white dark:bg-dark-100 shadow-lg bg-opacity-90 backdrop-blur; @apply p-4 rounded-xl bg-white dark:bg-dark-100 shadow-lg;
width: 40px;
height: 160px; height: 160px;
} }
@@ -588,8 +608,4 @@ const setMusicFull = () => {
} }
} }
} }
:deep(.n-popover){
background-color: transparent !important;
}
</style> </style>

View File

@@ -89,7 +89,7 @@
</div> </div>
<!-- 定时关闭按钮 --> <!-- 定时关闭按钮 -->
<!-- <SleepTimerPopover mode="mobile" /> --> <SleepTimerPopover mode="mobile" />
</template> </template>
<!-- Mini模式 - 在musicFullVisible为false时显示 --> <!-- Mini模式 - 在musicFullVisible为false时显示 -->
@@ -155,8 +155,10 @@ import { useThrottleFn } from '@vueuse/core';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import SongItem from '@/components/common/SongItem.vue'; import SongItem from '@/components/common/SongItem.vue';
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
import { allTime, artistList, nowTime, playMusic, sound, textColors } from '@/hooks/MusicHook'; import { allTime, artistList, nowTime, playMusic, sound, textColors } from '@/hooks/MusicHook';
import MusicFull from '@/layout/components/MusicFull.vue'; import MusicFull from '@/layout/components/MusicFull.vue';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
import type { SongResult } from '@/type/music'; import type { SongResult } from '@/type/music';
@@ -233,7 +235,25 @@ const toggleFavorite = () => {
// 播放暂停按钮事件 // 播放暂停按钮事件
const playMusicEvent = async () => { const playMusicEvent = async () => {
try { try {
playerStore.setPlay(playMusic.value); if (!playMusic.value?.id || !playerStore.playMusicUrl) {
console.warn('No valid music or URL available');
playerStore.setPlay(playMusic.value);
return;
}
if (play.value) {
if (audioService.getCurrentSound()) {
audioService.pause();
playerStore.setPlayMusic(false);
}
} else {
if (audioService.getCurrentSound()) {
audioService.play();
} else {
await audioService.play(playerStore.playMusicUrl, playMusic.value);
}
playerStore.setPlayMusic(true);
}
} catch (error) { } catch (error) {
console.error('播放出错:', error); console.error('播放出错:', error);
playerStore.nextPlay(); playerStore.nextPlay();

View File

@@ -39,9 +39,6 @@
lazy lazy
preview-disabled preview-disabled
/> />
<div v-if="playMusic?.playLoading" class="loading-overlay">
<i class="ri-loader-4-line loading-icon"></i>
</div>
<div class="hover-arrow"> <div class="hover-arrow">
<div class="hover-content"> <div class="hover-content">
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> --> <!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
@@ -127,12 +124,6 @@
</template> </template>
{{ playMusic.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }} {{ playMusic.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }}
</n-tooltip> </n-tooltip>
<n-tooltip v-if="playMusic.id && isElectron" trigger="hover" :z-index="9999999">
<template #trigger>
<reparse-popover v-if="playMusic.id" />
</template>
{{ t('player.playBar.reparse') }}
</n-tooltip>
<n-popover <n-popover
v-if="isElectron" v-if="isElectron"
trigger="click" trigger="click"
@@ -155,12 +146,42 @@
</n-popover> </n-popover>
<!-- 定时关闭功能 --> <!-- 定时关闭功能 -->
<sleep-timer-popover mode="desktop" /> <sleep-timer-popover mode="desktop" />
<n-tooltip trigger="hover" :z-index="9999999"> <n-popover
trigger="click"
:z-index="99999999"
content-class="music-play"
raw
:show-arrow="false"
:delay="200"
arrow-wrapper-style=" border-radius:1.5rem"
@update-show="scrollToPlayList"
>
<template #trigger> <template #trigger>
<i class="iconfont icon-list text-2xl hover:text-green-500 transition-colors cursor-pointer" @click="openPlayListDrawer"></i> <n-tooltip trigger="manual" :z-index="9999999">
<template #trigger>
<i class="iconfont icon-list"></i>
</template>
{{ t('player.playBar.playList') }}
</n-tooltip>
</template> </template>
{{ t('player.playBar.playList') }} <div class="music-play-list">
</n-tooltip> <div class="music-play-list-back"></div>
<n-virtual-list ref="palyListRef" :item-size="62" item-resizable :items="playList">
<template #default="{ item }">
<div class="music-play-list-content">
<div class="flex items-center justify-between">
<song-item :key="item.id" class="flex-1" :item="item" mini></song-item>
<div class="delete-btn" @click.stop="handleDeleteSong(item)">
<i
class="iconfont ri-delete-bin-line text-gray-400 hover:text-red-500 transition-colors"
></i>
</div>
</div>
</div>
</template>
</n-virtual-list>
</div>
</n-popover>
</div> </div>
<!-- 播放音乐 --> <!-- 播放音乐 -->
<music-full ref="MusicFullRef" v-model="musicFullVisible" :background="background" /> <music-full ref="MusicFullRef" v-model="musicFullVisible" :background="background" />
@@ -170,12 +191,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useThrottleFn } from '@vueuse/core'; import { useThrottleFn } from '@vueuse/core';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import { computed, ref, watch } from 'vue'; import { computed, ref, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SongItem from '@/components/common/SongItem.vue';
import EqControl from '@/components/EQControl.vue'; import EqControl from '@/components/EQControl.vue';
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue'; import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
import ReparsePopover from '@/components/player/ReparsePopover.vue';
import { import {
allTime, allTime,
artistList, artistList,
@@ -193,6 +214,7 @@ import {
usePlayerStore usePlayerStore
} from '@/store/modules/player'; } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
import type { SongResult } from '@/type/music';
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils'; import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
@@ -201,6 +223,8 @@ const { t } = useI18n();
const message = useMessage(); const message = useMessage();
// 是否播放 // 是否播放
const play = computed(() => playerStore.isPlay); const play = computed(() => playerStore.isPlay);
// 播放列表
const playList = computed(() => playerStore.playList as SongResult[]);
// 背景颜色 // 背景颜色
const background = ref('#000'); const background = ref('#000');
@@ -338,12 +362,42 @@ const showSliderTooltip = ref(false);
// 播放暂停按钮事件 // 播放暂停按钮事件
const playMusicEvent = async () => { const playMusicEvent = async () => {
try { try {
const result = await playerStore.setPlay({ ...playMusic.value}); // 检查是否有有效的音乐对象
if (result) { if (!playMusic.value?.id) {
console.warn('没有有效的播放对象');
return;
}
// 当前处于播放状态 -> 暂停
if (play.value) {
if (audioService.getCurrentSound()) {
audioService.pause();
playerStore.setPlayMusic(false);
}
return;
}
// 当前处于暂停状态 -> 播放
// 有音频实例,直接播放
if (audioService.getCurrentSound()) {
audioService.play();
playerStore.setPlayMusic(true); playerStore.setPlayMusic(true);
return;
}
// 没有音频实例重新获取并播放包括重新获取B站视频URL
try {
// 复用当前播放对象但强制重新获取URL
const result = await playerStore.setPlay({ ...playMusic.value, playMusicUrl: undefined });
if (result) {
playerStore.setPlayMusic(true);
}
} catch (error) {
console.error('重新获取播放链接失败:', error);
message.error(t('player.playFailed'));
} }
} catch (error) { } catch (error) {
console.error('重新获取播放链接失败:', error); console.error('播放出错:', error);
message.error(t('player.playFailed')); message.error(t('player.playFailed'));
} }
}; };
@@ -359,6 +413,15 @@ const setMusicFull = () => {
} }
}; };
const palyListRef = useTemplateRef('palyListRef') as any;
const scrollToPlayList = (val: boolean) => {
if (!val) return;
setTimeout(() => {
palyListRef.value?.scrollTo({ top: playerStore.playListIndex * 62 });
}, 50);
};
const isFavorite = computed(() => { const isFavorite = computed(() => {
// 对于B站视频使用ID匹配函数 // 对于B站视频使用ID匹配函数
if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) { if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) {
@@ -400,11 +463,25 @@ const handleArtistClick = (id: number) => {
navigateToArtist(id); navigateToArtist(id);
}; };
// 监听播放栏显示状态
watch(
() => MusicFullRef.value?.config?.hidePlayBar,
(newVal) => {
if (newVal && musicFullVisible.value) {
// 使用 animate.css 动画,不需要手动设置样式
}
}
);
const isEQVisible = ref(false); const isEQVisible = ref(false);
// 打开播放列表抽屉 // 在 script setup 部分添加删除歌曲的处理函数
const openPlayListDrawer = () => { const handleDeleteSong = (song: SongResult) => {
playerStore.setPlayListDrawerVisible(true); // 如果删除的是当前播放的歌曲,先切换到下一首
if (song.id === playMusic.value.id) {
playerStore.nextPlay();
}
playerStore.removeFromPlayList(song.id as number);
}; };
</script> </script>
@@ -496,10 +573,10 @@ const openPlayListDrawer = () => {
} }
.audio-button { .audio-button {
@apply flex items-center; @apply flex items-center mx-4;
.iconfont { .iconfont {
@apply text-2xl transition cursor-pointer mx-3; @apply text-2xl transition cursor-pointer m-4;
@apply hover:text-green-500; @apply hover:text-green-500;
} }
} }
@@ -681,25 +758,4 @@ const openPlayListDrawer = () => {
} }
} }
} }
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-overlay {
@apply absolute inset-0 flex items-center justify-center rounded-2xl;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
.loading-icon {
font-size: 24px;
color: white;
animation: spin 1s linear infinite;
}
</style> </style>

View File

@@ -1,287 +0,0 @@
<template>
<!-- 透明遮罩层点击任意位置关闭 -->
<div v-if="internalVisible" class="fixed-overlay" @click="closePanel"></div>
<!-- 使用animate.css进行动画效果 -->
<div
v-if="internalVisible"
class="playlist-panel"
:class="[
'animate__animated',
closing ? (isMobile ? 'animate__slideOutDown' : 'animate__slideOutRight') :
(isMobile ? 'animate__slideInUp' : 'animate__slideInRight')
]"
>
<div class="playlist-panel-header">
<div class="title">{{ t('player.playBar.playList') }}</div>
<div class="header-actions">
<n-tooltip trigger="hover">
<template #trigger>
<div class="action-btn" @click="handleClearPlaylist">
<i class="iconfont ri-delete-bin-line"></i>
</div>
</template>
{{ t('player.playList.clearAll')}}
</n-tooltip>
<div class="close-btn" @click="closePanel">
<i class="iconfont ri-close-line"></i>
</div>
</div>
</div>
<div class="playlist-panel-content">
<div v-if="playList.length === 0" class="empty-playlist">
<i class="iconfont ri-music-2-line"></i>
<p>{{ t('player.playList.empty')}}</p>
</div>
<n-virtual-list v-else ref="playListRef" :item-size="62" item-resizable :items="playList">
<template #default="{ item }">
<div class="music-play-list-content">
<div class="flex items-center justify-between">
<song-item :key="item.id" class="flex-1" :item="item" mini></song-item>
<div class="delete-btn" @click.stop="handleDeleteSong(item)">
<i
class="iconfont ri-delete-bin-line text-gray-400 hover:text-red-500 transition-colors"
></i>
</div>
</div>
</div>
</template>
</n-virtual-list>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMessage, useDialog } from 'naive-ui';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore } from '@/store/modules/player';
import type { SongResult } from '@/type/music';
import { isMobile } from '@/utils';
const { t } = useI18n();
const message = useMessage();
const dialog = useDialog();
const playerStore = usePlayerStore();
// 内部状态控制组件的可见性
const internalVisible = ref(false);
const closing = ref(false);
// 当前是否显示播放列表面板
const show = computed({
get: () => playerStore.playListDrawerVisible,
set: (value) => {
playerStore.setPlayListDrawerVisible(value);
}
});
// 监听外部可见性变化
watch(show, (newValue) => {
if (newValue) {
// 打开面板
internalVisible.value = true;
closing.value = false;
// 在下一个渲染周期后滚动到当前歌曲
nextTick(() => {
scrollToCurrentSong();
});
} else {
// 如果已经是关闭状态,不需要处理
if (!internalVisible.value) return;
// 开始关闭动画
closing.value = true;
// 等待动画完成后再隐藏组件
setTimeout(() => {
internalVisible.value = false;
}, 400); // 动画持续时间
}
}, { immediate: true });
// 播放列表
const playList = computed(() => playerStore.playList as SongResult[]);
// 播放列表引用
const playListRef = ref<any>(null);
// 关闭面板
const closePanel = () => {
show.value = false;
};
// 清空播放列表
const handleClearPlaylist = () => {
if (playList.value.length === 0) {
message.info(t('player.playList.alreadyEmpty'));
return;
}
dialog.warning({
title: t('player.playList.clearConfirmTitle'),
content: t('player.playList.clearConfirmContent'),
positiveText: t('common.confirm'),
negativeText: t('common.cancel'),
style: { zIndex: 999999999 }, // 确保对话框显示在遮罩之上
onPositiveClick: () => {
// 清空播放列表
playerStore.clearPlayAll();
message.success(t('player.playList.cleared'));
}
});
};
// 处理键盘事件
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && internalVisible.value) {
closePanel();
}
};
// 添加和移除键盘事件监听
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
});
// 滚动到当前播放歌曲
const scrollToCurrentSong = () => {
// 延长等待时间,确保列表已渲染完成
setTimeout(() => {
if (playListRef.value && playList.value.length > 0) {
const index = playerStore.playListIndex;
console.log('滚动到歌曲索引:', index);
playListRef.value.scrollTo({
top: (index > 3 ? (index - 3) : 0) * 62,
});
}
}, 100);
};
// 删除歌曲
const handleDeleteSong = (song: SongResult) => {
playerStore.removeFromPlayList(song.id as number);
};
</script>
<style lang="scss" scoped>
.fixed-overlay {
@apply fixed inset-0 z-[999999];
pointer-events: auto; // 允许点击关闭
cursor: default;
}
.playlist-panel {
@apply fixed right-0 z-[9999999] rounded-l-xl overflow-hidden;
width: 350px;
height: 70vh;
top: 15vh; // 距离顶部15%
animation-duration: 0.4s !important; // 动画持续时间
@apply bg-light dark:bg-dark shadow-2xl dark:border dark:border-gray-700;
&-header {
@apply flex items-center justify-between px-4 py-2 border-b border-gray-100 dark:border-gray-900;
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.7);
.dark & {
background-color: rgba(18, 18, 18, 0.7);
}
.title {
@apply text-base font-medium;
}
.header-actions {
@apply flex items-center;
}
.action-btn,
.close-btn {
@apply w-8 h-8 flex items-center justify-center rounded-full cursor-pointer mx-1;
@apply hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors;
.iconfont {
@apply text-xl;
}
}
.action-btn {
@apply text-gray-500 dark:text-gray-400;
&:hover {
@apply text-red-500 dark:text-red-400;
}
}
}
&-content {
@apply h-[calc(70vh-60px)] overflow-hidden;
}
}
.empty-playlist {
@apply flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500;
.iconfont {
@apply text-5xl mb-4;
}
p {
@apply text-sm;
}
}
.music-play-list-content {
@apply pr-2 hover:bg-light-100 dark:hover:bg-dark-100;
&:hover {
.delete-btn {
@apply visible;
}
}
.delete-btn {
@apply pr-2 cursor-pointer invisible;
.iconfont {
@apply text-lg;
}
}
}
// 移动端适配
@media (max-width: 768px) {
.playlist-panel {
width: 100%;
height: 60vh;
top: auto;
bottom: 56px; // 移动端底部留出导航栏高度
border-radius: 16px 16px 0 0;
border-left: none;
border-top: 1px solid theme('colors.gray.200');
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.1);
&-header {
@apply text-center relative;
&::before {
content: '';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 5px;
border-radius: 5px;
background-color: rgba(150, 150, 150, 0.3);
}
}
&-content {
height: calc(60vh - 60px);
}
}
}
</style>

View File

@@ -1,233 +0,0 @@
<template>
<n-popover
trigger="click"
:z-index="99999999"
placement="top"
content-class="music-source-popover"
raw
:show-arrow="false"
:delay="200"
>
<template #trigger>
<n-tooltip trigger="hover" :z-index="9999999">
<template #trigger>
<i
class="iconfont ri-refresh-line"
:class="{ 'text-green-500': isReparse, 'animate-spin': isReparsing }"
></i>
</template>
{{ t('player.playBar.reparse') }}
</n-tooltip>
</template>
<div class="reparse-popover bg-light-100 dark:bg-dark-100 p-4 rounded-xl max-w-60">
<div class="text-base font-medium mb-2">{{ t('player.reparse.title') }}</div>
<div class="text-sm opacity-70 mb-3">{{ t('player.reparse.desc') }}</div>
<div class="mb-3">
<div class="flex flex-col space-y-2">
<div
v-for="source in musicSourceOptions"
:key="source.value"
class="source-button flex items-center p-2 rounded-lg cursor-pointer transition-all duration-200 bg-light-200 dark:bg-dark-200 hover:bg-light-300 dark:hover:bg-dark-300"
:class="{
'bg-green-50 dark:bg-green-900/20 text-green-500': isCurrentSource(source.value),
'opacity-50 cursor-not-allowed': isReparsing || playMusic.source === 'bilibili'
}"
@click="directReparseMusic(source.value)"
>
<div class="flex items-center justify-center w-6 h-6 mr-3 text-lg">
<i :class="getSourceIcon(source.value)"></i>
</div>
<div class="flex-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis">
{{ source.label }}
</div>
<div v-if="isReparsing && currentReparsingSource === source.value" class="w-5 h-5 flex items-center justify-center">
<i class="ri-loader-4-line animate-spin"></i>
</div>
<div v-else-if="isCurrentSource(source.value)" class="w-5 h-5 flex items-center justify-center">
<i class="ri-check-line"></i>
</div>
</div>
</div>
</div>
<div v-if="playMusic.source === 'bilibili'" class="text-red-500 text-sm">
{{ t('player.reparse.bilibiliNotSupported') }}
</div>
</div>
</n-popover>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMessage } from 'naive-ui';
import { playMusic } from '@/hooks/MusicHook';
import { usePlayerStore } from '@/store/modules/player';
import type { Platform } from '@/types/music';
import { audioService } from '@/services/audioService';
const playerStore = usePlayerStore();
const { t } = useI18n();
const message = useMessage();
// 音源重新解析状态
const isReparsing = ref(false);
const currentReparsingSource = ref<Platform | null>(null);
// 实际存储选中音源的值
const selectedSourcesValue = ref<Platform[]>([]);
// 判断当前歌曲是否有自定义解析记录
const isReparse = computed(() => {
const songId = String(playMusic.value.id);
return localStorage.getItem(`song_source_${songId}`) !== null;
});
// 可选音源列表
const musicSourceOptions = ref([
{ label: 'MiGu', value: 'migu' as Platform },
{ label: 'KuGou', value: 'kugou' as Platform },
{ label: 'pyncmd', value: 'pyncmd' as Platform },
{ label: 'KuWo', value: 'kuwo' as Platform },
{ label: 'Bilibili', value: 'bilibili' as Platform },
{ label: 'GdMuisc', value: 'gdmusic' as Platform }
]);
// 检查音源是否被选中
const isCurrentSource = (source: Platform) => {
return selectedSourcesValue.value.includes(source);
};
// 获取音源图标
const getSourceIcon = (source: Platform) => {
const iconMap: Record<Platform, string> = {
'migu': 'ri-music-2-fill',
'kugou': 'ri-music-fill',
'kuwo': 'ri-album-fill',
'qq': 'ri-qq-fill',
'joox': 'ri-disc-fill',
'pyncmd': 'ri-netease-cloud-music-fill',
'bilibili': 'ri-bilibili-fill',
'gdmusic': 'ri-google-fill'
};
return iconMap[source] || 'ri-music-2-fill';
};
// 初始化选中的音源
const initSelectedSources = () => {
const songId = String(playMusic.value.id);
const savedSource = localStorage.getItem(`song_source_${songId}`);
if (savedSource) {
try {
selectedSourcesValue.value = JSON.parse(savedSource);
} catch (e) {
selectedSourcesValue.value = [];
}
} else {
selectedSourcesValue.value = [];
}
};
// 直接重新解析当前歌曲
const directReparseMusic = async (source: Platform) => {
if (isReparsing.value || playMusic.value.source === 'bilibili') {
return;
}
try {
isReparsing.value = true;
currentReparsingSource.value = source;
// 更新选中的音源值为当前点击的音源
const songId = String(playMusic.value.id);
selectedSourcesValue.value = [source];
// 保存到localStorage
localStorage.setItem(`song_source_${songId}`, JSON.stringify(selectedSourcesValue.value));
const success = await playerStore.reparseCurrentSong(source);
if (success) {
message.success(t('player.reparse.success'));
} else {
message.error(t('player.reparse.failed'));
}
} catch (error) {
console.error('解析失败:', error);
message.error(t('player.reparse.failed'));
} finally {
isReparsing.value = false;
currentReparsingSource.value = null;
}
};
// 监听歌曲ID变化初始化音源设置
watch(() => playMusic.value.id, () => {
if (playMusic.value.id) {
initSelectedSources();
}
}, { immediate: true });
// 监听歌曲变化,检查是否有自定义音源
watch(() => playMusic.value.id, async (newId) => {
if (newId) {
const songId = String(newId);
const savedSource = localStorage.getItem(`song_source_${songId}`);
// 如果有保存的音源设置但当前不是使用自定义解析的播放,尝试应用
if (savedSource && playMusic.value.source !== 'bilibili') {
try {
const sources = JSON.parse(savedSource) as Platform[];
console.log(`检测到歌曲ID ${songId} 有自定义音源设置:`, sources);
// 当URL加载失败或过期时自动应用自定义音源重新加载
audioService.on('url_expired', async (trackInfo) => {
if (trackInfo && trackInfo.id === playMusic.value.id) {
console.log('URL已过期自动应用自定义音源重新加载');
try {
isReparsing.value = true;
const success = await playerStore.reparseCurrentSong(sources[0]);
if (!success) {
message.error(t('player.reparse.failed'));
}
} catch (e) {
console.error('自动重新解析失败:', e);
message.error(t('player.reparse.failed'));
} finally {
isReparsing.value = false;
}
}
});
} catch (e) {
console.error('解析保存的音源设置失败:', e);
}
}
}
});
</script>
<style lang="scss" scoped>
.music-source-popover {
@apply w-64 rounded-xl overflow-hidden;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
.source-button {
&:hover:not(.opacity-50) {
@apply transform -translate-y-0.5 shadow-sm;
}
}
.iconfont {
@apply text-2xl mx-3;
}
</style>

View File

@@ -56,7 +56,7 @@ const props = defineProps({
}, },
sources: { sources: {
type: Array as () => Platform[], type: Array as () => Platform[],
default: () => ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo'] default: () => ['migu', 'kugou', 'pyncmd', 'bilibili', 'youtube']
} }
}); });
@@ -72,6 +72,7 @@ const musicSourceOptions = ref([
{ label: 'pyncmd', value: 'pyncmd' }, { label: 'pyncmd', value: 'pyncmd' },
{ label: '酷我音乐', value: 'kuwo' }, { label: '酷我音乐', value: 'kuwo' },
{ label: 'Bilibili音乐', value: 'bilibili' }, { label: 'Bilibili音乐', value: 'bilibili' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'GD音乐台', value: 'gdmusic' } { label: 'GD音乐台', value: 'gdmusic' }
]); ]);
@@ -102,7 +103,7 @@ watch(
const handleConfirm = () => { const handleConfirm = () => {
// 确保至少选择一个音源 // 确保至少选择一个音源
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo']; const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili', 'youtube'];
const valuesToEmit = selectedSources.value.length > 0 const valuesToEmit = selectedSources.value.length > 0
? [...new Set(selectedSources.value)] ? [...new Set(selectedSources.value)]
: defaultPlatforms; : defaultPlatforms;

View File

@@ -9,7 +9,6 @@ import pinia, { usePlayerStore } from '@/store';
import type { Artist, ILyricText, SongResult } from '@/type/music'; import type { Artist, ILyricText, SongResult } from '@/type/music';
import { isElectron } from '@/utils'; import { isElectron } from '@/utils';
import { getTextColors } from '@/utils/linearColor'; import { getTextColors } from '@/utils/linearColor';
import { getSongUrl } from '@/store/modules/player';
const windowData = window as any; const windowData = window as any;
@@ -906,7 +905,7 @@ audioService.on('url_expired', async (expiredTrack) => {
// 处理网易云音乐重新获取URL // 处理网易云音乐重新获取URL
console.log('重新获取网易云音乐URL'); console.log('重新获取网易云音乐URL');
try { try {
const { getSongUrl } = await import('@/store/modules/player');
const newUrl = await getSongUrl(expiredTrack.id, expiredTrack as any); const newUrl = await getSongUrl(expiredTrack.id, expiredTrack as any);
if (newUrl) { if (newUrl) {

View File

@@ -20,7 +20,7 @@
</keep-alive> </keep-alive>
</router-view> </router-view>
</div> </div>
<play-bottom /> <play-bottom height="5rem" />
<app-menu v-if="isMobile && !playerStore.musicFull" class="menu" :menus="menus" /> <app-menu v-if="isMobile && !playerStore.musicFull" class="menu" :menus="menus" />
</div> </div>
</div> </div>
@@ -46,8 +46,6 @@
settingsStore.setData?.hasDownloadingTasks) settingsStore.setData?.hasDownloadingTasks)
" "
/> />
<!-- 播放列表抽屉 -->
<play-list-drawer />
</div> </div>
<install-app-modal v-if="!isElectron"></install-app-modal> <install-app-modal v-if="!isElectron"></install-app-modal>
<update-modal v-if="isElectron" /> <update-modal v-if="isElectron" />
@@ -64,33 +62,27 @@ import InstallAppModal from '@/components/common/InstallAppModal.vue';
import PlayBottom from '@/components/common/PlayBottom.vue'; import PlayBottom from '@/components/common/PlayBottom.vue';
import UpdateModal from '@/components/common/UpdateModal.vue'; import UpdateModal from '@/components/common/UpdateModal.vue';
import homeRouter from '@/router/home'; import homeRouter from '@/router/home';
import otherRouter from '@/router/other';
import { useMenuStore } from '@/store/modules/menu'; import { useMenuStore } from '@/store/modules/menu';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
import { isElectron, isMobile } from '@/utils'; import { isElectron, isMobile } from '@/utils';
const keepAliveInclude = computed(() => { const keepAliveInclude = computed(() =>
const allRoutes = [...homeRouter, ...otherRouter]; homeRouter
return allRoutes
.filter((item) => { .filter((item) => {
return item.meta?.keepAlive; return item.meta.keepAlive;
}) })
.map((item) => { .map((item) => {
return typeof item.name === 'string' return item.name.charAt(0).toUpperCase() + item.name.slice(1);
? item.name.charAt(0).toUpperCase() + item.name.slice(1)
: '';
}) })
.filter(Boolean); );
});
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue')); const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const PlayBar = defineAsyncComponent(() => import('@/components/player/PlayBar.vue')); const PlayBar = defineAsyncComponent(() => import('@/components/player/PlayBar.vue'));
const MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue')); const MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue')); const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue')); const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
const PlayListDrawer = defineAsyncComponent(() => import('@/components/player/PlayListDrawer.vue'));
const PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue')); const PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue'));
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
@@ -150,7 +142,7 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
.mobile { .mobile {
.main-content { .main-content {
height: calc(100vh - 130px); height: calc(100vh - 154px);
overflow: auto; overflow: auto;
display: block; display: block;
flex: none; flex: none;

View File

@@ -9,16 +9,19 @@
</div> </div>
<div class="app-menu-list"> <div class="app-menu-list">
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item"> <div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
<n-tooltip :delay="200" :disabled="isText" placement="bottom"> <router-link class="app-menu-item-link" :to="item.path">
<template #trigger> <i
<router-link class="app-menu-item-link" :to="item.path"> class="iconfont app-menu-item-icon"
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i> :style="iconStyle(index)"
<span v-if="isText" class="app-menu-item-text ml-3" :class="isChecked(index) ? 'text-green-500' : ''">{{ :class="item.meta.icon"
item.meta.title }}</span> ></i>
</router-link> <span
</template> v-if="isText"
<div v-if="!isText">{{ item.meta.title }}</div> class="app-menu-item-text ml-3"
</n-tooltip> :class="isChecked(index) ? 'text-green-500' : ''"
>{{ item.meta.title }}</span
>
</router-link>
</div> </div>
</div> </div>
</div> </div>
@@ -80,7 +83,6 @@ const isText = ref(false);
.app-menu-expanded { .app-menu-expanded {
@apply w-[160px]; @apply w-[160px];
.app-menu-item { .app-menu-item {
@apply hover:bg-gray-100 dark:hover:bg-gray-800 rounded mr-4; @apply hover:bg-gray-100 dark:hover:bg-gray-800 rounded mr-4;
} }

View File

@@ -34,18 +34,13 @@
:class="{ 'only-cover': config.hideLyrics }" :class="{ 'only-cover': config.hideLyrics }"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }" :style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
> >
<div class="img-container relative"> <n-image
<n-image ref="PicImgRef"
ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '500y500')"
:src="getImgUrl(playMusic?.picUrl, '500y500')" class="img"
class="img" lazy
lazy preview-disabled
preview-disabled />
/>
<div v-if="playMusic?.playLoading" class="loading-overlay">
<i class="ri-loader-4-line loading-icon"></i>
</div>
</div>
<div class="music-info"> <div class="music-info">
<div class="music-content-name">{{ playMusic.name }}</div> <div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer"> <div class="music-content-singer">
@@ -554,12 +549,8 @@ defineExpose({
max-width: none; max-width: none;
max-height: none; max-height: none;
.img-container {
@apply w-[50vh] h-[50vh] mb-8;
}
.img { .img {
@apply w-full h-full; @apply w-[50vh] h-[50vh] mb-8;
} }
.music-info { .music-info {
@@ -577,10 +568,6 @@ defineExpose({
} }
} }
.img-container {
@apply relative w-full h-full;
}
.img { .img {
@apply rounded-xl w-full h-full shadow-2xl transition-all duration-300; @apply rounded-xl w-full h-full shadow-2xl transition-all duration-300;
} }
@@ -776,25 +763,4 @@ defineExpose({
} }
} }
} }
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-overlay {
@apply absolute inset-0 flex items-center justify-center rounded-xl;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
.loading-icon {
font-size: 48px;
color: white;
animation: spin 1s linear infinite;
}
</style> </style>

View File

@@ -266,7 +266,7 @@ const selectItem = async (key: string) => {
}; };
const toGithub = () => { const toGithub = () => {
window.open('http://donate.alger.fun/download', '_blank'); window.open('http://donate.alger.fun', '_blank');
}; };
const updateInfo = ref<UpdateResult>({ const updateInfo = ref<UpdateResult>({

View File

@@ -33,17 +33,6 @@ const layoutRouter = [
}, },
component: () => import('@/views/list/index.vue') component: () => import('@/views/list/index.vue')
}, },
{
path: '/toplist',
name: 'toplist',
meta: {
title: '排行榜',
icon: 'ri-bar-chart-grouped-fill',
keepAlive: true,
isMobile: true
},
component: () => import('@/views/toplist/index.vue')
},
{ {
path: '/mv', path: '/mv',
name: 'mv', name: 'mv',
@@ -51,10 +40,20 @@ const layoutRouter = [
title: 'MV', title: 'MV',
icon: 'icon-recordfill', icon: 'icon-recordfill',
keepAlive: true, keepAlive: true,
isMobile: false isMobile: true
}, },
component: () => import('@/views/mv/index.vue') component: () => import('@/views/mv/index.vue')
}, },
// {
// path: '/history',
// name: 'history',
// meta: {
// title: '历史',
// icon: 'icon-a-TicketStar',
// keepAlive: true,
// },
// component: () => import('@/views/history/index.vue'),
// },
{ {
path: '/history', path: '/history',
name: 'history', name: 'history',

View File

@@ -53,17 +53,6 @@ const otherRouter = [
back: true back: true
}, },
component: () => import('@/views/bilibili/BilibiliPlayer.vue') component: () => import('@/views/bilibili/BilibiliPlayer.vue')
},
{
path: '/music-list/:id?',
name: 'musicList',
meta: {
title: '音乐列表',
keepAlive: false,
showInMenu: false,
back: true
},
component: () => import('@/views/music/MusicListPage.vue')
} }
]; ];
export default otherRouter; export default otherRouter;

View File

@@ -45,8 +45,6 @@ class AudioService {
private operationLock = false; private operationLock = false;
private operationLockTimer: NodeJS.Timeout | null = null; private operationLockTimer: NodeJS.Timeout | null = null;
private operationLockTimeout = 5000; // 5秒超时 private operationLockTimeout = 5000; // 5秒超时
private operationLockStartTime: number = 0;
private operationLockId: string = '';
constructor() { constructor() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
@@ -55,14 +53,6 @@ class AudioService {
// 从本地存储加载 EQ 开关状态 // 从本地存储加载 EQ 开关状态
const bypassState = localStorage.getItem('eqBypass'); const bypassState = localStorage.getItem('eqBypass');
this.bypass = bypassState ? JSON.parse(bypassState) : false; this.bypass = bypassState ? JSON.parse(bypassState) : false;
// 页面加载时立即强制重置操作锁
this.forceResetOperationLock();
// 添加页面卸载事件,确保离开页面时清除锁
window.addEventListener('beforeunload', () => {
this.forceResetOperationLock();
});
} }
private initMediaSession() { private initMediaSession() {
@@ -373,37 +363,11 @@ class AudioService {
// 设置操作锁,带超时自动释放 // 设置操作锁,带超时自动释放
private setOperationLock(): boolean { private setOperationLock(): boolean {
// 生成唯一的锁ID
const lockId = Date.now().toString() + Math.random().toString(36).substring(2, 9);
// 如果锁已经存在,检查是否超时
if (this.operationLock) { if (this.operationLock) {
const currentTime = Date.now(); return false;
const lockDuration = currentTime - this.operationLockStartTime;
// 如果锁持续时间超过2秒直接强制重置
if (lockDuration > 2000) {
console.warn(`操作锁已激活 ${lockDuration}ms超过安全阈值强制重置`);
this.forceResetOperationLock();
} else {
console.log(`操作锁激活中,持续时间 ${lockDuration}ms`);
return false;
}
} }
this.operationLock = true; this.operationLock = true;
this.operationLockStartTime = Date.now();
this.operationLockId = lockId;
// 将锁信息存储到 localStorage仅用于调试实际不依赖此值
try {
localStorage.setItem('audioOperationLock', JSON.stringify({
id: this.operationLockId,
startTime: this.operationLockStartTime
}));
} catch (error) {
console.error('存储操作锁信息失败:', error);
}
// 清除之前的定时器 // 清除之前的定时器
if (this.operationLockTimer) { if (this.operationLockTimer) {
@@ -413,23 +377,16 @@ class AudioService {
// 设置超时自动释放锁 // 设置超时自动释放锁
this.operationLockTimer = setTimeout(() => { this.operationLockTimer = setTimeout(() => {
console.warn('操作锁超时自动释放'); console.warn('操作锁超时自动释放');
this.releaseOperationLock(); this.operationLock = false;
this.operationLockTimer = null;
}, this.operationLockTimeout); }, this.operationLockTimeout);
return true; return true;
} }
// 释放操作锁 // 释放操作锁
public releaseOperationLock(): void { private releaseOperationLock(): void {
this.operationLock = false; this.operationLock = false;
this.operationLockStartTime = 0;
// 从 localStorage 中移除锁信息
try {
localStorage.removeItem('audioOperationLock');
} catch (error) {
console.error('清除存储的操作锁信息失败:', error);
}
if (this.operationLockTimer) { if (this.operationLockTimer) {
clearTimeout(this.operationLockTimer); clearTimeout(this.operationLockTimer);
@@ -437,59 +394,12 @@ class AudioService {
} }
} }
// 强制重置操作锁,用于特殊情况
public forceResetOperationLock(): void {
console.log('强制重置操作锁');
this.operationLock = false;
this.operationLockStartTime = 0;
this.operationLockId = '';
if (this.operationLockTimer) {
clearTimeout(this.operationLockTimer);
this.operationLockTimer = null;
}
// 清除存储的锁
localStorage.removeItem('audioOperationLock');
}
// 播放控制相关 // 播放控制相关
play(url?: string, track?: SongResult, isPlay: boolean = true): Promise<Howl> { play(url?: string, track?: SongResult, isPlay: boolean = true): Promise<Howl> {
// 每次调用play方法时尝试强制重置锁注意仅在页面刷新后的第一次播放时应用 // 如果操作锁已激活,说明有操作正在进行中,直接返回
if (!this.currentSound) {
console.log('首次播放请求,强制重置操作锁');
this.forceResetOperationLock();
}
// 如果操作锁已激活,但持续时间超过安全阈值,强制重置
if (this.operationLock) {
const currentTime = Date.now();
const lockDuration = currentTime - this.operationLockStartTime;
if (lockDuration > 2000) {
console.warn(`操作锁已激活 ${lockDuration}ms超过安全阈值强制重置`);
this.forceResetOperationLock();
}
}
// 获取锁
if (!this.setOperationLock()) { if (!this.setOperationLock()) {
console.log('audioService: 操作锁激活,强制执行当前播放请求'); console.log('audioService: 操作锁激活,忽略当前播放请求');
return Promise.reject(new Error('操作锁激活,请等待当前操作完成'));
// 如果只是要继续播放当前音频,直接执行
if (this.currentSound && !url && !track) {
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.play();
return Promise.resolve(this.currentSound);
}
// 强制释放锁并继续执行
this.forceResetOperationLock();
// 这里不再返回错误,而是继续执行播放逻辑
} }
// 如果没有提供新的 URL 和 track且当前有音频实例则继续播放 // 如果没有提供新的 URL 和 track且当前有音频实例则继续播放
@@ -507,7 +417,7 @@ class AudioService {
// 如果没有提供必要的参数,返回错误 // 如果没有提供必要的参数,返回错误
if (!url || !track) { if (!url || !track) {
this.releaseOperationLock(); this.releaseOperationLock();
return Promise.reject(new Error('缺少必要参数: url和track')); return Promise.reject(new Error('Missing required parameters: url and track'));
} }
return new Promise<Howl>((resolve, reject) => { return new Promise<Howl>((resolve, reject) => {
@@ -575,7 +485,6 @@ class AudioService {
} else { } else {
// 发送URL过期事件通知外部需要重新获取URL // 发送URL过期事件通知外部需要重新获取URL
this.emit('url_expired', this.currentTrack); this.emit('url_expired', this.currentTrack);
this.releaseOperationLock();
reject(new Error('音频加载失败,请尝试切换其他歌曲')); reject(new Error('音频加载失败,请尝试切换其他歌曲'));
} }
}, },
@@ -588,7 +497,6 @@ class AudioService {
} else { } else {
// 发送URL过期事件通知外部需要重新获取URL // 发送URL过期事件通知外部需要重新获取URL
this.emit('url_expired', this.currentTrack); this.emit('url_expired', this.currentTrack);
this.releaseOperationLock();
reject(new Error('音频播放失败,请尝试切换其他歌曲')); reject(new Error('音频播放失败,请尝试切换其他歌曲'));
} }
}, },
@@ -666,33 +574,33 @@ class AudioService {
} }
stop() { stop() {
// 强制重置操作锁并继续执行 if (!this.setOperationLock()) {
this.forceResetOperationLock(); console.log('audioService: 操作锁激活,忽略当前停止请求');
return;
try {
if (this.currentSound) {
try {
// 确保任何进行中的seek操作被取消
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.stop();
this.currentSound.unload();
} catch (error) {
console.error('停止音频失败:', error);
}
this.currentSound = null;
}
this.currentTrack = null;
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'none';
}
this.disposeEQ();
} catch (error) {
console.error('停止音频时发生错误:', error);
} }
if (this.currentSound) {
try {
// 确保任何进行中的seek操作被取消
if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer);
this.seekLock = false;
}
this.currentSound.stop();
this.currentSound.unload();
} catch (error) {
console.error('Error stopping audio:', error);
}
this.currentSound = null;
}
this.currentTrack = null;
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'none';
}
this.disposeEQ();
this.releaseOperationLock();
} }
setVolume(volume: number) { setVolume(volume: number) {
@@ -703,12 +611,14 @@ class AudioService {
} }
seek(time: number) { seek(time: number) {
// 直接强制重置操作锁 if (!this.setOperationLock()) {
this.forceResetOperationLock(); console.log('audioService: 操作锁激活忽略当前seek请求');
return;
}
if (this.currentSound) { if (this.currentSound) {
try { try {
// 直接执行seek操作 // 直接执行seek操作,避免任何过滤或判断
this.currentSound.seek(time); this.currentSound.seek(time);
// 触发seek事件 // 触发seek事件
this.updateMediaSessionPositionState(); this.updateMediaSessionPositionState();
@@ -717,23 +627,30 @@ class AudioService {
console.error('Seek操作失败:', error); console.error('Seek操作失败:', error);
} }
} }
this.releaseOperationLock();
} }
pause() { pause() {
this.forceResetOperationLock(); if (!this.setOperationLock()) {
console.log('audioService: 操作锁激活,忽略当前暂停请求');
return;
}
if (this.currentSound) { if (this.currentSound) {
try { try {
// 确保任何进行中的seek操作被取消 // 如果有进行中的seek操作,等待其完成
if (this.seekLock && this.seekDebounceTimer) { if (this.seekLock && this.seekDebounceTimer) {
clearTimeout(this.seekDebounceTimer); clearTimeout(this.seekDebounceTimer);
this.seekLock = false; this.seekLock = false;
} }
this.currentSound.pause(); this.currentSound.pause();
} catch (error) { } catch (error) {
console.error('暂停音频失败:', error); console.error('Error pausing audio:', error);
} }
} }
this.releaseOperationLock();
} }
clearAllListeners() { clearAllListeners() {

View File

@@ -16,6 +16,5 @@ export * from './modules/player';
export * from './modules/search'; export * from './modules/search';
export * from './modules/settings'; export * from './modules/settings';
export * from './modules/user'; export * from './modules/user';
export * from './modules/music';
export default pinia; export default pinia;

View File

@@ -1,45 +0,0 @@
import { defineStore } from 'pinia';
interface MusicState {
currentMusicList: any[] | null;
currentMusicListName: string;
currentListInfo: any | null;
canRemoveSong: boolean;
}
export const useMusicStore = defineStore('music', {
state: (): MusicState => ({
currentMusicList: null,
currentMusicListName: '',
currentListInfo: null,
canRemoveSong: false
}),
actions: {
// 设置当前音乐列表
setCurrentMusicList(list: any[], name: string, listInfo: any = null, canRemove = false) {
this.currentMusicList = list;
this.currentMusicListName = name;
this.currentListInfo = listInfo;
this.canRemoveSong = canRemove;
},
// 清除当前音乐列表
clearCurrentMusicList() {
this.currentMusicList = null;
this.currentMusicListName = '';
this.currentListInfo = null;
this.canRemoveSong = false;
},
// 从列表中移除一首歌曲
removeSongFromList(id: number) {
if (!this.currentMusicList) return;
const index = this.currentMusicList.findIndex((song) => song.id === id);
if (index !== -1) {
this.currentMusicList.splice(index, 1);
}
}
}
});

View File

@@ -4,7 +4,7 @@ import { computed, ref } from 'vue';
import i18n from '@/../i18n/renderer'; import i18n from '@/../i18n/renderer';
import { getBilibiliAudioUrl } from '@/api/bilibili'; import { getBilibiliAudioUrl } from '@/api/bilibili';
import { getLikedList, getMusicLrc, getMusicUrl, getParsingMusicUrl, likeSong } from '@/api/music'; import { getLikedList, getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook'; import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { audioService } from '@/services/audioService'; import { audioService } from '@/services/audioService';
import type { ILyric, ILyricText, SongResult } from '@/type/music'; import type { ILyric, ILyricText, SongResult } from '@/type/music';
@@ -14,7 +14,6 @@ import { createDiscreteApi } from 'naive-ui';
import { useSettingsStore } from './settings'; import { useSettingsStore } from './settings';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { type Platform } from '@/types/music';
const musicHistory = useMusicHistory(); const musicHistory = useMusicHistory();
const { message } = createDiscreteApi(['message']); const { message } = createDiscreteApi(['message']);
@@ -103,28 +102,6 @@ export const getSongUrl = async (
} }
const numericId = typeof id === 'string' ? parseInt(id, 10) : id; const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
// 检查是否有自定义音源设置
const songId = String(id);
const savedSource = localStorage.getItem(`song_source_${songId}`);
// 如果有自定义音源设置直接使用getParsingMusicUrl获取URL
if (savedSource && songData.source !== 'bilibili') {
try {
console.log(`使用自定义音源解析歌曲 ID: ${songId}`);
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
if (res && res.data && res.data.data && res.data.data.url) {
return res.data.data.url;
}
// 如果自定义音源解析失败,继续使用正常的获取流程
console.warn('自定义音源解析失败,使用默认音源');
} catch (error) {
console.error('error',error)
console.error('自定义音源解析出错:', error);
}
}
// 正常获取URL流程
const { data } = await getMusicUrl(numericId, isDownloaded); const { data } = await getMusicUrl(numericId, isDownloaded);
let url = ''; let url = '';
let songDetail = null; let songDetail = null;
@@ -212,26 +189,17 @@ export const loadLrc = async (id: string | number): Promise<ILyric> => {
}; };
const getSongDetail = async (playMusic: SongResult) => { const getSongDetail = async (playMusic: SongResult) => {
// playMusic.playLoading 在 handlePlayMusic 中已设置,这里不再设置 playMusic.playLoading = true;
if (playMusic.source === 'bilibili') { if (playMusic.source === 'bilibili') {
console.log('处理B站音频详情'); console.log('处理B站音频详情');
try { const { backgroundColor, primaryColor } =
// 如果需要获取URL playMusic.backgroundColor && playMusic.primaryColor
if (!playMusic.playMusicUrl && playMusic.bilibiliData) { ? playMusic
playMusic.playMusicUrl = await getBilibiliAudioUrl( : await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
playMusic.bilibiliData.bvid,
playMusic.bilibiliData.cid playMusic.playLoading = false;
); return { ...playMusic, backgroundColor, primaryColor } as SongResult;
}
playMusic.playLoading = false;
return { ...playMusic} as SongResult;
} catch (error) {
console.error('获取B站音频详情失败:', error);
playMusic.playLoading = false;
throw error;
}
} }
if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) { if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) {
@@ -239,23 +207,17 @@ const getSongDetail = async (playMusic: SongResult) => {
playMusic.playMusicUrl = undefined; playMusic.playMusicUrl = undefined;
} }
try { const playMusicUrl = playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic));
const playMusicUrl = playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic)); playMusic.createdAt = Date.now();
playMusic.createdAt = Date.now(); // 半小时后过期
// 半小时后过期 playMusic.expiredAt = playMusic.createdAt + 1800000;
playMusic.expiredAt = playMusic.createdAt + 1800000; const { backgroundColor, primaryColor } =
const { backgroundColor, primaryColor } = playMusic.backgroundColor && playMusic.primaryColor
playMusic.backgroundColor && playMusic.primaryColor ? playMusic
? playMusic : await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
playMusic.playLoading = false; playMusic.playLoading = false;
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult; return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult;
} catch (error) {
console.error('获取音频URL失败:', error);
playMusic.playLoading = false;
throw error;
}
}; };
const preloadNextSong = (nextSongUrl: string) => { const preloadNextSong = (nextSongUrl: string) => {
@@ -390,29 +352,11 @@ export const usePlayerStore = defineStore('player', () => {
const favoriteList = ref<Array<number | string>>(getLocalStorageItem('favoriteList', [])); const favoriteList = ref<Array<number | string>>(getLocalStorageItem('favoriteList', []));
const savedPlayProgress = ref<number | undefined>(); const savedPlayProgress = ref<number | undefined>();
// 添加播放列表抽屉状态
const playListDrawerVisible = ref(false);
// 定时关闭相关状态 // 定时关闭相关状态
const sleepTimer = ref<SleepTimerInfo>(getLocalStorageItem('sleepTimer', { const sleepTimer = ref<SleepTimerInfo>(getLocalStorageItem('sleepTimer', {
type: SleepTimerType.NONE, type: SleepTimerType.NONE,
value: 0 value: 0
})); }));
// 清空播放列表
const clearPlayAll = async () => {
audioService.pause()
setTimeout(() => {
playMusic.value = {} as SongResult;
playMusicUrl.value = '';
playList.value = [];
playListIndex.value = 0;
localStorage.removeItem('currentPlayMusic');
localStorage.removeItem('currentPlayMusicUrl');
localStorage.removeItem('playList');
localStorage.removeItem('playListIndex');
}, 500);
};
const timerInterval = ref<number | null>(null); const timerInterval = ref<number | null>(null);
@@ -445,72 +389,71 @@ export const usePlayerStore = defineStore('player', () => {
const currentPlayListIndex = computed(() => playListIndex.value); const currentPlayListIndex = computed(() => playListIndex.value);
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => { const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
const currentSound = audioService.getCurrentSound(); // 处理B站视频确保URL有效
if (currentSound) { if (music.source === 'bilibili' && music.bilibiliData) {
console.log('主动停止并卸载当前音频实例'); try {
currentSound.stop(); console.log('处理B站视频检查URL有效性');
currentSound.unload(); // 清除之前的URL强制重新获取
music.playMusicUrl = undefined;
// 重新获取B站视频URL
if (music.bilibiliData.bvid && music.bilibiliData.cid) {
music.playMusicUrl = await getBilibiliAudioUrl(
music.bilibiliData.bvid,
music.bilibiliData.cid
);
console.log('获取B站URL成功:', music.playMusicUrl);
}
} catch (error) {
console.error('获取B站音频URL失败:', error);
message.error(i18n.global.t('player.playFailed'));
return false; // 返回失败状态
}
} }
// 先切换歌曲数据,更新播放状态
// 加载歌词
await loadLrcAsync(music);
const originalMusic = { ...music };
// 获取背景色
const { backgroundColor, primaryColor } =
music.backgroundColor && music.primaryColor
? music
: await getImageLinearBackground(getImgUrl(music?.picUrl, '30y30'));
music.backgroundColor = backgroundColor;
music.primaryColor = primaryColor;
music.playLoading = true; // 设置加载状态
playMusic.value = music;
// 更新播放相关状态
play.value = isPlay;
// 更新标题
let title = music.name;
if (music.source === 'netease' && music?.song?.artists) {
title += ` - ${music.song.artists.reduce(
(prev: string, curr: any) => `${prev}${curr.name}/`,
''
)}`;
} else if (music.source === 'bilibili' && music?.song?.ar?.[0]) {
title += ` - ${music.song.ar[0].name}`;
}
document.title = 'AlgerMusic - ' + title;
try { try {
const updatedPlayMusic = await getSongDetail(music);
// 添加到历史记录 playMusic.value = updatedPlayMusic;
musicHistory.addMusic(music); playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
// 查找歌曲在播放列表中的索引 play.value = isPlay;
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
localStorage.setItem('isPlaying', play.value.toString());
let title = updatedPlayMusic.name;
if (updatedPlayMusic.source === 'netease' && updatedPlayMusic?.song?.artists) {
title += ` - ${updatedPlayMusic.song.artists.reduce(
(prev: string, curr: any) => `${prev}${curr.name}/`,
''
)}`;
} else if (updatedPlayMusic.source === 'bilibili' && updatedPlayMusic?.song?.ar?.[0]) {
title += ` - ${updatedPlayMusic.song.ar[0].name}`;
}
document.title = title;
loadLrcAsync(playMusic.value);
musicHistory.addMusic(playMusic.value);
// 找到歌曲在播放列表中的索引,如果是通过 nextPlay/prevPlay 调用的,不会更新 playListIndex
const songIndex = playList.value.findIndex( const songIndex = playList.value.findIndex(
(item: SongResult) => item.id === music.id && item.source === music.source (item: SongResult) => item.id === music.id && item.source === music.source
); );
// 只有在 songIndex 有效,并且与当前 playListIndex 不同时才更新 // 只有在 songIndex 有效,并且与当前 playListIndex 不同时才更新
// 这样可以避免与 nextPlay/prevPlay 中的索引更新冲突
if (songIndex !== -1 && songIndex !== playListIndex.value) { if (songIndex !== -1 && songIndex !== playListIndex.value) {
console.log('歌曲索引不匹配,更新为:', songIndex); console.log('歌曲索引不匹配,更新为:', songIndex);
playListIndex.value = songIndex; playListIndex.value = songIndex;
} }
// 获取歌曲详情包括URL
const updatedPlayMusic = await getSongDetail(originalMusic);
playMusic.value = updatedPlayMusic;
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
// 保存到本地存储
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
localStorage.setItem('isPlaying', play.value.toString());
// 无论如何都预加载更多歌曲 // 无论如何都预加载更多歌曲
if (songIndex !== -1) { if (songIndex !== -1) {
setTimeout(() => { fetchSongs(playList.value, songIndex + 1, songIndex + 3);
fetchSongs(playList.value, songIndex + 1, songIndex + 2);
}, 3000);
} else { } else {
console.warn('当前歌曲未在播放列表中找到'); console.warn('当前歌曲未在播放列表中找到');
} }
@@ -518,7 +461,7 @@ export const usePlayerStore = defineStore('player', () => {
// 使用标记防止循环调用 // 使用标记防止循环调用
let playInProgress = false; let playInProgress = false;
// 直接调用 playAudio 方法播放音频 // 直接调用 playAudio 方法播放音频,不需要依赖外部监听
try { try {
if (playInProgress) { if (playInProgress) {
console.warn('播放操作正在进行中,避免重复调用'); console.warn('播放操作正在进行中,避免重复调用');
@@ -526,6 +469,8 @@ export const usePlayerStore = defineStore('player', () => {
} }
playInProgress = true; playInProgress = true;
// 因为调用 playAudio 前我们已经设置了 play.value所以不需要额外传递 shouldPlay 参数
const result = await playAudio(); const result = await playAudio();
playInProgress = false; playInProgress = false;
@@ -538,36 +483,19 @@ export const usePlayerStore = defineStore('player', () => {
} catch (error) { } catch (error) {
console.error('处理播放音乐失败:', error); console.error('处理播放音乐失败:', error);
message.error(i18n.global.t('player.playFailed')); message.error(i18n.global.t('player.playFailed'));
// 出现错误时,更新加载状态
if (playMusic.value) {
playMusic.value.playLoading = false;
}
return false; return false;
} }
}; };
const setPlay = async (song: SongResult) => { const setPlay = async (song: SongResult) => {
try { try {
// 如果是当前正在播放的音乐,则切换播放/暂停状态
if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl) {
if (play.value) {
setPlayMusic(false);
audioService.getCurrentSound()?.pause();
} else {
setPlayMusic(true);
audioService.getCurrentSound()?.play();
}
return;
}
// 直接调用 handlePlayMusic它会处理索引更新和播放逻辑 // 直接调用 handlePlayMusic它会处理索引更新和播放逻辑
const success = await handlePlayMusic(song); const success = await handlePlayMusic(song);
// 记录到本地存储,保持一致性 // 记录到本地存储,保持一致性
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
if (success) {
isPlay.value = true;
}
return success; return success;
} catch (error) { } catch (error) {
console.error('设置播放失败:', error); console.error('设置播放失败:', error);
@@ -599,11 +527,8 @@ export const usePlayerStore = defineStore('player', () => {
musicFull.value = value; musicFull.value = value;
}; };
const setPlayList = (list: SongResult[], keepIndex: boolean = false) => { const setPlayList = (list: SongResult[]) => {
// 如果指定保持当前索引,则不重新计算索引 playListIndex.value = list.findIndex((item) => item.id === playMusic.value.id);
if (!keepIndex) {
playListIndex.value = list.findIndex((item) => item.id === playMusic.value.id);
}
playList.value = list; playList.value = list;
localStorage.setItem('playList', JSON.stringify(list)); localStorage.setItem('playList', JSON.stringify(list));
localStorage.setItem('playListIndex', playListIndex.value.toString()); localStorage.setItem('playListIndex', playListIndex.value.toString());
@@ -789,13 +714,20 @@ export const usePlayerStore = defineStore('player', () => {
} }
}; };
// 修改nextPlay方法改进播放逻辑 // 修改nextPlay方法加入定时关闭检查逻辑
const nextPlay = async () => { const nextPlay = async () => {
// 静态标志,防止多次调用造成递归
if ((nextPlay as any).isRunning) {
console.log('下一首播放正在执行中,忽略重复调用');
return;
}
try { try {
(nextPlay as any).isRunning = true;
if (playList.value.length === 0) { if (playList.value.length === 0) {
play.value = true; play.value = true;
(nextPlay as any).isRunning = false;
return; return;
} }
@@ -804,195 +736,125 @@ export const usePlayerStore = defineStore('player', () => {
sleepTimer.value.type === SleepTimerType.PLAYLIST_END) { sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {
// 已是最后一首且为顺序播放模式,触发停止 // 已是最后一首且为顺序播放模式,触发停止
stopPlayback(); stopPlayback();
(nextPlay as any).isRunning = false;
return; return;
} }
// 保存当前索引,用于错误恢复 // 在切换前保存当前播放状态
const currentIndex = playListIndex.value; const shouldPlayNext = play.value;
console.log('切换到下一首,当前播放状态:', shouldPlayNext ? '播放' : '暂停');
let nowPlayListIndex: number; let nowPlayListIndex: number;
if (playMode.value === 2) { if (playMode.value === 2) {
// 随机播放模式
do { do {
nowPlayListIndex = Math.floor(Math.random() * playList.value.length); nowPlayListIndex = Math.floor(Math.random() * playList.value.length);
} while (nowPlayListIndex === playListIndex.value && playList.value.length > 1); } while (nowPlayListIndex === playListIndex.value && playList.value.length > 1);
} else { } else {
// 顺序播放或循环播放模式
nowPlayListIndex = (playListIndex.value + 1) % playList.value.length; nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;
} }
// 获取下一首歌曲 // 重要:首先更新当前播放索引
let nextSong = { ...playList.value[nowPlayListIndex] };
// 记录尝试播放过的索引,防止无限循环
const attemptedIndices = new Set<number>();
attemptedIndices.add(nowPlayListIndex);
// 先更新当前播放索引
playListIndex.value = nowPlayListIndex; playListIndex.value = nowPlayListIndex;
// 尝试播放 // 获取下一首歌曲
let success = false; const nextSong = playList.value[nowPlayListIndex];
let retryCount = 0;
const maxRetries = Math.min(3, playList.value.length);
// 尝试播放最多尝试maxRetries次 // 如果是B站视频确保重新获取URL
while (!success && retryCount < maxRetries) { if (nextSong.source === 'bilibili' && nextSong.bilibiliData) {
success = await handlePlayMusic(nextSong, true); // 清除之前的URL确保重新获取
nextSong.playMusicUrl = undefined;
console.log('下一首是B站视频已清除URL强制重新获取');
}
// 尝试播放,并明确传递应该播放的状态
const success = await handlePlayMusic(nextSong, shouldPlayNext);
if (!success) {
console.error('播放下一首失败,将从播放列表中移除此歌曲');
// 从播放列表中移除失败的歌曲
const newPlayList = [...playList.value];
newPlayList.splice(nowPlayListIndex, 1);
if (!success) { if (newPlayList.length > 0) {
retryCount++; // 更新播放列表后,重新尝试播放下一首
console.error(`播放失败,尝试 ${retryCount}/${maxRetries}`); setPlayList(newPlayList);
// 延迟一点时间再尝试下一首,避免立即触发可能导致的无限循环
if (retryCount >= maxRetries) { setTimeout(() => {
console.error('多次尝试播放失败,将从播放列表中移除此歌曲'); (nextPlay as any).isRunning = false;
// 从播放列表中移除失败的歌曲 nextPlay();
const newPlayList = [...playList.value]; }, 300);
newPlayList.splice(nowPlayListIndex, 1); return;
if (newPlayList.length > 0) {
// 更新播放列表,但保持当前索引不变
const keepCurrentIndexPosition = true;
setPlayList(newPlayList, keepCurrentIndexPosition);
// 继续尝试下一首
if (playMode.value === 2) {
// 随机模式,随机选择一首未尝试过的
const availableIndices = Array.from(
{ length: newPlayList.length },
(_, i) => i
).filter(i => !attemptedIndices.has(i));
if (availableIndices.length > 0) {
// 随机选择一个未尝试过的索引
nowPlayListIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
} else {
// 如果所有歌曲都尝试过了,选择下一个索引
nowPlayListIndex = (playListIndex.value + 1) % newPlayList.length;
}
} else {
// 顺序播放,选择下一首
// 如果当前索引已经是最后一首,循环到第一首
nowPlayListIndex = playListIndex.value >= newPlayList.length
? 0
: playListIndex.value;
}
playListIndex.value = nowPlayListIndex;
attemptedIndices.add(nowPlayListIndex);
if (newPlayList[nowPlayListIndex]) {
nextSong = { ...newPlayList[nowPlayListIndex] };
retryCount = 0; // 重置重试计数器,为新歌曲准备
} else {
// 处理索引无效的情况
console.error('无效的播放索引,停止尝试');
break;
}
} else {
// 播放列表为空,停止尝试
console.error('播放列表为空,停止尝试');
break;
}
}
} }
} }
// 歌曲切换成功,触发歌曲变更处理(用于定时关闭功能) // 歌曲切换成功,触发歌曲变更处理(用于定时关闭功能)
if (success) { handleSongChange();
handleSongChange();
} else {
console.error('所有尝试都失败,无法播放下一首歌曲');
// 如果尝试了所有可能的歌曲仍然失败,恢复到原始索引
playListIndex.value = currentIndex;
setIsPlay(false); // 停止播放
message.error(i18n.global.t('player.playFailed'));
}
} catch (error) { } catch (error) {
console.error('切换下一首出错:', error); console.error('切换下一首出错:', error);
} finally {
(nextPlay as any).isRunning = false;
} }
}; };
// 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进
const prevPlay = async () => { const prevPlay = async () => {
// 静态标志,防止多次调用造成递归
if ((prevPlay as any).isRunning) {
console.log('上一首播放正在执行中,忽略重复调用');
return;
}
try { try {
(prevPlay as any).isRunning = true;
if (playList.value.length === 0) { if (playList.value.length === 0) {
play.value = true; play.value = true;
(prevPlay as any).isRunning = false;
return; return;
} }
// 保存当前索引,用于错误恢复
const currentIndex = playListIndex.value;
const nowPlayListIndex = const nowPlayListIndex =
(playListIndex.value - 1 + playList.value.length) % playList.value.length; (playListIndex.value - 1 + playList.value.length) % playList.value.length;
// 获取上一首歌曲
const prevSong = { ...playList.value[nowPlayListIndex] };
// 重要:首先更新当前播放索引 // 重要:首先更新当前播放索引
playListIndex.value = nowPlayListIndex; playListIndex.value = nowPlayListIndex;
// 尝试播放
let success = false;
let retryCount = 0;
const maxRetries = 2;
// 尝试播放最多尝试maxRetries次
while (!success && retryCount < maxRetries) {
success = await handlePlayMusic(prevSong);
if (!success) { // 获取上一首歌曲
retryCount++; const prevSong = playList.value[nowPlayListIndex];
console.error(`播放上一首失败,尝试 ${retryCount}/${maxRetries}`);
// 如果是B站视频确保重新获取URL
// 最后一次尝试失败 if (prevSong.source === 'bilibili' && prevSong.bilibiliData) {
if (retryCount >= maxRetries) { // 清除之前的URL确保重新获取
console.error('多次尝试播放失败,将从播放列表中移除此歌曲'); prevSong.playMusicUrl = undefined;
// 从播放列表中移除失败的歌曲 console.log('上一首是B站视频已清除URL强制重新获取');
const newPlayList = [...playList.value];
newPlayList.splice(nowPlayListIndex, 1);
if (newPlayList.length > 0) {
// 更新播放列表,但保持当前索引不变
const keepCurrentIndexPosition = true;
setPlayList(newPlayList, keepCurrentIndexPosition);
// 恢复到原始索引或继续尝试上一首
if (newPlayList.length === 1) {
// 只剩一首歌,直接播放它
playListIndex.value = 0;
} else {
// 尝试上上一首
const newPrevIndex = (playListIndex.value - 1 + newPlayList.length) % newPlayList.length;
playListIndex.value = newPrevIndex;
}
// 延迟一点时间再尝试,避免可能的无限循环
setTimeout(() => {
prevPlay(); // 递归调用,尝试再上一首
}, 300);
return;
} else {
// 播放列表为空,停止尝试
console.error('播放列表为空,停止尝试');
break;
}
}
}
} }
// 尝试播放如果失败会返回false
const success = await handlePlayMusic(prevSong);
if (!success) { if (success) {
console.error('所有尝试都失败,无法播放上一首歌曲'); await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex);
// 如果尝试了所有可能的歌曲仍然失败,恢复到原始索引 } else {
playListIndex.value = currentIndex; console.error('播放上一首失败,将从播放列表中移除此歌曲');
setIsPlay(false); // 停止播放 // 从播放列表中移除失败的歌曲
message.error(i18n.global.t('player.playFailed')); const newPlayList = [...playList.value];
newPlayList.splice(nowPlayListIndex, 1);
if (newPlayList.length > 0) {
// 更新播放列表后,重新尝试播放上一首
setPlayList(newPlayList);
// 延迟一点时间再尝试上一首,避免立即触发可能导致的无限循环
setTimeout(() => {
(prevPlay as any).isRunning = false;
prevPlay();
}, 300);
return;
}
} }
} catch (error) { } catch (error) {
console.error('切换上一首出错:', error); console.error('切换上一首出错:', error);
} finally {
(prevPlay as any).isRunning = false;
} }
}; };
@@ -1012,7 +874,6 @@ export const usePlayerStore = defineStore('player', () => {
if (!isAlreadyInList) { if (!isAlreadyInList) {
favoriteList.value.push(id); favoriteList.value.push(id);
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value)); localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
typeof id === 'number' && useUserStore().user && likeSong(id, true);
} }
}; };
@@ -1022,7 +883,6 @@ export const usePlayerStore = defineStore('player', () => {
favoriteList.value = favoriteList.value.filter(existingId => !isBilibiliIdMatch(existingId, id)); favoriteList.value = favoriteList.value.filter(existingId => !isBilibiliIdMatch(existingId, id));
} else { } else {
favoriteList.value = favoriteList.value.filter(existingId => existingId !== id); favoriteList.value = favoriteList.value.filter(existingId => existingId !== id);
useUserStore().user && likeSong(Number(id), false);
} }
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value)); localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
}; };
@@ -1121,7 +981,7 @@ export const usePlayerStore = defineStore('player', () => {
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value)); localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
}; };
// 修改 playAudio 函数中的错误处理逻辑,避免在操作锁问题时频繁尝试播放 // 修改:处理音频播放的方法,使用事件触发机制
const playAudio = async () => { const playAudio = async () => {
if (!playMusicUrl.value || !playMusic.value) return null; if (!playMusicUrl.value || !playMusic.value) return null;
@@ -1181,40 +1041,23 @@ export const usePlayerStore = defineStore('player', () => {
console.error('播放音频失败:', error); console.error('播放音频失败:', error);
setPlayMusic(false); setPlayMusic(false);
// 避免直接调用 nextPlay改用延时避免无限循环
// 检查错误是否是由于操作锁引起的 // 检查错误是否是由于操作锁引起的
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
// 操作锁错误处理
if (errorMsg.includes('操作锁激活')) { if (errorMsg.includes('操作锁激活')) {
console.log('由于操作锁正在使用,将在1000ms后重试'); console.log('由于操作锁正在使用,将在500ms后重试');
// 操作锁错误,延迟后再尝试
// 强制重置操作锁并延迟再试
try {
// 尝试强制重置音频服务的操作锁
audioService.forceResetOperationLock();
console.log('已强制重置操作锁');
} catch (e) {
console.error('重置操作锁失败:', e);
}
// 延迟较长时间,确保锁已完全释放
setTimeout(() => { setTimeout(() => {
// 直接重试当前歌曲,而不是切换到下一首 // 检查当前播放列表是否有下一首
playAudio().catch(e => { if (playList.value.length > 1) {
console.error('重试播放失败,切换到下一首:', e); nextPlay();
}
// 只有再次失败才切换到下一首 }, 500);
if (playList.value.length > 1) {
nextPlay();
}
});
}, 1000);
} else { } else {
// 其他错误,切换到下一首 // 其他错误,延迟更短时间后切换
console.log('播放失败,切换到下一首');
setTimeout(() => { setTimeout(() => {
nextPlay(); nextPlay();
}, 300); }, 100);
} }
message.error(i18n.global.t('player.playFailed')); message.error(i18n.global.t('player.playFailed'));
@@ -1222,80 +1065,6 @@ export const usePlayerStore = defineStore('player', () => {
} }
}; };
// 使用指定的音源重新解析当前播放的歌曲
const reparseCurrentSong = async (sourcePlatform: Platform) => {
try {
const currentSong = playMusic.value;
if (!currentSong || !currentSong.id) {
console.warn('没有有效的播放对象');
return false;
}
// B站视频不支持重新解析
if (currentSong.source === 'bilibili') {
console.warn('B站视频不支持重新解析');
return false;
}
// 保存用户选择的音源
const songId = String(currentSong.id);
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
// 停止当前播放
const currentSound = audioService.getCurrentSound();
if (currentSound) {
currentSound.pause();
}
// 重新获取歌曲URL
const numericId = typeof currentSong.id === 'string'
? parseInt(currentSong.id, 10)
: currentSong.id;
const res = await getParsingMusicUrl(numericId, cloneDeep(currentSong));
if (res && res.data && res.data.data && res.data.data.url) {
// 更新URL
const newUrl = res.data.data.url;
// 使用新URL更新播放
const updatedMusic = {
...currentSong,
playMusicUrl: newUrl,
expiredAt: Date.now() + 1800000 // 半小时后过期
};
// 更新播放器状态并开始播放
await setPlay(updatedMusic);
setPlayMusic(true);
return true;
} else {
return false;
}
} catch (error) {
console.error('重新解析失败:', error);
return false;
}
};
// 设置播放列表抽屉显示状态
const setPlayListDrawerVisible = (value: boolean) => {
playListDrawerVisible.value = value;
};
// 播放
const handlePause = async () => {
try {
const currentSound = audioService.getCurrentSound();
if (currentSound) {
currentSound.pause();
}
setPlayMusic(false);
} catch (error) {
console.error('暂停播放失败:', error);
}
}
return { return {
play, play,
isPlay, isPlay,
@@ -1307,7 +1076,6 @@ export const usePlayerStore = defineStore('player', () => {
musicFull, musicFull,
savedPlayProgress, savedPlayProgress,
favoriteList, favoriteList,
playListDrawerVisible,
// 定时关闭相关 // 定时关闭相关
sleepTimer, sleepTimer,
@@ -1325,7 +1093,6 @@ export const usePlayerStore = defineStore('player', () => {
currentPlayList, currentPlayList,
currentPlayListIndex, currentPlayListIndex,
clearPlayAll,
setPlay, setPlay,
setIsPlay, setIsPlay,
nextPlay, nextPlay,
@@ -1340,9 +1107,6 @@ export const usePlayerStore = defineStore('player', () => {
addToFavorite, addToFavorite,
removeFromFavorite, removeFromFavorite,
removeFromPlayList, removeFromPlayList,
playAudio, playAudio
reparseCurrentSong,
setPlayListDrawerVisible,
handlePause
}; };
}); });

View File

@@ -36,8 +36,6 @@ export const useUserStore = defineStore('user', () => {
user.value = null; user.value = null;
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.removeItem('token'); localStorage.removeItem('token');
// 刷新
window.location.reload();
} catch (error) { } catch (error) {
console.error('登出失败:', error); console.error('登出失败:', error);
} }

View File

@@ -42,9 +42,6 @@ export interface SongResult {
expiredAt?: number; expiredAt?: number;
// 获取时间 // 获取时间
createdAt?: number; createdAt?: number;
// 时长
duration?: number;
dt?: number;
} }
export interface Song { export interface Song {

View File

@@ -1,5 +1,5 @@
// 音乐平台类型 // 音乐平台类型
export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'gdmusic'; export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube' | 'gdmusic';
// 默认平台列表 // 默认平台列表
export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo']; export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'youtube'];

View File

@@ -11,12 +11,6 @@ import { showShortcutToast } from './shortcutToast';
let actionTimeout: NodeJS.Timeout | null = null; let actionTimeout: NodeJS.Timeout | null = null;
const ACTION_DELAY = 300; // 毫秒 const ACTION_DELAY = 300; // 毫秒
// 添加一个操作锁,记录最后一次操作的时间和动作
let lastActionInfo = {
action: '',
timestamp: 0
};
interface ShortcutConfig { interface ShortcutConfig {
key: string; key: string;
enabled: boolean; enabled: boolean;
@@ -37,33 +31,17 @@ let appShortcuts: ShortcutsConfig = {};
* @param action 快捷键动作 * @param action 快捷键动作
*/ */
export async function handleShortcutAction(action: string) { export async function handleShortcutAction(action: string) {
const now = Date.now();
// 如果存在未完成的动作,则忽略当前请求 // 如果存在未完成的动作,则忽略当前请求
if (actionTimeout) { if (actionTimeout) {
console.log('[AppShortcuts] 忽略快速连续的动作请求:', action); console.log('忽略快速连续的动作请求:', action);
return; return;
} }
// 检查是否是同一个动作的重复触发300ms内
if (lastActionInfo.action === action && now - lastActionInfo.timestamp < ACTION_DELAY) {
console.log(`[AppShortcuts] 忽略重复的 ${action} 动作,距上次仅 ${now - lastActionInfo.timestamp}ms`);
return;
}
// 更新最后一次操作信息
lastActionInfo = {
action,
timestamp: now
};
// 设置防抖锁 // 设置防抖锁
actionTimeout = setTimeout(() => { actionTimeout = setTimeout(() => {
actionTimeout = null; actionTimeout = null;
}, ACTION_DELAY); }, ACTION_DELAY);
console.log(`[AppShortcuts] 执行动作: ${action}, 时间戳: ${now}`);
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -115,19 +93,16 @@ export async function handleShortcutAction(action: string) {
case 'toggleFavorite': { case 'toggleFavorite': {
const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id)); const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));
const numericId = Number(playerStore.playMusic.id); const numericId = Number(playerStore.playMusic.id);
console.log(`[AppShortcuts] toggleFavorite 当前状态: ${isFavorite}, ID: ${numericId}`);
if (isFavorite) { if (isFavorite) {
playerStore.removeFromFavorite(numericId); playerStore.removeFromFavorite(numericId);
console.log(`[AppShortcuts] 已从收藏中移除: ${numericId}`);
} else { } else {
playerStore.addToFavorite(numericId); playerStore.addToFavorite(numericId);
console.log(`[AppShortcuts] 已添加到收藏: ${numericId}`);
} }
showToast( showToast(
isFavorite isFavorite
? t('player.playBar.unFavorite', { name: playerStore.playMusic.name }) ? t('player.playBar.favorite', { name: playerStore.playMusic.name })
: t('player.playBar.favorite', { name: playerStore.playMusic.name }), : t('player.playBar.unFavorite', { name: playerStore.playMusic.name }),
isFavorite ? 'ri-heart-line' : 'ri-heart-fill' isFavorite ? 'ri-heart-fill' : 'ri-heart-line'
); );
break; break;
} }
@@ -139,9 +114,10 @@ export async function handleShortcutAction(action: string) {
console.error(`执行快捷键动作 ${action} 时出错:`, error); console.error(`执行快捷键动作 ${action} 时出错:`, error);
} finally { } finally {
// 确保在出错时也能清除超时 // 确保在出错时也能清除超时
clearTimeout(actionTimeout); if (actionTimeout) {
actionTimeout = null; clearTimeout(actionTimeout);
console.log(`[AppShortcuts] 动作完成: ${action}, 时间戳: ${Date.now()}, 耗时: ${Date.now() - now}ms`); actionTimeout = null;
}
} }
} }

View File

@@ -17,7 +17,7 @@ const baseURL = window.electron
const request = axios.create({ const request = axios.create({
baseURL, baseURL,
timeout: 15000, timeout: 5000,
withCredentials: true withCredentials: true
}); });

View File

@@ -129,13 +129,20 @@ export const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null>
try { try {
const token = import.meta.env.VITE_GITHUB_TOKEN; const token = import.meta.env.VITE_GITHUB_TOKEN;
const headers = {}; const headers = {};
// 获取代理节点列表
const proxyHosts = await getProxyNodes();
// 构建 API URL 列表 // 构建 API URL 列表
const apiUrls = [ const apiUrls = [
// 原始地址 // 原始地址
'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest', 'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest',
// 使用代理节点 // 使用代理节点
'https://music.alger.fun/package.json', ...proxyHosts.map(
(host) =>
`${host}/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/package.json`
)
]; ];
if (token) { if (token) {

View File

@@ -73,7 +73,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDateFormat } from '@vueuse/core'; import { useDateFormat } from '@vueuse/core';
import { computed, onMounted, onUnmounted, ref, watch, nextTick, onActivated, onDeactivated } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@@ -86,10 +86,6 @@ import { usePlayerStore } from '@/store';
import { IArtist } from '@/type/artist'; import { IArtist } from '@/type/artist';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
defineOptions({
name: 'ArtistDetail'
});
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const playerStore = usePlayerStore(); const playerStore = usePlayerStore();
@@ -126,33 +122,10 @@ const albumsLoadMoreRef = ref<HTMLElement | null>(null);
let songsObserver: IntersectionObserver | null = null; let songsObserver: IntersectionObserver | null = null;
let albumsObserver: IntersectionObserver | null = null; let albumsObserver: IntersectionObserver | null = null;
// 添加上一个ID的引用用于比较
const previousId = ref<string | null>(null);
// 简化缓存机制
const artistDataCache = new Map();
// 单个缓存键函数
const getCacheKey = (id: string | number) => `artist_${id}`;
// 加载歌手信息 // 加载歌手信息
const loadArtistInfo = async () => { const loadArtistInfo = async () => {
if (!artistId.value) return; if (!artistId.value) return;
// 简化缓存检查
const cacheKey = getCacheKey(artistId.value);
if (artistDataCache.has(cacheKey)) {
console.log('使用缓存数据');
const cachedData = artistDataCache.get(cacheKey);
artistInfo.value = cachedData.artistInfo;
songs.value = cachedData.songs;
albums.value = cachedData.albums;
songPage.value = cachedData.songPage;
albumPage.value = cachedData.albumPage;
return;
}
// 加载新数据
loading.value = true; loading.value = true;
try { try {
const info = await getArtistDetail(artistId.value); const info = await getArtistDetail(artistId.value);
@@ -162,15 +135,6 @@ const loadArtistInfo = async () => {
// 重置分页并加载初始数据 // 重置分页并加载初始数据
resetPagination(); resetPagination();
await Promise.all([loadSongs(), loadAlbums()]); await Promise.all([loadSongs(), loadAlbums()]);
// 保存到缓存
artistDataCache.set(cacheKey, {
artistInfo: artistInfo.value,
songs: [...songs.value],
albums: [...albums.value],
songPage: { ...songPage.value },
albumPage: { ...albumPage.value }
});
} catch (error) { } catch (error) {
console.error('加载歌手信息失败:', error); console.error('加载歌手信息失败:', error);
} finally { } finally {
@@ -277,99 +241,79 @@ const handlePlay = () => {
); );
}; };
// 简化观察器设置 // 设置无限滚动观察器
const setupObservers = () => { const setupIntersectionObservers = () => {
// 清理之前的观察器 // 清除现有的观察器
if (songsObserver) songsObserver.disconnect(); if (songsObserver) songsObserver.disconnect();
if (albumsObserver) albumsObserver.disconnect(); if (albumsObserver) albumsObserver.disconnect();
// 创建观察器(如果不存在) // 创建歌曲观察器
if (!songsObserver) { songsObserver = new IntersectionObserver((entries) => {
songsObserver = new IntersectionObserver( if (entries[0].isIntersecting && !songLoading.value && songPage.value.hasMore) {
(entries) => { loadSongs();
if (entries[0].isIntersecting && songPage.value.hasMore) { }
loadSongs(); }, { threshold: 0.1 });
}
}, // 创建专辑观察器
{ threshold: 0.1 } albumsObserver = new IntersectionObserver((entries) => {
); if (entries[0].isIntersecting && !albumLoading.value && albumPage.value.hasMore) {
} loadAlbums();
}
if (!albumsObserver) { }, { threshold: 0.1 });
albumsObserver = new IntersectionObserver(
(entries) => { // 监听标签页更改,重新设置观察器
if (entries[0].isIntersecting && albumPage.value.hasMore) { watch(activeTab, (newTab) => {
loadAlbums(); nextTick(() => {
} if (newTab === 'songs' && songsLoadMoreRef.value && songPage.value.hasMore) {
}, songsObserver?.observe(songsLoadMoreRef.value);
{ threshold: 0.1 } } else if (newTab === 'albums' && albumsLoadMoreRef.value && albumPage.value.hasMore) {
); albumsObserver?.observe(albumsLoadMoreRef.value);
} }
});
// 观察当前标签页的元素 });
nextTick(() => {
if (activeTab.value === 'songs' && songsLoadMoreRef.value) { // 监听引用元素的变化
songsObserver?.observe(songsLoadMoreRef.value); watch(songsLoadMoreRef, (el) => {
} else if (activeTab.value === 'albums' && albumsLoadMoreRef.value) { if (el && activeTab.value === 'songs' && songPage.value.hasMore) {
albumsObserver?.observe(albumsLoadMoreRef.value); songsObserver?.observe(el);
}
});
watch(albumsLoadMoreRef, (el) => {
if (el && activeTab.value === 'albums' && albumPage.value.hasMore) {
albumsObserver?.observe(el);
} }
}); });
}; };
// 监听标签切换
watch(activeTab, () => {
setupObservers();
});
// 监听引用元素的变化
watch([songsLoadMoreRef, albumsLoadMoreRef], () => {
setupObservers();
});
onActivated(() => {
// 确保当前路由是艺术家详情页
if (route.name === 'artistDetail') {
const currentId = route.params.id as string;
// 首次加载或ID变化时加载数据
if (!previousId.value || previousId.value !== currentId) {
console.log('ID已变化加载新数据');
previousId.value = currentId;
activeTab.value = 'songs';
loadArtistInfo();
}
// 重新设置观察器
setupObservers();
}
});
onMounted(() => { onMounted(() => {
// 首次挂载时加载数据 loadArtistInfo();
if (route.params.id) {
previousId.value = route.params.id as string; // 添加nextTick以确保DOM已更新
loadArtistInfo(); nextTick(() => {
setupObservers(); setupIntersectionObservers();
} });
}); });
onDeactivated(() => { onUnmounted(() => {
// 断开观察器但不清除引用 // 清理观察器
if (songsObserver) songsObserver.disconnect(); if (songsObserver) songsObserver.disconnect();
if (albumsObserver) albumsObserver.disconnect(); if (albumsObserver) albumsObserver.disconnect();
}); });
onUnmounted(() => { // 监听路由参数变化
// 完全清理观察器 watch(
if (songsObserver) { () => route.params.id,
songsObserver.disconnect(); (newId) => {
songsObserver = null; if (newId) {
loadArtistInfo();
// 添加nextTick以确保DOM已更新
nextTick(() => {
setupIntersectionObservers();
});
}
} }
if (albumsObserver) { );
albumsObserver.disconnect();
albumsObserver = null;
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -425,7 +425,7 @@ const playCurrentAudio = async () => {
// 播放当前选中的分P // 播放当前选中的分P
console.log('播放当前选中的分P:', currentAudio.name, '音频URL:', currentAudio.playMusicUrl); console.log('播放当前选中的分P:', currentAudio.name, '音频URL:', currentAudio.playMusicUrl);
playerStore.setPlay(currentAudio); playerStore.setPlayMusic(currentAudio);
// 播放后通知用户已开始播放 // 播放后通知用户已开始播放
message.success('已开始播放'); message.success('已开始播放');

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
class="recommend-item" class="recommend-item"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="getItemAnimationDelay(index)" :style="getItemAnimationDelay(index)"
@click.stop="openPlaylist(item)" @click.stop="selectRecommendItem(item)"
> >
<div class="recommend-item-img"> <div class="recommend-item-img">
<n-image <n-image
@@ -57,15 +57,22 @@
</div> </div>
<div v-if="!hasMore && recommendList.length > 0" class="no-more">没有更多了</div> <div v-if="!hasMore && recommendList.length > 0" class="no-more">没有更多了</div>
</n-scrollbar> </n-scrollbar>
<music-list
v-model:show="showMusic"
v-model:loading="listLoading"
:name="recommendItem?.name || ''"
:song-list="listDetail?.playlist.tracks || []"
:list-info="listDetail?.playlist"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import { getPlaylistCategory } from '@/api/home'; import { getPlaylistCategory } from '@/api/home';
import { getListByCat, getListDetail } from '@/api/list'; import { getListByCat, getListDetail } from '@/api/list';
import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import MusicList from '@/components/MusicList.vue';
import type { IRecommendItem } from '@/type/list'; import type { IRecommendItem } from '@/type/list';
import type { IListDetail } from '@/type/listDetail'; import type { IListDetail } from '@/type/listDetail';
import type { IPlayListSort } from '@/type/playlist'; import type { IPlayListSort } from '@/type/playlist';
@@ -78,6 +85,7 @@ defineOptions({
const TOTAL_ITEMS = 42; // 每页数量 const TOTAL_ITEMS = 42; // 每页数量
const recommendList = ref<any[]>([]); const recommendList = ref<any[]>([]);
const showMusic = ref(false);
const page = ref(0); const page = ref(0);
const hasMore = ref(true); const hasMore = ref(true);
const isLoadingMore = ref(false); const isLoadingMore = ref(false);
@@ -92,25 +100,15 @@ const recommendItem = ref<IRecommendItem | null>();
const listDetail = ref<IListDetail | null>(); const listDetail = ref<IListDetail | null>();
const listLoading = ref(true); const listLoading = ref(true);
const router = useRouter(); const selectRecommendItem = async (item: IRecommendItem) => {
const openPlaylist = (item: any) => {
recommendItem.value = item;
listLoading.value = true; listLoading.value = true;
recommendItem.value = null;
getListDetail(item.id).then(res => { listDetail.value = null;
listDetail.value = res.data; showMusic.value = true;
listLoading.value = false; recommendItem.value = item;
const { data } = await getListDetail(item.id);
navigateToMusicList(router, { listDetail.value = data;
id: item.id, listLoading.value = false;
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: false
});
});
}; };
const route = useRoute(); const route = useRoute();

View File

@@ -682,7 +682,7 @@ body,
--text-secondary: #ffffffea; --text-secondary: #ffffffea;
--highlight-color: #1db954; --highlight-color: #1db954;
--control-bg: rgba(124, 124, 124, 0.3); --control-bg: rgba(124, 124, 124, 0.3);
&:hover:not(.lyric_lock) { &:hover {
background: rgba(44, 44, 44, 0.466) !important; background: rgba(44, 44, 44, 0.466) !important;
} }
} }
@@ -692,7 +692,7 @@ body,
--text-secondary: #39393989; --text-secondary: #39393989;
--highlight-color: #1db954; --highlight-color: #1db954;
--control-bg: rgba(255, 255, 255, 0.3); --control-bg: rgba(255, 255, 255, 0.3);
&:hover:not(.lyric_lock) { &:hover {
background: rgba(0, 0, 0, 0.434) !important; background: rgba(0, 0, 0, 0.434) !important;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,8 @@ onMounted(async () => {
const handleShowMv = async (item: IMvItem, index: number) => { const handleShowMv = async (item: IMvItem, index: number) => {
playerStore.setIsPlay(false); playerStore.setIsPlay(false);
audioService.pause(); playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
showMv.value = true; showMv.value = true;
currentIndex.value = index; currentIndex.value = index;
playMvItem.value = item; playMvItem.value = item;

View File

@@ -37,12 +37,6 @@
@click="searchDetail = null" @click="searchDetail = null"
></i> ></i>
{{ hotKeyword }} {{ hotKeyword }}
<div v-if="searchDetail?.songs?.length" class="title-play-all">
<div class="play-all-btn" @click="handlePlayAll">
<i class="ri-play-circle-fill"></i>
<span>{{ t('search.button.playAll') }}</span>
</div>
</div>
</div> </div>
<div v-loading="searchDetailLoading" class="search-list-box"> <div v-loading="searchDetailLoading" class="search-list-box">
<template v-if="searchDetail"> <template v-if="searchDetail">
@@ -52,7 +46,7 @@
v-for="(item, index) in searchDetail?.bilibili" v-for="(item, index) in searchDetail?.bilibili"
:key="item.bvid" :key="item.bvid"
:class="setAnimationClass('animate__bounceInRight')" :class="setAnimationClass('animate__bounceInRight')"
:style="getSearchListAnimation(index)" :style="setAnimationDelay(index, 50)"
> >
<bilibili-item :item="item" @play="handlePlayBilibili" /> <bilibili-item :item="item" @play="handlePlayBilibili" />
</div> </div>
@@ -68,9 +62,9 @@
v-for="(item, index) in searchDetail?.songs" v-for="(item, index) in searchDetail?.songs"
:key="item.id" :key="item.id"
:class="setAnimationClass('animate__bounceInRight')" :class="setAnimationClass('animate__bounceInRight')"
:style="getSearchListAnimation(index)" :style="setAnimationDelay(index, 50)"
> >
<song-item :item="item" @play="handlePlay" :is-next="true" /> <song-item :item="item" @play="handlePlay" />
</div> </div>
<template v-for="(list, key) in searchDetail"> <template v-for="(list, key) in searchDetail">
<template v-if="key.toString() !== 'songs'"> <template v-if="key.toString() !== 'songs'">
@@ -79,7 +73,7 @@
:key="item.id" :key="item.id"
class="mb-3" class="mb-3"
:class="setAnimationClass('animate__bounceInRight')" :class="setAnimationClass('animate__bounceInRight')"
:style="getSearchListAnimation(index)" :style="setAnimationDelay(index, 50)"
> >
<search-item :item="item" /> <search-item :item="item" />
</div> </div>
@@ -110,7 +104,7 @@
v-for="(item, index) in searchHistory" v-for="(item, index) in searchHistory"
:key="index" :key="index"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="getSearchListAnimation(index)" :style="setAnimationDelay(index, 50)"
class="search-history-item" class="search-history-item"
round round
closable closable
@@ -168,10 +162,6 @@ const hasMore = ref(true);
const isLoadingMore = ref(false); const isLoadingMore = ref(false);
const currentKeyword = ref(''); const currentKeyword = ref('');
const getSearchListAnimation = (index: number) => {
return setAnimationDelay(index % ITEMS_PER_PAGE, 50);
};
// 从 localStorage 加载搜索历史 // 从 localStorage 加载搜索历史
const loadSearchHistory = () => { const loadSearchHistory = () => {
const history = localStorage.getItem('searchHistory'); const history = localStorage.getItem('searchHistory');
@@ -408,9 +398,9 @@ watch(
{ immediate: true } { immediate: true }
); );
const handlePlay = (item: any) => { const handlePlay = () => {
// 添加到下一首 const tracks = searchDetail.value?.songs || [];
playerStore.addToNextPlay(item); playerStore.setPlayList(tracks);
}; };
// 点击搜索历史 // 点击搜索历史
@@ -428,18 +418,6 @@ const handlePlayBilibili = (item: IBilibiliSearchResult) => {
// 使用路由导航到B站播放页面 // 使用路由导航到B站播放页面
router.push(`/bilibili/${item.bvid}`); router.push(`/bilibili/${item.bvid}`);
}; };
const handlePlayAll = () => {
if (!searchDetail.value?.songs?.length) return;
// 设置播放列表为搜索结果中的所有歌曲
playerStore.setPlayList(searchDetail.value.songs);
// 开始播放第一首歌
if (searchDetail.value.songs[0]) {
playerStore.setPlay(searchDetail.value.songs[0]);
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -491,21 +469,8 @@ const handlePlayAll = () => {
} }
.title { .title {
@apply text-xl font-bold my-2 mx-4 flex items-center; @apply text-xl font-bold my-2 mx-4;
@apply text-gray-900 dark:text-white; @apply text-gray-900 dark:text-white;
&-play-all {
@apply ml-auto;
}
}
.play-all-btn {
@apply flex items-center gap-1 px-3 py-1 rounded-full cursor-pointer transition-all;
@apply text-sm font-normal text-gray-900 dark:text-white hover:bg-light-300 dark:hover:bg-dark-300 hover:text-green-500;
i {
@apply text-xl;
}
} }
.search-history { .search-history {

View File

@@ -40,7 +40,7 @@
<language-switcher /> <language-switcher />
</div> </div>
<div class="set-item" v-if="isElectron"> <div class="set-item">
<div> <div>
<div class="set-item-title">{{ t('settings.basic.font') }}</div> <div class="set-item-title">{{ t('settings.basic.font') }}</div>
<div class="set-item-content">{{ t('settings.basic.fontDesc') }}</div> <div class="set-item-content">{{ t('settings.basic.fontDesc') }}</div>
@@ -103,9 +103,9 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm text-gray-400" v-if="!isMobile">{{ setData.animationSpeed }}x</span> <span class="text-sm text-gray-400">{{ setData.animationSpeed }}x</span>
<div> <div class="w-60">
<template v-if="!isMobile"><n-slider <n-slider
v-model:value="setData.animationSpeed" v-model:value="setData.animationSpeed"
:min="0.1" :min="0.1"
:max="3" :max="3"
@@ -117,19 +117,7 @@
}" }"
:disabled="setData.noAnimate" :disabled="setData.noAnimate"
class="w-40" class="w-40"
/></template> />
<template v-else>
<n-input-number
v-model:value="setData.animationSpeed"
:min="0.1"
:max="3"
:step="0.1"
:placeholder="t('settings.basic.animationSpeedPlaceholder')"
:disabled="setData.noAnimate"
button-placement="both"
style="width: 100px"
/>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -140,42 +128,28 @@
<div id="playback" ref="playbackRef" class="settings-section"> <div id="playback" ref="playbackRef" class="settings-section">
<div class="settings-section-title">{{ t('settings.sections.playback') }}</div> <div class="settings-section-title">{{ t('settings.sections.playback') }}</div>
<div class="settings-section-content"> <div class="settings-section-content">
<div> <div class="set-item">
<div class="set-item"> <div>
<div> <div class="set-item-title">{{ t('settings.playback.quality') }}</div>
<div class="set-item-title">{{ t('settings.playback.quality') }}</div> <div class="set-item-content">{{ t('settings.playback.qualityDesc') }}</div>
<div class="set-item-content">
{{ t('settings.playback.qualityDesc') }}
</div>
</div>
<n-select
v-model:value="setData.musicQuality"
:options="[
{ 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"
/>
</div>
<!-- 网易云 QQ 音乐 酷我 酷狗 会员购买链接 -->
<div class="p-2 bg-light-100 dark:bg-dark-100 rounded-lg mt-2">
<div>大家还是需要支持正版本软件只做开源探讨</div>
<div class="mt-2">各大音乐会员购买链接</div>
<div class="flex gap-5 flex-wrap">
<a class="text-green-400 hover:text-green-500" href="https://music.163.com/store/vip" target="_blank">网易云音乐会员</a>
<a class="text-green-400 hover:text-green-500" href="https://y.qq.com/portal/vipportal/" target="_blank">QQ音乐会员</a>
<a class="text-green-400 hover:text-green-500" href="https://vip.kugou.com/" target="_blank">酷狗音乐会员</a>
<a class="text-green-400 hover:text-green-500" href="https://vip1.kuwo.cn/" target="_blank">酷我音乐会员</a>
</div>
</div> </div>
<n-select
v-model:value="setData.musicQuality"
:options="[
{ 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"
/>
</div> </div>
<div class="set-item" v-if="isElectron"> <div class="set-item" v-if="isElectron">
<div> <div>
<div class="set-item-title">{{ t('settings.playback.musicSources') }}</div> <div class="set-item-title">{{ t('settings.playback.musicSources') }}</div>
@@ -209,17 +183,6 @@
</n-button> </n-button>
</div> </div>
<div class="set-item" v-if="platform === 'darwin'">
<div>
<div class="set-item-title">{{ t('settings.playback.showStatusBar') }}</div>
<div class="set-item-content">{{ t('settings.playback.showStatusBarContent') }}</div>
</div>
<n-switch v-model:value="setData.showTopAction">
<template #checked>{{ t('common.on') }}</template>
<template #unchecked>{{ t('common.off') }}</template>
</n-switch>
</div>
<div class="set-item"> <div class="set-item">
<div> <div>
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div> <div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
@@ -444,7 +407,7 @@
<!-- 捐赠支持 --> <!-- 捐赠支持 -->
<div id="donation" ref="donationRef" class="settings-section"> <div id="donation" ref="donationRef" class="settings-section">
<div class="settings-section-title">{{ t('settings.sectio ns.donation') }}</div> <div class="settings-section-title">{{ t('settings.sections.donation') }}</div>
<div class="settings-section-content"> <div class="settings-section-content">
<div class="set-item"> <div class="set-item">
<div> <div>
@@ -523,9 +486,7 @@ import { type Platform } from '@/types/music';
import config from '../../../../package.json'; import config from '../../../../package.json';
// 所有平台默认值 // 所有平台默认值
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo']; const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'youtube'];
const platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const userStore = useUserStore(); const userStore = useUserStore();

View File

@@ -1,178 +0,0 @@
<template>
<div class="toplist-page">
<n-scrollbar class="toplist-container" style="height: 100%" :size="100">
<div v-loading="loading" class="toplist-list">
<div
v-for="(item, index) in topList"
:key="item.id"
class="toplist-item"
:class="setAnimationClass('animate__bounceIn')"
:style="getItemAnimationDelay(index)"
@click.stop="openToplist(item)"
>
<div class="toplist-item-img">
<n-image
class="toplist-item-img-img"
:src="getImgUrl(item.coverImgUrl, '300y300')"
width="200"
height="200"
lazy
preview-disabled
/>
<div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i>
</div>
</div>
<div class="toplist-item-title">{{ item.name }}</div>
<div class="toplist-item-desc">{{ item.updateFrequency || '' }}</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-more">
<n-spin size="small" />
<span class="ml-2">加载中...</span>
</div>
</n-scrollbar>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { getToplist, getListDetail } from '@/api/list';
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
import type { IListDetail } from '@/type/listDetail';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'Toplist'
});
const topList = ref<any[]>([]);
// 计算每个项目的动画延迟
const getItemAnimationDelay = (index: number) => {
return setAnimationDelay(index, 30);
};
const listDetail = ref<IListDetail | null>();
const listLoading = ref(true);
const router = useRouter();
const openToplist = (item: any) => {
listLoading.value = true;
getListDetail(item.id).then(res => {
listDetail.value = res.data;
listLoading.value = false;
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: false
});
});
};
const loading = ref(false);
const loadToplist = async () => {
loading.value = true;
try {
const { data } = await getToplist();
topList.value = data.list || [];
} catch (error) {
console.error('加载排行榜列表失败:', error);
} finally {
loading.value = false;
}
};
onMounted(() => {
loadToplist();
});
</script>
<style lang="scss" scoped>
.toplist-page {
@apply relative h-full w-full;
@apply bg-light dark:bg-black;
}
.toplist-container {
@apply p-4;
}
.toplist-list {
@apply grid gap-x-8 gap-y-6 pb-28 pr-4;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.toplist-item {
@apply flex flex-col;
&-img {
@apply rounded-xl overflow-hidden relative w-full aspect-square;
&-img {
@apply block w-full h-full;
}
img {
@apply absolute top-0 left-0 w-full h-full object-cover rounded-xl;
}
&:hover img {
@apply hover:scale-110 transition-all duration-300 ease-in-out;
}
.top {
@apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;
@apply bg-black bg-opacity-50;
opacity: 0;
i {
@apply text-5xl text-white transition-all duration-500 ease-in-out opacity-0;
}
&:hover {
@apply opacity-100;
}
&:hover i {
@apply transform scale-150 opacity-100;
}
.play-count {
@apply absolute top-2 left-2 text-sm text-white;
}
}
}
&-title {
@apply mt-2 text-sm line-clamp-1 font-bold;
@apply text-gray-900 dark:text-white;
}
&-desc {
@apply mt-1 text-xs line-clamp-1;
@apply text-gray-500 dark:text-gray-400;
}
}
.loading-more {
@apply flex justify-center items-center py-4;
@apply text-gray-500 dark:text-gray-400;
}
.mobile {
.toplist-list {
@apply px-4 gap-4;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>

View File

@@ -63,7 +63,7 @@
class="playlist-item" class="playlist-item"
:class="setAnimationClass('animate__fadeInUp')" :class="setAnimationClass('animate__fadeInUp')"
:style="setAnimationDelay(index, 50)" :style="setAnimationDelay(index, 50)"
@click="openPlaylist(item)" @click="showPlaylist(item.id, item.name)"
> >
<div class="playlist-cover"> <div class="playlist-cover">
<n-image <n-image
@@ -120,6 +120,14 @@
<div class="pb-20"></div> <div class="pb-20"></div>
</div> </div>
</n-scrollbar> </n-scrollbar>
<music-list
v-model:show="isShowList"
:name="currentList?.name || ''"
:song-list="currentList?.tracks || []"
:list-info="currentList"
:loading="listLoading"
/>
</div> </div>
</template> </template>
@@ -132,7 +140,7 @@ import { useRoute, useRouter } from 'vue-router';
import { getListDetail } from '@/api/list'; import { getListDetail } from '@/api/list';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user'; import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import SongItem from '@/components/common/SongItem.vue'; import SongItem from '@/components/common/SongItem.vue';
import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import MusicList from '@/components/MusicList.vue';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import type { Playlist } from '@/type/listDetail'; import type { Playlist } from '@/type/listDetail';
import type { IUserDetail } from '@/type/user'; import type { IUserDetail } from '@/type/user';
@@ -158,6 +166,7 @@ const recordList = ref<any[]>([]);
const loading = ref(true); const loading = ref(true);
// 歌单详情相关 // 歌单详情相关
const isShowList = ref(false);
const currentList = ref<Playlist>(); const currentList = ref<Playlist>();
const listLoading = ref(false); const listLoading = ref(false);
@@ -199,23 +208,21 @@ const loadUserData = async () => {
} }
}; };
// 替换显示歌单的方法 // 展示歌单
const openPlaylist = (item: any) => { const showPlaylist = async (id: number, name: string) => {
isShowList.value = true;
listLoading.value = true; listLoading.value = true;
getListDetail(item.id).then(res => { try {
currentList.value = res.data.playlist; currentList.value = { id, name } as Playlist;
const { data } = await getListDetail(id);
currentList.value = data.playlist;
} catch (error) {
console.error('加载歌单详情失败:', error);
message.error('加载歌单详情失败');
} finally {
listLoading.value = false; listLoading.value = false;
}
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: false
});
});
}; };
// 播放歌曲 // 播放歌曲

View File

@@ -34,7 +34,7 @@
v-for="(item, index) in playList" v-for="(item, index) in playList"
:key="index" :key="index"
class="play-list-item" class="play-list-item"
@click="openPlaylist(item)" @click="showPlaylist(item.id, item.name)"
> >
<n-image <n-image
:src="getImgUrl(item.coverImgUrl, '50y50')" :src="getImgUrl(item.coverImgUrl, '50y50')"
@@ -82,6 +82,15 @@
</n-scrollbar> </n-scrollbar>
</div> </div>
</div> </div>
<music-list
v-model:show="isShowList"
:name="list?.name || ''"
:song-list="list?.tracks || []"
:list-info="list"
:loading="listLoading"
:can-remove="true"
@remove-song="handleRemoveFromPlaylist"
/>
</div> </div>
</template> </template>
@@ -92,10 +101,11 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getListDetail } from '@/api/list'; import { getListDetail } from '@/api/list';
import { updatePlaylistTracks } from '@/api/music';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user'; import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import PlayBottom from '@/components/common/PlayBottom.vue'; import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from '@/components/common/SongItem.vue'; import SongItem from '@/components/common/SongItem.vue';
import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import MusicList from '@/components/MusicList.vue';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import type { Playlist } from '@/type/listDetail'; import type { Playlist } from '@/type/listDetail';
@@ -115,6 +125,7 @@ const playList = ref<any[]>([]);
const recordList = ref(); const recordList = ref();
const infoLoading = ref(false); const infoLoading = ref(false);
const mounted = ref(true); const mounted = ref(true);
const isShowList = ref(false);
const list = ref<Playlist>(); const list = ref<Playlist>();
const listLoading = ref(false); const listLoading = ref(false);
const message = useMessage(); const message = useMessage();
@@ -223,23 +234,47 @@ onMounted(() => {
checkLoginStatus() && loadData(); checkLoginStatus() && loadData();
}); });
// 替换显示歌单的方法 // 展示歌单
const openPlaylist = (item: any) => { const showPlaylist = async (id: number, name: string) => {
isShowList.value = true;
listLoading.value = true; listLoading.value = true;
getListDetail(item.id).then(res => { list.value = {
list.value = res.data.playlist; name,
listLoading.value = false; id
} as Playlist;
navigateToMusicList(router, { await loadPlaylistDetail(id);
id: item.id, listLoading.value = false;
type: 'playlist', };
name: item.name,
songList: res.data.playlist.tracks || [], // 加载歌单详情
listInfo: res.data.playlist, const loadPlaylistDetail = async (id: number) => {
canRemove: true // 保留可移除功能 const { data } = await getListDetail(id);
list.value = data.playlist;
};
// 从歌单中删除歌曲
const handleRemoveFromPlaylist = async (songId: number) => {
if (!list.value?.id) return;
try {
const res = await updatePlaylistTracks({
op: 'del',
pid: list.value.id,
tracks: songId.toString()
}); });
});
if (res.status === 200) {
message.success(t('user.message.deleteSuccess'));
// 重新加载歌单详情
await loadPlaylistDetail(list.value.id);
} else {
throw new Error(res.data?.msg || t('user.message.deleteFailed'));
}
} catch (error: any) {
console.error('删除歌曲失败:', error);
message.error(error.message || t('user.message.deleteFailed'));
}
}; };
const handlePlay = () => { const handlePlay = () => {