12 Commits

Author SHA1 Message Date
alger
7b18d9eba3 🐞 fix: 修复登录状态问题 修复播放退出登录的问题 2025-01-23 11:42:03 +08:00
alger
599b0251af 🌈 style: v3.9.0 2025-01-22 23:43:17 +08:00
alger
25c2180247 feat: 添加右键添加到歌单 可以创建歌单 可以在我的歌单中右键取消收藏 2025-01-22 23:37:50 +08:00
alger
a6ff0e7f5c feat: 歌曲右键 添加下一首播放功能 2025-01-22 22:22:32 +08:00
alger
2e06711600 feat: 添加自动播放 和自动保存正在播放列表功能 2025-01-22 22:16:52 +08:00
Alger
80770d6c75 Update README.md 2025-01-20 09:53:24 +08:00
Alger
1e068df2ad Update README.md 2025-01-20 09:46:26 +08:00
alger
4172ff9fc6 🐞 fix: 修复我的搜藏 查看更多跳转空白页的问题 2025-01-19 18:46:36 +08:00
alger
83a7df9fe8 Merge branch 'feat/new-update' into dev_electron 2025-01-19 15:06:16 +08:00
alger
ba95dc11fe feat: 优化歌词下一首的滚动 2025-01-19 13:35:10 +08:00
alger
93829acdab feat: 升级依赖包 升级 electron 版本 2025-01-19 13:34:31 +08:00
alger
fb0831f2eb feat: 应用更新在内部更新 自动打开安装包 2025-01-17 00:02:57 +08:00
23 changed files with 934 additions and 140 deletions

View File

@@ -1,15 +1,9 @@
# 更新日志
## v3.8.0
### ✨ 新功能
- 添加歌词界面样式配置功能
- 添加字体配置功能,支持歌词页面或全局字体配置
- 实现应用单例模式,防止重复启动
## v3.9.1
### ⚡ 优化
- 优化播放体验,去除歌曲地址缓存
- 优化播放下一首逻辑
- 优化播放控制逻辑
### 🐞 修复
- 修复登录状态问题 修复播放退出登录的问题
## 咖啡☕️

View File

@@ -46,6 +46,9 @@ QQ群:789288579
## 咖啡☕️
[<img src="https://api.gitsponsors.com/api/badge/img?id=710867462" height="90">](https://api.gitsponsors.com/api/badge/link?p=GTUHmTNQ9W5XzPhaLd8cPBm26uhtP/QOon9hexaWh9gnfaDT3ivj1ID0uKScVHL61jTFrK1fRWyigScIYvcLh/no+3zgtdW3TK0+vN0TVs84Mt3RibhEqAgBHSd8KhNLxaMd4vMIY37P5gOA2/QYcw==)
| 微信 | 支付宝 |
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
| <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> |

View File

@@ -1,6 +1,6 @@
{
"name": "AlgerMusicPlayer",
"version": "3.8.0",
"version": "3.9.1",
"description": "Alger Music Player",
"author": "Alger <algerkc@qq.com>",
"main": "./out/main/index.js",
@@ -51,8 +51,8 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron": "^34.0.0",
"electron-builder": "^25.1.8",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
@@ -69,8 +69,8 @@
"postcss": "^8.4.49",
"prettier": "^3.3.2",
"remixicon": "^4.2.0",
"sass": "^1.82.0",
"tailwindcss": "^3.4.15",
"sass": "^1.83.4",
"tailwindcss": "^3.4.17",
"tinycolor2": "^1.6.0",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",
@@ -79,8 +79,8 @@
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.4.0",
"vue": "^3.4.30",
"vue-router": "^4.4.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tsc": "^2.0.22",
"vuex": "^4.1.0"
},

View File

@@ -8,6 +8,7 @@ import { initializeFileManager } from './modules/fileManager';
import { initializeFonts } from './modules/fonts';
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
import { initializeTray } from './modules/tray';
import { setupUpdateHandlers } from './modules/update';
import { createMainWindow, initializeWindowManager } from './modules/window';
import { startMusicApi } from './server';
@@ -48,6 +49,9 @@ function initialize() {
// 初始化快捷键
initializeShortcuts(mainWindow);
// 初始化更新处理程序
setupUpdateHandlers(mainWindow);
}
// 检查是否为第一个实例

View File

@@ -16,5 +16,7 @@
"closeAction": "ask",
"musicQuality": "higher",
"fontFamily": "system-ui",
"fontScope": "global"
"fontScope": "global",
"autoPlay": false,
"downloadPath": ""
}

View File

@@ -13,6 +13,10 @@ declare global {
miniTray: () => void;
restart: () => void;
unblockMusic: (id: number, data: any) => Promise<any>;
startDownload: (url: string) => void;
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
removeDownloadListeners: () => void;
invoke: (channel: string, ...args: any[]) => Promise<any>;
};
$message: any;

