mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-08 10:00:50 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc59ea893 | ||
|
|
0b409f38d6 | ||
|
|
f9878ed88a | ||
|
|
e43e85480d | ||
|
|
b97170d1b2 | ||
|
|
b9aa1d574a | ||
|
|
dd7b06d7e5 | ||
|
|
ddafcfba10 | ||
|
|
da5b8c408a | ||
|
|
fb35d42fc4 | ||
|
|
dfd5d4c8b7 | ||
|
|
e5309cedee | ||
|
|
d335f57a1a | ||
|
|
c703d9c197 | ||
|
|
87a0ceb5b0 |
@@ -38,6 +38,7 @@ module.exports = {
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'no-console': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,9 +1,17 @@
|
||||
# 更新日志
|
||||
|
||||
## v3.9.3
|
||||
## v4.0.0
|
||||
|
||||
### ✨ 新功能
|
||||
- 实现国际化(i18n)功能
|
||||
- 增加动态代理节点获取和缓存机制
|
||||
- 优化更新检查逻辑,增加多个代理源支持
|
||||
- 修改捐赠列表 API
|
||||
- **音效均衡器 (EQ)**: 新增音效调节功能,支持低音、高音等实时调整,并提供流行、摇滚、古典等多种预设。
|
||||
- **歌词体验升级**: 优化歌词窗口交互,滚动更流畅,同步更精准。
|
||||
- **播放状态记忆**: 应用重启后自动恢复上次的播放进度和状态。
|
||||
|
||||
### 🔧 功能优化
|
||||
- **音乐列表**: 优化列表加载速度和响应性能,大量歌曲也能快速加载。
|
||||
- **随机播放修复**: 修复手动点击"下一首"按钮不随机的问题。
|
||||
- **下载体验优化**: 改进网页版下载代理,提高下载速度和成功率。
|
||||
|
||||
### 🧹 其他改进
|
||||
- **细节优化**: 改进按钮反馈、音量调节等交互细节,提升整体使用体验。
|
||||
- 其他已知问题优化
|
||||
12
README.md
12
README.md
@@ -45,16 +45,16 @@ QQ群:789288579
|
||||
- Naive UI - 基于 Vue 3 的组件库
|
||||
|
||||
|
||||
## 咖啡☕️
|
||||
|
||||
| 微信 | 支付宝 |
|
||||
## 赞赏☕️
|
||||
[赞赏列表](http://donate.alger.fun/)
|
||||
| 微信赞赏 | 支付宝赞赏 |
|
||||
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
|
||||
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> <br><small>喝点咖啡继续干</small> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> <br><small>来包辣条吧~</small> |
|
||||
|
||||
|
||||
## Stargazers over time
|
||||
## 项目统计
|
||||
[](https://starchart.cc/algerkong/AlgerMusicPlayer)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AlgerMusicPlayer",
|
||||
"version": "3.9.3",
|
||||
"version": "4.0.0",
|
||||
"description": "Alger Music Player",
|
||||
"author": "Alger <algerkc@qq.com>",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -83,7 +83,8 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tsc": "^2.0.22",
|
||||
"vuex": "^4.1.0"
|
||||
"vuex": "^4.1.0",
|
||||
"tunajs": "^1.0.15"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.alger.music",
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
volumeDown: 'Volume Down',
|
||||
mute: 'Mute',
|
||||
unmute: 'Unmute',
|
||||
songNum: 'Song Number: {num}',
|
||||
playMode: {
|
||||
sequence: 'Sequence',
|
||||
loop: 'Loop',
|
||||
@@ -32,6 +33,8 @@ export default {
|
||||
collapse: 'Collapse Lyrics',
|
||||
like: 'Like',
|
||||
lyric: 'Lyric',
|
||||
noSongPlaying: 'No song playing',
|
||||
eq: 'Equalizer',
|
||||
playList: 'Play List',
|
||||
playMode: {
|
||||
sequence: 'Sequence',
|
||||
@@ -45,5 +48,29 @@ export default {
|
||||
volume: 'Volume',
|
||||
favorite: 'Favorite {name}',
|
||||
unFavorite: 'Unfavorite {name}'
|
||||
},
|
||||
eq: {
|
||||
title: 'Equalizer',
|
||||
reset: 'Reset',
|
||||
on: 'On',
|
||||
off: 'Off',
|
||||
bass: 'Bass',
|
||||
midrange: 'Midrange',
|
||||
treble: 'Treble',
|
||||
presets: {
|
||||
flat: 'Flat',
|
||||
pop: 'Pop',
|
||||
rock: 'Rock',
|
||||
classical: 'Classical',
|
||||
jazz: 'Jazz',
|
||||
electronic: 'Electronic',
|
||||
hiphop: 'Hip-Hop',
|
||||
rb: 'R&B',
|
||||
metal: 'Metal',
|
||||
vocal: 'Vocal',
|
||||
dance: 'Dance',
|
||||
acoustic: 'Acoustic',
|
||||
custom: 'Custom'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
volumeDown: '音量减少',
|
||||
mute: '静音',
|
||||
unmute: '取消静音',
|
||||
songNum: '歌曲总数:{num}',
|
||||
playMode: {
|
||||
sequence: '顺序播放',
|
||||
loop: '循环播放',
|
||||
@@ -32,6 +33,8 @@ export default {
|
||||
collapse: '收起歌词',
|
||||
like: '喜欢',
|
||||
lyric: '歌词',
|
||||
noSongPlaying: '没有正在播放的歌曲',
|
||||
eq: '均衡器',
|
||||
playList: '播放列表',
|
||||
playMode: {
|
||||
sequence: '顺序播放',
|
||||
@@ -45,5 +48,29 @@ export default {
|
||||
volume: '音量',
|
||||
favorite: '已收藏{name}',
|
||||
unFavorite: '已取消收藏{name}'
|
||||
},
|
||||
eq: {
|
||||
title: '均衡器',
|
||||
reset: '重置',
|
||||
on: '开启',
|
||||
off: '关闭',
|
||||
bass: '低音',
|
||||
midrange: '中音',
|
||||
treble: '高音',
|
||||
presets: {
|
||||
flat: '平坦',
|
||||
pop: '流行',
|
||||
rock: '摇滚',
|
||||
classical: '古典',
|
||||
jazz: '爵士',
|
||||
electronic: '电子',
|
||||
hiphop: '嘻哈',
|
||||
rb: 'R&B',
|
||||
metal: '金属',
|
||||
vocal: '人声',
|
||||
dance: '舞曲',
|
||||
acoustic: '原声',
|
||||
custom: '自定义'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,9 +25,15 @@ const createWin = () => {
|
||||
const validPosition =
|
||||
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
||||
|
||||
// 确保宽高合理
|
||||
const defaultWidth = 800;
|
||||
const defaultHeight = 200;
|
||||
const validWidth = width && width > 0 ? width : defaultWidth;
|
||||
const validHeight = height && height > 0 ? height : defaultHeight;
|
||||
|
||||
lyricWindow = new BrowserWindow({
|
||||
width: width || 800,
|
||||
height: height || 200,
|
||||
width: validWidth,
|
||||
height: validHeight,
|
||||
x: validPosition ? x : undefined,
|
||||
y: validPosition ? y : undefined,
|
||||
frame: false,
|
||||
@@ -50,6 +56,17 @@ const createWin = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口大小变化事件,保存新的尺寸
|
||||
lyricWindow.on('resize', () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
const [width, height] = lyricWindow.getSize();
|
||||
const [x, y] = lyricWindow.getPosition();
|
||||
|
||||
// 保存窗口位置和大小
|
||||
store.set('lyricWindowBounds', { x, y, width, height });
|
||||
}
|
||||
});
|
||||
|
||||
return lyricWindow;
|
||||
};
|
||||
|
||||
@@ -118,6 +135,7 @@ export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.webContents.send('lyric-window-close');
|
||||
mainWin.webContents.send('lyric-control-back', 'close');
|
||||
mainWin.webContents.send('lyric-window-closed');
|
||||
lyricWindow.destroy();
|
||||
lyricWindow = null;
|
||||
}
|
||||
@@ -150,12 +168,14 @@ export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void
|
||||
|
||||
lyricWindow.setPosition(newX, newY);
|
||||
|
||||
// 保存新位置
|
||||
store.set('lyricWindowBounds', {
|
||||
...lyricWindow.getBounds(),
|
||||
// 保存新位置,但只保存位置信息,不使用getBounds()避免在Windows下引起尺寸变化
|
||||
const bounds = {
|
||||
x: newX,
|
||||
y: newY
|
||||
});
|
||||
y: newY,
|
||||
width: windowWidth, // 使用当前保存的宽度
|
||||
height: windowHeight // 使用当前保存的高度
|
||||
};
|
||||
store.set('lyricWindowBounds', bounds);
|
||||
});
|
||||
|
||||
// 添加鼠标穿透事件处理
|
||||
|
||||
@@ -91,7 +91,8 @@ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: true
|
||||
contextIsolation: true,
|
||||
webSecurity: false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -13,6 +13,7 @@ declare global {
|
||||
miniTray: () => void;
|
||||
restart: () => void;
|
||||
unblockMusic: (id: number, data: any) => Promise<any>;
|
||||
onLyricWindowClosed: (callback: () => void) => void;
|
||||
startDownload: (url: string) => void;
|
||||
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
||||
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
|
||||
|
||||
@@ -12,6 +12,10 @@ const api = {
|
||||
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
|
||||
// 歌词窗口关闭事件
|
||||
onLyricWindowClosed: (callback: () => void) => {
|
||||
ipcRenderer.on('lyric-window-closed', () => callback());
|
||||
},
|
||||
// 更新相关
|
||||
startDownload: (url: string) => ipcRenderer.send('start-download', url),
|
||||
onDownloadProgress: (callback: (progress: number, status: string) => void) => {
|
||||
|
||||
@@ -88,11 +88,15 @@ onMounted(() => {
|
||||
homeRouter.filter((item) => item.meta.isMobile)
|
||||
);
|
||||
}
|
||||
window.electron.ipcRenderer.on('set-language', handleSetLanguage);
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('set-language', handleSetLanguage);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.electron.ipcRenderer.removeListener('set-language', handleSetLanguage);
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.removeListener('set-language', handleSetLanguage);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
355
src/renderer/components/EQControl.vue
Normal file
355
src/renderer/components/EQControl.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<div class="eq-control">
|
||||
<div class="eq-header">
|
||||
<h3>{{ t('player.eq.title') }}</h3>
|
||||
<div class="eq-controls">
|
||||
<n-switch v-model:value="isEnabled" @update:value="toggleEQ">
|
||||
<template #checked>{{ t('player.eq.on') }}</template>
|
||||
<template #unchecked>{{ t('player.eq.off') }}</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="eq-presets">
|
||||
<n-scrollbar x-scrollable>
|
||||
<n-space :size="6" :wrap="false">
|
||||
<n-tag
|
||||
v-for="preset in presetOptions"
|
||||
:key="preset.value"
|
||||
:type="currentPreset === preset.value ? 'success' : 'default'"
|
||||
:bordered="false"
|
||||
size="medium"
|
||||
round
|
||||
clickable
|
||||
@click="applyPreset(preset.value)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<div class="eq-sliders">
|
||||
<div v-for="freq in frequencies" :key="freq" class="eq-slider">
|
||||
<div class="freq-label">{{ formatFreq(freq) }}</div>
|
||||
<n-slider
|
||||
v-model:value="eqValues[freq.toString()]"
|
||||
:min="-12"
|
||||
:max="12"
|
||||
:step="0.1"
|
||||
vertical
|
||||
:disabled="!isEnabled"
|
||||
@update:value="updateEQ(freq.toString(), $event)"
|
||||
/>
|
||||
<div class="gain-value">{{ eqValues[freq.toString()] }}dB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { audioService } from '@/services/audioService';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
|
||||
const eqValues = ref<{ [key: string]: number }>({});
|
||||
const isEnabled = ref(audioService.isEQEnabled());
|
||||
const currentPreset = ref(audioService.getCurrentPreset() || 'flat');
|
||||
|
||||
// 预设配置
|
||||
const presets = {
|
||||
flat: {
|
||||
label: t('player.eq.presets.flat'),
|
||||
values: Object.fromEntries(frequencies.map((f) => [f, 0]))
|
||||
},
|
||||
pop: {
|
||||
label: t('player.eq.presets.pop'),
|
||||
values: {
|
||||
31: -1.5,
|
||||
62: 3.5,
|
||||
125: 5.5,
|
||||
250: 3.5,
|
||||
500: -0.5,
|
||||
1000: -1.5,
|
||||
2000: 1.5,
|
||||
4000: 2.5,
|
||||
8000: 2.5,
|
||||
16000: 2.5
|
||||
}
|
||||
},
|
||||
rock: {
|
||||
label: t('player.eq.presets.rock'),
|
||||
values: {
|
||||
31: 4.5,
|
||||
62: 3.5,
|
||||
125: 2,
|
||||
250: 0.5,
|
||||
500: -0.5,
|
||||
1000: -1,
|
||||
2000: 0.5,
|
||||
4000: 2,
|
||||
8000: 2.5,
|
||||
16000: 3.5
|
||||
}
|
||||
},
|
||||
classical: {
|
||||
label: t('player.eq.presets.classical'),
|
||||
values: {
|
||||
31: 3.5,
|
||||
62: 3,
|
||||
125: 2.5,
|
||||
250: 1.5,
|
||||
500: -0.5,
|
||||
1000: -1.5,
|
||||
2000: -1.5,
|
||||
4000: 0.5,
|
||||
8000: 2,
|
||||
16000: 3
|
||||
}
|
||||
},
|
||||
jazz: {
|
||||
label: t('player.eq.presets.jazz'),
|
||||
values: {
|
||||
31: 3,
|
||||
62: 2,
|
||||
125: 1.5,
|
||||
250: 2,
|
||||
500: -1,
|
||||
1000: -1.5,
|
||||
2000: -0.5,
|
||||
4000: 1,
|
||||
8000: 2.5,
|
||||
16000: 3
|
||||
}
|
||||
},
|
||||
hiphop: {
|
||||
label: t('player.eq.presets.hiphop'),
|
||||
values: {
|
||||
31: 5,
|
||||
62: 4.5,
|
||||
125: 3,
|
||||
250: 1.5,
|
||||
500: -0.5,
|
||||
1000: -1,
|
||||
2000: 0.5,
|
||||
4000: 1.5,
|
||||
8000: 2,
|
||||
16000: 2.5
|
||||
}
|
||||
},
|
||||
vocal: {
|
||||
label: t('player.eq.presets.vocal'),
|
||||
values: {
|
||||
31: -2,
|
||||
62: -1.5,
|
||||
125: -1,
|
||||
250: 0.5,
|
||||
500: 2,
|
||||
1000: 3.5,
|
||||
2000: 3,
|
||||
4000: 1.5,
|
||||
8000: 0.5,
|
||||
16000: 0
|
||||
}
|
||||
},
|
||||
dance: {
|
||||
label: t('player.eq.presets.dance'),
|
||||
values: {
|
||||
31: 4,
|
||||
62: 3.5,
|
||||
125: 2.5,
|
||||
250: 1,
|
||||
500: 0,
|
||||
1000: -0.5,
|
||||
2000: 1.5,
|
||||
4000: 2.5,
|
||||
8000: 3,
|
||||
16000: 2.5
|
||||
}
|
||||
},
|
||||
acoustic: {
|
||||
label: t('player.eq.presets.acoustic'),
|
||||
values: {
|
||||
31: 2,
|
||||
62: 1.5,
|
||||
125: 1,
|
||||
250: 1.5,
|
||||
500: 2,
|
||||
1000: 1.5,
|
||||
2000: 2,
|
||||
4000: 2.5,
|
||||
8000: 2,
|
||||
16000: 1.5
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const presetOptions = Object.entries(presets).map(([value, preset]) => ({
|
||||
label: preset.label,
|
||||
value
|
||||
}));
|
||||
|
||||
const toggleEQ = (enabled: boolean) => {
|
||||
audioService.setEQEnabled(enabled);
|
||||
};
|
||||
|
||||
const applyPreset = (presetName: string) => {
|
||||
currentPreset.value = presetName;
|
||||
audioService.setCurrentPreset(presetName);
|
||||
const preset = presets[presetName as keyof typeof presets];
|
||||
if (preset) {
|
||||
Object.entries(preset.values).forEach(([freq, gain]) => {
|
||||
updateEQ(freq, gain);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 恢复 EQ 设置
|
||||
const settings = audioService.getAllEQSettings();
|
||||
eqValues.value = settings;
|
||||
|
||||
// 如果有保存的预设,应用该预设
|
||||
const savedPreset = audioService.getCurrentPreset();
|
||||
if (savedPreset && presets[savedPreset as keyof typeof presets]) {
|
||||
currentPreset.value = savedPreset;
|
||||
}
|
||||
});
|
||||
|
||||
const updateEQ = (frequency: string, gain: number) => {
|
||||
audioService.setEQFrequencyGain(frequency, gain);
|
||||
eqValues.value = {
|
||||
...eqValues.value,
|
||||
[frequency]: gain
|
||||
};
|
||||
|
||||
// 检查当前值是否与任何预设匹配
|
||||
const currentValues = eqValues.value;
|
||||
let matchedPreset: string | null = null;
|
||||
|
||||
// 检查是否与任何预设完全匹配
|
||||
Object.entries(presets).forEach(([presetName, preset]) => {
|
||||
const isMatch = Object.entries(preset.values).every(
|
||||
([freq, value]) => Math.abs(currentValues[freq] - value) < 0.1
|
||||
);
|
||||
if (isMatch) {
|
||||
matchedPreset = presetName;
|
||||
}
|
||||
});
|
||||
|
||||
// 更新当前预设状态
|
||||
if (matchedPreset !== null) {
|
||||
currentPreset.value = matchedPreset;
|
||||
audioService.setCurrentPreset(matchedPreset);
|
||||
} else if (currentPreset.value !== 'custom') {
|
||||
// 如果与任何预设都不匹配,将状态设置为自定义
|
||||
currentPreset.value = 'custom';
|
||||
audioService.setCurrentPreset('custom');
|
||||
}
|
||||
};
|
||||
|
||||
const formatFreq = (freq: number) => {
|
||||
if (freq >= 1000) {
|
||||
return `${freq / 1000}kHz`;
|
||||
}
|
||||
return `${freq}Hz`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eq-control {
|
||||
@apply p-6 rounded-lg;
|
||||
@apply bg-light dark:bg-dark;
|
||||
@apply shadow-lg dark:shadow-none;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
.eq-header {
|
||||
@apply flex justify-between items-center mb-4;
|
||||
|
||||
h3 {
|
||||
@apply text-xl font-semibold;
|
||||
@apply text-gray-800 dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.eq-presets {
|
||||
@apply mb-2 relative;
|
||||
height: 40px;
|
||||
|
||||
:deep(.n-scrollbar) {
|
||||
@apply -mx-2 px-2;
|
||||
}
|
||||
|
||||
:deep(.n-tag) {
|
||||
@apply cursor-pointer transition-all duration-200;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-space) {
|
||||
flex-wrap: nowrap;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.eq-sliders {
|
||||
@apply flex justify-between items-end;
|
||||
@apply bg-gray-50 dark:bg-gray-800 gap-1;
|
||||
@apply rounded-lg p-2;
|
||||
height: 300px;
|
||||
|
||||
.eq-slider {
|
||||
@apply flex flex-col items-center;
|
||||
width: 45px;
|
||||
height: 100%;
|
||||
|
||||
.n-slider {
|
||||
flex: 1;
|
||||
margin: 12px 0;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.freq-label {
|
||||
@apply text-xs font-medium text-center;
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
white-space: nowrap;
|
||||
margin: 8px 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.gain-value {
|
||||
@apply text-xs font-medium text-center;
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
white-space: nowrap;
|
||||
margin: 4px 0;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-slider) {
|
||||
--n-rail-height: 4px;
|
||||
--n-rail-color: theme('colors.gray.200');
|
||||
--n-rail-color-hover: theme('colors.gray.300');
|
||||
--n-fill-color: theme('colors.green.500');
|
||||
--n-fill-color-hover: theme('colors.green.600');
|
||||
--n-handle-color: theme('colors.green.500');
|
||||
--n-handle-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.n-slider-handle {
|
||||
@apply transition-all duration-200;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -37,12 +37,12 @@
|
||||
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
||||
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
||||
</div>
|
||||
<div v-if="total" class="music-total">{{ t('player.songNum', { num: total }) }}</div>
|
||||
|
||||
<n-scrollbar style="max-height: 200">
|
||||
<n-scrollbar style="max-height: 200px">
|
||||
<div v-if="listInfo?.description" class="music-desc">
|
||||
{{ listInfo.description }}
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
:style="getItemAnimationDelay(index)"
|
||||
>
|
||||
<song-item
|
||||
:item="formatDetail(item)"
|
||||
:item="formatSong(item)"
|
||||
:can-remove="canRemove"
|
||||
@play="handlePlay"
|
||||
@remove-song="(id) => emit('remove-song', id)"
|
||||
@@ -69,6 +69,9 @@
|
||||
<div v-if="isLoadingMore" class="loading-more">
|
||||
{{ t('common.loadingMore') }}
|
||||
</div>
|
||||
<div v-if="!hasMore" class="loading-more">
|
||||
{{ t('common.noMore') }}
|
||||
</div>
|
||||
<play-bottom />
|
||||
</div>
|
||||
</n-spin>
|
||||
@@ -82,6 +85,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
@@ -119,10 +123,14 @@ const props = withDefaults(
|
||||
const emit = defineEmits(['update:show', 'update:loading', 'remove-song']);
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = 20;
|
||||
const pageSize = 40;
|
||||
const isLoadingMore = ref(false);
|
||||
const displayedSongs = ref<any[]>([]);
|
||||
const loadingList = ref(false);
|
||||
const loadedIds = ref(new Set<number>()); // 用于追踪已加载的歌曲ID
|
||||
const isPlaylistLoading = ref(false); // 标记是否正在加载播放列表
|
||||
const completePlaylist = ref<any[]>([]); // 存储完整的播放列表
|
||||
const hasMore = ref(true); // 标记是否还有更多数据可加载
|
||||
|
||||
// 计算总数
|
||||
const total = computed(() => {
|
||||
@@ -132,108 +140,229 @@ const total = computed(() => {
|
||||
return props.songList.length;
|
||||
});
|
||||
|
||||
const formatDetail = computed(() => (detail: any) => {
|
||||
const song = {
|
||||
artists: detail.ar,
|
||||
name: detail.al.name,
|
||||
id: detail.al.id
|
||||
// 格式化歌曲数据
|
||||
const formatSong = (item: any) => {
|
||||
return {
|
||||
...item,
|
||||
picUrl: item.al?.picUrl || item.picUrl,
|
||||
song: {
|
||||
artists: item.ar || item.artists,
|
||||
name: item.al?.name || item.name,
|
||||
id: item.al?.id || item.id
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
detail.song = song;
|
||||
detail.picUrl = detail.al.picUrl;
|
||||
return detail;
|
||||
});
|
||||
/**
|
||||
* 加载歌曲数据的核心函数
|
||||
* @param ids 要加载的歌曲ID数组
|
||||
* @param appendToList 是否将加载的歌曲追加到现有列表
|
||||
* @param updateComplete 是否更新完整播放列表
|
||||
*/
|
||||
const loadSongs = async (ids: number[], appendToList = true, updateComplete = false) => {
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const handlePlay = () => {
|
||||
const tracks = props.songList || [];
|
||||
store.commit(
|
||||
'setPlayList',
|
||||
tracks.map((item) => ({
|
||||
...item,
|
||||
picUrl: item.al.picUrl,
|
||||
song: {
|
||||
artists: item.ar
|
||||
try {
|
||||
const { data } = await getMusicDetail(ids);
|
||||
if (data?.songs) {
|
||||
// 更新已加载ID集合
|
||||
const newSongs = data.songs.filter((song: any) => !loadedIds.value.has(song.id));
|
||||
|
||||
newSongs.forEach((song: any) => {
|
||||
loadedIds.value.add(song.id);
|
||||
});
|
||||
|
||||
if (appendToList) {
|
||||
displayedSongs.value.push(...newSongs);
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
if (updateComplete) {
|
||||
completePlaylist.value.push(...newSongs);
|
||||
}
|
||||
|
||||
return newSongs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载歌曲失败:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// 加载完整播放列表
|
||||
const loadFullPlaylist = async () => {
|
||||
if (isPlaylistLoading.value) return;
|
||||
isPlaylistLoading.value = true;
|
||||
completePlaylist.value = [...displayedSongs.value]; // 先用当前已加载的歌曲初始化
|
||||
|
||||
try {
|
||||
// 如果没有trackIds,直接使用当前歌曲列表
|
||||
if (!props.listInfo?.trackIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有未加载的歌曲ID
|
||||
const allIds = props.listInfo.trackIds.map((item) => item.id);
|
||||
const unloadedIds = allIds.filter((id) => !loadedIds.value.has(id));
|
||||
|
||||
// 如果所有歌曲都已加载,直接返回
|
||||
if (unloadedIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 分批加载未加载的歌曲
|
||||
const batchSize = 500; // 每批加载的歌曲数量
|
||||
for (let i = 0; i < unloadedIds.length; i += batchSize) {
|
||||
const batchIds = unloadedIds.slice(i, i + batchSize);
|
||||
if (batchIds.length === 0) continue;
|
||||
|
||||
await loadSongs(batchIds, false, true);
|
||||
|
||||
// 添加小延迟避免请求过于密集
|
||||
if (i + batchSize < unloadedIds.length) {
|
||||
// 使用 setTimeout 直接延迟,避免 Promise 相关的 linter 错误
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载完整播放列表失败:', error);
|
||||
} finally {
|
||||
isPlaylistLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理播放
|
||||
const handlePlay = async () => {
|
||||
// 先使用当前已加载的歌曲开始播放
|
||||
store.commit('setPlayList', displayedSongs.value.map(formatSong));
|
||||
|
||||
// 在后台加载完整播放列表
|
||||
loadFullPlaylist().then(() => {
|
||||
// 加载完成后,更新播放列表为完整列表
|
||||
if (completePlaylist.value.length > 0) {
|
||||
store.commit('setPlayList', completePlaylist.value.map(formatSong));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
// 优化加载更多歌曲的函数
|
||||
// 加载更多歌曲
|
||||
const loadMoreSongs = async () => {
|
||||
if (isLoadingMore.value || displayedSongs.value.length >= total.value) return;
|
||||
// 检查是否正在加载或已经加载完成
|
||||
if (isLoadingMore.value || displayedSongs.value.length >= total.value) {
|
||||
hasMore.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingMore.value = true;
|
||||
try {
|
||||
if (props.listInfo?.trackIds) {
|
||||
// 如果有 trackIds,需要分批请求歌曲详情
|
||||
const start = page.value * pageSize;
|
||||
const end = Math.min((page.value + 1) * pageSize, total.value);
|
||||
const trackIds = props.listInfo.trackIds.slice(start, end).map((item) => item.id);
|
||||
const start = displayedSongs.value.length;
|
||||
const end = Math.min(start + pageSize, total.value);
|
||||
|
||||
if (trackIds.length > 0) {
|
||||
const { data } = await getMusicDetail(trackIds);
|
||||
displayedSongs.value = [...displayedSongs.value, ...data.songs];
|
||||
page.value++;
|
||||
if (props.listInfo?.trackIds) {
|
||||
// 获取这一批次需要加载的所有ID
|
||||
const trackIdsToLoad = props.listInfo.trackIds
|
||||
.slice(start, end)
|
||||
.map((item) => item.id)
|
||||
.filter((id) => !loadedIds.value.has(id));
|
||||
|
||||
if (trackIdsToLoad.length > 0) {
|
||||
await loadSongs(trackIdsToLoad, true, false);
|
||||
}
|
||||
} else {
|
||||
// 如果没有 trackIds,直接使用 songList 分页
|
||||
const start = page.value * pageSize;
|
||||
const end = Math.min((page.value + 1) * pageSize, props.songList.length);
|
||||
} else if (start < props.songList.length) {
|
||||
// 直接使用 songList 分页
|
||||
const newSongs = props.songList.slice(start, end);
|
||||
displayedSongs.value = [...displayedSongs.value, ...newSongs];
|
||||
page.value++;
|
||||
newSongs.forEach((song) => {
|
||||
if (!loadedIds.value.has(song.id)) {
|
||||
loadedIds.value.add(song.id);
|
||||
displayedSongs.value.push(song);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新是否还有更多数据的状态
|
||||
hasMore.value = displayedSongs.value.length < total.value;
|
||||
} catch (error) {
|
||||
console.error('加载歌曲失败:', error);
|
||||
console.error('加载更多歌曲失败:', error);
|
||||
} finally {
|
||||
isLoadingMore.value = false;
|
||||
loadingList.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getItemAnimationDelay = (index: number) => {
|
||||
const currentPageIndex = index % pageSize;
|
||||
return setAnimationDelay(currentPageIndex, 20);
|
||||
};
|
||||
|
||||
// 修改滚动处理函数
|
||||
const handleScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||
if (scrollHeight - scrollTop - clientHeight < 100 && !isLoadingMore.value) {
|
||||
const threshold = 200;
|
||||
|
||||
if (
|
||||
scrollHeight - scrollTop - clientHeight < threshold &&
|
||||
!isLoadingMore.value &&
|
||||
hasMore.value
|
||||
) {
|
||||
loadMoreSongs();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
loadingList.value = newVal;
|
||||
if (!props.cover) {
|
||||
loadingList.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
const getItemAnimationDelay = (index: number) => {
|
||||
const currentPageIndex = index % pageSize;
|
||||
return setAnimationDelay(currentPageIndex, 20);
|
||||
};
|
||||
|
||||
// 监听 songList 变化,重置分页状态
|
||||
// 重置列表状态
|
||||
const resetListState = () => {
|
||||
page.value = 0;
|
||||
loadedIds.value.clear();
|
||||
displayedSongs.value = [];
|
||||
completePlaylist.value = [];
|
||||
hasMore.value = true;
|
||||
loadingList.value = false;
|
||||
};
|
||||
|
||||
// 初始化歌曲列表
|
||||
const initSongList = (songs: any[]) => {
|
||||
if (songs.length > 0) {
|
||||
displayedSongs.value = [...songs];
|
||||
songs.forEach((song) => loadedIds.value.add(song.id));
|
||||
page.value = Math.ceil(songs.length / pageSize);
|
||||
}
|
||||
|
||||
// 检查是否还有更多数据可加载
|
||||
hasMore.value = displayedSongs.value.length < total.value;
|
||||
};
|
||||
|
||||
// 修改 songList 监听器
|
||||
watch(
|
||||
() => props.songList,
|
||||
(newSongs) => {
|
||||
page.value = 0;
|
||||
displayedSongs.value = newSongs.slice(0, pageSize);
|
||||
if (newSongs.length > pageSize) {
|
||||
page.value = 1;
|
||||
// 重置所有状态
|
||||
resetListState();
|
||||
|
||||
// 初始化歌曲列表
|
||||
initSongList(newSongs);
|
||||
|
||||
// 如果还有更多歌曲需要加载,且差距较小,立即加载
|
||||
if (hasMore.value && props.listInfo?.trackIds) {
|
||||
setTimeout(() => {
|
||||
loadMoreSongs();
|
||||
}, 300);
|
||||
}
|
||||
loadingList.value = false;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 组件卸载时清理状态
|
||||
onUnmounted(() => {
|
||||
isPlaylistLoading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -242,6 +371,10 @@ watch(
|
||||
@apply text-xl font-bold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
&-total {
|
||||
@apply text-sm font-normal text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-page {
|
||||
@apply px-8 w-full h-full bg-light dark:bg-black bg-opacity-75 dark:bg-opacity-75 rounded-t-2xl;
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
@@ -46,7 +46,7 @@ import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
import { getLatestReleaseInfo } from '@/utils/update';
|
||||
import { getLatestReleaseInfo, getProxyNodes } from '@/utils/update';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
@@ -63,6 +63,8 @@ const closeModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const proxyHosts = ref<string[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
// 如果是 electron 环境,不显示安装提示
|
||||
if (isElectron || isMobile.value) {
|
||||
@@ -78,6 +80,7 @@ onMounted(async () => {
|
||||
// 获取最新版本信息
|
||||
releaseInfo.value = await getLatestReleaseInfo();
|
||||
showModal.value = true;
|
||||
proxyHosts.value = await getProxyNodes();
|
||||
});
|
||||
|
||||
const handleInstall = async (): Promise<void> => {
|
||||
@@ -118,7 +121,8 @@ const handleInstall = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
if (downloadUrl) {
|
||||
window.open(`https://ghproxy.cn/${downloadUrl}`, '_blank');
|
||||
const proxyDownloadUrl = `${proxyHosts.value[0]}/${downloadUrl}`;
|
||||
window.open(proxyDownloadUrl, '_blank');
|
||||
} else {
|
||||
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||
|
||||
@@ -54,19 +54,205 @@ document.onkeyup = (e) => {
|
||||
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
// 全局变量
|
||||
let progressAnimationInitialized = false;
|
||||
let globalAnimationFrameId: number | null = null;
|
||||
|
||||
// 全局停止函数
|
||||
const stopProgressAnimation = () => {
|
||||
if (globalAnimationFrameId) {
|
||||
cancelAnimationFrame(globalAnimationFrameId);
|
||||
globalAnimationFrameId = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 全局更新函数
|
||||
const updateProgress = () => {
|
||||
if (!store.state.play) {
|
||||
stopProgressAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound) {
|
||||
console.log('进度更新:无效的 sound 对象');
|
||||
// 不是立即返回,而是设置定时器稍后再次尝试
|
||||
globalAnimationFrameId = setTimeout(() => {
|
||||
requestAnimationFrame(updateProgress);
|
||||
}, 100) as unknown as number;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof currentSound.seek !== 'function') {
|
||||
console.log('进度更新:无效的 seek 函数');
|
||||
// 不是立即返回,而是设置定时器稍后再次尝试
|
||||
globalAnimationFrameId = setTimeout(() => {
|
||||
requestAnimationFrame(updateProgress);
|
||||
}, 100) as unknown as number;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { start, end } = currentLrcTiming.value;
|
||||
if (typeof start !== 'number' || typeof end !== 'number' || start === end) {
|
||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentTime;
|
||||
try {
|
||||
currentTime = currentSound.seek() as number;
|
||||
|
||||
// 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入)
|
||||
if (Math.floor(currentTime) % 2 === 0) {
|
||||
if (store.state.playMusic && store.state.playMusic.id) {
|
||||
localStorage.setItem(
|
||||
'playProgress',
|
||||
JSON.stringify({
|
||||
songId: store.state.playMusic.id,
|
||||
progress: currentTime
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (seekError) {
|
||||
console.error('调用 seek() 方法出错:', seekError);
|
||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
||||
console.error('无效的当前时间:', currentTime);
|
||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = currentTime - start;
|
||||
const duration = end - start;
|
||||
const progress = (elapsed / duration) * 100;
|
||||
|
||||
// 确保进度在 0-100 之间
|
||||
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
||||
} catch (error) {
|
||||
console.error('更新进度出错:', error);
|
||||
}
|
||||
|
||||
// 继续下一帧更新
|
||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
||||
};
|
||||
|
||||
// 全局启动函数
|
||||
const startProgressAnimation = () => {
|
||||
stopProgressAnimation(); // 先停止之前的动画
|
||||
updateProgress();
|
||||
};
|
||||
|
||||
// 全局初始化函数
|
||||
const initProgressAnimation = () => {
|
||||
if (progressAnimationInitialized) return;
|
||||
|
||||
console.log('初始化进度动画');
|
||||
progressAnimationInitialized = true;
|
||||
|
||||
// 监听播放状态变化,这里使用防抖,避免频繁触发
|
||||
let debounceTimer: any = null;
|
||||
|
||||
watch(
|
||||
() => store.state.play,
|
||||
(newIsPlaying) => {
|
||||
console.log('播放状态变化:', newIsPlaying);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
// 使用防抖,延迟 100ms 再执行
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (newIsPlaying) {
|
||||
// 确保 sound 对象有效时才启动进度更新
|
||||
if (sound.value) {
|
||||
console.log('sound 对象已存在,立即启动进度更新');
|
||||
startProgressAnimation();
|
||||
} else {
|
||||
console.log('等待 sound 对象初始化...');
|
||||
// 定时检查 sound 对象是否已初始化
|
||||
const checkInterval = setInterval(() => {
|
||||
if (sound.value) {
|
||||
clearInterval(checkInterval);
|
||||
console.log('sound 对象已初始化,开始进度更新');
|
||||
startProgressAnimation();
|
||||
}
|
||||
}, 100);
|
||||
// 设置超时,防止无限等待
|
||||
setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
console.log('等待 sound 对象超时,已停止等待');
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
stopProgressAnimation();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听当前歌词索引变化
|
||||
watch(nowIndex, () => {
|
||||
currentLrcProgress.value = 0;
|
||||
if (store.state.play) {
|
||||
startProgressAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听音频对象变化
|
||||
watch(sound, (newSound) => {
|
||||
console.log('sound 对象变化:', !!newSound);
|
||||
if (newSound && store.state.play) {
|
||||
startProgressAnimation();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化进度动画
|
||||
initProgressAnimation();
|
||||
|
||||
// 简化后的 watch 函数,只保留核心逻辑
|
||||
watch(
|
||||
() => store.state.playMusicUrl,
|
||||
async (newVal) => {
|
||||
if (newVal && playMusic.value) {
|
||||
try {
|
||||
const newSound = await audioService.play(newVal, playMusic.value);
|
||||
// 保存当前播放状态
|
||||
const shouldPlay = store.state.play;
|
||||
|
||||
// 检查是否有保存的进度
|
||||
let initialPosition = 0;
|
||||
const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}');
|
||||
if (savedProgress.songId === playMusic.value.id) {
|
||||
initialPosition = savedProgress.progress;
|
||||
}
|
||||
|
||||
// 播放新音频,传递是否应该播放的状态
|
||||
const newSound = await audioService.play(newVal, playMusic.value, shouldPlay);
|
||||
sound.value = newSound as Howl;
|
||||
|
||||
// 如果有保存的进度,设置播放位置
|
||||
if (initialPosition > 0) {
|
||||
newSound.seek(initialPosition);
|
||||
// 同时更新进度条显示
|
||||
nowTime.value = initialPosition;
|
||||
}
|
||||
|
||||
setupAudioListeners();
|
||||
|
||||
// 确保状态与 localStorage 同步
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(store.state.playMusic));
|
||||
localStorage.setItem('currentPlayMusicUrl', newVal);
|
||||
} catch (error) {
|
||||
console.error('播放音频失败:', error);
|
||||
store.commit('setPlayMusic', false);
|
||||
message.error('当前歌曲播放失败,播放下一首');
|
||||
// 下一首
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
}
|
||||
@@ -77,11 +263,21 @@ watch(
|
||||
() => store.state.playMusic,
|
||||
() => {
|
||||
nextTick(async () => {
|
||||
console.log('歌曲切换,更新歌词数据');
|
||||
// 更新歌词数据
|
||||
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
||||
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
||||
|
||||
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
|
||||
if (isElectron && isLyricWindowOpen.value && lrcArray.value.length > 0) {
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
console.log('歌词窗口已打开,同步最新歌词数据');
|
||||
// 不管歌词数组是否为空,都发送最新数据
|
||||
sendLyricToWin();
|
||||
|
||||
// 再次延迟发送,确保歌词窗口已完全加载
|
||||
setTimeout(() => {
|
||||
sendLyricToWin();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -104,6 +300,26 @@ const setupAudioListeners = () => {
|
||||
// 清理所有事件监听器
|
||||
audioService.clearAllListeners();
|
||||
|
||||
// 立即更新一次时间和进度(解决初始化时进度条不显示问题)
|
||||
const updateCurrentTimeAndDuration = () => {
|
||||
const currentSound = audioService.getCurrentSound();
|
||||
if (currentSound) {
|
||||
try {
|
||||
// 更新当前时间和总时长
|
||||
const currentTime = currentSound.seek() as number;
|
||||
if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {
|
||||
nowTime.value = currentTime;
|
||||
allTime.value = currentSound.duration() as number;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化时间和进度失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行一次更新
|
||||
updateCurrentTimeAndDuration();
|
||||
|
||||
// 监听播放
|
||||
audioService.on('play', () => {
|
||||
store.commit('setPlayMusic', true);
|
||||
@@ -111,8 +327,15 @@ const setupAudioListeners = () => {
|
||||
interval = window.setInterval(() => {
|
||||
try {
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound || typeof currentSound.seek !== 'function') {
|
||||
console.error('Invalid sound object or seek function');
|
||||
if (!currentSound) {
|
||||
console.error('Invalid sound object: sound is null or undefined');
|
||||
clearInterval();
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保 seek 方法存在且可调用
|
||||
if (typeof currentSound.seek !== 'function') {
|
||||
console.error('Invalid sound object: seek function not available');
|
||||
clearInterval();
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +352,8 @@ const setupAudioListeners = () => {
|
||||
const newIndex = getLrcIndex(nowTime.value);
|
||||
if (newIndex !== nowIndex.value) {
|
||||
nowIndex.value = newIndex;
|
||||
currentLrcProgress.value = 0;
|
||||
// 注意:我们不在这里设置 currentLrcProgress 为 0
|
||||
// 因为这会与全局进度更新冲突
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
@@ -146,6 +370,7 @@ const setupAudioListeners = () => {
|
||||
|
||||
// 监听暂停
|
||||
audioService.on('pause', () => {
|
||||
console.log('音频暂停事件触发');
|
||||
store.commit('setPlayMusic', false);
|
||||
clearInterval();
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
@@ -179,6 +404,7 @@ const setupAudioListeners = () => {
|
||||
|
||||
// 监听结束
|
||||
audioService.on('end', () => {
|
||||
console.log('音频播放结束事件触发');
|
||||
clearInterval();
|
||||
|
||||
if (store.state.playMode === 1) {
|
||||
@@ -213,10 +439,27 @@ export const play = () => {
|
||||
};
|
||||
|
||||
export const pause = () => {
|
||||
audioService.getCurrentSound()?.pause();
|
||||
};
|
||||
const currentSound = audioService.getCurrentSound();
|
||||
if (currentSound) {
|
||||
try {
|
||||
// 保存当前播放进度
|
||||
const currentTime = currentSound.seek() as number;
|
||||
if (store.state.playMusic && store.state.playMusic.id) {
|
||||
localStorage.setItem(
|
||||
'playProgress',
|
||||
JSON.stringify({
|
||||
songId: store.state.playMusic.id,
|
||||
progress: currentTime
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const isPlaying = computed(() => store.state.play as boolean);
|
||||
currentSound.pause();
|
||||
} catch (error) {
|
||||
console.error('暂停播放出错:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 增加矫正时间
|
||||
export const addCorrectionTime = (time: number) => (correctionTime.value += time);
|
||||
@@ -267,103 +510,8 @@ export const getLrcStyle = (index: number) => {
|
||||
|
||||
// 播放进度
|
||||
export const useLyricProgress = () => {
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
const updateProgress = () => {
|
||||
if (!isPlaying.value) {
|
||||
stopProgressAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound || typeof currentSound.seek !== 'function') {
|
||||
console.error('Invalid sound object or seek function');
|
||||
stopProgressAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { start, end } = currentLrcTiming.value;
|
||||
if (typeof start !== 'number' || typeof end !== 'number' || start === end) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = currentSound.seek() as number;
|
||||
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
||||
console.error('Invalid current time:', currentTime);
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = currentTime - start;
|
||||
const duration = end - start;
|
||||
const progress = (elapsed / duration) * 100;
|
||||
|
||||
// 确保进度在 0-100 之间
|
||||
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
||||
} catch (error) {
|
||||
console.error('Error updating progress:', error);
|
||||
}
|
||||
|
||||
// 继续下一帧更新
|
||||
animationFrameId = requestAnimationFrame(updateProgress);
|
||||
};
|
||||
|
||||
const startProgressAnimation = () => {
|
||||
stopProgressAnimation(); // 先停止之前的动画
|
||||
if (isPlaying.value) {
|
||||
updateProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const stopProgressAnimation = () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听播放状态变化
|
||||
watch(
|
||||
isPlaying,
|
||||
(newIsPlaying) => {
|
||||
if (newIsPlaying) {
|
||||
startProgressAnimation();
|
||||
} else {
|
||||
stopProgressAnimation();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听当前歌词索引变化
|
||||
watch(nowIndex, () => {
|
||||
currentLrcProgress.value = 0;
|
||||
if (isPlaying.value) {
|
||||
startProgressAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听音频对象变化
|
||||
watch(sound, (newSound) => {
|
||||
if (newSound && isPlaying.value) {
|
||||
startProgressAnimation();
|
||||
} else {
|
||||
stopProgressAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (isPlaying.value) {
|
||||
startProgressAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopProgressAnimation();
|
||||
});
|
||||
|
||||
// 如果已经在全局更新进度,立即返回
|
||||
return {
|
||||
currentLrcProgress,
|
||||
getLrcStyle
|
||||
};
|
||||
};
|
||||
@@ -405,54 +553,183 @@ watch(
|
||||
// 发送歌词更新数据
|
||||
export const sendLyricToWin = () => {
|
||||
if (!isElectron || !isLyricWindowOpen.value) {
|
||||
console.log('Cannot send lyric: electron or lyric window not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有播放的歌曲
|
||||
if (!playMusic.value || !playMusic.value.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (lrcArray.value.length > 0) {
|
||||
// 记录歌词发送状态
|
||||
if (lrcArray.value && lrcArray.value.length > 0) {
|
||||
const nowIndex = getLrcIndex(nowTime.value);
|
||||
// 构建完整的歌词更新数据
|
||||
const updateData = {
|
||||
type: 'full',
|
||||
nowIndex,
|
||||
nowTime: nowTime.value,
|
||||
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||
nextTime: lrcTimeArray.value[nowIndex + 1],
|
||||
isPlay: isPlaying.value,
|
||||
startCurrentTime: lrcTimeArray.value[nowIndex] || 0,
|
||||
nextTime: lrcTimeArray.value[nowIndex + 1] || 0,
|
||||
isPlay: store.state.play,
|
||||
lrcArray: lrcArray.value,
|
||||
lrcTimeArray: lrcTimeArray.value,
|
||||
allTime: allTime.value,
|
||||
playMusic: playMusic.value
|
||||
};
|
||||
|
||||
// 发送数据到歌词窗口
|
||||
window.api.sendLyric(JSON.stringify(updateData));
|
||||
} else {
|
||||
console.log('No lyric data available, sending empty lyric message');
|
||||
|
||||
// 发送没有歌词的提示
|
||||
const emptyLyricData = {
|
||||
type: 'empty',
|
||||
nowIndex: 0,
|
||||
nowTime: nowTime.value,
|
||||
startCurrentTime: 0,
|
||||
nextTime: 0,
|
||||
isPlay: store.state.play,
|
||||
lrcArray: [{ text: '当前歌曲暂无歌词', trText: '' }],
|
||||
lrcTimeArray: [0],
|
||||
allTime: allTime.value,
|
||||
playMusic: playMusic.value
|
||||
};
|
||||
window.api.sendLyric(JSON.stringify(emptyLyricData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending lyric update:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 歌词同步定时器
|
||||
let lyricSyncInterval: any = null;
|
||||
|
||||
// 开始歌词同步
|
||||
const startLyricSync = () => {
|
||||
// 清除已有的定时器
|
||||
if (lyricSyncInterval) {
|
||||
clearInterval(lyricSyncInterval);
|
||||
}
|
||||
|
||||
// 每秒同步一次歌词数据
|
||||
lyricSyncInterval = setInterval(() => {
|
||||
if (isElectron && isLyricWindowOpen.value && store.state.play && playMusic.value?.id) {
|
||||
// 发送当前播放进度的更新
|
||||
try {
|
||||
const updateData = {
|
||||
type: 'update',
|
||||
nowIndex: getLrcIndex(nowTime.value),
|
||||
nowTime: nowTime.value,
|
||||
isPlay: store.state.play
|
||||
};
|
||||
window.api.sendLyric(JSON.stringify(updateData));
|
||||
} catch (error) {
|
||||
console.error('发送歌词进度更新失败:', error);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 停止歌词同步
|
||||
const stopLyricSync = () => {
|
||||
if (lyricSyncInterval) {
|
||||
clearInterval(lyricSyncInterval);
|
||||
lyricSyncInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改openLyric函数,添加定时同步
|
||||
export const openLyric = () => {
|
||||
if (!isElectron) return;
|
||||
|
||||
// 检查是否有播放中的歌曲
|
||||
if (!playMusic.value || !playMusic.value.id) {
|
||||
console.log('没有正在播放的歌曲,无法打开歌词窗口');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Opening lyric window with current song:', playMusic.value?.name);
|
||||
|
||||
isLyricWindowOpen.value = !isLyricWindowOpen.value;
|
||||
if (isLyricWindowOpen.value) {
|
||||
// 立即打开窗口
|
||||
window.api.openLyric();
|
||||
|
||||
// 确保有歌词数据,如果没有,则使用默认的"无歌词"提示
|
||||
if (!lrcArray.value || lrcArray.value.length === 0) {
|
||||
// 如果当前播放的歌曲有ID但没有歌词,则尝试加载歌词
|
||||
console.log('尝试加载歌词数据...');
|
||||
// 发送默认的"无歌词"数据
|
||||
const emptyLyricData = {
|
||||
type: 'empty',
|
||||
nowIndex: 0,
|
||||
nowTime: nowTime.value,
|
||||
startCurrentTime: 0,
|
||||
nextTime: 0,
|
||||
isPlay: store.state.play,
|
||||
lrcArray: [{ text: '加载歌词中...', trText: '' }],
|
||||
lrcTimeArray: [0],
|
||||
allTime: allTime.value,
|
||||
playMusic: playMusic.value
|
||||
};
|
||||
window.api.sendLyric(JSON.stringify(emptyLyricData));
|
||||
} else {
|
||||
// 发送完整歌词数据
|
||||
sendLyricToWin();
|
||||
}
|
||||
|
||||
// 设置定时器,确保500ms后再次发送数据,以防窗口加载延迟
|
||||
setTimeout(() => {
|
||||
window.api.openLyric();
|
||||
sendLyricToWin();
|
||||
}, 500);
|
||||
sendLyricToWin();
|
||||
|
||||
// 启动歌词同步
|
||||
startLyricSync();
|
||||
} else {
|
||||
closeLyric();
|
||||
// 停止歌词同步
|
||||
stopLyricSync();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加关闭歌词窗口的方法
|
||||
// 修改closeLyric函数,确保停止定时同步
|
||||
export const closeLyric = () => {
|
||||
if (!isElectron) return;
|
||||
isLyricWindowOpen.value = false; // 确保状态更新
|
||||
windowData.electron.ipcRenderer.send('close-lyric');
|
||||
|
||||
// 停止歌词同步
|
||||
stopLyricSync();
|
||||
};
|
||||
|
||||
// 在组件挂载时设置对播放状态的监听
|
||||
watch(
|
||||
() => store.state.play,
|
||||
(isPlaying) => {
|
||||
// 如果歌词窗口打开,根据播放状态控制同步
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
if (isPlaying) {
|
||||
startLyricSync();
|
||||
} else {
|
||||
// 如果暂停播放,发送一次暂停状态的更新
|
||||
const pauseData = {
|
||||
type: 'update',
|
||||
isPlay: false
|
||||
};
|
||||
window.api.sendLyric(JSON.stringify(pauseData));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 在组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
stopLyricSync();
|
||||
});
|
||||
|
||||
// 添加播放控制命令监听
|
||||
if (isElectron) {
|
||||
windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {
|
||||
@@ -473,7 +750,7 @@ if (isElectron) {
|
||||
store.commit('nextPlay');
|
||||
break;
|
||||
case 'close':
|
||||
closeLyric();
|
||||
isLyricWindowOpen.value = false; // 确保状态更新
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown command:', command);
|
||||
@@ -484,6 +761,36 @@ if (isElectron) {
|
||||
|
||||
// 在组件挂载时设置监听器
|
||||
onMounted(() => {
|
||||
// 初始化音频监听器
|
||||
setupAudioListeners();
|
||||
useLyricProgress(); // 直接调用,不需要解构返回值
|
||||
|
||||
// 监听歌词窗口关闭事件
|
||||
if (isElectron) {
|
||||
window.api.onLyricWindowClosed(() => {
|
||||
isLyricWindowOpen.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否需要初始化 sound 对象
|
||||
if (!sound.value && audioService.getCurrentSound()) {
|
||||
sound.value = audioService.getCurrentSound();
|
||||
|
||||
// 如果当前处于播放状态,启动进度更新
|
||||
if (store.state.play && sound.value) {
|
||||
// 如果有保存的播放进度,应用它
|
||||
if (store.state.savedPlayProgress !== undefined && sound.value) {
|
||||
try {
|
||||
// 设置音频位置
|
||||
sound.value.seek(store.state.savedPlayProgress);
|
||||
// 同时更新时间显示,这样进度条也会更新
|
||||
nowTime.value = store.state.savedPlayProgress;
|
||||
console.log('恢复播放进度:', store.state.savedPlayProgress);
|
||||
} catch (e) {
|
||||
console.error('恢复播放进度失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
startProgressAnimation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,11 +49,19 @@ const getSongDetail = async (playMusic: SongResult) => {
|
||||
|
||||
// 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据
|
||||
export const useMusicListHook = () => {
|
||||
const handlePlayMusic = async (state: any, playMusic: SongResult) => {
|
||||
const handlePlayMusic = async (state: any, playMusic: SongResult, isPlay: boolean = true) => {
|
||||
const updatedPlayMusic = await getSongDetail(playMusic);
|
||||
state.playMusic = updatedPlayMusic;
|
||||
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
|
||||
state.play = true;
|
||||
|
||||
// 记录当前设置的播放状态
|
||||
state.play = isPlay;
|
||||
|
||||
// 每次设置新歌曲时,立即更新 localStorage
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic));
|
||||
localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl);
|
||||
localStorage.setItem('isPlaying', state.play.toString());
|
||||
|
||||
// 设置网页标题
|
||||
document.title = `${updatedPlayMusic.name} - ${updatedPlayMusic?.song?.artists?.reduce((prev, curr) => `${prev}${curr.name}/`, '')}`;
|
||||
loadLrcAsync(state, updatedPlayMusic.id);
|
||||
@@ -156,7 +164,20 @@ export const useMusicListHook = () => {
|
||||
state.play = true;
|
||||
return;
|
||||
}
|
||||
const playListIndex = (state.playListIndex + 1) % state.playList.length;
|
||||
|
||||
let playListIndex: number;
|
||||
|
||||
if (state.playMode === 2) {
|
||||
// 随机播放模式
|
||||
do {
|
||||
playListIndex = Math.floor(Math.random() * state.playList.length);
|
||||
} while (playListIndex === state.playListIndex && state.playList.length > 1);
|
||||
} else {
|
||||
// 列表循环模式
|
||||
playListIndex = (state.playListIndex + 1) % state.playList.length;
|
||||
}
|
||||
|
||||
state.playListIndex = playListIndex;
|
||||
await handlePlayMusic(state, state.playList[playListIndex]);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
<!-- 样式表 -->
|
||||
<link rel="stylesheet" href="./assets/icon/iconfont.css" />
|
||||
<link rel="stylesheet" href="./assets/css/base.css" />
|
||||
<script defer src="https://cn.vercount.one/js"></script>
|
||||
|
||||
<!-- 动画配置 -->
|
||||
<style>
|
||||
@@ -45,12 +44,6 @@
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div style="display: none">
|
||||
Total Page View <span id="vercount_value_page_pv">Loading</span> Total Visits
|
||||
<span id="vercount_value_site_pv">Loading</span> Site Total Visitors
|
||||
<span id="vercount_value_site_uv">Loading</span>
|
||||
</div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -113,12 +113,32 @@
|
||||
<template #trigger>
|
||||
<i
|
||||
class="iconfont ri-netease-cloud-music-line"
|
||||
:class="{ 'text-green-500': isLyricWindowOpen }"
|
||||
@click="openLyricWindow"
|
||||
:class="{ 'text-green-500': isLyricWindowOpen, 'disabled-icon': !playMusic.id }"
|
||||
@click="playMusic.id && openLyricWindow()"
|
||||
></i>
|
||||
</template>
|
||||
{{ t('player.playBar.lyric') }}
|
||||
{{ playMusic.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }}
|
||||
</n-tooltip>
|
||||
<n-popover
|
||||
v-if="isElectron"
|
||||
trigger="click"
|
||||
:z-index="99999999"
|
||||
content-class="music-eq"
|
||||
raw
|
||||
:show-arrow="false"
|
||||
:delay="200"
|
||||
placement="top"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont ri-equalizer-line" :class="{ 'text-green-500': isEQVisible }"></i>
|
||||
</template>
|
||||
{{ t('player.playBar.eq') }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<eq-control />
|
||||
</n-popover>
|
||||
<n-popover
|
||||
trigger="click"
|
||||
:z-index="99999999"
|
||||
@@ -161,6 +181,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import EqControl from '@/components/EQControl.vue';
|
||||
import {
|
||||
allTime,
|
||||
artistList,
|
||||
@@ -426,6 +447,8 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const isEQVisible = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -655,6 +678,13 @@ watch(
|
||||
@apply text-red-500 hover:text-red-600 !important;
|
||||
}
|
||||
|
||||
.disabled-icon {
|
||||
@apply opacity-50 cursor-not-allowed !important;
|
||||
&:hover {
|
||||
@apply text-inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-loop,
|
||||
.icon-single-loop {
|
||||
font-size: 1.5rem;
|
||||
@@ -667,4 +697,10 @@ watch(
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.music-eq {
|
||||
@apply p-4 rounded-3xl;
|
||||
backdrop-filter: blur(20px);
|
||||
@apply bg-light dark:bg-black bg-opacity-75;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
import { Howl } from 'howler';
|
||||
import { Howl, Howler } from 'howler';
|
||||
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { isElectron } from '@/utils'; // 导入isElectron常量
|
||||
|
||||
class AudioService {
|
||||
private currentSound: Howl | null = null;
|
||||
|
||||
private currentTrack: SongResult | null = null;
|
||||
|
||||
private context: AudioContext | null = null;
|
||||
|
||||
private filters: BiquadFilterNode[] = [];
|
||||
|
||||
private source: MediaElementAudioSourceNode | null = null;
|
||||
|
||||
private gainNode: GainNode | null = null;
|
||||
|
||||
private bypass = false;
|
||||
|
||||
// 预设的 EQ 频段
|
||||
private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
|
||||
|
||||
// 默认的 EQ 设置
|
||||
private defaultEQSettings: { [key: string]: number } = {
|
||||
'31': 0,
|
||||
'62': 0,
|
||||
'125': 0,
|
||||
'250': 0,
|
||||
'500': 0,
|
||||
'1000': 0,
|
||||
'2000': 0,
|
||||
'4000': 0,
|
||||
'8000': 0,
|
||||
'16000': 0
|
||||
};
|
||||
|
||||
private retryCount = 0;
|
||||
|
||||
constructor() {
|
||||
if ('mediaSession' in navigator) {
|
||||
this.initMediaSession();
|
||||
}
|
||||
// 从本地存储加载 EQ 开关状态
|
||||
const bypassState = localStorage.getItem('eqBypass');
|
||||
this.bypass = bypassState ? JSON.parse(bypassState) : false;
|
||||
}
|
||||
|
||||
private initMediaSession() {
|
||||
@@ -120,8 +153,206 @@ class AudioService {
|
||||
}
|
||||
}
|
||||
|
||||
// EQ 相关方法
|
||||
public isEQEnabled(): boolean {
|
||||
return !this.bypass;
|
||||
}
|
||||
|
||||
public setEQEnabled(enabled: boolean) {
|
||||
this.bypass = !enabled;
|
||||
localStorage.setItem('eqBypass', JSON.stringify(this.bypass));
|
||||
|
||||
if (this.source && this.gainNode && this.context) {
|
||||
this.applyBypassState();
|
||||
}
|
||||
}
|
||||
|
||||
public setEQFrequencyGain(frequency: string, gain: number) {
|
||||
const filterIndex = this.frequencies.findIndex((f) => f.toString() === frequency);
|
||||
if (filterIndex !== -1 && this.filters[filterIndex]) {
|
||||
this.filters[filterIndex].gain.setValueAtTime(gain, this.context?.currentTime || 0);
|
||||
this.saveEQSettings(frequency, gain);
|
||||
}
|
||||
}
|
||||
|
||||
public resetEQ() {
|
||||
this.filters.forEach((filter) => {
|
||||
filter.gain.setValueAtTime(0, this.context?.currentTime || 0);
|
||||
});
|
||||
localStorage.removeItem('eqSettings');
|
||||
}
|
||||
|
||||
public getAllEQSettings(): { [key: string]: number } {
|
||||
return this.loadEQSettings();
|
||||
}
|
||||
|
||||
private saveEQSettings(frequency: string, gain: number) {
|
||||
const settings = this.loadEQSettings();
|
||||
settings[frequency] = gain;
|
||||
localStorage.setItem('eqSettings', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
private loadEQSettings(): { [key: string]: number } {
|
||||
const savedSettings = localStorage.getItem('eqSettings');
|
||||
return savedSettings ? JSON.parse(savedSettings) : { ...this.defaultEQSettings };
|
||||
}
|
||||
|
||||
private async disposeEQ(keepContext = false) {
|
||||
try {
|
||||
// 清理音频节点连接
|
||||
if (this.source) {
|
||||
this.source.disconnect();
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
// 清理滤波器
|
||||
this.filters.forEach((filter) => {
|
||||
try {
|
||||
filter.disconnect();
|
||||
} catch (e) {
|
||||
console.warn('清理滤波器时出错:', e);
|
||||
}
|
||||
});
|
||||
this.filters = [];
|
||||
|
||||
// 清理增益节点
|
||||
if (this.gainNode) {
|
||||
this.gainNode.disconnect();
|
||||
this.gainNode = null;
|
||||
}
|
||||
|
||||
// 如果不需要保持上下文,则关闭它
|
||||
if (!keepContext && this.context) {
|
||||
try {
|
||||
await this.context.close();
|
||||
this.context = null;
|
||||
} catch (e) {
|
||||
console.warn('关闭音频上下文时出错:', e);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清理EQ资源时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async setupEQ(sound: Howl) {
|
||||
try {
|
||||
if (!isElectron) {
|
||||
console.log('Web环境中跳过EQ设置,避免CORS问题');
|
||||
this.bypass = true;
|
||||
return;
|
||||
}
|
||||
const howl = sound as any;
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const audioNode = howl._sounds?.[0]?._node;
|
||||
|
||||
if (!audioNode || !(audioNode instanceof HTMLMediaElement)) {
|
||||
if (this.retryCount < 3) {
|
||||
console.warn('等待音频节点初始化,重试次数:', this.retryCount + 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
this.retryCount++;
|
||||
return await this.setupEQ(sound);
|
||||
}
|
||||
throw new Error('无法获取音频节点,请重试');
|
||||
}
|
||||
|
||||
this.retryCount = 0;
|
||||
|
||||
// 确保使用 Howler 的音频上下文
|
||||
this.context = Howler.ctx as AudioContext;
|
||||
|
||||
if (!this.context || this.context.state === 'closed') {
|
||||
Howler.ctx = new AudioContext();
|
||||
this.context = Howler.ctx;
|
||||
Howler.masterGain = this.context.createGain();
|
||||
Howler.masterGain.connect(this.context.destination);
|
||||
}
|
||||
|
||||
if (this.context.state === 'suspended') {
|
||||
await this.context.resume();
|
||||
}
|
||||
|
||||
// 清理现有连接
|
||||
await this.disposeEQ(true);
|
||||
|
||||
try {
|
||||
// 检查节点是否已经有源
|
||||
const existingSource = (audioNode as any).source as MediaElementAudioSourceNode;
|
||||
if (existingSource?.context === this.context) {
|
||||
console.log('复用现有音频源节点');
|
||||
this.source = existingSource;
|
||||
} else {
|
||||
// 创建新的源节点
|
||||
console.log('创建新的音频源节点');
|
||||
this.source = this.context.createMediaElementSource(audioNode);
|
||||
(audioNode as any).source = this.source;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建音频源节点失败:', e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 创建增益节点
|
||||
this.gainNode = this.context.createGain();
|
||||
|
||||
// 创建滤波器
|
||||
this.filters = this.frequencies.map((freq) => {
|
||||
const filter = this.context!.createBiquadFilter();
|
||||
filter.type = 'peaking';
|
||||
filter.frequency.value = freq;
|
||||
filter.Q.value = 1;
|
||||
filter.gain.value = this.loadEQSettings()[freq.toString()] || 0;
|
||||
return filter;
|
||||
});
|
||||
|
||||
// 应用EQ状态
|
||||
this.applyBypassState();
|
||||
|
||||
// 设置音量
|
||||
const volume = localStorage.getItem('volume');
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = volume ? parseFloat(volume) : 1;
|
||||
}
|
||||
|
||||
console.log('EQ初始化成功');
|
||||
} catch (error) {
|
||||
console.error('EQ初始化失败:', error);
|
||||
await this.disposeEQ();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private applyBypassState() {
|
||||
if (!this.source || !this.gainNode || !this.context) return;
|
||||
|
||||
try {
|
||||
// 断开所有现有连接
|
||||
this.source.disconnect();
|
||||
this.filters.forEach((filter) => filter.disconnect());
|
||||
this.gainNode.disconnect();
|
||||
|
||||
if (this.bypass) {
|
||||
// EQ被禁用时,直接连接到输出
|
||||
this.source.connect(this.gainNode);
|
||||
this.gainNode.connect(this.context.destination);
|
||||
} else {
|
||||
// EQ启用时,通过滤波器链连接
|
||||
this.source.connect(this.filters[0]);
|
||||
this.filters.forEach((filter, index) => {
|
||||
if (index < this.filters.length - 1) {
|
||||
filter.connect(this.filters[index + 1]);
|
||||
}
|
||||
});
|
||||
this.filters[this.filters.length - 1].connect(this.gainNode);
|
||||
this.gainNode.connect(this.context.destination);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用EQ状态时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 播放控制相关
|
||||
play(url?: string, track?: SongResult): Promise<Howl> {
|
||||
play(url?: string, track?: SongResult, isPlay: boolean = true): Promise<Howl> {
|
||||
// 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放
|
||||
if (this.currentSound && !url && !track) {
|
||||
this.currentSound.play();
|
||||
@@ -137,19 +368,49 @@ class AudioService {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 1;
|
||||
|
||||
const tryPlay = () => {
|
||||
// 清理现有的音频实例
|
||||
if (this.currentSound) {
|
||||
this.currentSound.unload();
|
||||
this.currentSound = null;
|
||||
}
|
||||
|
||||
const tryPlay = async () => {
|
||||
try {
|
||||
console.log('audioService: 开始创建音频对象');
|
||||
|
||||
// 确保 Howler 上下文已初始化
|
||||
if (!Howler.ctx) {
|
||||
console.log('audioService: 初始化 Howler 上下文');
|
||||
Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
}
|
||||
|
||||
// 确保使用同一个音频上下文
|
||||
if (Howler.ctx.state === 'closed') {
|
||||
console.log('audioService: 重新创建音频上下文');
|
||||
Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
this.context = Howler.ctx;
|
||||
Howler.masterGain = this.context.createGain();
|
||||
Howler.masterGain.connect(this.context.destination);
|
||||
}
|
||||
|
||||
// 恢复上下文状态
|
||||
if (Howler.ctx.state === 'suspended') {
|
||||
console.log('audioService: 恢复暂停的音频上下文');
|
||||
await Howler.ctx.resume();
|
||||
}
|
||||
|
||||
// 先停止并清理现有的音频实例
|
||||
if (this.currentSound) {
|
||||
console.log('audioService: 停止并清理现有的音频实例');
|
||||
this.currentSound.stop();
|
||||
this.currentSound.unload();
|
||||
this.currentSound = null;
|
||||
}
|
||||
|
||||
// 清理 EQ 但保持上下文
|
||||
console.log('audioService: 清理 EQ');
|
||||
await this.disposeEQ(true);
|
||||
|
||||
this.currentTrack = track;
|
||||
console.log('audioService: 创建新的 Howl 对象');
|
||||
this.currentSound = new Howl({
|
||||
src: [url],
|
||||
html5: true,
|
||||
autoplay: true,
|
||||
autoplay: false, // 修改为 false,不自动播放,等待完全初始化后手动播放
|
||||
volume: localStorage.getItem('volume')
|
||||
? parseFloat(localStorage.getItem('volume') as string)
|
||||
: 1,
|
||||
@@ -174,13 +435,32 @@ class AudioService {
|
||||
reject(new Error('音频播放失败,请尝试切换其他歌曲'));
|
||||
}
|
||||
},
|
||||
onload: () => {
|
||||
// 音频加载成功后更新媒体会话
|
||||
if (track && this.currentSound) {
|
||||
this.updateMediaSessionMetadata(track);
|
||||
this.updateMediaSessionPositionState();
|
||||
this.emit('load');
|
||||
resolve(this.currentSound);
|
||||
onload: async () => {
|
||||
// 音频加载成功后设置 EQ 和更新媒体会话
|
||||
if (this.currentSound) {
|
||||
try {
|
||||
console.log('audioService: 音频加载成功,设置 EQ');
|
||||
await this.setupEQ(this.currentSound);
|
||||
this.updateMediaSessionMetadata(track);
|
||||
this.updateMediaSessionPositionState();
|
||||
this.emit('load');
|
||||
|
||||
// 此时音频已完全初始化,根据 isPlay 参数决定是否播放
|
||||
console.log('audioService: 音频完全初始化,isPlay =', isPlay);
|
||||
if (isPlay) {
|
||||
console.log('audioService: 开始播放');
|
||||
this.currentSound.play();
|
||||
}
|
||||
|
||||
resolve(this.currentSound);
|
||||
} catch (error) {
|
||||
console.error('设置 EQ 失败:', error);
|
||||
// 即使 EQ 设置失败,也继续播放(如果需要)
|
||||
if (isPlay) {
|
||||
this.currentSound.play();
|
||||
}
|
||||
resolve(this.currentSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -238,6 +518,7 @@ class AudioService {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = 'none';
|
||||
}
|
||||
this.disposeEQ();
|
||||
}
|
||||
|
||||
setVolume(volume: number) {
|
||||
@@ -267,6 +548,14 @@ class AudioService {
|
||||
clearAllListeners() {
|
||||
this.callbacks = {};
|
||||
}
|
||||
|
||||
public getCurrentPreset(): string | null {
|
||||
return localStorage.getItem('currentPreset');
|
||||
}
|
||||
|
||||
public setCurrentPreset(preset: string): void {
|
||||
localStorage.setItem('currentPreset', preset);
|
||||
}
|
||||
}
|
||||
|
||||
export const audioService = new AudioService();
|
||||
|
||||
186
src/renderer/services/eqService.ts
Normal file
186
src/renderer/services/eqService.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Howl, Howler } from 'howler';
|
||||
import Tuna from 'tunajs';
|
||||
|
||||
// 类型定义扩展
|
||||
interface HowlSound {
|
||||
_sounds: Array<{
|
||||
_node: HTMLMediaElement & {
|
||||
destination?: MediaElementAudioSourceNode;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface EQSettings {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export class EQService {
|
||||
private context: AudioContext | null = null;
|
||||
|
||||
private tuna: any = null;
|
||||
|
||||
private equalizer: any = null;
|
||||
|
||||
private source: MediaElementAudioSourceNode | null = null;
|
||||
|
||||
private gainNode: GainNode | null = null;
|
||||
|
||||
private bypass = false;
|
||||
|
||||
// 预设频率
|
||||
private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
|
||||
|
||||
// 默认EQ设置
|
||||
private defaultEQSettings: EQSettings = Object.fromEntries(
|
||||
this.frequencies.map((f) => [f.toString(), 0])
|
||||
);
|
||||
|
||||
constructor() {
|
||||
this.loadSavedSettings();
|
||||
this.bypass = localStorage.getItem('eqBypass') === 'true';
|
||||
this.initializeUserGestureHandler();
|
||||
}
|
||||
|
||||
// 初始化用户手势处理
|
||||
private initializeUserGestureHandler() {
|
||||
const handler = async () => {
|
||||
if (this.context?.state === 'suspended') {
|
||||
await this.context.resume();
|
||||
}
|
||||
document.removeEventListener('click', handler);
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
}
|
||||
|
||||
// 初始化音频上下文
|
||||
public async setupAudioContext(howl: Howl) {
|
||||
try {
|
||||
// 使用Howler的现有上下文
|
||||
this.context = (Howler.ctx as AudioContext) || new AudioContext();
|
||||
|
||||
// 初始化Howler的音频系统(如果需要)
|
||||
if (!Howler.ctx) {
|
||||
Howler.ctx = this.context;
|
||||
Howler.masterGain = this.context.createGain();
|
||||
Howler.masterGain.connect(this.context.destination);
|
||||
}
|
||||
|
||||
// 确保上下文处于运行状态
|
||||
if (this.context.state === 'suspended') {
|
||||
await this.context.resume();
|
||||
}
|
||||
|
||||
const sound = (howl as unknown as HowlSound)._sounds[0];
|
||||
if (!sound?._node) throw new Error('无法获取音频节点');
|
||||
|
||||
// 清理现有资源
|
||||
await this.dispose();
|
||||
|
||||
// 创建新的处理链
|
||||
this.tuna = new Tuna(this.context);
|
||||
|
||||
// 创建/复用源节点
|
||||
if (!sound._node.destination) {
|
||||
this.source = this.context.createMediaElementSource(sound._node);
|
||||
sound._node.destination = this.source;
|
||||
} else {
|
||||
this.source = sound._node.destination;
|
||||
}
|
||||
|
||||
// 创建效果节点
|
||||
this.gainNode = this.context.createGain();
|
||||
this.equalizer = new this.tuna.Equalizer({
|
||||
frequencies: this.frequencies,
|
||||
gains: this.frequencies.map((f) => this.getSavedGain(f.toString())),
|
||||
bypass: this.bypass
|
||||
});
|
||||
|
||||
// 连接节点链
|
||||
this.source!.connect(this.equalizer.input).connect(this.gainNode).connect(Howler.masterGain);
|
||||
|
||||
// 恢复音量设置
|
||||
const volume = localStorage.getItem('volume');
|
||||
this.gainNode.gain.value = volume ? parseFloat(volume) : 1;
|
||||
} catch (error) {
|
||||
console.error('音频上下文初始化失败:', error);
|
||||
await this.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// EQ功能开关
|
||||
public setEnabled(enabled: boolean) {
|
||||
this.bypass = !enabled;
|
||||
localStorage.setItem('eqBypass', JSON.stringify(this.bypass));
|
||||
if (this.equalizer) this.equalizer.bypass = this.bypass;
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return !this.bypass;
|
||||
}
|
||||
|
||||
// 调整频率增益
|
||||
public setFrequencyGain(frequency: string, gain: number) {
|
||||
const index = this.frequencies.findIndex((f) => f.toString() === frequency);
|
||||
if (index !== -1 && this.equalizer) {
|
||||
this.equalizer.setGain(index, gain);
|
||||
this.saveSettings(frequency, gain);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置EQ设置
|
||||
public resetEQ() {
|
||||
this.frequencies.forEach((f) => {
|
||||
this.setFrequencyGain(f.toString(), 0);
|
||||
});
|
||||
localStorage.removeItem('eqSettings');
|
||||
}
|
||||
|
||||
// 获取当前设置
|
||||
public getAllSettings(): EQSettings {
|
||||
return this.loadSavedSettings();
|
||||
}
|
||||
|
||||
// 保存/加载设置
|
||||
private saveSettings(frequency: string, gain: number) {
|
||||
const settings = this.loadSavedSettings();
|
||||
settings[frequency] = gain;
|
||||
localStorage.setItem('eqSettings', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
private loadSavedSettings(): EQSettings {
|
||||
const saved = localStorage.getItem('eqSettings');
|
||||
return saved ? JSON.parse(saved) : { ...this.defaultEQSettings };
|
||||
}
|
||||
|
||||
private getSavedGain(frequency: string): number {
|
||||
return this.loadSavedSettings()[frequency] || 0;
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
public async dispose() {
|
||||
try {
|
||||
[this.source, this.equalizer, this.gainNode].forEach((node) => {
|
||||
if (node) {
|
||||
node.disconnect();
|
||||
// 特殊清理Tuna节点
|
||||
if (node instanceof Tuna.Equalizer) node.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.context && this.context !== Howler.ctx) {
|
||||
await this.context.close();
|
||||
}
|
||||
|
||||
this.context = null;
|
||||
this.tuna = null;
|
||||
this.source = null;
|
||||
this.equalizer = null;
|
||||
this.gainNode = null;
|
||||
} catch (error) {
|
||||
console.error('资源清理失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eqService = new EQService();
|
||||
@@ -94,6 +94,7 @@ export interface State {
|
||||
currentArtistId: number | null;
|
||||
systemFonts: { label: string; value: string }[];
|
||||
showDownloadDrawer: boolean;
|
||||
savedPlayProgress?: number;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
@@ -139,6 +140,14 @@ const mutations = {
|
||||
async setPlayMusic(state: State, play: boolean) {
|
||||
state.play = play;
|
||||
localStorage.setItem('isPlaying', play.toString());
|
||||
|
||||
// 每次更改播放状态时,确保当前播放歌曲信息也被保存
|
||||
if (state.playMusic && Object.keys(state.playMusic).length > 0) {
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic));
|
||||
if (state.playMusicUrl) {
|
||||
localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl);
|
||||
}
|
||||
}
|
||||
},
|
||||
setMusicFull(state: State, musicFull: boolean) {
|
||||
state.musicFull = musicFull;
|
||||
@@ -329,6 +338,7 @@ const actions = {
|
||||
commit('setCurrentArtistId', id);
|
||||
},
|
||||
async initializeSystemFonts({ commit, state }) {
|
||||
if (!isElectron) return;
|
||||
// 如果已经有字体列表(不只是默认字体),则不重复获取
|
||||
if (state.systemFonts.length > 1) return;
|
||||
|
||||
@@ -341,7 +351,8 @@ const actions = {
|
||||
},
|
||||
async initializePlayState({ state, commit }: { state: State; commit: any }) {
|
||||
const savedPlayList = getLocalStorageItem('playList', []);
|
||||
const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null);
|
||||
const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null) as SongResult | null;
|
||||
const savedPlayProgress = localStorage.getItem('playProgress');
|
||||
|
||||
if (savedPlayList.length > 0) {
|
||||
commit('setPlayList', savedPlayList);
|
||||
@@ -354,11 +365,26 @@ const actions = {
|
||||
|
||||
// 根据自动播放设置决定是否恢复播放状态
|
||||
const shouldAutoPlay = state.setData.autoPlay;
|
||||
if (shouldAutoPlay) {
|
||||
await handlePlayMusic(state, savedPlayMusic);
|
||||
}
|
||||
await handlePlayMusic(state, savedPlayMusic, shouldAutoPlay);
|
||||
state.play = shouldAutoPlay;
|
||||
state.isPlay = true;
|
||||
|
||||
// 如果有保存的播放进度,则提供给前端组件使用
|
||||
if (savedPlayProgress) {
|
||||
try {
|
||||
const progress = JSON.parse(savedPlayProgress);
|
||||
if (progress && progress.songId === savedPlayMusic.id) {
|
||||
// 在全局状态中添加播放进度
|
||||
state.savedPlayProgress = progress.progress;
|
||||
} else {
|
||||
// 如果歌曲ID不匹配,清除保存的进度
|
||||
localStorage.removeItem('playProgress');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析保存的播放进度失败', e);
|
||||
localStorage.removeItem('playProgress');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新获取音乐链接失败:', error);
|
||||
// 清除无效的播放状态
|
||||
@@ -369,6 +395,7 @@ const actions = {
|
||||
localStorage.removeItem('currentPlayMusic');
|
||||
localStorage.removeItem('currentPlayMusicUrl');
|
||||
localStorage.removeItem('isPlaying');
|
||||
localStorage.removeItem('playProgress');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -59,6 +59,13 @@ export const formatNumber = (num: string | number) => {
|
||||
};
|
||||
|
||||
export const getImgUrl = (url: string | undefined, size: string = '') => {
|
||||
if (!url) return '';
|
||||
|
||||
if (url.includes('thumbnail')) {
|
||||
// 只替换最后一个 thumbnail 参数的尺寸
|
||||
return url.replace(/thumbnail=\d+y\d+(?!.*thumbnail)/, `thumbnail=${size}`);
|
||||
}
|
||||
|
||||
const imgUrl = `${url}?param=${size}`;
|
||||
return imgUrl;
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="recommend-item-img">
|
||||
<n-image
|
||||
class="recommend-item-img-img"
|
||||
:src="getImgUrl(item.picUrl || item.coverImgUrl, '200y200')"
|
||||
:src="getImgUrl(item.picUrl || item.coverImgUrl, '300y300')"
|
||||
width="200"
|
||||
height="200"
|
||||
lazy
|
||||
|
||||
@@ -378,27 +378,46 @@ watch(
|
||||
|
||||
// 修改数据更新处
|
||||
const handleDataUpdate = (parsedData: {
|
||||
type?: string;
|
||||
nowTime: number;
|
||||
startCurrentTime: number;
|
||||
nextTime: number;
|
||||
isPlay: boolean;
|
||||
nowIndex: number;
|
||||
lrcArray: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray: number[];
|
||||
allTime: number;
|
||||
playMusic: SongResult;
|
||||
lrcArray?: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray?: number[];
|
||||
allTime?: number;
|
||||
playMusic?: SongResult;
|
||||
}) => {
|
||||
// 确保数据存在且格式正确
|
||||
if (!parsedData) {
|
||||
console.error('Invalid update data received:', parsedData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据数据类型处理
|
||||
if (parsedData.type === 'update') {
|
||||
// 增量更新,只更新动态数据
|
||||
dynamicData.value = {
|
||||
...dynamicData.value,
|
||||
nowTime: parsedData.nowTime || dynamicData.value.nowTime,
|
||||
isPlay: typeof parsedData.isPlay === 'boolean' ? parsedData.isPlay : dynamicData.value.isPlay
|
||||
};
|
||||
|
||||
// 更新索引(如果提供)
|
||||
if (typeof parsedData.nowIndex === 'number') {
|
||||
currentIndex.value = parsedData.nowIndex;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 完整更新或空歌词提示
|
||||
// 更新静态数据
|
||||
staticData.value = {
|
||||
lrcArray: parsedData.lrcArray || [],
|
||||
lrcTimeArray: parsedData.lrcTimeArray || [],
|
||||
allTime: parsedData.allTime || 0,
|
||||
playMusic: parsedData.playMusic || {}
|
||||
playMusic: parsedData.playMusic || ({} as SongResult)
|
||||
};
|
||||
|
||||
// 更新动态数据
|
||||
@@ -472,7 +491,7 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 添<EFBFBD><EFBFBD>拖动相关变量
|
||||
// 添加拖动相关变量
|
||||
const isDragging = ref(false);
|
||||
const startPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user