View File

@@ -12,6 +12,18 @@ const api = {
openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
// 更新相关
startDownload: (url: string) => ipcRenderer.send('start-download', url),
onDownloadProgress: (callback: (progress: number, status: string) => void) => {
ipcRenderer.on('download-progress', (_event, progress, status) => callback(progress, status));
},
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => {
ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath));
},
removeDownloadListeners: () => {
ipcRenderer.removeAllListeners('download-progress');
ipcRenderer.removeAllListeners('download-complete');
},
// 歌词缓存相关
invoke: (channel: string, ...args: any[]) => {
const validChannels = [

View File

@@ -63,6 +63,7 @@ onMounted(() => {
store.dispatch('initializeSettings');
store.dispatch('initializeTheme');
store.dispatch('initializeSystemFonts');
store.dispatch('initializePlayState');
if (isMobile.value) {
store.commit(
'setMenus',
@@ -72,7 +73,7 @@ onMounted(() => {
});
</script>
<style lang="scss">
<style lang="scss" scoped>
.app-container {
@apply h-full w-full;
user-select: none;

View File

@@ -14,15 +14,19 @@ export const getMusicQualityDetail = (id: number) => {
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = async (id: number) => {
const res = await request.get('/song/download/url/v1', {
params: {
id,
level: store.state.setData.musicQuality || 'higher'
}
});
// 判断是否登录
if (store.state.user) {
const res = await request.get('/song/download/url/v1', {
params: {
id,
level: store.state.setData.musicQuality || 'higher',
cookie: `${localStorage.getItem('token')} os=pc;`
}
});
if (res.data.data.url) {
return { data: { data: [{ ...res.data.data }] } };
if (res.data.data.url) {
return { data: { data: [{ ...res.data.data }] } };
}
}
return await request.get('/song/url/v1', {
@@ -83,3 +87,17 @@ export const likeSong = (id: number, like: boolean = true) => {
export const getLikedList = () => {
return request.get('/likelist');
};
// 创建歌单
export const createPlaylist = (params: { name: string; privacy: number }) => {
return request.post('/playlist/create', params);
};
// 添加或删除歌单歌曲
export const updatePlaylistTracks = (params: {
op: 'add' | 'del';
pid: number;
tracks: string;
}) => {
return request.get('/playlist/tracks', { params });
};

View File

@@ -27,14 +27,11 @@ declare module 'vue' {
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NPopover: typeof import('naive-ui')['NPopover']
NProgress: typeof import('naive-ui')['NProgress']
NRadio: typeof import('naive-ui')['NRadio']
NRadioButton: typeof import('naive-ui')['NRadioButton']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
@@ -46,7 +43,6 @@ declare module 'vue' {
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NTooltip: typeof import('naive-ui')['NTooltip']
NTransfer: typeof import('naive-ui')['NTransfer']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -59,7 +59,12 @@
:class="setAnimationClass('animate__bounceInUp')"
:style="getItemAnimationDelay(index)"
>
<song-item :item="formatDetail(item)" @play="handlePlay" />
<song-item
:item="formatDetail(item)"
:can-remove="canRemove"
@play="handlePlay"
@remove-song="(id) => emit('remove-song', id)"
/>
</div>
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
<play-bottom />
@@ -97,15 +102,17 @@ const props = withDefaults(
[key: string]: any;
};
cover?: boolean;
canRemove?: boolean;
}>(),
{
loading: false,
cover: true,
zIndex: 9996
zIndex: 9996,
canRemove: false
}
);
const emit = defineEmits(['update:show', 'update:loading']);
const emit = defineEmits(['update:show', 'update:loading', 'remove-song']);
const page = ref(0);
const pageSize = 20;

View File

@@ -0,0 +1,355 @@
<template>
<n-drawer
:show="modelValue"
:width="400"
placement="right"
@update:show="$emit('update:modelValue', $event)"
>
<n-drawer-content title="添加到歌单" class="mac-style-drawer">
<n-scrollbar class="h-full">
<div class="playlist-drawer">
<!-- 创建新歌单按钮和表单 -->
<div class="create-playlist-section">
<div
class="create-playlist-button"
:class="{ 'is-expanded': isCreating }"
@click="toggleCreateForm"
>
<div class="create-playlist-icon">
<i class="iconfont" :class="isCreating ? 'ri-close-line' : 'ri-add-line'"></i>
</div>
<div class="create-playlist-text">{{ isCreating ? '取消创建' : '创建新歌单' }}</div>
</div>
<!-- 创建歌单表单 -->
<div class="create-playlist-form" :class="{ 'is-visible': isCreating }">
<n-input
v-model:value="formValue.name"
placeholder="歌单名称"
maxlength="40"
class="mac-style-input"
:status="inputError ? 'error' : undefined"
>
<template #prefix>
<i class="iconfont ri-music-2-line"></i>
</template>
</n-input>
<div class="privacy-switch">
<div class="privacy-label">
<i
class="iconfont"
:class="formValue.privacy ? 'ri-lock-line' : 'ri-earth-line'"
></i>
<span>{{ formValue.privacy ? '私密歌单' : '公开歌单' }}</span>
</div>
<n-switch v-model:value="formValue.privacy" class="mac-style-switch">
<template #checked>私密</template>
<template #unchecked>公开</template>
</n-switch>
</div>
<div class="form-actions">
<n-button
type="primary"
quaternary
class="mac-style-button"
:loading="creating"
:disabled="!formValue.name"
@click="handleCreatePlaylist"
>
创建歌单
</n-button>
</div>
</div>
</div>
<!-- 歌单列表 -->
<div class="playlist-list">
<div
v-for="playlist in playlists"
:key="playlist.id"
class="playlist-item"
@click="handleAddToPlaylist(playlist)"
>
<n-image
:src="getImgUrl(playlist.coverImgUrl || playlist.picUrl, '100y100')"
class="playlist-item-img"
preview-disabled
:img-props="{
crossorigin: 'anonymous'
}"
/>
<div class="playlist-item-info">
<div class="playlist-item-name">{{ playlist.name }}</div>
<div class="playlist-item-count">{{ playlist.trackCount }}首歌曲</div>
</div>
<div class="playlist-item-action">
<i class="iconfont ri-add-line"></i>
</div>
</div>
</div>
</div>
</n-scrollbar>
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { computed, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { createPlaylist, updatePlaylistTracks } from '@/api/music';
import { getUserPlaylist } from '@/api/user';
import { getImgUrl } from '@/utils';
const props = defineProps<{
modelValue: boolean;
songId?: number;
}>();
const emit = defineEmits(['update:modelValue']);
const message = useMessage();
const playlists = ref<any[]>([]);
const creating = ref(false);
const store = useStore();
const isCreating = ref(false);
const formValue = ref({
name: '',
privacy: false
});
const inputError = computed(() => {
return isCreating.value && !formValue.value.name;
});
const toggleCreateForm = () => {
if (creating.value) return;
isCreating.value = !isCreating.value;
if (!isCreating.value) {
formValue.value.name = '';
formValue.value.privacy = false;
}
};
// 获取用户歌单
const fetchUserPlaylists = async () => {
try {
const { user } = store.state;
if (!user?.userId) {
message.error('请先登录');
emit('update:modelValue', false);
return;
}
const res = await getUserPlaylist(user.userId);
if (res.data?.playlist) {
playlists.value = res.data.playlist;
}
} catch (error) {
console.error('获取歌单失败:', error);
message.error('获取歌单失败');
}
};
// 添加到歌单
const handleAddToPlaylist = async (playlist: any) => {
if (!props.songId) return;
try {
const res = await updatePlaylistTracks({
op: 'add',
pid: playlist.id,
tracks: props.songId.toString()
});
console.log('res.data', res.data);
if (res.status === 200) {
message.success('添加成功');
emit('update:modelValue', false);
} else {
throw new Error(res.data?.msg || '添加失败');
}
} catch (error: any) {
console.error('添加到歌单失败:', error);
message.error(error.message || '添加到歌单失败');
}
};
// 创建歌单
const handleCreatePlaylist = async () => {
if (!formValue.value.name) {
message.error('请输入歌单名称');
return;
}
try {
creating.value = true;
const res = await createPlaylist({
name: formValue.value.name,
privacy: formValue.value.privacy ? 10 : 0
});
if (res.data?.id) {
message.success('创建成功');
isCreating.value = false;
formValue.value.name = '';
formValue.value.privacy = false;
await fetchUserPlaylists();
}
} catch (error) {
console.error('创建歌单失败:', error);
message.error('创建歌单失败');
} finally {
creating.value = false;
}
};
// 监听显示状态变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
fetchUserPlaylists();
}
}
);
</script>
<style lang="scss" scoped>
.mac-style-drawer {
@apply h-full;
:deep(.n-drawer-header__main) {
@apply text-base font-medium;
}
:deep(.n-drawer-content) {
@apply h-full;
}
:deep(.n-drawer-content-wrapper) {
@apply h-full;
}
:deep(.n-scrollbar-rail) {
@apply right-0.5;
}
}
.playlist-drawer {
@apply flex flex-col gap-6;
}
.create-playlist-section {
@apply flex flex-col;
}
.create-playlist-button {
@apply flex items-center gap-4 p-3 rounded-xl cursor-pointer transition-all duration-200
bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700;
&.is-expanded {
@apply bg-gray-100 dark:bg-gray-700;
.create-playlist-icon {
transform: rotate(45deg);
}
}
&-icon {
@apply w-10 h-10 rounded-xl bg-green-500 flex items-center justify-center text-white
transition-all duration-300;
.iconfont {
@apply text-xl transition-transform duration-300;
}
}
&-text {
@apply text-sm font-medium transition-colors duration-300;
}
}
.create-playlist-form {
@apply max-h-0 overflow-hidden transition-all duration-300 ease-in-out opacity-0;
&.is-visible {
@apply max-h-[200px] mt-4 opacity-100;
}
.mac-style-input {
@apply rounded-lg;
:deep(.n-input-wrapper) {
@apply bg-gray-50 dark:bg-gray-800 border-0;
}
:deep(.n-input__input) {
@apply text-sm;
}
:deep(.n-input__prefix) {
@apply text-gray-400;
}
}
.form-actions {
@apply mt-4;
.mac-style-button {
@apply w-full rounded-lg text-sm py-2 bg-green-500 hover:bg-green-600 text-white;
}
}
}
.privacy-switch {
@apply flex items-center justify-between mt-4 px-2;
.privacy-label {
@apply flex items-center gap-2;
.iconfont {
@apply text-base text-gray-500 dark:text-gray-400;
}
span {
@apply text-sm;
}
}
:deep(.n-switch) {
@apply h-5 min-w-[40px];
}
}
.playlist-list {
@apply flex flex-col gap-2;
}
.playlist-item {
@apply flex items-center gap-3 p-2 rounded-xl cursor-pointer transition-all duration-200
hover:bg-gray-50 dark:hover:bg-gray-800;
&-img {
@apply w-10 h-10 rounded-xl;
}
&-info {
@apply flex-1 min-w-0;
}
&-name {
@apply text-sm font-medium truncate;
}
&-count {
@apply text-xs text-gray-500 dark:text-gray-400;
}
&-action {
@apply w-8 h-8 rounded-lg flex items-center justify-center
text-gray-400 hover:text-green-500 transition-colors duration-200;
.iconfont {
@apply text-xl;
}
}
}
</style>

View File

@@ -73,9 +73,10 @@
<n-dropdown
v-if="isElectron"
:show="showDropdown"
:options="dropdownOptions"
:x="dropdownX"
:y="dropdownY"
:options="dropdownOptions"
:z-index="99999"
placement="bottom-start"
@clickoutside="showDropdown = false"
@select="handleSelect"
@@ -86,8 +87,8 @@
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import type { MenuOption } from 'naive-ui';
import { useMessage } from 'naive-ui';
import { computed, h, ref, useTemplateRef } from 'vue';
import { NImage, NText, useMessage } from 'naive-ui';
import { computed, h, inject, ref, useTemplateRef } from 'vue';
import { useStore } from 'vuex';
import { getSongUrl } from '@/hooks/MusicListHook';
@@ -104,13 +105,15 @@ const props = withDefaults(
favorite?: boolean;
selectable?: boolean;
selected?: boolean;
canRemove?: boolean;
}>(),
{
mini: false,
list: false,
favorite: true,
selectable: false,
selected: false
selected: false,
canRemove: false
}
);
@@ -132,14 +135,109 @@ const dropdownY = ref(0);
const isDownloading = ref(false);
const dropdownOptions = computed<MenuOption[]>(() => [
{
label: isDownloading.value ? '下载中...' : `下载 ${props.item.name}`,
key: 'download',
icon: () => h('i', { class: 'iconfont ri-download-line' }),
disabled: isDownloading.value
const openPlaylistDrawer = inject<(songId: number) => void>('openPlaylistDrawer');
const renderSongPreview = () => {
return h(
'div',
{
class: 'flex items-center gap-3 px-2 py-1 dark:border-gray-800'
},
[
h(NImage, {
src: getImgUrl(props.item.picUrl || props.item.al?.picUrl, '100y100'),
class: 'w-10 h-10 rounded-lg flex-shrink-0',
previewDisabled: true,
imgProps: {
crossorigin: 'anonymous'
}
}),
h(
'div',
{
class: 'flex-1 min-w-0 py-1'
},
[
h(
'div',
{
class: 'mb-1'
},
[
h(
NText,
{
depth: 1,
class: 'text-sm font-medium'
},
{
default: () => props.item.name
}
)
]
)
]
)
]
);
};
const dropdownOptions = computed<MenuOption[]>(() => {
const options: MenuOption[] = [
{
key: 'header',
type: 'render',
render: renderSongPreview
},
{
key: 'divider1',
type: 'divider'
},
{
label: '播放',
key: 'play',
icon: () => h('i', { class: 'iconfont ri-play-circle-line' })
},
{
label: '下一首播放',
key: 'playNext',
icon: () => h('i', { class: 'iconfont ri-play-list-2-line' })
},
{
type: 'divider',
key: 'd1'
},
{
label: '添加到歌单',
key: 'addToPlaylist',
icon: () => h('i', { class: 'iconfont ri-folder-add-line' })
},
{
label: isFavorite.value ? '取消喜欢' : '喜欢',
key: 'favorite',
icon: () =>
h('i', {
class: `iconfont ${isFavorite.value ? 'ri-heart-fill text-red-500' : 'ri-heart-line'}`
})
}
];
if (props.canRemove) {
options.push(
{
type: 'divider',
key: 'd2'
},
{
label: '从歌单中删除',
key: 'remove',
icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })
}
);
}
]);
return options;
});
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault();
@@ -152,6 +250,16 @@ const handleSelect = (key: string | number) => {
showDropdown.value = false;
if (key === 'download') {
downloadMusic();
} else if (key === 'playNext') {
handlePlayNext();
} else if (key === 'addToPlaylist') {
openPlaylistDrawer?.(props.item.id);
} else if (key === 'favorite') {
toggleFavorite(new Event('click'));
} else if (key === 'play') {
playMusicEvent(props.item);
} else if (key === 'remove') {
emits('remove-song', props.item.id);
}
};
@@ -222,7 +330,7 @@ const downloadMusic = async () => {
}
};
const emits = defineEmits(['play', 'select']);
const emits = defineEmits(['play', 'select', 'remove-song']);
const songImageRef = useTemplateRef('songImg');
const imageLoad = async () => {
@@ -281,6 +389,12 @@ const handleArtistClick = (id: number) => {
const artists = computed(() => {
return (props.item.ar || props.item.song?.artists)?.slice(0, 4) || [];
});
// 添加到下一首播放
const handlePlayNext = () => {
store.commit('addToNextPlay', props.item);
message.success('已添加到下一首播放');
};
</script>
<style lang="scss" scoped>
@@ -450,4 +564,56 @@ const artists = computed(() => {
}
}
}
:deep(.n-dropdown-menu) {
@apply min-w-[240px] overflow-hidden rounded-lg border dark:border-gray-800;
.n-dropdown-option {
@apply h-9 text-sm;
&:hover {
@apply bg-gray-100 dark:bg-gray-800;
}
.n-dropdown-option-body {
@apply h-full;
.n-dropdown-option-body__prefix {
@apply w-8 flex justify-center items-center;
.iconfont {
@apply text-base;
}
}
}
}
.n-dropdown-divider {
@apply my-1;
}
}
:deep(.song-preview) {
@apply flex items-center gap-3 p-3 border-b dark:border-gray-800;
.n-image {
@apply w-12 h-12 rounded-lg flex-shrink-0;
}
.song-preview-info {
@apply flex-1 min-w-0 py-1;
.song-preview-name {
@apply text-sm font-medium truncate mb-1;
}
.song-preview-artist {
@apply text-xs text-gray-500 dark:text-gray-400 truncate;
}
}
}
:deep(.n-dropdown-option-body--render) {
@apply p-0;
}
</style>

View File

@@ -32,11 +32,12 @@
<install-app-modal v-if="!isElectron"></install-app-modal>
<update-modal v-if="isElectron" />
<artist-drawer ref="artistDrawerRef" :show="artistDrawerShow" />
<playlist-drawer v-model="showPlaylistDrawer" :song-id="currentSongId" />
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue';
import { computed, defineAsyncComponent, nextTick, onMounted, provide, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
@@ -63,6 +64,7 @@ const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
const ArtistDrawer = defineAsyncComponent(() => import('@/components/common/ArtistDrawer.vue'));
const PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue'));
const store = useStore();
@@ -93,6 +95,18 @@ watch(
}
}
);
const showPlaylistDrawer = ref(false);
const currentSongId = ref<number | undefined>();
// 提供一个方法来打开歌单抽屉
const openPlaylistDrawer = (songId: number) => {
currentSongId.value = songId;
showPlaylistDrawer.value = true;
};
// 将方法提供给全局
provide('openPlaylistDrawer', openPlaylistDrawer);
</script>
<style lang="scss" scoped>

View File

@@ -227,9 +227,21 @@ const isVisible = computed({
});
// 歌词滚动方法
const lrcScroll = (behavior: ScrollBehavior = 'smooth') => {
const lrcScroll = (behavior: ScrollBehavior = 'smooth', forceTop: boolean = false) => {
if (!isVisible.value || !lrcSider.value) return;
if (forceTop) {
lrcSider.value.scrollTo({
top: 0,
behavior
});
return;
}
if (isMouse.value) return;
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`) as HTMLElement;
if (isVisible.value && !isMouse.value && nowEl && lrcSider.value) {
if (nowEl) {
const containerHeight = lrcSider.value.$el.clientHeight;
const elementTop = nowEl.offsetTop;
const scrollTop = elementTop - containerHeight / 2 + nowEl.clientHeight / 2;
@@ -479,6 +491,13 @@ onMounted(() => {
}
});
// 添加对 playMusic 的监听
watch(playMusic, () => {
nextTick(() => {
lrcScroll('instant', true);
});
});
defineExpose({
lrcScroll,
config

View File

@@ -288,18 +288,33 @@ const MusicFullRef = ref<any>(null);
// 播放暂停按钮事件
const playMusicEvent = async () => {
try {
// 检查是否有有效的音乐对象和 URL
if (!playMusic.value?.id || !store.state.playMusicUrl) {
console.warn('No valid music or URL available');
store.commit('setPlay', playMusic.value);
return;
}
if (play.value) {
audioService.pause();
store.commit('setPlayMusic', false);
// 暂停播放
if (audioService.getCurrentSound()) {
audioService.pause();
store.commit('setPlayMusic', false);
}
} else {
audioService.play();
// 开始播放
if (audioService.getCurrentSound()) {
// 如果已经有音频实例,直接播放
audioService.play();
} else {
// 如果没有音频实例,重新创建并播放
await audioService.play(store.state.playMusicUrl, playMusic.value);
}
store.commit('setPlayMusic', true);
}
} catch (error) {
console.log('error', error);
if (play.value) {
store.commit('nextPlay');
}
console.error('播放出错:', error);
store.commit('nextPlay');
}
};

View File

@@ -122,78 +122,94 @@ class AudioService {
// 播放控制相关
play(url?: string, track?: SongResult): Promise<Howl> {
// 如果没有提供新的 URL 和 track且当前有音频实例则继续播放
if (this.currentSound && !url && !track) {
this.currentSound.play();
return Promise.resolve(this.currentSound as Howl);
return Promise.resolve(this.currentSound);
}
// 如果没有提供必要的参数,返回错误
if (!url || !track) {
return Promise.reject(new Error('Missing required parameters: url and track'));
}
return new Promise((resolve, reject) => {
let retryCount = 0;
const maxRetries = 1;
const tryPlay = () => {
// 清理现有的音频实例
if (this.currentSound) {
this.currentSound.unload();
this.currentSound = null;
}
this.currentSound = null;
this.currentTrack = track as SongResult;
this.currentSound = new Howl({
src: [url as string],
html5: true,
autoplay: true,
volume: localStorage.getItem('volume')
? parseFloat(localStorage.getItem('volume') as string)
: 1,
onloaderror: () => {
console.error('Audio load error');
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
reject(new Error('音频加载失败,请尝试切换其他歌曲'));
}
},
onplayerror: () => {
console.error('Audio play error');
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
reject(new Error('音频播放失败,请尝试切换其他歌曲'));
try {
this.currentTrack = track;
this.currentSound = new Howl({
src: [url],
html5: true,
autoplay: true,
volume: localStorage.getItem('volume')
? parseFloat(localStorage.getItem('volume') as string)
: 1,
format: ['mp3', 'aac'],
onloaderror: (_, error) => {
console.error('Audio load error:', error);
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
reject(new Error('音频加载失败,请尝试切换其他歌曲'));
}
},
onplayerror: (_, error) => {
console.error('Audio play error:', error);
if (retryCount < maxRetries) {
retryCount++;
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
setTimeout(tryPlay, 1000 * retryCount);
} else {
reject(new Error('音频播放失败,请尝试切换其他歌曲'));
}
},
onload: () => {
// 音频加载成功后更新媒体会话
if (track && this.currentSound) {
this.updateMediaSessionMetadata(track);
this.updateMediaSessionPositionState();
this.emit('load');
resolve(this.currentSound);
}
}
});
// 设置音频事件监听
if (this.currentSound) {
this.currentSound.on('play', () => {
this.updateMediaSessionState(true);
this.emit('play');
});
this.currentSound.on('pause', () => {
this.updateMediaSessionState(false);
this.emit('pause');
});
this.currentSound.on('end', () => {
this.emit('end');
});
this.currentSound.on('seek', () => {
this.updateMediaSessionPositionState();
this.emit('seek');
});
}
});
// 更新媒体会话元数据
this.updateMediaSessionMetadata(track as SongResult);
// 设置音频事件监听
this.currentSound.on('play', () => {
this.updateMediaSessionState(true);
this.emit('play');
});
this.currentSound.on('pause', () => {
this.updateMediaSessionState(false);
this.emit('pause');
});
this.currentSound.on('end', () => {
this.emit('end');
});
this.currentSound.on('seek', () => {
this.updateMediaSessionPositionState();
this.emit('seek');
});
this.currentSound.on('load', () => {
this.updateMediaSessionPositionState();
this.emit('load');
resolve(this.currentSound as Howl);
});
} catch (error) {
console.error('Error creating audio instance:', error);
reject(error);
}
};
tryPlay();
@@ -210,8 +226,12 @@ class AudioService {
stop() {
if (this.currentSound) {
this.currentSound.stop();
this.currentSound.unload();
try {
this.currentSound.stop();
this.currentSound.unload();
} catch (error) {
console.error('Error stopping audio:', error);
}
this.currentSound = null;
}
this.currentTrack = null;
@@ -236,10 +256,13 @@ class AudioService {
pause() {
if (this.currentSound) {
this.currentSound.pause();
try {
this.currentSound.pause();
} catch (error) {
console.error('Error pausing audio:', error);
}
}
}
clearAllListeners() {
this.callbacks = {};

View File

@@ -11,9 +11,63 @@ import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
// 默认设置
const defaultSettings = setData;
function isValidUrl(urlString: string): boolean {
try {
return Boolean(new URL(urlString));
} catch (e) {
return false;
}
}
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
try {
const item = localStorage.getItem(key);
if (!item) return defaultValue;
// 尝试解析 JSON
const parsedItem = JSON.parse(item);
// 对于音乐 URL检查是否是有效的 URL 格式或本地文件路径
if (key === 'currentPlayMusicUrl' && typeof parsedItem === 'string') {
if (!parsedItem.startsWith('local://') && !isValidUrl(parsedItem)) {
console.warn(`Invalid URL in localStorage for key ${key}, using default value`);
localStorage.removeItem(key);
return defaultValue;
}
}
// 对于播放列表,检查是否是数组且每个项都有必要的字段
if (key === 'playList') {
if (!Array.isArray(parsedItem)) {
console.warn(`Invalid playList format in localStorage, using default value`);
localStorage.removeItem(key);
return defaultValue;
}
// 检查每个歌曲对象是否有必要的字段
const isValid = parsedItem.every((item) => item && typeof item === 'object' && 'id' in item);
if (!isValid) {
console.warn(`Invalid song objects in playList, using default value`);
localStorage.removeItem(key);
return defaultValue;
}
}
// 对于当前播放音乐,检查是否是对象且包含必要的字段
if (key === 'currentPlayMusic') {
if (!parsedItem || typeof parsedItem !== 'object' || !('id' in parsedItem)) {
console.warn(`Invalid currentPlayMusic format in localStorage, using default value`);
localStorage.removeItem(key);
return defaultValue;
}
}
return parsedItem;
} catch (error) {
console.warn(`Error parsing localStorage item for key ${key}:`, error);
// 如果解析失败,删除可能损坏的数据
localStorage.removeItem(key);
return defaultValue;
}
}
export interface State {
@@ -44,11 +98,11 @@ const state: State = {
menus: homeRouter,
play: false,
isPlay: false,
playMusic: {} as SongResult,
playMusicUrl: '',
playMusic: getLocalStorageItem('currentPlayMusic', {} as SongResult),
playMusicUrl: getLocalStorageItem('currentPlayMusicUrl', ''),
user: getLocalStorageItem('user', null),
playList: [],
playListIndex: 0,
playList: getLocalStorageItem('playList', []),
playListIndex: getLocalStorageItem('playListIndex', 0),
setData: defaultSettings,
lyric: {},
isMobile: false,
@@ -72,12 +126,16 @@ const mutations = {
},
async setPlay(state: State, playMusic: SongResult) {
await handlePlayMusic(state, playMusic);
localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic));
localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl);
},
setIsPlay(state: State, isPlay: boolean) {
state.isPlay = isPlay;
localStorage.setItem('isPlaying', isPlay.toString());
},
setPlayMusic(state: State, play: boolean) {
async setPlayMusic(state: State, play: boolean) {
state.play = play;
localStorage.setItem('isPlaying', play.toString());
},
setMusicFull(state: State, musicFull: boolean) {
state.musicFull = musicFull;
@@ -85,6 +143,8 @@ const mutations = {
setPlayList(state: State, playList: SongResult[]) {
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
state.playList = playList;
localStorage.setItem('playList', JSON.stringify(playList));
localStorage.setItem('playListIndex', state.playListIndex.toString());
},
async nextPlay(state: State) {
await nextPlay(state);
@@ -92,6 +152,27 @@ const mutations = {
async prevPlay(state: State) {
await prevPlay(state);
},
// 添加到下一首播放
addToNextPlay(state: State, song: SongResult) {
const playList = [...state.playList];
const currentIndex = state.playListIndex;
// 检查歌曲是否已经在播放列表中
const existingIndex = playList.findIndex((item) => item.id === song.id);
if (existingIndex !== -1) {
// 如果歌曲已经在列表中,将其移动到当前播放歌曲的下一个位置
playList.splice(existingIndex, 1);
}
// 在当前播放歌曲后插入新歌曲
playList.splice(currentIndex + 1, 0, song);
// 更新播放列表
state.playList = playList;
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
localStorage.setItem('playList', JSON.stringify(playList));
localStorage.setItem('playListIndex', state.playListIndex.toString());
},
setSetData(state: State, setData: any) {
state.setData = setData;
if (isElectron) {
@@ -238,6 +319,39 @@ const actions = {
} catch (error) {
console.error('获取系统字体失败:', error);
}
},
async initializePlayState({ state, commit }: { state: State; commit: any }) {
const savedPlayList = getLocalStorageItem('playList', []);
const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null);
if (savedPlayList.length > 0) {
commit('setPlayList', savedPlayList);
}
if (savedPlayMusic && Object.keys(savedPlayMusic).length > 0) {
// 不直接使用保存的 URL而是重新获取
try {
// 使用 handlePlayMusic 来重新获取音乐 URL
// 根据自动播放设置决定是否恢复播放状态
const shouldAutoPlay = state.setData.autoPlay;
if (shouldAutoPlay) {
await handlePlayMusic(state, savedPlayMusic);
}
state.play = shouldAutoPlay;
state.isPlay = true;
} catch (error) {
console.error('重新获取音乐链接失败:', error);
// 清除无效的播放状态
state.play = false;
state.isPlay = false;
state.playMusic = {} as SongResult;
state.playMusicUrl = '';
localStorage.removeItem('currentPlayMusic');
localStorage.removeItem('currentPlayMusicUrl');
localStorage.removeItem('isPlaying');
}
}
}
};

View File

@@ -152,7 +152,6 @@ export const getTextColors = (gradient: string = ''): ITextColors => {
if (!gradient) return defaultColors;
const colors = parseGradient(gradient);
console.log('colors', colors);
if (!colors.length) return defaultColors;
const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0];

View File

@@ -44,19 +44,14 @@ request.interceptors.request.use(
// 在请求发送之前做一些处理
// 在get请求params中添加timestamp
if (config.method === 'get') {
config.params = {
...config.params,
timestamp: Date.now()
};
const token = localStorage.getItem('token');
if (token) {
config.params.cookie = `${token} os=pc;`;
} else {
config.params.cookie = 'os=pc;';
}
config.params = {
...config.params,
timestamp: Date.now()
};
const token = localStorage.getItem('token');
if (token) {
config.params.cookie = config.params.cookie || token;
}
if (isElectron) {
const proxyConfig = setData?.proxyConfig;
if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) {

View File

@@ -298,7 +298,7 @@ const getItemAnimationDelay = (index: number) => {
const router = useRouter();
const handleMore = () => {
router.push('/favorite');
router.push('/history');
};
// 全选相关

View File

@@ -136,6 +136,17 @@
style="width: 160px"
/>
</div>
<div class="set-item">
<div>
<div class="set-item-title">自动播放</div>
<div class="set-item-content">重新打开应用时是否自动继续播放</div>
</div>
<n-switch v-model:value="setData.autoPlay">
<template #checked>开启</template>
<template #unchecked>关闭</template>
</n-switch>
</div>
</div>
</div>

View File

@@ -84,16 +84,20 @@
:song-list="list?.tracks || []"
:list-info="list"
:loading="listLoading"
:can-remove="true"
@remove-song="handleRemoveFromPlaylist"
/>
</div>
</template>
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getListDetail } from '@/api/list';
import { updatePlaylistTracks } from '@/api/music';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from '@/components/common/SongItem.vue';
@@ -116,6 +120,7 @@ const mounted = ref(true);
const isShowList = ref(false);
const list = ref<Playlist>();
const listLoading = ref(false);
const message = useMessage();
const user = computed(() => store.state.user);
@@ -127,6 +132,7 @@ onBeforeUnmount(() => {
const checkLoginStatus = () => {
const token = localStorage.getItem('token');
const userData = localStorage.getItem('user');
console.log('触发了', token, userData);
if (!token || !userData) {
router.push('/login');
@@ -147,6 +153,10 @@ const loadPage = async () => {
// 检查登录状态
if (!checkLoginStatus()) return;
await loadData();
};
const loadData = async () => {
try {
infoLoading.value = true;
@@ -183,8 +193,10 @@ const loadPage = async () => {
watch(
() => router.currentRoute.value.path,
(newPath) => {
console.log('newPath', newPath);
if (newPath === '/user') {
checkLoginStatus();
loadData();
}
}
);
@@ -215,11 +227,41 @@ const showPlaylist = async (id: number, name: string) => {
listLoading.value = true;
list.value = {
name
name,
id
} as Playlist;
await loadPlaylistDetail(id);
listLoading.value = false;
};
// 加载歌单详情
const loadPlaylistDetail = async (id: number) => {
const { data } = await getListDetail(id);
list.value = data.playlist;
listLoading.value = false;
};
// 从歌单中删除歌曲
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('删除成功');
// 重新加载歌单详情
await loadPlaylistDetail(list.value.id);
} else {
throw new Error(res.data?.msg || '删除失败');
}
} catch (error: any) {
console.error('删除歌曲失败:', error);
message.error(error.message || '删除失败');
}
};
const handlePlay = () => {