22 Commits

Author SHA1 Message Date
alger
9f5bac29a0 🚀new: v4.1.0 更新
### 🐛 Bug 修复
- 修复歌词窗口处理逻辑,解决 Windows 系统下桌面歌词窗口拖动问题
- 解决歌词初始化重复播放问题

###  新功能
- 优化移动端和网页端效果和体验
- 增加系统控制的音频服务的上一曲和下一曲功能
- 优化用户数据加载逻辑和错误处理
- 增强语言切换功能
- 首页添加用户歌单推荐
- 优化音频监听器初始化和设置保存逻辑

### 🔄 重构
- 将 Vuex 替换为 Pinia 状态管理
- 更新依赖版本
2025-03-23 12:33:13 +08:00
Alger
2fe1f0c04c Merge pull request #83 from algerkong/fix/duplicate-playback
 feat: 增强歌词窗口处理逻辑,修复可能引起的歌词初始化重复播放问题
2025-03-23 00:49:08 +08:00
alger
2a12f57cb2 feat: 增强歌词窗口处理逻辑,修复可能引起的歌词初始化重复播放问题 2025-03-23 00:47:01 +08:00
alger
4c10533a3d 🛠️ lint: 修复格式问题 2025-03-23 00:33:49 +08:00
alger
cda440b01a 🛠️ feat: 移除不必要的字体设置 2025-03-23 00:28:42 +08:00
alger
7b9e23743b lint: 修复格式问题 2025-03-22 15:01:38 +08:00
alger
e43270f35d lint: 修复格式问题 2025-03-22 14:54:24 +08:00
alger
9431faf932 feat: 优化移动端和网页端效果和体验
feat: #82
2025-03-22 13:45:23 +08:00
Alger
be03b5f8fc Merge pull request #82 from algerkong/feat/next-play
 feat: 增加音频服务的上一曲和下一曲功能
2025-03-22 10:40:19 +08:00
alger
8a414d0c25 feat: 增加音频服务的上一曲和下一曲功能 2025-03-22 10:37:57 +08:00
alger
f9fd9afcdd feat: 优化用户数据加载逻辑和错误处理 2025-03-22 10:31:05 +08:00
alger
b114cf4a33 feat: 增强语言切换功能和用户播放列表显示 2025-03-22 10:30:57 +08:00
alger
fa39d4ca55 feat: 优化音频监听器初始化和设置保存逻辑
- 在 App.vue 中引入 initAudioListeners 函数,确保在播放音乐时初始化音频监听器。
- 在 MusicHook.ts 中重构音频监听器的初始化逻辑,增加音频加载的超时处理。
- 在设置页面中实现防抖保存功能,避免频繁更新设置,提高性能和用户体验。

这些更改旨在提升音频播放的稳定性和设置管理的效率。
2025-03-21 00:19:15 +08:00
alger
650e4ff786 🔧 feat: 更新依赖版本 修复类型错误 优化首页推荐样式 2025-03-20 01:07:39 +08:00
alger
e355341596 🦄 refactor: 重构代码将 Vuex替换为 Pinia
集成 Pinia 状态管理
2025-03-19 22:48:28 +08:00
algerkong
4fa5ed0ca6 feat: 更新依赖和配置,增强开发体验
- 在 electron.vite.config.ts 中启用 Vue DevTools 插件
- 更新 package.json 中多个依赖版本,确保兼容性和性能
- 调整 tsconfig.node.json 的配置,优化模块解析
- 删除不再使用的组件 PlaylistType.vue、RecommendAlbum.vue、RecommendSinger.vue 和 RecommendSonglist.vue
- 在请求处理逻辑中改进错误日志输出,使用 console.error 替代 console.log
- 在首页视图中替换推荐歌手组件为顶部横幅组件

这些更改旨在提升开发效率和用户体验,确保项目的稳定性和可维护性。
2025-03-19 21:25:32 +08:00
alger
df9a1370c3 🐞 fix: 添加文件名清理功能以处理非法字符
- 新增 sanitizeFilename 函数,清理文件名中的非法字符
- 在下载音乐功能中应用清理后的文件名,确保文件名有效性

fixed: #78
2025-03-14 21:19:23 +08:00
alger
6a8813531f 🐞 fix: 修复歌词窗口拖动变大问题和多屏幕支持,优化字体样式
fixed: #77
2025-03-11 23:28:04 +08:00
alger
e5e45148c3 feat: 优化标题栏交互和下载按钮
- 为非 Electron 环境添加下载桌面版按钮
- 调整标题栏按钮显示逻辑,支持 Web 和桌面端不同交互
- 新增打开下载页面的方法,增强用户引导体验
2025-03-09 19:34:05 +08:00
alger
4a66796747 🚀 chore: 优化 GitHub Actions Web 部署工作 2025-03-09 12:13:19 +08:00
alger
7f8ab8be7c 🔒 chore: 添加 GitHub 部署密钥到 .gitignore 2025-03-08 23:52:32 +08:00
alger
ce276df55c feat: 优化赞赏支持 2025-03-08 23:22:56 +08:00
66 changed files with 2697 additions and 1242 deletions

51
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Deploy Web
on:
push:
branches:
- dev_electron # 或者您的主分支名称
workflow_dispatch: # 允许手动触发
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: 创建环境变量文件
run: |
echo "VITE_API=${{ secrets.VITE_API }}" > .env.production.local
echo "VITE_API_MUSIC=${{ secrets.VITE_API_MUSIC }}" >> .env.production.local
# 添加其他需要的环境变量
cat .env.production.local # 查看创建的文件内容,调试用
- name: Install Dependencies
run: npm install
- name: Build
run: npm run build
- name: Deploy to Server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
source: "out/renderer/*"
target: ${{ secrets.DEPLOY_PATH }}
strip_components: 2
- name: Execute Remote Commands
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd ${{ secrets.DEPLOY_PATH }}
echo "部署完成于 $(date)"

5
.gitignore vendored
View File

@@ -16,9 +16,12 @@ dist.zip
.vscode
bun.lockb
bun.lock
.env.*.local
out
.cursorrules
.cursorrules
.github/deploy_keys

View File

@@ -1,17 +1,19 @@
# 更新日志
## v4.0.0
## v4.1.0
### 🐛 Bug 修复
- 修复歌词窗口处理逻辑, 修复windows系统下桌面歌词窗口拖动问题
- 解决歌词初始化重复播放问题
### ✨ 新功能
- **音效均衡器 (EQ)**: 新增音效调节功能,支持低音、高音等实时调整,并提供流行、摇滚、古典等多种预设。
- **歌词体验升级**: 优化歌词窗口交互,滚动更流畅,同步更精准。
- **播放状态记忆**: 应用重启后自动恢复上次的播放进度和状态。
- 优化移动端和网页端效果和体验
- 增加系统控制的音频服务的上一曲和下一曲功能
- 优化用户数据加载逻辑和错误处理
- 增强语言切换功能
- 首页添加用户歌单推荐
- 优化音频监听器初始化和设置保存逻辑
### 🔧 功能优化
- **音乐列表**: 优化列表加载速度和响应性能,大量歌曲也能快速加载。
- **随机播放修复**: 修复手动点击"下一首"按钮不随机的问题。
- **下载体验优化**: 改进网页版下载代理,提高下载速度和成功率。
### 🧹 其他改进
- **细节优化**: 改进按钮反馈、音量调节等交互细节,提升整体使用体验。
- 其他已知问题优化
### 🔄 重构
- 将 Vuex 替换为 Pinia 状态管理
- 更新依赖版本

View File

@@ -5,6 +5,7 @@ import AutoImport from 'unplugin-auto-import/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import viteCompression from 'vite-plugin-compression';
import VueDevTools from 'vite-plugin-vue-devtools';
export default defineConfig({
main: {
@@ -23,7 +24,7 @@ export default defineConfig({
plugins: [
vue(),
viteCompression(),
// VueDevTools(),
VueDevTools(),
AutoImport({
imports: [
'vue',

View File

@@ -1,6 +1,6 @@
{
"name": "AlgerMusicPlayer",
"version": "4.0.0",
"version": "4.1.0",
"description": "Alger Music Player",
"author": "Alger <algerkc@qq.com>",
"main": "./out/main/index.js",
@@ -52,9 +52,9 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^34.0.0",
"electron": "^35.0.2",
"electron-builder": "^25.1.8",
"electron-vite": "^2.3.0",
"electron-vite": "^3.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
@@ -67,24 +67,23 @@
"lodash": "^4.17.21",
"marked": "^15.0.4",
"naive-ui": "^2.41.0",
"postcss": "^8.4.49",
"pinia": "^3.0.1",
"postcss": "^8.5.3",
"prettier": "^3.3.2",
"remixicon": "^4.2.0",
"sass": "^1.83.4",
"remixicon": "^4.6.0",
"sass": "^1.86.0",
"tailwindcss": "^3.4.17",
"tinycolor2": "^1.6.0",
"tunajs": "^1.0.15",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vfonts": "^0.1.0",
"vite": "^5.3.1",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.4.0",
"vite-plugin-vue-devtools": "7.7.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tsc": "^2.0.22",
"vuex": "^4.1.0",
"tunajs": "^1.0.15"
"vue-tsc": "^2.0.22"
},
"build": {
"appId": "com.alger.music",

View File

@@ -52,7 +52,8 @@ export default {
qqGroup: 'QQ group: 789288579',
messages: {
copySuccess: 'Copied to clipboard'
}
},
donateList: 'Buy me a coffee'
},
playlistType: {
title: 'Playlist Category',
@@ -84,6 +85,10 @@ export default {
closeTitle: 'Choose how to close',
minimizeToTray: 'Minimize to Tray',
exitApp: 'Exit App',
rememberChoice: 'Remember my choice'
rememberChoice: 'Remember my choice',
closeApp: 'Close App'
},
userPlayList: {
title: "{name}'s Playlist"
}
};

View File

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

View File

@@ -11,6 +11,7 @@ export default {
mute: 'Mute',
unmute: 'Unmute',
songNum: 'Song Number: {num}',
playFailed: 'Play Failed, Play Next Song',
playMode: {
sequence: 'Sequence',
loop: 'Loop',

View File

@@ -51,7 +51,8 @@ export default {
qqGroup: 'QQ群789288579',
messages: {
copySuccess: '已复制到剪贴板'
}
},
donateList: '请我喝咖啡'
},
playlistType: {
title: '歌单分类',
@@ -83,6 +84,10 @@ export default {
closeTitle: '请选择关闭方式',
minimizeToTray: '最小化到托盘',
exitApp: '退出应用',
rememberChoice: '记住我的选择'
rememberChoice: '记住我的选择',
closeApp: '关闭应用'
},
userPlayList: {
title: '{name}的常听'
}
};

View File

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

View File

@@ -11,6 +11,7 @@ export default {
mute: '静音',
unmute: '取消静音',
songNum: '歌曲总数:{num}',
playFailed: '当前歌曲播放失败,播放下一首',
playMode: {
sequence: '顺序播放',
loop: '循环播放',

View File

@@ -106,6 +106,7 @@ if (!isSingleInstance) {
// 监听语言切换
ipcMain.on('change-language', (_, locale: Language) => {
console.log('locale',locale)
// 更新主进程的语言设置
i18n.global.locale = locale;
// 更新托盘菜单

View File

@@ -5,6 +5,12 @@ import path, { join } from 'path';
const store = new Store();
let lyricWindow: BrowserWindow | null = null;
// 跟踪拖动状态
let isDragging = false;
// 添加窗口大小变化防护
let originalSize = { width: 0, height: 0 };
const createWin = () => {
console.log('Creating lyric window');
@@ -15,32 +21,73 @@ const createWin = () => {
y?: number;
width?: number;
height?: number;
displayId?: number;
}) || {};
const { x, y, width, height } = windowBounds;
// 获取屏幕尺寸
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
const { x, y, width, height, displayId } = windowBounds;
// 验证保存的位置是否有效
const validPosition =
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
// 获取所有屏幕的信息
const displays = screen.getAllDisplays();
let isValidPosition = false;
let targetDisplay = displays[0]; // 默认使用主显示器
// 如果有显示器ID尝试按ID匹配
if (displayId) {
const matchedDisplay = displays.find((d) => d.id === displayId);
if (matchedDisplay) {
targetDisplay = matchedDisplay;
console.log('Found matching display by ID:', displayId);
}
}
// 验证位置是否在任何显示器的范围内
if (x !== undefined && y !== undefined) {
for (const display of displays) {
const { bounds } = display;
if (
x >= bounds.x - 50 && // 允许一点偏移,避免卡在边缘
x < bounds.x + bounds.width + 50 &&
y >= bounds.y - 50 &&
y < bounds.y + bounds.height + 50
) {
isValidPosition = true;
targetDisplay = display;
break;
}
}
}
// 确保宽高合理
const defaultWidth = 800;
const defaultHeight = 200;
const validWidth = width && width > 0 ? width : defaultWidth;
const validHeight = height && height > 0 ? height : defaultHeight;
const maxWidth = 1600; // 设置最大宽度限制
const maxHeight = 800; // 设置最大高度限制
const validWidth = width && width > 0 && width <= maxWidth ? width : defaultWidth;
const validHeight = height && height > 0 && height <= maxHeight ? height : defaultHeight;
// 确定窗口位置
let windowX = isValidPosition ? x : undefined;
let windowY = isValidPosition ? y : undefined;
// 如果位置无效,默认在当前显示器中居中
if (windowX === undefined || windowY === undefined) {
windowX = targetDisplay.bounds.x + (targetDisplay.bounds.width - validWidth) / 2;
windowY = targetDisplay.bounds.y + (targetDisplay.bounds.height - validHeight) / 2;
}
lyricWindow = new BrowserWindow({
width: validWidth,
height: validHeight,
x: validPosition ? x : undefined,
y: validPosition ? y : undefined,
x: windowX,
y: windowY,
frame: false,
show: false,
transparent: true,
hasShadow: false,
alwaysOnTop: true,
resizable: true,
// 添加跨屏幕支持选项
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -58,6 +105,9 @@ const createWin = () => {
// 监听窗口大小变化事件,保存新的尺寸
lyricWindow.on('resize', () => {
// 如果正在拖动,忽略大小调整事件
if (isDragging) return;
if (lyricWindow && !lyricWindow.isDestroyed()) {
const [width, height] = lyricWindow.getSize();
const [x, y] = lyricWindow.getPosition();
@@ -154,28 +204,75 @@ export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void
}
});
// 开始拖动时设置标志
ipcMain.on('lyric-drag-start', () => {
isDragging = true;
if (lyricWindow && !lyricWindow.isDestroyed()) {
// 记录原始窗口大小
const [width, height] = lyricWindow.getSize();
originalSize = { width, height };
// 在拖动时暂时禁用大小调整
lyricWindow.setResizable(false);
}
});
// 结束拖动时清除标志
ipcMain.on('lyric-drag-end', () => {
isDragging = false;
if (lyricWindow && !lyricWindow.isDestroyed()) {
// 确保窗口大小恢复原样
lyricWindow.setSize(originalSize.width, originalSize.height);
// 拖动结束后恢复可调整大小
lyricWindow.setResizable(true);
}
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
if (!lyricWindow || lyricWindow.isDestroyed() || !isDragging) return;
const [currentX, currentY] = lyricWindow.getPosition();
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
const [windowWidth, windowHeight] = lyricWindow.getSize();
// 计算新位置,确保窗口不会移出屏幕
const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth));
const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight));
// 使用记录的原始大小,而不是当前大小
const windowWidth = originalSize.width;
const windowHeight = originalSize.height;
lyricWindow.setPosition(newX, newY);
// 计算新位置
const newX = currentX + deltaX;
const newY = currentY + deltaY;
// 保存新位置但只保存位置信息不使用getBounds()避免在Windows下引起尺寸变化
const bounds = {
x: newX,
y: newY,
width: windowWidth, // 使用当前保存的宽度
height: windowHeight // 使用当前保存的高度
};
store.set('lyricWindowBounds', bounds);
try {
// 获取当前鼠标所在的显示器
const mousePoint = screen.getCursorScreenPoint();
const currentDisplay = screen.getDisplayNearestPoint(mousePoint);
// 拖动期间使用setBounds确保大小不变使用false避免动画卡顿
lyricWindow.setBounds(
{
x: newX,
y: newY,
width: windowWidth,
height: windowHeight
},
false
);
// 更新存储的位置
const windowBounds = {
x: newX,
y: newY,
width: windowWidth,
height: windowHeight,
displayId: currentDisplay.id // 记录当前显示器ID有助于多屏幕处理
};
store.set('lyricWindowBounds', windowBounds);
} catch (error) {
console.error('Error during window drag:', error);
// 出错时尝试使用更简单的方法
lyricWindow.setPosition(newX, newY);
}
});
// 添加鼠标穿透事件处理

View File

@@ -257,6 +257,17 @@ async function processDownloadQueue(event: Electron.IpcMainEvent) {
}
}
/**
* 清理文件名中的非法字符
*/
function sanitizeFilename(filename: string): string {
// 替换 Windows 和 Unix 系统中的非法字符
return filename
.replace(/[<>:"/\\|?*]/g, '_') // 替换特殊字符为下划线
.replace(/\s+/g, ' ') // 将多个空格替换为单个空格
.trim(); // 移除首尾空格
}
/**
* 下载音乐功能
*/
@@ -276,9 +287,12 @@ async function downloadMusic(
const store = new Store();
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
// 清理文件名中的非法字符
const sanitizedFilename = sanitizeFilename(filename);
// 从URL中获取文件扩展名如果没有则使用传入的type或默认mp3
const urlExt = type ? `.${type}` : '.mp3';
const filePath = path.join(downloadPath, `${filename}${urlExt}`);
const filePath = path.join(downloadPath, `${sanitizedFilename}${urlExt}`);
// 检查文件是否已存在,如果存在则添加序号
finalFilePath = filePath;

View File

@@ -35,9 +35,12 @@ export function updateTrayMenu() {
i18n.global.locale = value;
// 更新托盘菜单
updateTrayMenu();
// 通知渲染进程
const win = BrowserWindow.getAllWindows()[0];
win?.webContents.send('set-language', value);
// 直接通知主窗口
const windows = BrowserWindow.getAllWindows();
for (const win of windows) {
win.webContents.send('language-changed', value);
console.log('向窗口发送语言变更事件:', value);
}
}
}))
},

View File

@@ -18,6 +18,7 @@ declare global {
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
removeDownloadListeners: () => void;
onLanguageChanged: (callback: (locale: string) => void) => void;
invoke: (channel: string, ...args: any[]) => Promise<any>;
};
$message: any;

View File

@@ -24,6 +24,14 @@ const api = {
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => {
ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath));
},
// 语言相关
onLanguageChanged: (callback: (locale: string) => void) => {
console.log('注册语言变更监听器');
ipcRenderer.on('language-changed', (_event, locale) => {
console.log('收到语言变更事件:', locale);
callback(locale);
});
},
removeDownloadListeners: () => {
ipcRenderer.removeAllListeners('download-progress');
ipcRenderer.removeAllListeners('download-complete');

View File

@@ -12,90 +12,84 @@
<script setup lang="ts">
import { darkTheme, lightTheme } from 'naive-ui';
import { computed, onMounted, onUnmounted, watch } from 'vue';
import { computed, nextTick, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import homeRouter from '@/router/home';
import globalStore from '@/store';
import { isElectron } from '@/utils';
import { useMenuStore } from '@/store/modules/menu';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { isElectron, isLyricWindow } from '@/utils';
import { initAudioListeners } from './hooks/MusicHook';
import { isMobile } from './utils';
const { locale } = useI18n();
const settingsStore = useSettingsStore();
const menuStore = useMenuStore();
const playerStore = usePlayerStore();
const savedLanguage = isElectron
? window.electron.ipcRenderer.sendSync('get-store-value', 'set.language')
: JSON.parse(localStorage.getItem('appSettings') || '{}').language || 'zh-CN';
if (savedLanguage) {
locale.value = savedLanguage;
}
const theme = computed(() => {
return globalStore.state.theme;
});
// 监听字体变化并应用
// 监听语言变化
watch(
() => [globalStore.state.setData.fontFamily, globalStore.state.setData.fontScope],
([newFont, fontScope]) => {
const appElement = document.body;
if (!appElement) return;
const defaultFonts =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
// 只有在全局模式下才应用字体
if (fontScope !== 'global') {
appElement.style.fontFamily = defaultFonts;
return;
}
if (newFont === 'system-ui') {
appElement.style.fontFamily = defaultFonts;
} else {
// 处理多个字体,确保每个字体名都被正确引用
const fontList = newFont.split(',').map((font) => {
const trimmedFont = font.trim();
// 如果字体名包含空格或特殊字符,添加引号(如果还没有引号的话)
return /[\s'"()]/.test(trimmedFont) && !/^['"].*['"]$/.test(trimmedFont)
? `"${trimmedFont}"`
: trimmedFont;
});
// 将选择的字体和默认字体组合
appElement.style.fontFamily = `${fontList.join(', ')}, ${defaultFonts}`;
() => settingsStore.setData.language,
(newLanguage) => {
if (newLanguage && newLanguage !== locale.value) {
locale.value = newLanguage;
}
},
{ immediate: true }
);
// 监听来自主进程的语言切换事件
const handleSetLanguage = (_: any, value: string) => {
// 更新 i18n locale
locale.value = value;
// 通过 mutation 更新 store
globalStore.commit('setLanguage', value);
};
onMounted(() => {
globalStore.dispatch('initializeSettings');
globalStore.dispatch('initializeTheme');
globalStore.dispatch('initializeSystemFonts');
globalStore.dispatch('initializePlayState');
if (isMobile.value) {
globalStore.commit(
'setMenus',
homeRouter.filter((item) => item.meta.isMobile)
);
}
if (isElectron) {
window.electron.ipcRenderer.on('set-language', handleSetLanguage);
}
const theme = computed(() => {
return settingsStore.theme;
});
onUnmounted(() => {
if (isElectron) {
window.electron.ipcRenderer.removeListener('set-language', handleSetLanguage);
// 监听字体变化并应用
watch(
() => [settingsStore.setData.fontFamily, settingsStore.setData.fontScope],
([newFont, fontScope]) => {
const appElement = document.body;
if (newFont && fontScope === 'global') {
appElement.style.fontFamily = newFont;
} else {
appElement.style.fontFamily = '';
}
}
);
const handleSetLanguage = (value: string) => {
console.log('应用语言变更:', value);
if (value) {
locale.value = value;
}
};
if (!isLyricWindow.value) {
settingsStore.initializeSettings();
settingsStore.initializeTheme();
settingsStore.initializeSystemFonts();
if (isMobile.value) {
menuStore.setMenus(homeRouter.filter((item) => item.meta.isMobile));
}
}
handleSetLanguage(settingsStore.setData.language);
if (isElectron) {
window.api.onLanguageChanged(handleSetLanguage);
}
onMounted(async () => {
if (isLyricWindow.value) {
return;
}
// 先初始化播放状态
await playerStore.initializePlayState();
// 如果有正在播放的音乐,则初始化音频监听器
if (playerStore.playMusic && playerStore.playMusic.id) {
// 使用 nextTick 确保 DOM 更新后再初始化
await nextTick();
initAudioListeners();
}
});
</script>

View File

@@ -1,5 +1,5 @@
import { musicDB } from '@/hooks/MusicHook';
import store from '@/store';
import { useSettingsStore, useUserStore } from '@/store';
import type { ILyric } from '@/type/lyric';
import { isElectron } from '@/utils';
import request from '@/utils/request';
@@ -14,14 +14,16 @@ export const getMusicQualityDetail = (id: number) => {
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = async (id: number, isDownloaded: boolean = false) => {
const userStore = useUserStore();
const settingStore = useSettingsStore();
// 判断是否登录
try {
if (store.state.user && isDownloaded && store.state.user.vipType !== 0) {
if (userStore.user && isDownloaded && userStore.user.vipType !== 0) {
const url = '/song/download/url/v1';
const res = await request.get(url, {
params: {
id,
level: store.state.setData.musicQuality || 'higher',
level: settingStore.setData.musicQuality || 'higher',
cookie: `${localStorage.getItem('token')} os=pc;`
}
});
@@ -37,7 +39,7 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) =>
return await request.get('/song/url/v1', {
params: {
id,
level: store.state.setData.musicQuality || 'higher'
level: settingStore.setData.musicQuality || 'higher'
}
});
};
@@ -91,7 +93,7 @@ export const likeSong = (id: number, like: boolean = true) => {
// 获取用户喜欢的音乐列表
export const getLikedList = (uid: number) => {
return request.get('/likelist', {
params: { uid }
params: { uid, noLogin: true }
});
};

View File

@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
@@ -11,6 +12,8 @@ declare module 'vue' {
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCarousel: typeof import('naive-ui')['NCarousel']
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']

View File

@@ -46,6 +46,15 @@
{{ t('comp.coffee.qqGroup') }}
</p>
</div>
<div class="mt-4">
<!-- 赞赏列表地址 -->
<p
class="text-sm text-green-600 dark:text-gray-200 text-center cursor-pointer hover:text-green-500"
@click="toDonateList"
>
{{ t('comp.coffee.donateList') }}
</p>
</div>
</div>
</n-popover>
</div>
@@ -66,6 +75,10 @@ const copyQQ = () => {
message.success('已复制到剪贴板');
};
const toDonateList = () => {
window.open('http://donate.alger.fun', '_blank');
};
defineProps({
alipayQR: {
type: String,

View File

@@ -1,11 +1,10 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { isElectron } from '@/utils';
import { useSettingsStore } from '@/store/modules/settings';
const store = useStore();
const settingsStore = useSettingsStore();
const { locale } = useI18n();
const languages = [
@@ -13,40 +12,12 @@ const languages = [
{ label: 'English', value: 'en-US' }
];
console.log('locale', locale);
// 使用计算属性来获取当前语言
const currentLanguage = computed({
get: () => store.state.setData.language || 'zh-CN',
set: (value: string) => {
handleLanguageChange(value);
}
});
// 当语言改变时的处理函数
const handleLanguageChange = (value: string) => {
// 更新 i18n locale
locale.value = value;
// 通过 mutation 更新 store
store.commit('setLanguage', value);
// 通知主进程语言已更改
if (isElectron) {
window.electron.ipcRenderer.send('change-language', value);
}
};
// 监听来自主进程的语言切换事件
const handleSetLanguage = (_: any, value: string) => {
handleLanguageChange(value);
};
onMounted(() => {
if (isElectron) {
window.electron.ipcRenderer.on('set-language', handleSetLanguage);
}
});
onUnmounted(() => {
if (isElectron) {
window.electron.ipcRenderer.removeListener('set-language', handleSetLanguage);
get: () => locale.value,
set: (value) => {
settingsStore.setLanguage(value);
}
});
</script>

View File

@@ -87,16 +87,16 @@
<script setup lang="ts">
import { computed, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore } from '@/store/modules/player';
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
import PlayBottom from './common/PlayBottom.vue';
const { t } = useI18n();
const store = useStore();
const playerStore = usePlayerStore();
const props = withDefaults(
defineProps<{
@@ -236,13 +236,13 @@ const loadFullPlaylist = async () => {
// 处理播放
const handlePlay = async () => {
// 先使用当前已加载的歌曲开始播放
store.commit('setPlayList', displayedSongs.value.map(formatSong));
playerStore.setPlayList(displayedSongs.value.map(formatSong));
// 在后台加载完整播放列表
loadFullPlaylist().then(() => {
// 加载完成后,更新播放列表为完整列表
if (completePlaylist.value.length > 0) {
store.commit('setPlayList', completePlaylist.value.map(formatSong));
playerStore.setPlayList(completePlaylist.value.map(formatSong));
}
});
};
@@ -439,6 +439,7 @@ onUnmounted(() => {
.music-content {
@apply flex-col;
width: 100vw !important;
}
.music-info {

View File

@@ -191,9 +191,9 @@
import { NButton, NIcon, NSlider, NTooltip } from 'naive-ui';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getMvUrl } from '@/api/mv';
import { usePlayerStore } from '@/store/modules/player';
import { IMvItem } from '@/type/mv';
const { t } = useI18n();
@@ -222,7 +222,7 @@ const emit = defineEmits<{
(e: 'prev', loading: (value: boolean) => void): void;
}>();
const store = useStore();
const playerStore = usePlayerStore();
const mvUrl = ref<string>();
const playMode = ref<PlayMode>(PLAY_MODE.Auto);
@@ -359,8 +359,8 @@ const loadMvUrl = async (mv: IMvItem) => {
const handleClose = () => {
emit('update:show', false);
if (store.state.playMusicUrl) {
store.commit('setIsPlay', true);
if (playerStore.playMusicUrl) {
playerStore.setIsPlay(true);
}
};
@@ -543,7 +543,7 @@ watch(showControls, (newValue) => {
}
});
const isMobile = computed(() => store.state.isMobile);
const isMobile = computed(() => false); // TODO: 从 settings store 获取
</script>
<style scoped lang="scss">

View File

@@ -1,179 +0,0 @@
<template>
<!-- 推荐歌手 -->
<n-scrollbar :size="100" :x-scrollable="true">
<div class="recommend-singer">
<div class="recommend-singer-list">
<div
v-if="dayRecommendData"
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(0, 100)"
>
<div
:style="
setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))
"
class="recommend-singer-item-bg"
></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
@click="showMusic = true"
>
<div class="font-bold text-lg">
{{ t('comp.recommendSinger.title') }}
</div>
<div class="mt-2">
<p
v-for="item in dayRecommendData?.dailySongs.slice(0, 5)"
:key="item.id"
class="text-el"
>
{{ item.name }}
<br />
</p>
</div>
</div>
</div>
<div
v-for="(item, index) in hotSingerData?.artists"
:key="item.id"
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(index + 1, 100)"
>
<div
:style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))"
class="recommend-singer-item-bg"
></div>
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">
{{ t('common.songCount', { count: item.musicSize }) }}
</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<music-list
v-if="dayRecommendData?.dailySongs.length"
v-model:show="showMusic"
:name="t('comp.recommendSinger.songlist')"
:song-list="dayRecommendData?.dailySongs"
:cover="false"
/>
</div>
</n-scrollbar>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getDayRecommend, getHotSinger } from '@/api/home';
import MusicList from '@/components/MusicList.vue';
import router from '@/router';
import { IDayRecommend } from '@/type/day_recommend';
import type { IHotSinger } from '@/type/singer';
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
const store = useStore();
const { t } = useI18n();
// 歌手信息
const hotSingerData = ref<IHotSinger>();
const dayRecommendData = ref<IDayRecommend>();
const showMusic = ref(false);
onMounted(async () => {
await loadData();
});
const loadData = async () => {
try {
// 第一个请求:获取热门歌手
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
// 第二个请求:获取每日推荐
try {
const {
data: { data: dayRecommend }
} = await getDayRecommend();
// 处理数据
if (dayRecommend) {
singerData.artists = singerData.artists.slice(0, 4);
}
dayRecommendData.value = dayRecommend as unknown as IDayRecommend;
} catch (error) {
console.error('error', error);
}
hotSingerData.value = singerData;
} catch (error) {
console.error('error', error);
}
};
const toSearchSinger = (keyword: string) => {
router.push({
path: '/search',
query: {
keyword
}
});
};
// 监听登录状态
watchEffect(() => {
if (store.state.user) {
loadData();
}
});
</script>
<style lang="scss" scoped>
.recommend-singer {
&-list {
@apply flex;
height: 280px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between overflow-hidden;
&-bg {
@apply bg-gray-900 dark:bg-gray-800 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(60%);
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer text-white;
}
&-name {
@apply text-gray-100 dark:text-gray-100;
}
}
&-count {
@apply text-gray-100 dark:text-gray-100;
}
}
}
.mobile .recommend-singer {
&-list {
height: 180px;
@apply ml-4;
}
&-item {
@apply p-4 rounded-xl;
&-bg {
@apply rounded-xl;
}
}
}
</style>

View File

@@ -90,14 +90,14 @@
<script setup lang="ts">
import { useDateFormat } from '@vueuse/core';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getArtistAlbums, getArtistDetail, getArtistTopSongs } from '@/api/artist';
import { getMusicDetail } from '@/api/music';
import SearchItem from '@/components/common/SearchItem.vue';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore, useSettingsStore } from '@/store';
import { IArtist } from '@/type/artist';
import { getImgUrl } from '@/utils';
@@ -105,11 +105,17 @@ import PlayBottom from './PlayBottom.vue';
const { t } = useI18n();
const settingsStore = useSettingsStore();
const playerStore = usePlayerStore();
const currentArtistId = computed({
get: () => settingsStore.currentArtistId,
set: (val) => settingsStore.setCurrentArtistId(val as number)
});
const modelValue = defineModel<boolean>('show', { required: true });
const store = useStore();
const activeTab = ref('songs');
const currentArtistId = ref<number>();
// 歌手信息
const artistInfo = ref<IArtist>();
@@ -134,15 +140,18 @@ const albumPage = ref({
});
watch(modelValue, (newVal) => {
store.commit('setShowArtistDrawer', newVal);
settingsStore.setShowArtistDrawer(newVal);
});
const loading = ref(false);
// 加载歌手信息
const previousArtistId = ref<number>();
const loadArtistInfo = async (id: number) => {
if (currentArtistId.value === id) return;
// if (currentArtistId.value === id) return;
if (previousArtistId.value === id) return;
activeTab.value = 'songs';
loading.value = true;
currentArtistId.value = id;
previousArtistId.value = id;
try {
const info = await getArtistDetail(id);
if (info.data?.data?.artist) {
@@ -260,8 +269,7 @@ const formatPublishTime = (time: number) => {
};
const handlePlay = () => {
store.commit(
'setPlayList',
playerStore.setPlayList(
songs.value.map((item) => ({
...item,
picUrl: item.al.picUrl

View File

@@ -76,12 +76,12 @@
</n-button>
</div>
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
<div class="p-6 rounded-lg shadow-lg">
<div class="description text-center text-sm text-gray-700 dark:text-gray-200">
<p>{{ t('donation.description') }}</p>
<p>{{ t('donation.message') }}</p>
</div>
<div class="flex justify-between">
<div class="flex justify-between mt-6">
<div class="flex flex-col items-center gap-2">
<n-image
:src="alipay"
@@ -91,6 +91,13 @@
/>
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.alipay') }}</span>
</div>
<n-button type="primary" @click="toDonateList">
<template #icon>
<i class="ri-cup-line"></i>
</template>
{{ t('donation.toDonateList') }}
<i class="ri-arrow-right-s-line"></i>
</n-button>
<div class="flex flex-col items-center gap-2">
<n-image
:src="wechat"
@@ -225,6 +232,10 @@ const displayDonors = computed(() => {
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
};
const toDonateList = () => {
window.open('http://donate.alger.fun', '_blank');
};
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="download-drawer-trigger">
<n-badge :value="downloadingCount" :max="99" :show="downloadingCount > 0">
<n-button circle @click="store.commit('setShowDownloadDrawer', true)">
<n-button circle @click="settingsStore.showDownloadDrawer = true">
<template #icon>
<i class="iconfont ri-download-cloud-2-line"></i>
</template>
@@ -179,11 +179,13 @@ import type { ProgressStatus } from 'naive-ui';
import { useMessage } from 'naive-ui';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getMusicDetail } from '@/api/music';
// import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
// import { audioService } from '@/services/audioService';
import { getImgUrl } from '@/utils';
// import { SongResult } from '@/type/music';
const { t } = useI18n();
@@ -208,11 +210,14 @@ interface DownloadedItem {
}
const message = useMessage();
const store = useStore();
// const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const showDrawer = computed({
get: () => store.state.showDownloadDrawer,
set: (val) => store.commit('setShowDownloadDrawer', val)
get: () => settingsStore.showDownloadDrawer,
set: (val) => {
settingsStore.showDownloadDrawer = val;
}
});
const downloadList = ref<DownloadItem[]>([]);
@@ -224,10 +229,6 @@ const downList = computed(() => {
return (downloadedList.value as DownloadedItem[]).reverse();
});
// 获取播放状态
// const play = computed(() => store.state.play as boolean);
// const currentMusic = computed(() => store.state.playMusic);
// 计算下载中的任务数量
const downloadingCount = computed(() => {
return downloadList.value.filter((item) => item.status === 'downloading').length;
@@ -343,50 +344,10 @@ const confirmDelete = async () => {
};
// 播放音乐
// const handlePlayMusic = async (item: DownloadedItem) => {
// // 确保路径正确编码
// const encodedPath = encodeURIComponent(item.path);
// const localUrl = `local://${encodedPath}`;
// const musicInfo = {
// name: item.filename,
// id: item.id,
// url: localUrl,
// playMusicUrl: localUrl,
// picUrl: item.picUrl,
// ar: item.ar || [{ name: '本地音乐' }],
// song: {
// artists: item.ar || [{ name: '本地音乐' }]
// },
// al: {
// picUrl: item.picUrl || '/images/default_cover.png'
// }
// };
// // 如果是当前播放的音乐,则切换播放状态
// if (currentMusic.value?.id === item.id) {
// if (play.value) {
// audioService.getCurrentSound()?.pause();
// store.commit('setPlayMusic', false);
// } else {
// audioService.getCurrentSound()?.play();
// store.commit('setPlayMusic', true);
// }
// return;
// }
// // 播放新的音乐
// store.commit('setPlay', musicInfo);
// store.commit('setPlayMusic', true);
// store.commit('setIsPlay', true);
// store.commit(
// 'setPlayList',
// downloadedList.value.map((item) => ({
// ...item,
// playMusicUrl: `local://${encodeURIComponent(item.path)}`
// }))
// );
// const handlePlay = async (musicInfo: SongResult) => {
// await playerStore.setPlay(musicInfo);
// playerStore.setPlayMusic(true);
// playerStore.setIsPlay(true);
// };
// 获取已下载音乐列表
@@ -522,7 +483,7 @@ onMounted(() => {
});
const handleDrawerClose = () => {
store.commit('setShowDownloadDrawer', false);
settingsStore.showDownloadDrawer = false;
};
</script>

View File

@@ -3,10 +3,13 @@
</template>
<script setup lang="ts">
import { useStore } from 'vuex';
import { computed } from 'vue';
import { usePlayerStore } from '@/store/modules/player';
const playerStore = usePlayerStore();
const isPlay = computed(() => playerStore.playMusicUrl);
const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean);
defineProps({
height: {
type: String,

View File

@@ -110,12 +110,13 @@
import { useMessage } from 'naive-ui';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { createPlaylist, updatePlaylistTracks } from '@/api/music';
import { getUserPlaylist } from '@/api/user';
import { useUserStore } from '@/store';
import { getImgUrl } from '@/utils';
const store = useUserStore();
const { t } = useI18n();
const props = defineProps<{
modelValue: boolean;
@@ -127,7 +128,6 @@ 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({
@@ -151,7 +151,7 @@ const toggleCreateForm = () => {
// 获取用户歌单
const fetchUserPlaylists = async () => {
try {
const { user } = store.state;
const { user } = store;
if (!user?.userId) {
message.error(t('comp.playlistDrawer.loginFirst'));
emit('update:modelValue', false);

View File

@@ -1,5 +1,5 @@
<template>
<div class="search-item" :class="[item.type, shape]" @click="handleClick">
<div class="search-item" :class="[shape, item.type]" @click="handleClick">
<div class="search-item-img">
<n-image
class="w-full h-full"
@@ -40,11 +40,10 @@
</template>
<script setup lang="ts">
import { useStore } from 'vuex';
import { getAlbum, getListDetail } from '@/api/list';
import MvPlayer from '@/components/MvPlayer.vue';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { IMvItem } from '@/type/mv';
import { getImgUrl } from '@/utils';
@@ -72,6 +71,8 @@ const songList = ref<any[]>([]);
const showPop = ref(false);
const listInfo = ref<any>(null);
const playerStore = usePlayerStore();
const getCurrentMv = () => {
return {
id: props.item.id,
@@ -79,8 +80,6 @@ const getCurrentMv = () => {
} as unknown as IMvItem;
};
const store = useStore();
const handleClick = async () => {
listInfo.value = null;
if (props.item.type === '专辑') {
@@ -108,12 +107,16 @@ const handleClick = async () => {
}
if (props.item.type === 'mv') {
store.commit('setIsPlay', false);
store.commit('setPlayMusic', false);
audioService.getCurrentSound()?.pause();
showPop.value = true;
handleShowMv();
}
};
const handleShowMv = async () => {
playerStore.setIsPlay(false);
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
showPop.value = true;
};
</script>
<style scoped lang="scss">
@@ -168,15 +171,15 @@ const handleClick = async () => {
}
}
.mv {
.search-item.mv {
&:hover {
.play {
@apply opacity-60;
}
}
.search-item-img {
width: 160px;
height: 90px;
width: 160px !important;
height: 90px !important;
@apply rounded-lg relative;
}
.play {

View File

@@ -90,10 +90,10 @@ import type { MenuOption } from 'naive-ui';
import { NImage, NText, useMessage } from 'naive-ui';
import { computed, h, inject, ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getSongUrl } from '@/hooks/MusicListHook';
import { audioService } from '@/services/audioService';
import { usePlayerStore, useSettingsStore } from '@/store';
import type { SongResult } from '@/type/music';
import { getImgUrl, isElectron } from '@/utils';
import { getImageBackground } from '@/utils/linearColor';
@@ -120,11 +120,13 @@ const props = withDefaults(
}
);
const store = useStore();
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const message = useMessage();
const play = computed(() => store.state.play as boolean);
const playMusic = computed(() => store.state.playMusic);
const play = computed(() => playerStore.isPlay);
const playMusic = computed(() => playerStore.playMusic);
const playLoading = computed(
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
);
@@ -356,31 +358,31 @@ const imageLoad = async () => {
const playMusicEvent = async (item: SongResult) => {
if (playMusic.value.id === item.id) {
if (play.value) {
store.commit('setPlayMusic', false);
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
playerStore.setPlayMusic(true);
audioService.getCurrentSound()?.play();
}
return;
}
await store.commit('setPlay', item);
store.commit('setIsPlay', true);
await playerStore.setPlay(item);
playerStore.isPlay = true;
emits('play', item);
};
// 判断是否已收藏
const isFavorite = computed(() => {
return store.state.favoriteList.includes(props.item.id);
return playerStore.favoriteList.includes(props.item.id);
});
// 切换收藏状态
const toggleFavorite = async (e: Event) => {
e.stopPropagation();
if (isFavorite.value) {
store.commit('removeFromFavorite', props.item.id);
playerStore.removeFromFavorite(props.item.id);
} else {
store.commit('addToFavorite', props.item.id);
playerStore.addToFavorite(props.item.id);
}
};
@@ -390,7 +392,8 @@ const toggleSelect = () => {
};
const handleArtistClick = (id: number) => {
store.commit('setCurrentArtistId', id);
settingsStore.setCurrentArtistId(id);
settingsStore.setShowArtistDrawer(true);
};
// 获取歌手列表最多显示5个
@@ -400,7 +403,7 @@ const artists = computed(() => {
// 添加到下一首播放
const handlePlayNext = () => {
store.commit('addToNextPlay', props.item);
playerStore.addToNextPlay(props.item);
message.success(t('songItem.message.addedToNextPlay'));
};
</script>

View File

@@ -71,10 +71,10 @@
<script setup lang="ts">
import { marked } from 'marked';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { useSettingsStore } from '@/store/modules/settings';
import { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
@@ -87,7 +87,13 @@ marked.setOptions({
gfm: true // 启用 GitHub 风格的 Markdown
});
const showModal = ref(false);
const settingsStore = useSettingsStore();
const showModal = computed({
get: () => settingsStore.showUpdateModal,
set: (val) => settingsStore.setShowUpdateModal(val)
});
const updateInfo = ref<UpdateResult>({
hasUpdate: false,
latestVersion: '',
@@ -95,28 +101,6 @@ const updateInfo = ref<UpdateResult>({
releaseInfo: null
});
const store = useStore();
// 添加计算属性
const showUpdateModalState = computed({
get: () => store.state.showUpdateModal,
set: (val) => store.commit('setShowUpdateModal', val)
});
// 替换原来的 watch
watch(showUpdateModalState, (newVal) => {
if (newVal) {
showModal.value = true;
}
});
watch(
() => showModal.value,
(newVal) => {
showUpdateModalState.value = newVal;
}
);
// 解析 Markdown
const parsedReleaseNotes = computed(() => {
if (!updateInfo.value.releaseInfo?.body) return '';

View File

@@ -24,16 +24,15 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getRecommendMusic } from '@/api/home';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore } from '@/store/modules/player';
import type { IRecommendMusic } from '@/type/music';
import { setAnimationClass, setAnimationDelay } from '@/utils';
import SongItem from './common/SongItem.vue';
const { t } = useI18n();
const store = useStore();
const playerStore = usePlayerStore();
//
const recommendMusic = ref<IRecommendMusic>();
const loading = ref(false);
@@ -52,7 +51,9 @@ onMounted(() => {
});
const handlePlay = () => {
store.commit('setPlayList', recommendMusic.value?.result);
if (recommendMusic.value?.result) {
playerStore.setPlayList(recommendMusic.value.result);
}
};
</script>

View File

@@ -0,0 +1,464 @@
<template>
<div class="recommend-singer">
<div class="recommend-singer-list">
<n-carousel
slides-per-view="auto"
:show-dots="false"
:space-between="20"
draggable
show-arrow
:autoplay="false"
>
<n-carousel-item
:class="setAnimationClass('animate__backInRight')"
:style="getCarouselItemStyle(0, 100, 6)"
>
<div v-if="dayRecommendData" class="recommend-singer-item relative">
<div
:style="
setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))
"
class="recommend-singer-item-bg"
></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
@click="showMusic = true"
>
<div class="font-bold text-lg">
{{ t('comp.recommendSinger.title') }}
</div>
<div class="mt-2">
<p
v-for="item in dayRecommendData?.dailySongs.slice(0, 5)"
:key="item.id"
class="text-el"
>
{{ item.name }}
<br />
</p>
</div>
</div>
</div>
</n-carousel-item>
<n-carousel-item
v-if="userStore.user && userPlaylist.length"
:class="setAnimationClass('animate__backInRight')"
:style="getCarouselItemStyleForPlaylist(userPlaylist.length)"
>
<div class="user-play">
<div class="user-play-title mb-3">
{{ t('comp.userPlayList.title', { name: userStore.user?.nickname }) }}
</div>
<div class="user-play-list" :class="getPlaylistGridClass(userPlaylist.length)">
<div
v-for="item in userPlaylist"
:key="item.id"
class="user-play-item"
@click="toPlaylist(item.id)"
>
<div class="user-play-item-img">
<img :src="getImgUrl(item.coverImgUrl, '200y200')" alt="" />
<div class="user-play-item-overlay">
<div class="user-play-item-play-btn">
<i class="iconfont icon-playfill text-3xl text-white"></i>
</div>
</div>
<div class="user-play-item-title">
<div class="user-play-item-title-name">{{ item.name }}</div>
</div>
<div class="user-play-item-count">
<div class="user-play-item-count-tag">
{{ t('common.songCount', { count: item.trackCount }) }}
</div>
</div>
</div>
</div>
</div>
</div>
</n-carousel-item>
<n-carousel-item
v-for="(item, index) in hotSingerData?.artists"
:key="item.id"
:class="setAnimationClass('animate__backInRight')"
:style="getCarouselItemStyle(index + 1, 100, 6)"
>
<div
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(index + 2, 100)"
@click="handleOpenSinger(item.id)"
>
<div
:style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))"
class="recommend-singer-item-bg"
></div>
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">
{{ t('common.songCount', { count: item.musicSize }) }}
</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-name text-el text-right line-clamp-1">
{{ item.name }}
</div>
</div>
<!-- 播放按钮(hover时显示) -->
<div class="recommend-singer-item-play-overlay" @click.stop="handleOpenSinger(item.id)">
<div class="recommend-singer-item-play-btn">
<i class="iconfont icon-playfill text-4xl"></i>
</div>
</div>
</div>
</n-carousel-item>
</n-carousel>
</div>
<music-list
v-if="dayRecommendData?.dailySongs.length"
v-model:show="showMusic"
:name="t('comp.recommendSinger.songlist')"
:song-list="dayRecommendData?.dailySongs"
:cover="false"
/>
<!-- 添加用户歌单弹窗 -->
<music-list
v-model:show="showPlaylist"
v-model:loading="playlistLoading"
:name="playlistItem?.name || ''"
:song-list="playlistDetail?.playlist?.tracks || []"
:list-info="playlistDetail?.playlist"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { getDayRecommend, getHotSinger } from '@/api/home';
import { getListDetail } from '@/api/list';
import { getUserPlaylist } from '@/api/user';
import MusicList from '@/components/MusicList.vue';
import { useSettingsStore, useUserStore } from '@/store';
import { IDayRecommend } from '@/type/day_recommend';
import { Playlist } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import type { IHotSinger } from '@/type/singer';
import {
getImgUrl,
isMobile,
setAnimationClass,
setAnimationDelay,
setBackgroundImg
} from '@/utils';
const userStore = useUserStore();
const { t } = useI18n();
// 歌手信息
const hotSingerData = ref<IHotSinger>();
const dayRecommendData = ref<IDayRecommend>();
const showMusic = ref(false);
const userPlaylist = ref<Playlist[]>([]);
// 为歌单弹窗添加的状态
const showPlaylist = ref(false);
const playlistLoading = ref(false);
const playlistItem = ref<Playlist | null>(null);
const playlistDetail = ref<IListDetail | null>(null);
/**
* 获取轮播项的样式
* @param index 项目索引(用于动画延迟)
* @param delayStep 动画延迟的步长(毫秒)
* @param totalItems 总共分成几等分默认为5
* @param maxWidth 最大宽度可选单位为px
* @returns 样式字符串
*/
const getCarouselItemStyle = (
index: number,
delayStep: number,
totalItems: number,
maxWidth?: number
) => {
if (isMobile.value) {
return 'width: 30%;';
}
const animationDelay = setAnimationDelay(index, delayStep);
const width = `calc((100% / ${totalItems}) - 16px)`;
const maxWidthStyle = maxWidth ? `max-width: ${maxWidth}px;` : '';
return `${animationDelay}; width: ${width}; ${maxWidthStyle}`;
};
/**
* 根据歌单数量获取轮播项的样式
* @param playlistCount 歌单数量
* @returns 样式字符串
*/
const getCarouselItemStyleForPlaylist = (playlistCount: number) => {
if (isMobile.value) {
return 'width: 100%;';
}
const animationDelay = setAnimationDelay(1, 100);
let width = '';
let maxWidth = '';
switch (playlistCount) {
case 1:
width = 'calc(100% / 4 - 16px)';
maxWidth = 'max-width: 180px;';
break;
case 2:
width = 'calc(100% / 3 - 16px)';
maxWidth = 'max-width: 380px;';
break;
case 3:
width = 'calc(100% / 2 - 16px)';
maxWidth = 'max-width: 520px;';
break;
default:
width = 'calc(100% / 1 - 16px)';
maxWidth = 'max-width: 656px;';
}
return `${animationDelay}; width: ${width}; ${maxWidth}`;
};
onMounted(async () => {
await loadData();
});
const loadData = async () => {
try {
// 第一个请求:获取热门歌手
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
// 第二个请求:获取每日推荐
try {
const {
data: { data: dayRecommend }
} = await getDayRecommend();
dayRecommendData.value = dayRecommend as unknown as IDayRecommend;
} catch (error) {
console.error('error', error);
}
hotSingerData.value = singerData;
if (userStore.user) {
const { data: playlistData } = await getUserPlaylist(userStore.user?.userId);
// 确保最多只显示4个歌单并按播放次数排序
userPlaylist.value = (playlistData.playlist as Playlist[])
.sort((a, b) => b.playCount - a.playCount)
.slice(0, 4);
}
} catch (error) {
console.error('error', error);
}
};
const settingsStore = useSettingsStore();
const handleOpenSinger = (id: number) => {
settingsStore.setCurrentArtistId(id);
settingsStore.setShowArtistDrawer(true);
};
const toPlaylist = async (id: number) => {
playlistLoading.value = true;
playlistItem.value = null;
playlistDetail.value = null;
showPlaylist.value = true;
// 设置当前点击的歌单信息
const selectedPlaylist = userPlaylist.value.find((item) => item.id === id);
if (selectedPlaylist) {
playlistItem.value = selectedPlaylist;
}
try {
// 获取歌单详情
const { data } = await getListDetail(id);
playlistDetail.value = data;
} catch (error) {
console.error('获取歌单详情失败:', error);
} finally {
playlistLoading.value = false;
}
};
// 监听登录状态
watchEffect(() => {
if (userStore.user) {
loadData();
}
});
const getPlaylistGridClass = (length: number) => {
switch (length) {
case 1:
return 'one-column';
case 2:
return 'two-columns';
case 3:
return 'three-columns';
default:
return 'four-columns';
}
};
</script>
<style lang="scss" scoped>
.recommend-singer {
&-list {
@apply flex;
height: 220px;
margin-right: 20px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 flex flex-col justify-between overflow-hidden relative;
cursor: pointer;
transition: transform 0.3s ease;
&:hover {
transform: translateY(-5px);
}
&-bg {
@apply bg-gray-900 dark:bg-gray-800 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(60%);
}
&-info {
@apply flex flex-col p-2;
&-name {
@apply text-gray-100 dark:text-gray-100;
}
}
&-count {
@apply text-gray-100 dark:text-gray-100;
}
&-play {
&-overlay {
@apply absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-black/20 z-20 opacity-0 transition-all duration-300 flex items-center justify-center;
backdrop-filter: blur(1px);
.recommend-singer-item:hover & {
opacity: 1;
}
}
&-btn {
@apply w-20 h-20 bg-transparent flex justify-center items-center text-white;
transform: translateY(50px) scale(0.8);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
.recommend-singer-item:hover & {
transform: translateY(0) scale(1);
}
}
}
}
}
.user-play {
@apply bg-light-300 dark:bg-dark-300 rounded-3xl px-4 py-3 h-full;
backdrop-filter: blur(20px);
&-title {
@apply text-gray-900 dark:text-gray-100 font-bold text-lg line-clamp-1;
}
&-list {
@apply grid gap-3 h-full;
&.one-column {
grid-template-columns: repeat(1, minmax(0, 1fr));
.user-play-item {
max-width: 100%;
}
}
&.two-columns {
grid-template-columns: repeat(2, minmax(0, 1fr));
.user-play-item {
max-width: 100%;
}
}
&.three-columns {
grid-template-columns: repeat(3, minmax(0, 1fr));
.user-play-item {
max-width: 100%;
}
}
&.four-columns {
grid-template-columns: repeat(4, minmax(0, 1fr));
.user-play-item {
max-width: 100%;
}
}
}
&-item {
@apply rounded-2xl overflow-hidden flex flex-col;
height: 176px;
&-img {
@apply relative cursor-pointer transition-all duration-300;
height: 0;
width: 100%;
padding-bottom: 100%; /* 确保宽高比为1:1即正方形 */
border-radius: 12px;
overflow: hidden;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
.user-play-item-overlay {
opacity: 1;
}
}
img {
@apply absolute inset-0 w-full h-full object-cover;
}
}
&-overlay {
@apply absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center opacity-0 transition-opacity duration-300;
}
&-title {
@apply absolute top-0 left-0 right-0 p-2 bg-gradient-to-b from-black/70 to-transparent z-10;
&-name {
@apply text-white font-medium text-sm line-clamp-3;
}
}
&-count {
@apply absolute bottom-2 right-2 z-10;
&-tag {
@apply px-2 py-0.5 text-xs text-white bg-black/50 backdrop-blur-sm rounded-full;
}
}
&-play-btn {
@apply flex items-center justify-center;
transform: scale(0.8);
transition: transform 0.3s ease;
.user-play-item:hover & {
transform: scale(1);
}
}
}
}
.mobile {
.recommend-singer {
&-list {
height: 180px;
@apply ml-4;
}
&-item {
@apply p-2 rounded-xl;
&-bg {
@apply rounded-xl;
}
}
}
}
</style>

View File

@@ -1,15 +1,17 @@
import { createDiscreteApi } from 'naive-ui';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
import i18n from '@/../i18n/renderer';
import useIndexedDB from '@/hooks/IndexDBHook';
import { audioService } from '@/services/audioService';
import store from '@/store';
import pinia, { usePlayerStore } from '@/store';
import type { Artist, ILyricText, SongResult } from '@/type/music';
import { isElectron } from '@/utils';
import { getTextColors } from '@/utils/linearColor';
const windowData = window as any;
const playerStore = usePlayerStore(pinia);
export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
export const nowTime = ref(0); // 当前播放时间
@@ -17,12 +19,12 @@ export const allTime = ref(0); // 总播放时间
export const nowIndex = ref(0); // 当前播放歌词
export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
export const playMusic = computed(() => playerStore.playMusic as SongResult); // 当前播放歌曲
export const sound = ref<Howl | null>(audioService.getCurrentSound());
export const isLyricWindowOpen = ref(false); // 新增状态
export const textColors = ref<any>(getTextColors());
export const artistList = computed(
() => (store.state.playMusic.ar || store.state.playMusic?.song?.artists) as Artist[]
() => (playerStore.playMusic.ar || playerStore.playMusic?.song?.artists) as Artist[]
);
export const musicDB = await useIndexedDB('musicDB', [
@@ -40,11 +42,11 @@ document.onkeyup = (e) => {
switch (e.code) {
case 'Space':
if (store.state.play) {
store.commit('setPlayMusic', false);
if (playerStore.play) {
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
playerStore.setPlayMusic(true);
audioService.getCurrentSound()?.play();
}
break;
@@ -68,7 +70,7 @@ const stopProgressAnimation = () => {
// 全局更新函数
const updateProgress = () => {
if (!store.state.play) {
if (!playerStore.play) {
stopProgressAnimation();
return;
}
@@ -105,11 +107,11 @@ const updateProgress = () => {
// 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入)
if (Math.floor(currentTime) % 2 === 0) {
if (store.state.playMusic && store.state.playMusic.id) {
if (playerStore.playMusic && playerStore.playMusic.id) {
localStorage.setItem(
'playProgress',
JSON.stringify({
songId: store.state.playMusic.id,
songId: playerStore.playMusic.id,
progress: currentTime
})
);
@@ -158,7 +160,7 @@ const initProgressAnimation = () => {
let debounceTimer: any = null;
watch(
() => store.state.play,
() => playerStore.play,
(newIsPlaying) => {
console.log('播放状态变化:', newIsPlaying);
@@ -200,7 +202,7 @@ const initProgressAnimation = () => {
// 监听当前歌词索引变化
watch(nowIndex, () => {
currentLrcProgress.value = 0;
if (store.state.play) {
if (playerStore.play) {
startProgressAnimation();
}
});
@@ -208,7 +210,7 @@ const initProgressAnimation = () => {
// 监听音频对象变化
watch(sound, (newSound) => {
console.log('sound 对象变化:', !!newSound);
if (newSound && store.state.play) {
if (newSound && playerStore.play) {
startProgressAnimation();
}
});
@@ -219,12 +221,12 @@ initProgressAnimation();
// 简化后的 watch 函数,只保留核心逻辑
watch(
() => store.state.playMusicUrl,
() => playerStore.playMusicUrl,
async (newVal) => {
if (newVal && playMusic.value) {
try {
// 保存当前播放状态
const shouldPlay = store.state.play;
const shouldPlay = playerStore.play;
// 检查是否有保存的进度
let initialPosition = 0;
@@ -247,20 +249,20 @@ watch(
setupAudioListeners();
// 确保状态与 localStorage 同步
localStorage.setItem('currentPlayMusic', JSON.stringify(store.state.playMusic));
localStorage.setItem('currentPlayMusic', JSON.stringify(playerStore.playMusic));
localStorage.setItem('currentPlayMusicUrl', newVal);
} catch (error) {
console.error('播放音频失败:', error);
store.commit('setPlayMusic', false);
message.error('当前歌曲播放失败,播放下一首');
store.commit('nextPlay');
// store.commit('setPlayMusic', false);
playerStore.setPlayMusic(false);
message.error(i18n.global.t('player.playFailed'));
}
}
}
);
watch(
() => store.state.playMusic,
() => playerStore.playMusic,
() => {
nextTick(async () => {
console.log('歌曲切换,更新歌词数据');
@@ -322,7 +324,7 @@ const setupAudioListeners = () => {
// 监听播放
audioService.on('play', () => {
store.commit('setPlayMusic', true);
playerStore.setPlayMusic(true);
clearInterval();
interval = window.setInterval(() => {
try {
@@ -371,7 +373,7 @@ const setupAudioListeners = () => {
// 监听暂停
audioService.on('pause', () => {
console.log('音频暂停事件触发');
store.commit('setPlayMusic', false);
playerStore.setPlayMusic(false);
clearInterval();
if (isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
@@ -388,17 +390,17 @@ const setupAudioListeners = () => {
}
// 重新播放当前歌曲
if (store.state.playMusicUrl && playMusic.value) {
const newSound = await audioService.play(store.state.playMusicUrl, playMusic.value);
if (playerStore.playMusicUrl && playMusic.value) {
const newSound = await audioService.play(playerStore.playMusicUrl, playMusic.value);
sound.value = newSound as Howl;
setupAudioListeners();
} else {
console.error('No music URL or playMusic data available');
store.commit('nextPlay');
playerStore.nextPlay();
}
} catch (error) {
console.error('Error replaying song:', error);
store.commit('nextPlay');
playerStore.nextPlay();
}
};
@@ -407,30 +409,38 @@ const setupAudioListeners = () => {
console.log('音频播放结束事件触发');
clearInterval();
if (store.state.playMode === 1) {
if (playerStore.playMode === 1) {
// 单曲循环模式
if (sound.value) {
replayMusic();
}
} else if (store.state.playMode === 2) {
} else if (playerStore.playMode === 2) {
// 随机播放模式
const { playList } = store.state;
if (playList.length <= 1) {
if (playerStore.playList.length <= 1) {
replayMusic();
} else {
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * playList.length);
} while (randomIndex === store.state.playListIndex && playList.length > 1);
store.state.playListIndex = randomIndex;
store.commit('setPlay', playList[randomIndex]);
randomIndex = Math.floor(Math.random() * playerStore.playList.length);
} while (randomIndex === playerStore.playListIndex && playerStore.playList.length > 1);
playerStore.playListIndex = randomIndex;
playerStore.setPlay(playerStore.playList[randomIndex]);
}
} else {
// 列表循环模式
store.commit('nextPlay');
playerStore.nextPlay();
}
});
audioService.on('previoustrack', () => {
playerStore.prevPlay();
});
audioService.on('nexttrack', () => {
playerStore.nextPlay();
});
return clearInterval;
};
@@ -444,11 +454,11 @@ export const pause = () => {
try {
// 保存当前播放进度
const currentTime = currentSound.seek() as number;
if (store.state.playMusic && store.state.playMusic.id) {
if (playerStore.playMusic && playerStore.playMusic.id) {
localStorage.setItem(
'playProgress',
JSON.stringify({
songId: store.state.playMusic.id,
songId: playerStore.playMusic.id,
progress: currentTime
})
);
@@ -572,7 +582,7 @@ export const sendLyricToWin = () => {
nowTime: nowTime.value,
startCurrentTime: lrcTimeArray.value[nowIndex] || 0,
nextTime: lrcTimeArray.value[nowIndex + 1] || 0,
isPlay: store.state.play,
isPlay: playerStore.play,
lrcArray: lrcArray.value,
lrcTimeArray: lrcTimeArray.value,
allTime: allTime.value,
@@ -591,7 +601,7 @@ export const sendLyricToWin = () => {
nowTime: nowTime.value,
startCurrentTime: 0,
nextTime: 0,
isPlay: store.state.play,
isPlay: playerStore.play,
lrcArray: [{ text: '当前歌曲暂无歌词', trText: '' }],
lrcTimeArray: [0],
allTime: allTime.value,
@@ -616,14 +626,14 @@ const startLyricSync = () => {
// 每秒同步一次歌词数据
lyricSyncInterval = setInterval(() => {
if (isElectron && isLyricWindowOpen.value && store.state.play && playMusic.value?.id) {
if (isElectron && isLyricWindowOpen.value && playerStore.play && playMusic.value?.id) {
// 发送当前播放进度的更新
try {
const updateData = {
type: 'update',
nowIndex: getLrcIndex(nowTime.value),
nowTime: nowTime.value,
isPlay: store.state.play
isPlay: playerStore.play
};
window.api.sendLyric(JSON.stringify(updateData));
} catch (error) {
@@ -669,7 +679,7 @@ export const openLyric = () => {
nowTime: nowTime.value,
startCurrentTime: 0,
nextTime: 0,
isPlay: store.state.play,
isPlay: playerStore.play,
lrcArray: [{ text: '加载歌词中...', trText: '' }],
lrcTimeArray: [0],
allTime: allTime.value,
@@ -707,7 +717,7 @@ export const closeLyric = () => {
// 在组件挂载时设置对播放状态的监听
watch(
() => store.state.play,
() => playerStore.play,
(isPlaying) => {
// 如果歌词窗口打开,根据播放状态控制同步
if (isElectron && isLyricWindowOpen.value) {
@@ -735,19 +745,20 @@ if (isElectron) {
windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {
switch (command) {
case 'playpause':
if (store.state.play) {
store.commit('setPlayMusic', false);
if (playerStore.play) {
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
playerStore.setPlayMusic(true);
audioService.getCurrentSound()?.play();
}
break;
case 'prev':
store.commit('prevPlay');
playerStore.prevPlay();
break;
case 'next':
store.commit('nextPlay');
playerStore.nextPlay();
break;
case 'close':
isLyricWindowOpen.value = false; // 确保状态更新
@@ -760,37 +771,74 @@ if (isElectron) {
}
// 在组件挂载时设置监听器
onMounted(() => {
// 初始化音频监听器
setupAudioListeners();
// 监听歌词窗口关闭事件
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();
export const initAudioListeners = async () => {
try {
// 确保有正在播放的音乐
if (!playerStore.playMusic || !playerStore.playMusic.id) {
console.log('没有正在播放的音乐,跳过音频监听器初始化');
return;
}
// 确保有音频实例
const initialSound = audioService.getCurrentSound();
if (!initialSound) {
console.log('没有音频实例,等待音频加载...');
// 等待音频加载完成
await new Promise<void>((resolve) => {
const checkInterval = setInterval(() => {
const sound = audioService.getCurrentSound();
if (sound) {
clearInterval(checkInterval);
resolve();
}
}, 100);
// 设置超时
setTimeout(() => {
clearInterval(checkInterval);
console.log('等待音频加载超时');
resolve();
}, 5000);
});
}
// 初始化音频监听器
setupAudioListeners();
// 监听歌词窗口关闭事件
if (isElectron) {
window.api.onLyricWindowClosed(() => {
isLyricWindowOpen.value = false;
});
}
// 获取最新的音频实例
const finalSound = audioService.getCurrentSound();
if (finalSound) {
// 更新全局 sound 引用
sound.value = finalSound;
// 如果当前处于播放状态,启动进度更新
if (playerStore.play) {
// 如果有保存的播放进度,应用它
if (playerStore.savedPlayProgress !== undefined) {
try {
// 设置音频位置
finalSound.seek(playerStore.savedPlayProgress);
// 同时更新时间显示
nowTime.value = playerStore.savedPlayProgress;
console.log('恢复播放进度:', playerStore.savedPlayProgress);
} catch (e) {
console.error('恢复播放进度失败:', e);
}
}
startProgressAnimation();
}
} else {
console.warn('无法获取音频实例,跳过进度更新初始化');
}
} catch (error) {
console.error('初始化音频监听器失败:', error);
}
});
};

View File

@@ -1,8 +1,8 @@
<template>
<div class="layout-page">
<div id="layout-main" class="layout-main">
<title-bar v-if="isElectron" />
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
<title-bar />
<div class="layout-main-page">
<!-- 侧边菜单栏 -->
<app-menu v-if="!isMobile" class="menu" :menus="menus" />
<div class="main">
@@ -21,18 +21,27 @@
</router-view>
</div>
<play-bottom height="5rem" />
<app-menu v-if="isMobile && !store.state.musicFull" class="menu" :menus="menus" />
<app-menu v-if="isMobile && !playerStore.musicFull" class="menu" :menus="menus" />
</div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-show="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
<play-bar
v-if="!isMobile"
v-show="isPlay"
:style="playerStore.musicFull ? 'bottom: 0;' : ''"
/>
<mobile-play-bar
v-else
v-show="isPlay"
:style="isMobile && playerStore.musicFull ? 'bottom: 0;' : ''"
/>
<!-- 下载管理抽屉 -->
<download-drawer
v-if="
isElectron &&
(store.state.setData?.alwaysShowDownloadButton ||
store.state.showDownloadDrawer ||
store.state.hasDownloadingTasks)
(settingsStore.setData?.alwaysShowDownloadButton ||
settingsStore.showDownloadDrawer ||
settingsStore.setData?.hasDownloadingTasks)
"
/>
</div>
@@ -46,13 +55,15 @@
<script lang="ts" setup>
import { computed, defineAsyncComponent, nextTick, onMounted, provide, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import DownloadDrawer from '@/components/common/DownloadDrawer.vue';
import InstallAppModal from '@/components/common/InstallAppModal.vue';
import PlayBottom from '@/components/common/PlayBottom.vue';
import UpdateModal from '@/components/common/UpdateModal.vue';
import homeRouter from '@/router/home';
import { useMenuStore } from '@/store/modules/menu';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { isElectron, isMobile } from '@/utils';
const keepAliveInclude = computed(() =>
@@ -67,34 +78,38 @@ const keepAliveInclude = computed(() =>
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
const MobilePlayBar = defineAsyncComponent(() => import('./components/MobilePlayBar.vue'));
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();
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const menuStore = useMenuStore();
const isPlay = computed(() => store.state.isPlay as boolean);
const { menus } = store.state;
const isPlay = computed(() => playerStore.playMusic && playerStore.playMusic.id);
const { menus } = menuStore;
const route = useRoute();
onMounted(() => {
store.dispatch('initializeSettings');
store.dispatch('initializeTheme');
settingsStore.initializeSettings();
settingsStore.initializeTheme();
});
const artistDrawerRef = ref<InstanceType<typeof ArtistDrawer>>();
const artistDrawerShow = computed({
get: () => store.state.showArtistDrawer,
set: (val) => store.commit('setShowArtistDrawer', val)
get: () => settingsStore.showArtistDrawer,
set: (val) => settingsStore.setShowArtistDrawer(val)
});
// 监听歌手ID变化
watch(
() => store.state.currentArtistId,
() => settingsStore.currentArtistId,
(newId) => {
if (newId) {
console.log('newId', newId);
artistDrawerShow.value = true;
nextTick(() => {
artistDrawerRef.value?.loadArtistInfo(newId);
@@ -147,7 +162,7 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
.mobile {
.main-content {
height: calc(100vh - 146px);
height: calc(100vh - 154px);
overflow: auto;
display: block;
flex: none;

View File

@@ -127,7 +127,7 @@ const isText = ref(false);
&-item {
&-link {
@apply my-4 w-auto;
@apply my-2 w-auto;
}
}
}

View File

@@ -0,0 +1,505 @@
<template>
<div
class="mobile-play-bar"
:class="[
setAnimationClass('animate__fadeInUp'),
musicFullVisible ? 'play-bar-expanded' : 'play-bar-mini'
]"
:style="{
color: musicFullVisible
? textColors.theme === 'dark'
? '#ffffff'
: '#ffffff'
: settingsStore.theme === 'dark'
? '#ffffff'
: '#000000'
}"
>
<!-- 完整模式 - 在musicFullVisible为true时显示 -->
<template v-if="musicFullVisible">
<!-- 顶部信息区域 -->
<div class="music-info-header">
<div class="music-info-main">
<h1 class="music-title">{{ playMusic.name }}</h1>
<div class="artist-info">
<span class="artist-name">
<span v-for="(artists, artistsindex) in artistList" :key="artistsindex">
{{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}
</span>
</span>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="music-progress-bar">
<span class="current-time">{{ secondToMinute(nowTime) }}</span>
<div class="progress-wrapper">
<n-slider
v-model:value="timeSlider"
:step="1"
:max="allTime"
:min="0"
:tooltip="false"
class="progress-slider"
></n-slider>
</div>
<span class="total-time">{{ secondToMinute(allTime) }}</span>
</div>
<!-- 主控制区 -->
<div class="player-controls">
<div class="control-btn like" @click="toggleFavorite">
<i class="iconfont ri-heart-3-fill" :class="{ 'like-active': isFavorite }"></i>
</div>
<div class="control-btn prev" @click="handlePrev">
<i class="iconfont ri-skip-back-fill"></i>
</div>
<div class="control-btn play-pause" @click="playMusicEvent">
<i class="iconfont" :class="play ? 'ri-pause-fill' : 'ri-play-fill'"></i>
</div>
<div class="control-btn next" @click="handleNext">
<i class="iconfont ri-skip-forward-fill"></i>
</div>
<n-popover
trigger="click"
:z-index="99999999"
content-class="mobile-play-list"
raw
:show-arrow="false"
placement="top"
@update-show="scrollToPlayList"
>
<template #trigger>
<div class="control-btn list">
<i class="iconfont ri-menu-line"></i>
</div>
</template>
<div class="mobile-play-list-container">
<div class="mobile-play-list-back"></div>
<n-virtual-list ref="playListRef" :item-size="56" item-resizable :items="playList">
<template #default="{ item }">
<div class="mobile-play-list-item">
<song-item :key="item.id" :item="item" mini></song-item>
</div>
</template>
</n-virtual-list>
</div>
</n-popover>
</div>
</template>
<!-- Mini模式 - 在musicFullVisible为false时显示 -->
<div v-else class="mobile-mini-controls">
<!-- 歌曲信息 -->
<div class="mini-song-info" @click="setMusicFull">
<n-image
:src="getImgUrl(playMusic?.picUrl, '100y100')"
class="mini-song-cover"
lazy
preview-disabled
/>
<div class="mini-song-text">
<n-ellipsis class="mini-song-title" line-clamp="1">
{{ playMusic.name }}
</n-ellipsis>
<n-ellipsis class="mini-song-artist" line-clamp="1">
<span v-for="(artists, artistsindex) in artistList" :key="artistsindex">
{{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}
</span>
</n-ellipsis>
</div>
</div>
<!-- 播放按钮 -->
<div class="mini-playback-controls">
<div class="mini-control-btn play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<n-popover
trigger="click"
:z-index="99999999"
content-class="mobile-play-list"
raw
:show-arrow="false"
placement="top"
@update-show="scrollToPlayList"
>
<template #trigger>
<i class="iconfont icon-list mini-list-icon"></i>
</template>
<div class="mobile-play-list-container">
<div class="mobile-play-list-back"></div>
<n-virtual-list ref="playListRef" :item-size="56" item-resizable :items="playList">
<template #default="{ item }">
<div class="mobile-play-list-item">
<song-item :key="item.id" :item="item" mini></song-item>
</div>
</template>
</n-virtual-list>
</div>
</n-popover>
</div>
</div>
<!-- 全屏播放器 -->
<music-full ref="MusicFullRef" v-model="musicFullVisible" :background="background" />
</div>
</template>
<script lang="ts" setup>
import { useThrottleFn } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
import SongItem from '@/components/common/SongItem.vue';
import { allTime, artistList, nowTime, playMusic, sound, textColors } from '@/hooks/MusicHook';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
import MusicFull from './MusicFull.vue';
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
// 是否播放
const play = computed(() => playerStore.isPlay);
// 播放列表
const playList = computed(() => playerStore.playList as SongResult[]);
// 背景颜色
const background = ref('#000');
// 播放进度条
const throttledSeek = useThrottleFn((value: number) => {
if (!sound.value) return;
sound.value.seek(value);
nowTime.value = value;
}, 50);
const timeSlider = computed({
get: () => nowTime.value,
set: throttledSeek
});
// 播放控制
function handleNext() {
playerStore.nextPlay();
}
function handlePrev() {
playerStore.prevPlay();
}
// 全屏播放器
const MusicFullRef = ref<any>(null);
const musicFullVisible = ref(false);
// 设置musicFull
const setMusicFull = () => {
musicFullVisible.value = !musicFullVisible.value;
playerStore.setMusicFull(musicFullVisible.value);
if (musicFullVisible.value) {
settingsStore.showArtistDrawer = false;
}
};
// 播放列表引用
const playListRef = ref<any>(null);
const scrollToPlayList = (val: boolean) => {
if (!val) return;
setTimeout(() => {
playListRef.value?.scrollTo({ top: playerStore.playListIndex * 56 });
}, 50);
};
// 收藏功能
const isFavorite = computed(() => {
return playerStore.favoriteList.includes(playMusic.value.id);
});
const toggleFavorite = () => {
console.log('isFavorite.value', isFavorite.value);
if (isFavorite.value) {
playerStore.removeFromFavorite(playMusic.value.id);
} else {
playerStore.addToFavorite(playMusic.value.id);
}
};
// 播放暂停按钮事件
const playMusicEvent = async () => {
try {
if (!playMusic.value?.id || !playerStore.playMusicUrl) {
console.warn('No valid music or URL available');
playerStore.setPlay(playMusic.value);
return;
}
if (play.value) {
if (audioService.getCurrentSound()) {
audioService.pause();
playerStore.setPlayMusic(false);
}
} else {
if (audioService.getCurrentSound()) {
audioService.play();
} else {
await audioService.play(playerStore.playMusicUrl, playMusic.value);
}
playerStore.setPlayMusic(true);
}
} catch (error) {
console.error('播放出错:', error);
playerStore.nextPlay();
}
};
watch(
() => playerStore.playMusic,
async () => {
background.value = playMusic.value.backgroundColor as string;
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.mobile-play-bar {
@apply fixed bottom-[56px] left-0 w-full flex flex-col shadow-lg;
z-index: 10000;
animation-duration: 0.3s !important;
transition: all 0.3s ease;
&.play-bar-expanded {
@apply bg-transparent;
height: auto; /* 自动适应内容高度 */
max-height: 230px; /* 限制最大高度 */
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.5) 20%,
rgba(0, 0, 0, 0.8) 80%,
rgba(0, 0, 0, 0.9) 100%
);
&::before {
content: '';
position: absolute;
top: -50px; /* 延伸到上方 */
left: 0;
right: 0;
bottom: 0;
background-image: v-bind('`url(${getImgUrl(playMusic?.picUrl, "300y300")})`');
background-size: cover;
background-position: center;
filter: blur(20px);
opacity: 0.2;
z-index: -1;
}
}
&.play-bar-mini {
@apply h-14 py-0 bg-light dark:bg-dark;
}
// 顶部信息区域
.music-info-header {
@apply flex justify-between items-start px-6 pt-3 pb-2 relative z-10;
.music-info-main {
@apply flex flex-col;
.music-title {
@apply text-xl font-bold text-white mb-1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.artist-info {
@apply flex items-center;
.artist-name {
@apply text-sm text-white opacity-90;
}
}
}
.action-stats {
@apply flex items-center gap-4;
.like-count,
.comment-count {
@apply flex items-center text-white;
i {
@apply text-base mr-1;
}
span {
@apply text-xs font-medium;
}
}
}
}
// 进度条
.music-progress-bar {
@apply flex items-center justify-between px-4 py-2 relative z-10;
.current-time,
.total-time {
@apply text-xs text-white opacity-80;
}
.progress-wrapper {
@apply flex-1 mx-3 flex flex-col items-center;
.progress-slider {
@apply w-full;
:deep(.n-slider) {
--n-rail-height: 3px;
--n-rail-color: rgba(255, 255, 255, 0.15);
--n-rail-color-dark: rgba(255, 255, 255, 0.15);
--n-fill-color: #1ed760; /* Spotify绿色可调整为其他绿色 */
--n-handle-size: 0px; /* 隐藏滑块 */
--n-handle-color: #1ed760;
&:hover {
--n-handle-size: 10px; /* 鼠标悬停时显示滑块 */
}
.n-slider-rail {
@apply rounded-full !important; /* 圆角进度条 */
}
.n-slider-fill {
@apply rounded-full !important;
box-shadow: 0 0 4px rgba(30, 215, 96, 0.5); /* 发光效果 */
}
.n-slider-handle {
@apply transition-all duration-200;
opacity: 0;
box-shadow: 0 0 4px rgba(255, 255, 255, 0.7);
}
&:hover .n-slider-handle,
&:active .n-slider-handle {
opacity: 1;
}
}
}
.quality-label {
@apply text-xs text-white opacity-70 mt-1;
}
}
}
// 主控制区
.player-controls {
@apply flex items-center justify-between px-8 py-3 relative z-10 pb-8;
.control-btn {
@apply flex items-center justify-center cursor-pointer transition;
i {
@apply text-white transition-all;
}
&.like i {
@apply text-2xl;
}
&.prev i,
&.next i {
@apply text-3xl;
}
&.play-pause {
@apply w-12 h-12 rounded-full flex items-center justify-center;
background: rgba(255, 255, 255, 0.2);
i {
@apply text-4xl;
}
}
&.list i {
@apply text-2xl;
}
.like-active {
@apply text-red-500;
}
}
}
// Mini模式样式
.mobile-mini-controls {
@apply flex items-center justify-between px-4 h-14;
.mini-song-info {
@apply flex items-center flex-1 min-w-0 cursor-pointer;
.mini-song-cover {
@apply w-8 h-8 rounded-md;
}
.mini-song-text {
@apply ml-3 min-w-0 flex-1;
.mini-song-title {
@apply text-sm font-medium;
}
.mini-song-artist {
@apply text-xs text-gray-500 dark:text-gray-400 mt-0.5;
}
}
}
.mini-playback-controls {
@apply flex items-center;
.mini-control-btn {
@apply flex items-center justify-center cursor-pointer transition;
&.play {
@apply w-9 h-9 rounded-full flex items-center justify-center mr-2;
@apply bg-gray-100 dark:bg-gray-800;
.iconfont {
@apply text-xl text-green-500 transition hover:text-green-600;
}
}
}
.mini-list-icon {
@apply text-xl p-1 transition cursor-pointer;
@apply hover:text-green-500;
}
}
}
}
.mobile-play-list-container {
height: 60vh;
width: 90vw;
max-width: 400px;
@apply relative rounded-t-2xl overflow-hidden;
.mobile-play-list-back {
backdrop-filter: blur(20px);
@apply absolute top-0 left-0 w-full h-full;
@apply bg-light dark:bg-black bg-opacity-90;
}
.mobile-play-list-item {
@apply px-3 py-1;
}
}
</style>

View File

@@ -11,7 +11,7 @@
<div
class="control-btn absolute top-8 left-8"
:class="{ 'pure-mode': config.pureModeEnabled }"
@click="isVisible = false"
@click="closeMusicFull"
>
<i class="ri-arrow-down-s-line"></i>
</div>
@@ -70,7 +70,7 @@
class="music-lrc"
:style="{
height: config.hidePlayBar ? '85vh' : '65vh',
width: config.hideCover ? '50vw' : '500px'
width: isMobile ? '100vw' : config.hideCover ? '50vw' : '500px'
}"
:native-scrollbar="false"
@mouseover="mouseOverLayout"
@@ -131,7 +131,6 @@
import { useDebounceFn } from '@vueuse/core';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import LyricSettings from '@/components/lyric/LyricSettings.vue';
import {
@@ -143,6 +142,8 @@ import {
textColors,
useLyricProgress
} from '@/hooks/MusicHook';
import { usePlayerStore } from '@/store';
import { useSettingsStore } from '@/store/modules/settings';
import { getImgUrl, isMobile } from '@/utils';
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
@@ -372,13 +373,14 @@ onBeforeUnmount(() => {
}
});
const store = useStore();
const settingsStore = useSettingsStore();
const handleArtistClick = (id: number) => {
isVisible.value = false;
store.commit('setCurrentArtistId', id);
settingsStore.currentArtistId = id;
};
const setData = computed(() => store.state.setData);
const setData = computed(() => settingsStore.setData);
// 监听字体变化并更新 CSS 变量
watch(
@@ -431,6 +433,13 @@ const handleScroll = () => {
showStickyHeader.value = scrollTop > 100;
};
const playerStore = usePlayerStore();
const closeMusicFull = () => {
isVisible.value = false;
playerStore.setMusicFull(false);
};
// 添加滚动监听
onMounted(() => {
if (lrcSider.value?.$el) {
@@ -647,12 +656,18 @@ defineExpose({
span {
padding-right: 0px !important;
}
}
.music-lrc-text {
@apply text-xl text-center;
.hover-text {
&:hover {
background-color: transparent;
}
}
.music-lrc-text {
@apply text-xl text-center;
}
}
.music-content {
@apply h-[calc(100vh-120px)];
width: 100vw !important;
}
}
}
@@ -663,8 +678,9 @@ defineExpose({
// 添加全局字体样式
:root {
--current-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
--current-font-family:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
}
#drawer-target {
@@ -686,7 +702,7 @@ defineExpose({
}
.control-btn {
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300;
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999];
background: rgba(142, 142, 142, 0.192);
backdrop-filter: blur(12px);

View File

@@ -13,7 +13,7 @@
? textColors.theme === 'dark'
? '#000000'
: '#ffffff'
: store.state.theme === 'dark'
: settingsStore.theme === 'dark'
? '#ffffff'
: '#000000'
}"
@@ -178,7 +178,6 @@
import { useThrottleFn } from '@vueuse/core';
import { computed, ref, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue';
import EqControl from '@/components/EQControl.vue';
@@ -193,23 +192,26 @@ import {
textColors
} from '@/hooks/MusicHook';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import type { SongResult } from '@/type/music';
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
import { showShortcutToast } from '@/utils/shortcutToast';
import MusicFull from './MusicFull.vue';
const store = useStore();
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const { t } = useI18n();
// 是否播放
const play = computed(() => store.state.play as boolean);
const play = computed(() => playerStore.isPlay);
// 播放列表
const playList = computed(() => store.state.playList as SongResult[]);
const playList = computed(() => playerStore.playList as SongResult[]);
// 背景颜色
const background = ref('#000');
watch(
() => store.state.playMusic,
() => playerStore.playMusic,
async () => {
background.value = playMusic.value.backgroundColor as string;
},
@@ -268,7 +270,7 @@ const mute = () => {
};
// 播放模式
const playMode = computed(() => store.state.playMode);
const playMode = computed(() => playerStore.playMode);
const playModeIcon = computed(() => {
switch (playMode.value) {
case 0:
@@ -296,15 +298,15 @@ const playModeText = computed(() => {
// 切换播放模式
const togglePlayMode = () => {
store.commit('togglePlayMode');
playerStore.togglePlayMode();
};
function handleNext() {
store.commit('nextPlay');
playerStore.nextPlay();
}
function handlePrev() {
store.commit('prevPlay');
playerStore.prevPlay();
}
const MusicFullRef = ref<any>(null);
@@ -313,9 +315,9 @@ const MusicFullRef = ref<any>(null);
const playMusicEvent = async () => {
try {
// 检查是否有有效的音乐对象和 URL
if (!playMusic.value?.id || !store.state.playMusicUrl) {
if (!playMusic.value?.id || !playerStore.playMusicUrl) {
console.warn('No valid music or URL available');
store.commit('setPlay', playMusic.value);
playerStore.setPlay(playMusic.value);
return;
}
@@ -323,7 +325,7 @@ const playMusicEvent = async () => {
// 暂停播放
if (audioService.getCurrentSound()) {
audioService.pause();
store.commit('setPlayMusic', false);
playerStore.setPlayMusic(false);
}
} else {
// 开始播放
@@ -332,13 +334,13 @@ const playMusicEvent = async () => {
audioService.play();
} else {
// 如果没有音频实例,重新创建并播放
await audioService.play(store.state.playMusicUrl, playMusic.value);
await audioService.play(playerStore.playMusicUrl, playMusic.value);
}
store.commit('setPlayMusic', true);
playerStore.setPlayMusic(true);
}
} catch (error) {
console.error('播放出错:', error);
store.commit('nextPlay');
playerStore.nextPlay();
}
};
@@ -347,31 +349,31 @@ const musicFullVisible = ref(false);
// 设置musicFull
const setMusicFull = () => {
musicFullVisible.value = !musicFullVisible.value;
store.commit('setMusicFull', musicFullVisible.value);
playerStore.setMusicFull(musicFullVisible.value);
if (musicFullVisible.value) {
store.commit('setShowArtistDrawer', false);
settingsStore.showArtistDrawer = false;
}
};
const palyListRef = useTemplateRef('palyListRef');
const palyListRef = useTemplateRef('palyListRef') as any;
const scrollToPlayList = (val: boolean) => {
if (!val) return;
setTimeout(() => {
palyListRef.value?.scrollTo({ top: store.state.playListIndex * 62 });
palyListRef.value?.scrollTo({ top: playerStore.playListIndex * 62 });
}, 50);
};
const isFavorite = computed(() => {
return store.state.favoriteList.includes(playMusic.value.id);
return playerStore.favoriteList.includes(playMusic.value.id);
});
const toggleFavorite = async (e: Event) => {
e.stopPropagation();
if (isFavorite.value) {
store.commit('removeFromFavorite', playMusic.value.id);
playerStore.removeFromFavorite(playMusic.value.id);
} else {
store.commit('addToFavorite', playMusic.value.id);
playerStore.addToFavorite(playMusic.value.id);
}
};
@@ -381,7 +383,7 @@ const openLyricWindow = () => {
const handleArtistClick = (id: number) => {
musicFullVisible.value = false;
store.commit('setCurrentArtistId', id);
settingsStore.currentArtistId = id;
};
// 添加全局快捷键处理
@@ -392,8 +394,8 @@ if (isElectron) {
case 'togglePlay':
playMusicEvent();
showShortcutToast(
store.state.play ? t('player.playBar.play') : t('player.playBar.pause'),
store.state.play ? 'ri-pause-circle-line' : 'ri-play-circle-line'
play.value ? t('player.playBar.play') : t('player.playBar.pause'),
play.value ? 'ri-pause-circle-line' : 'ri-play-circle-line'
);
break;
case 'prevPlay':
@@ -565,7 +567,7 @@ const isEQVisible = ref(false);
.mobile {
.music-play-bar {
@apply px-4 bottom-[70px] transition-all duration-300;
@apply px-4 bottom-[56px] transition-all duration-300;
}
.music-time {
display: none;

View File

@@ -16,7 +16,7 @@
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
<div class="w-20 px-3 flex justify-between items-center">
<div>
{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}
{{ searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label }}
</div>
<i class="iconfont icon-xiasanjiaoxing"></i>
</div>
@@ -28,11 +28,11 @@
<template #trigger>
<div class="user-box">
<n-avatar
v-if="store.state.user"
v-if="userStore.user"
class="cursor-pointer"
circle
size="medium"
:src="getImgUrl(store.state.user.avatarUrl)"
:src="getImgUrl(userStore.user.avatarUrl)"
@click="selectItem('user')"
/>
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">
@@ -41,16 +41,16 @@
</div>
</template>
<div class="user-popover">
<div v-if="store.state.user" class="user-header" @click="selectItem('user')">
<n-avatar circle size="small" :src="getImgUrl(store.state.user?.avatarUrl)" />
<span class="username">{{ store.state.user?.nickname || 'Theodore' }}</span>
<div v-if="userStore.user" class="user-header" @click="selectItem('user')">
<n-avatar circle size="small" :src="getImgUrl(userStore.user?.avatarUrl)" />
<span class="username">{{ userStore.user?.nickname || 'Theodore' }}</span>
</div>
<div class="menu-items">
<div v-if="!store.state.user" class="menu-item" @click="toLogin">
<div v-if="!userStore.user" class="menu-item" @click="toLogin">
<i class="iconfont ri-login-box-line"></i>
<span>{{ t('comp.searchBar.toLogin') }}</span>
</div>
<div v-if="store.state.user" class="menu-item" @click="selectItem('logout')">
<div v-if="userStore.user" class="menu-item" @click="selectItem('logout')">
<i class="iconfont ri-logout-box-r-line"></i>
<span>{{ t('comp.searchBar.logout') }}</span>
</div>
@@ -60,9 +60,9 @@
<span>{{ t('comp.searchBar.set') }}</span>
</div>
<div class="menu-item">
<i class="iconfont" :class="isDarkTheme ? 'ri-moon-line' : 'ri-sun-line'"></i>
<i class="iconfont" :class="isDark ? 'ri-moon-line' : 'ri-sun-line'"></i>
<span>{{ t('comp.searchBar.theme') }}</span>
<n-switch v-model:value="isDarkTheme" class="ml-auto">
<n-switch v-model:value="isDark" class="ml-auto">
<template #checked>
<i class="ri-moon-line"></i>
</template>
@@ -105,7 +105,6 @@
import { computed, onMounted, ref, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getSearchKeyword } from '@/api/home';
import { getUserDetail } from '@/api/login';
@@ -113,13 +112,18 @@ import alipay from '@/assets/alipay.png';
import wechat from '@/assets/wechat.png';
import Coffee from '@/components/Coffee.vue';
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
import { useSearchStore } from '@/store/modules/search';
import { useSettingsStore } from '@/store/modules/settings';
import { useUserStore } from '@/store/modules/user';
import { getImgUrl } from '@/utils';
import { checkUpdate, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
const router = useRouter();
const store = useStore();
const searchStore = useSearchStore();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const userSetOptions = ref(USER_SET_OPTIONS);
const { t } = useI18n();
@@ -137,15 +141,15 @@ const loadPage = async () => {
if (!token) return;
const { data } = await getUserDetail();
console.log('data', data);
store.state.user =
data.profile || store.state.user || JSON.parse(localStorage.getItem('user') || '{}');
localStorage.setItem('user', JSON.stringify(store.state.user));
userStore.user =
data.profile || userStore.user || JSON.parse(localStorage.getItem('user') || '{}');
localStorage.setItem('user', JSON.stringify(userStore.user));
};
loadPage();
watchEffect(() => {
if (store.state.user) {
if (userStore.user) {
userSetOptions.value = USER_SET_OPTIONS;
} else {
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
@@ -167,9 +171,9 @@ onMounted(() => {
checkForUpdates();
});
const isDarkTheme = computed({
get: () => store.state.theme === 'dark',
set: () => store.commit('toggleTheme')
const isDark = computed({
get: () => settingsStore.theme === 'dark',
set: () => settingsStore.toggleTheme()
});
// 搜索词
@@ -182,7 +186,7 @@ const search = () => {
}
if (router.currentRoute.value.path === '/search') {
store.state.searchValue = value;
searchStore.searchValue = value;
return;
}
@@ -190,13 +194,13 @@ const search = () => {
path: '/search',
query: {
keyword: value,
type: store.state.searchType
type: searchStore.searchType
}
});
};
const selectSearchType = (key: number) => {
store.state.searchType = key;
searchStore.searchType = key;
if (searchValue.value) {
search();
}
@@ -208,7 +212,7 @@ const selectItem = async (key: string) => {
// switch 判断
switch (key) {
case 'logout':
store.commit('logout');
userStore.handleLogout();
break;
case 'login':
router.push('/login');
@@ -227,7 +231,7 @@ const selectItem = async (key: string) => {
};
const toGithub = () => {
window.open('https://github.com/algerkong/AlgerMusicPlayer', '_blank');
window.open('http://donate.alger.fun', '_blank');
};
const updateInfo = ref<UpdateResult>({
@@ -250,7 +254,7 @@ const checkForUpdates = async () => {
const toGithubRelease = () => {
if (updateInfo.value.hasUpdate) {
store.commit('setShowUpdateModal', true);
settingsStore.showUpdateModal = true;
} else {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
}

View File

@@ -2,19 +2,32 @@
<div id="title-bar" @mousedown="drag">
<div id="title">Alger Music</div>
<div id="buttons">
<div class="button" @click="minimize">
<i class="iconfont icon-minisize"></i>
</div>
<div class="button" @click="close">
<i class="iconfont icon-close"></i>
</div>
<n-button
v-if="!isElectron"
type="primary"
size="small"
text
title="下载应用"
@click="openDownloadPage"
>
<i class="ri-download-line"></i>
下载桌面版
</n-button>
<template v-if="isElectron">
<div class="button" @click="minimize">
<i class="iconfont icon-minisize"></i>
</div>
<div class="button" @click="handleClose">
<i class="iconfont icon-close"></i>
</div>
</template>
</div>
</div>
<n-modal
v-model:show="showCloseModal"
preset="dialog"
title="关闭应用"
:title="t('comp.titleBar.closeApp')"
:style="{ width: '400px' }"
:mask-closable="true"
>
@@ -42,16 +55,22 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { useSettingsStore } from '@/store/modules/settings';
import { isElectron } from '@/utils';
const { t } = useI18n();
const store = useStore();
const settingsStore = useSettingsStore();
const showCloseModal = ref(false);
const rememberChoice = ref(false);
const openDownloadPage = () => {
if (!isElectron) {
window.open('http://donate.alger.fun/download', '_blank');
}
};
const minimize = () => {
if (!isElectron) {
return;
@@ -61,38 +80,30 @@ const minimize = () => {
const handleAction = (action: 'minimize' | 'close') => {
if (rememberChoice.value) {
store.commit('setSetData', {
...store.state.setData,
settingsStore.setSetData({
...settingsStore.setData,
closeAction: action
});
}
if (action === 'minimize') {
window.api.miniTray();
window.api.minimize();
} else {
window.api.close();
}
showCloseModal.value = false;
};
const close = () => {
if (!isElectron) {
return;
}
const { closeAction } = store.state.setData;
const handleClose = () => {
const { closeAction } = settingsStore.setData;
if (closeAction === 'minimize') {
window.api.miniTray();
return;
}
if (closeAction === 'close') {
window.api.minimize();
} else if (closeAction === 'close') {
window.api.close();
return;
} else {
showCloseModal.value = true;
}
showCloseModal.value = true;
};
const drag = (event: MouseEvent) => {

View File

@@ -1,6 +1,3 @@
import 'vfonts/Lato.css';
import 'vfonts/FiraCode.css';
// tailwind css
import './index.css';
import 'animate.css';
import 'remixicon/fonts/remixicon.css';
@@ -9,7 +6,7 @@ import { createApp } from 'vue';
import i18n from '@/../i18n/renderer';
import router from '@/router';
import store from '@/store';
import pinia from '@/store';
import App from './App.vue';
import directives from './directive';
@@ -20,7 +17,7 @@ Object.keys(directives).forEach((key: string) => {
app.directive(key, directives[key as keyof typeof directives]);
});
app.use(pinia);
app.use(router);
app.use(store);
app.use(i18n);
app.mount('#app');

View File

@@ -1,418 +1,20 @@
import { createStore } from 'vuex';
import { createPinia } from 'pinia';
import router from '@/router';
import setData from '@/../main/set.json';
import { logout } from '@/api/login';
import { getLikedList, likeSong } from '@/api/music';
import { useMusicListHook } from '@/hooks/MusicListHook';
import homeRouter from '@/router/home';
import type { SongResult } from '@/type/music';
import { isElectron } from '@/utils';
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
// 创建 pinia 实例
const pinia = createPinia();
// 默认设置
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 {
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 {
menus: any[];
play: boolean;
isPlay: boolean;
playMusic: SongResult;
playMusicUrl: string;
user: any;
playList: SongResult[];
playListIndex: number;
setData: typeof defaultSettings;
lyric: any;
isMobile: boolean;
searchValue: string;
searchType: number;
favoriteList: number[];
playMode: number;
theme: ThemeType;
musicFull: boolean;
showUpdateModal: boolean;
showArtistDrawer: boolean;
currentArtistId: number | null;
systemFonts: { label: string; value: string }[];
showDownloadDrawer: boolean;
savedPlayProgress?: number;
}
const state: State = {
menus: homeRouter,
play: false,
isPlay: false,
playMusic: getLocalStorageItem('currentPlayMusic', {} as SongResult),
playMusicUrl: getLocalStorageItem('currentPlayMusicUrl', ''),
user: getLocalStorageItem('user', null),
playList: getLocalStorageItem('playList', []),
playListIndex: getLocalStorageItem('playListIndex', 0),
setData: defaultSettings,
lyric: {},
isMobile: false,
searchValue: '',
searchType: 1,
favoriteList: getLocalStorageItem('favoriteList', []),
playMode: getLocalStorageItem('playMode', 0),
theme: getCurrentTheme(),
musicFull: false,
showUpdateModal: false,
showArtistDrawer: false,
currentArtistId: null,
systemFonts: [{ label: '系统默认', value: 'system-ui' }],
showDownloadDrawer: false
};
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
const mutations = {
setMenus(state: State, menus: any[]) {
state.menus = menus;
},
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());
},
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;
},
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);
},
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) {
// (window as any).electron.ipcRenderer.setStoreValue(
// 'set',
// JSON.parse(JSON.stringify(setData))
// );
window.electron.ipcRenderer.send(
'set-store-value',
'set',
JSON.parse(JSON.stringify(setData))
);
} else {
localStorage.setItem('appSettings', JSON.stringify(setData));
}
},
async addToFavorite(state: State, songId: number) {
// 先添加到本地
if (!state.favoriteList.includes(songId)) {
state.favoriteList = [songId, ...state.favoriteList];
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
}
// 如果用户已登录,尝试同步到服务器
if (state.user && localStorage.getItem('token')) {
try {
await likeSong(songId, true);
} catch (error) {
console.error('同步收藏到服务器失败,但已保存在本地:', error);
}
}
},
async removeFromFavorite(state: State, songId: number) {
// 先从本地移除
state.favoriteList = state.favoriteList.filter((id) => id !== songId);
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
// 如果用户已登录,尝试同步到服务器
if (state.user && localStorage.getItem('token')) {
try {
await likeSong(songId, false);
} catch (error) {
console.error('同步取消收藏到服务器失败,但已在本地移除:', error);
}
}
},
togglePlayMode(state: State) {
state.playMode = (state.playMode + 1) % 3;
localStorage.setItem('playMode', JSON.stringify(state.playMode));
},
toggleTheme(state: State) {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
applyTheme(state.theme);
},
setShowUpdateModal(state, value) {
state.showUpdateModal = value;
},
logout(state: State) {
logout().then(() => {
state.user = null;
localStorage.removeItem('user');
localStorage.removeItem('token');
});
},
setShowArtistDrawer(state, show: boolean) {
state.showArtistDrawer = show;
if (!show) {
state.currentArtistId = null;
}
},
setCurrentArtistId(state, id: number) {
state.currentArtistId = id;
},
setSystemFonts(state, fonts: string[]) {
state.systemFonts = [
{ label: '系统默认', value: 'system-ui' },
...fonts.map((font) => ({
label: font,
value: font
}))
];
},
setShowDownloadDrawer(state: State, show: boolean) {
state.showDownloadDrawer = show;
},
setLanguage(state: State, language: string) {
state.setData.language = language;
if (isElectron) {
window.electron.ipcRenderer.send('set-store-value', 'set.language', language);
} else {
localStorage.setItem('appSettings', JSON.stringify(state.setData));
}
},
getLanguage(state: State) {
return state.setData.language;
}
};
const actions = {
initializeSettings({ commit }: { commit: any }) {
if (isElectron) {
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
commit('setSetData', {
...defaultSettings,
...setData
});
} else {
const savedSettings = localStorage.getItem('appSettings');
if (savedSettings) {
commit('setSetData', {
...defaultSettings,
...JSON.parse(savedSettings)
});
} else {
commit('setSetData', defaultSettings);
}
}
},
initializeTheme({ state }: { state: State }) {
applyTheme(state.theme);
},
async initializeFavoriteList({ state }: { state: State }) {
// 先获取本地收藏列表
const localFavoriteList = localStorage.getItem('favoriteList');
const localList: number[] = localFavoriteList ? JSON.parse(localFavoriteList) : [];
// 如果用户已登录,尝试获取服务器收藏列表并合并
if (state.user && state.user.userId) {
try {
const res = await getLikedList(state.user.userId);
if (res.data?.ids) {
// 合并本地和服务器的收藏列表,去重
const serverList = res.data.ids.reverse();
const mergedList = Array.from(new Set([...localList, ...serverList]));
state.favoriteList = mergedList;
} else {
state.favoriteList = localList;
}
} catch (error) {
console.error('获取服务器收藏列表失败,使用本地数据:', error);
state.favoriteList = localList;
}
} else {
state.favoriteList = localList;
}
// 更新本地存储
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
},
showArtist({ commit }, id: number) {
commit('setCurrentArtistId', id);
},
async initializeSystemFonts({ commit, state }) {
if (!isElectron) return;
// 如果已经有字体列表(不只是默认字体),则不重复获取
if (state.systemFonts.length > 1) return;
try {
const fonts = await window.api.invoke('get-system-fonts');
commit('setSystemFonts', fonts);
} catch (error) {
console.error('获取系统字体失败:', error);
}
},
async initializePlayState({ state, commit }: { state: State; commit: any }) {
const savedPlayList = getLocalStorageItem('playList', []);
const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null) as SongResult | null;
const savedPlayProgress = localStorage.getItem('playProgress');
if (savedPlayList.length > 0) {
commit('setPlayList', savedPlayList);
}
if (savedPlayMusic && Object.keys(savedPlayMusic).length > 0) {
// 不直接使用保存的 URL而是重新获取
try {
// 使用 handlePlayMusic 来重新获取音乐 URL
// 根据自动播放设置决定是否恢复播放状态
const shouldAutoPlay = state.setData.autoPlay;
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);
// 清除无效的播放状态
state.play = false;
state.isPlay = false;
state.playMusic = {} as SongResult;
state.playMusicUrl = '';
localStorage.removeItem('currentPlayMusic');
localStorage.removeItem('currentPlayMusicUrl');
localStorage.removeItem('isPlaying');
localStorage.removeItem('playProgress');
}
}
},
initializeLanguage({ state }: { state: State }) {
state.setData.language = getLocalStorageItem('appSettings', { language: 'zh-CN' }).language;
if (isElectron) {
window.electron.ipcRenderer.send('set-store-value', 'set.language', state.setData.language);
} else {
localStorage.setItem('appSettings', JSON.stringify(state.setData));
}
}
};
const store = createStore({
state,
mutations,
actions
// 添加路由到 Pinia
pinia.use(({ store }) => {
store.router = markRaw(router);
});
export default store;
// 导出所有 store
export * from './modules/lyric';
export * from './modules/menu';
export * from './modules/player';
export * from './modules/search';
export * from './modules/settings';
export * from './modules/user';
export default pinia;

View File

@@ -0,0 +1,15 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useLyricStore = defineStore('lyric', () => {
const lyric = ref({});
const setLyric = (newLyric: any) => {
lyric.value = newLyric;
};
return {
lyric,
setLyric
};
});

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import homeRouter from '@/router/home';
export const useMenuStore = defineStore('menu', () => {
const menus = ref(homeRouter);
const setMenus = (newMenus: any[]) => {
menus.value = newMenus;
};
return {
menus,
setMenus
};
});

View File

@@ -0,0 +1,455 @@
import { cloneDeep } from 'lodash';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { getLikedList, getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import type { ILyric, ILyricText, SongResult } from '@/type/music';
import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
import { useSettingsStore } from './settings';
import { useUserStore } from './user';
const musicHistory = useMusicHistory();
const preloadingSounds = ref<Howl[]>([]);
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
}
export const getSongUrl = async (id: number, songData: any, isDownloaded: boolean = false) => {
const { data } = await getMusicUrl(id, isDownloaded);
let url = '';
let songDetail = null;
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id, songData);
url = res.data.data.url;
songDetail = res.data.data;
} else {
songDetail = data.data[0] as any;
}
} catch (error) {
console.error('error', error);
}
if (isDownloaded) {
return songDetail;
}
url = url || data.data[0].url;
return url;
};
const parseTime = (timeString: string): number => {
const [minutes, seconds] = timeString.split(':');
return Number(minutes) * 60 + Number(seconds);
};
const parseLyricLine = (lyricLine: string): { time: number; text: string } => {
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
};
const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {
const lines = lyricsString.split('\n');
const lyrics: ILyricText[] = [];
const times: number[] = [];
lines.forEach((line) => {
const { time, text } = parseLyricLine(line);
times.push(time);
lyrics.push({ text, trText: '' });
});
return { lyrics, times };
};
export const loadLrc = async (playMusicId: number): Promise<ILyric> => {
try {
const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric);
const tlyric: Record<string, string> = {};
if (data.tlyric && data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tLyrics.forEach((lyric, index) => {
tlyric[tTimes[index].toString()] = lyric.text;
});
}
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] || '' : '';
});
return {
lrcTimeArray: times,
lrcArray: lyrics
};
} catch (err) {
console.error('Error loading lyrics:', err);
return {
lrcTimeArray: [],
lrcArray: []
};
}
};
const getSongDetail = async (playMusic: SongResult) => {
playMusic.playLoading = true;
const playMusicUrl =
playMusic.playMusicUrl || (await getSongUrl(playMusic.id, cloneDeep(playMusic)));
const { backgroundColor, primaryColor } =
playMusic.backgroundColor && playMusic.primaryColor
? playMusic
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
playMusic.playLoading = false;
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult;
};
const preloadNextSong = (nextSongUrl: string) => {
try {
// 限制同时预加载的数量
if (preloadingSounds.value.length >= 2) {
const oldestSound = preloadingSounds.value.shift();
if (oldestSound) {
oldestSound.unload();
}
}
const sound = new Howl({
src: [nextSongUrl],
html5: true,
preload: true,
autoplay: false
});
preloadingSounds.value.push(sound);
// 添加加载错误处理
sound.on('loaderror', () => {
console.error('预加载音频失败:', nextSongUrl);
const index = preloadingSounds.value.indexOf(sound);
if (index > -1) {
preloadingSounds.value.splice(index, 1);
}
sound.unload();
});
return sound;
} catch (error) {
console.error('预加载音频出错:', error);
return null;
}
};
const fetchSongs = async (playList: SongResult[], startIndex: number, endIndex: number) => {
try {
const songs = playList.slice(Math.max(0, startIndex), Math.min(endIndex, playList.length));
const detailedSongs = await Promise.all(
songs.map(async (song: SongResult) => {
try {
// 如果歌曲详情已经存在,就不重复请求
if (!song.playMusicUrl) {
return await getSongDetail(song);
}
return song;
} catch (error) {
console.error('获取歌曲详情失败:', error);
return song;
}
})
);
// 加载下一首的歌词
const nextSong = detailedSongs[0];
if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
try {
nextSong.lyric = await loadLrc(nextSong.id);
} catch (error) {
console.error('加载歌词失败:', error);
}
}
// 更新播放列表中的歌曲详情
detailedSongs.forEach((song, index) => {
if (song && startIndex + index < playList.length) {
playList[startIndex + index] = song;
}
});
// 只预加载下一首歌曲
if (nextSong && nextSong.playMusicUrl) {
preloadNextSong(nextSong.playMusicUrl);
}
} catch (error) {
console.error('获取歌曲列表失败:', error);
}
};
// 异步加载歌词的方法
const loadLrcAsync = async (playMusic: SongResult) => {
if (playMusic.lyric && playMusic.lyric.lrcTimeArray.length > 0) {
return;
}
const lyrics = await loadLrc(playMusic.id);
playMusic.lyric = lyrics;
};
export const usePlayerStore = defineStore('player', () => {
// 状态
const play = ref(false);
const isPlay = ref(false);
const playMusic = ref<SongResult>(getLocalStorageItem('currentPlayMusic', {} as SongResult));
const playMusicUrl = ref(getLocalStorageItem('currentPlayMusicUrl', ''));
const playList = ref<SongResult[]>(getLocalStorageItem('playList', []));
const playListIndex = ref(getLocalStorageItem('playListIndex', 0));
const playMode = ref(getLocalStorageItem('playMode', 0));
const musicFull = ref(false);
const favoriteList = ref<number[]>(getLocalStorageItem('favoriteList', []));
const savedPlayProgress = ref<number | undefined>();
// 计算属性
const currentSong = computed(() => playMusic.value);
const isPlaying = computed(() => isPlay.value);
const currentPlayList = computed(() => playList.value);
const currentPlayListIndex = computed(() => playListIndex.value);
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
const updatedPlayMusic = await getSongDetail(music);
playMusic.value = updatedPlayMusic;
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
// 记录当前设置的播放状态
play.value = isPlay;
// 每次设置新歌曲时,立即更新 localStorage
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
localStorage.setItem('isPlaying', play.value.toString());
// 设置网页标题
document.title = `${updatedPlayMusic.name} - ${updatedPlayMusic?.song?.artists?.reduce((prev, curr) => `${prev}${curr.name}/`, '')}`;
loadLrcAsync(playMusic.value);
musicHistory.addMusic(playMusic.value);
playListIndex.value = playList.value.findIndex((item: SongResult) => item.id === music.id);
// 请求后续五首歌曲的详情
fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 6);
};
// 方法
const setPlay = async (song: SongResult) => {
await handlePlayMusic(song);
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
};
const setIsPlay = (value: boolean) => {
isPlay.value = value;
localStorage.setItem('isPlaying', value.toString());
};
const setPlayMusic = async (value: boolean | SongResult) => {
if (typeof value === 'boolean') {
play.value = value;
isPlay.value = value;
localStorage.setItem('isPlaying', value.toString());
} else {
await handlePlayMusic(value);
play.value = true;
isPlay.value = true;
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
}
};
const setMusicFull = (value: boolean) => {
musicFull.value = value;
};
const setPlayList = (list: SongResult[]) => {
playListIndex.value = list.findIndex((item) => item.id === playMusic.value.id);
playList.value = list;
localStorage.setItem('playList', JSON.stringify(list));
localStorage.setItem('playListIndex', playListIndex.value.toString());
};
const addToNextPlay = (song: SongResult) => {
const list = [...playList.value];
const currentIndex = playListIndex.value;
const existingIndex = list.findIndex((item) => item.id === song.id);
if (existingIndex !== -1) {
list.splice(existingIndex, 1);
}
list.splice(currentIndex + 1, 0, song);
setPlayList(list);
};
const nextPlay = async () => {
if (playList.value.length === 0) {
play.value = true;
return;
}
let nowPlayListIndex: number;
if (playMode.value === 2) {
// 随机播放模式
do {
nowPlayListIndex = Math.floor(Math.random() * playList.value.length);
} while (nowPlayListIndex === playListIndex.value && playList.value.length > 1);
} else {
// 列表循环模式
nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;
}
playListIndex.value = nowPlayListIndex;
await handlePlayMusic(playList.value[playListIndex.value]);
};
const prevPlay = async () => {
if (playList.value.length === 0) {
play.value = true;
return;
}
const nowPlayListIndex =
(playListIndex.value - 1 + playList.value.length) % playList.value.length;
await handlePlayMusic(playList.value[nowPlayListIndex]);
await fetchSongs(playList.value, playListIndex.value - 5, nowPlayListIndex);
};
const togglePlayMode = () => {
playMode.value = (playMode.value + 1) % 3;
localStorage.setItem('playMode', JSON.stringify(playMode.value));
};
const addToFavorite = async (id: number) => {
if (!favoriteList.value.includes(id)) {
favoriteList.value.push(id);
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
}
};
const removeFromFavorite = async (id: number) => {
favoriteList.value = favoriteList.value.filter((item) => item !== id);
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
};
// 初始化播放状态
const initializePlayState = async () => {
const settingStore = useSettingsStore();
const savedPlayList = getLocalStorageItem('playList', []);
const savedPlayMusic = getLocalStorageItem<SongResult | null>('currentPlayMusic', null);
const savedProgress = localStorage.getItem('playProgress');
if (savedPlayList.length > 0) {
setPlayList(savedPlayList);
}
if (savedPlayMusic && Object.keys(savedPlayMusic).length > 0) {
try {
console.log('settingStore.setData', settingStore.setData);
const isPlaying = settingStore.setData.autoPlay;
await handlePlayMusic({ ...savedPlayMusic, playMusicUrl: undefined }, isPlaying);
if (savedProgress) {
try {
const progress = JSON.parse(savedProgress);
if (progress && progress.songId === savedPlayMusic.id) {
savedPlayProgress.value = progress.progress;
} else {
localStorage.removeItem('playProgress');
}
} catch (e) {
console.error('解析保存的播放进度失败', e);
localStorage.removeItem('playProgress');
}
}
} catch (error) {
console.error('重新获取音乐链接失败:', error);
play.value = false;
isPlay.value = false;
playMusic.value = {} as SongResult;
playMusicUrl.value = '';
localStorage.removeItem('currentPlayMusic');
localStorage.removeItem('currentPlayMusicUrl');
localStorage.removeItem('isPlaying');
localStorage.removeItem('playProgress');
}
}
};
const initializeFavoriteList = async () => {
const userStore = useUserStore();
// 先获取本地收藏列表
const localFavoriteList = localStorage.getItem('favoriteList');
const localList: number[] = localFavoriteList ? JSON.parse(localFavoriteList) : [];
// 如果用户已登录,尝试获取服务器收藏列表并合并
if (userStore.user && userStore.user.userId) {
try {
const res = await getLikedList(userStore.user.userId);
if (res.data?.ids) {
// 合并本地和服务器的收藏列表,去重
const serverList = res.data.ids.reverse();
const mergedList = Array.from(new Set([...localList, ...serverList]));
favoriteList.value = mergedList;
} else {
favoriteList.value = localList;
}
} catch (error) {
console.error('获取服务器收藏列表失败,使用本地数据:', error);
favoriteList.value = localList;
}
} else {
favoriteList.value = localList;
}
// 更新本地存储
localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value));
};
return {
// 状态
play,
isPlay,
playMusic,
playMusicUrl,
playList,
playListIndex,
playMode,
musicFull,
savedPlayProgress,
favoriteList,
// 计算属性
currentSong,
isPlaying,
currentPlayList,
currentPlayListIndex,
// 方法
setPlay,
setIsPlay,
nextPlay,
prevPlay,
setPlayMusic,
setMusicFull,
setPlayList,
addToNextPlay,
togglePlayMode,
initializePlayState,
initializeFavoriteList,
addToFavorite,
removeFromFavorite
};
});

View File

@@ -0,0 +1,22 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useSearchStore = defineStore('search', () => {
const searchValue = ref('');
const searchType = ref(1);
const setSearchValue = (value: string) => {
searchValue.value = value;
};
const setSearchType = (type: number) => {
searchType.value = type;
};
return {
searchValue,
searchType,
setSearchValue,
setSearchType
};
});

View File

@@ -0,0 +1,129 @@
import { cloneDeep } from 'lodash';
import { defineStore } from 'pinia';
import { ref } from 'vue';
import setDataDefault from '@/../main/set.json';
import { isElectron } from '@/utils';
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
export const useSettingsStore = defineStore('settings', () => {
// 初始化时先从存储中读取设置
const getInitialSettings = () => {
if (isElectron) {
const savedSettings = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
return savedSettings || setDataDefault;
}
const savedSettings = localStorage.getItem('appSettings');
return savedSettings ? JSON.parse(savedSettings) : setDataDefault;
};
const setData = ref(getInitialSettings());
const theme = ref<ThemeType>(getCurrentTheme());
const isMobile = ref(false);
const showUpdateModal = ref(false);
const showArtistDrawer = ref(false);
const currentArtistId = ref<number | null>(null);
const systemFonts = ref<{ label: string; value: string }[]>([
{ label: '系统默认', value: 'system-ui' }
]);
const showDownloadDrawer = ref(false);
const setSetData = (data: any) => {
// 合并现有设置和新设置
const mergedData = {
...setData.value,
...data
};
if (isElectron) {
window.electron.ipcRenderer.send('set-store-value', 'set', cloneDeep(mergedData));
} else {
localStorage.setItem('appSettings', JSON.stringify(cloneDeep(mergedData)));
}
setData.value = cloneDeep(mergedData);
};
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
applyTheme(theme.value);
};
const setShowUpdateModal = (value: boolean) => {
showUpdateModal.value = value;
};
const setShowArtistDrawer = (show: boolean) => {
showArtistDrawer.value = show;
if (!show) {
currentArtistId.value = null;
}
};
const setCurrentArtistId = (id: number) => {
currentArtistId.value = id;
};
const setSystemFonts = (fonts: string[]) => {
systemFonts.value = [
{ label: '系统默认', value: 'system-ui' },
...fonts.map((font) => ({
label: font,
value: font
}))
];
};
const setShowDownloadDrawer = (show: boolean) => {
showDownloadDrawer.value = show;
};
const setLanguage = (language: string) => {
setSetData({ language });
if (isElectron) {
window.electron.ipcRenderer.send('change-language', language);
}
};
const initializeSettings = () => {
// const savedSettings = getInitialSettings();
// setData.value = savedSettings;
};
const initializeTheme = () => {
applyTheme(theme.value);
};
const initializeSystemFonts = async () => {
if (!isElectron) return;
if (systemFonts.value.length > 1) return;
try {
const fonts = await window.api.invoke('get-system-fonts');
setSystemFonts(fonts);
} catch (error) {
console.error('获取系统字体失败:', error);
}
};
return {
setData,
theme,
isMobile,
showUpdateModal,
showArtistDrawer,
currentArtistId,
systemFonts,
showDownloadDrawer,
setSetData,
toggleTheme,
setShowUpdateModal,
setShowArtistDrawer,
setCurrentArtistId,
setSystemFonts,
setShowDownloadDrawer,
setLanguage,
initializeSettings,
initializeTheme,
initializeSystemFonts
};
});

View File

@@ -0,0 +1,84 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { logout } from '@/api/login';
import { getLikedList } from '@/api/music';
interface UserData {
userId: number;
[key: string]: any;
}
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
}
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref<UserData | null>(getLocalStorageItem('user', null));
const searchValue = ref('');
const searchType = ref(1);
// 方法
const setUser = (userData: UserData) => {
user.value = userData;
localStorage.setItem('user', JSON.stringify(userData));
};
const handleLogout = async () => {
try {
await logout();
user.value = null;
localStorage.removeItem('user');
localStorage.removeItem('token');
} catch (error) {
console.error('登出失败:', error);
}
};
const setSearchValue = (value: string) => {
searchValue.value = value;
};
const setSearchType = (type: number) => {
searchType.value = type;
};
// 初始化
const initializeUser = async () => {
const savedUser = getLocalStorageItem<UserData | null>('user', null);
if (savedUser) {
user.value = savedUser;
// 如果用户已登录,获取收藏列表
if (localStorage.getItem('token')) {
try {
const { data } = await getLikedList(savedUser.userId);
return data?.ids || [];
} catch (error) {
console.error('获取收藏列表失败:', error);
return [];
}
}
}
return [];
};
return {
// 状态
user,
searchValue,
searchType,
// 方法
setUser,
handleLogout,
setSearchValue,
setSearchType,
initializeUser
};
});

View File

@@ -8,6 +8,7 @@ export interface IElectronAPI {
openLyric: () => void;
sendLyric: (data: string) => void;
unblockMusic: (id: number) => Promise<string>;
onLanguageChanged: (callback: (locale: string) => void) => void;
store: {
get: (key: string) => Promise<any>;
set: (key: string, value: any) => Promise<boolean>;

View File

@@ -1,6 +1,7 @@
import { useWindowSize } from '@vueuse/core';
import { computed } from 'vue';
import store from '@/store';
import { useSettingsStore } from '@/store/modules/settings';
// 设置歌手背景图片
export const setBackgroundImg = (url: String) => {
@@ -8,10 +9,11 @@ export const setBackgroundImg = (url: String) => {
};
// 设置动画类型
export const setAnimationClass = (type: String) => {
if (store.state.setData && store.state.setData.noAnimate) {
const settingsStore = useSettingsStore();
if (settingsStore.setData && settingsStore.setData.noAnimate) {
return '';
}
const speed = store.state.setData?.animationSpeed || 1;
const speed = settingsStore.setData?.animationSpeed || 1;
let speedClass = '';
if (speed <= 0.3) speedClass = 'animate__slower';
@@ -23,10 +25,11 @@ export const setAnimationClass = (type: String) => {
};
// 设置动画延时
export const setAnimationDelay = (index: number = 6, time: number = 50) => {
if (store.state.setData?.noAnimate) {
const settingsStore = useSettingsStore();
if (settingsStore.setData?.noAnimate) {
return '';
}
const speed = store.state.setData?.animationSpeed || 1;
const speed = settingsStore.setData?.animationSpeed || 1;
return `animation-delay:${(index * time) / (speed * 2)}ms`;
};
@@ -71,15 +74,30 @@ export const getImgUrl = (url: string | undefined, size: string = '') => {
};
export const isMobile = computed(() => {
const flag = navigator.userAgent.match(
const { width } = useWindowSize();
const userAgentFlag = navigator.userAgent.match(
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
);
store.state.isMobile = !!flag;
const isMobileWidth = width.value < 500;
const isMobileDevice = !!userAgentFlag || isMobileWidth;
// 给html标签 添加mobile
if (flag) document.documentElement.classList.add('mobile');
return !!flag;
const settingsStore = useSettingsStore();
settingsStore.isMobile = isMobileDevice;
// 给html标签 添加或移除mobile类
if (isMobileDevice) {
document.documentElement.classList.add('mobile');
} else {
document.documentElement.classList.add('pc');
document.documentElement.classList.remove('mobile');
}
return isMobileDevice;
});
export const isElectron = (window as any).electron !== undefined;
export const isLyricWindow = computed(() => {
return window.location.hash.includes('lyric');
});

View File

@@ -1,6 +1,7 @@
import axios, { InternalAxiosRequestConfig } from 'axios';
import store from '@/store';
import { useSettingsStore } from '@/store/modules/settings';
import { useUserStore } from '@/store/modules/user';
import { isElectron } from '.';
@@ -8,9 +9,13 @@ let setData: any = null;
const getSetData = () => {
if (window.electron) {
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
} else {
const settingsStore = useSettingsStore();
setData = settingsStore.setData;
}
return setData;
};
getSetData();
// 扩展请求配置接口
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
retryCount?: number;
@@ -34,6 +39,9 @@ const RETRY_DELAY = 500;
request.interceptors.request.use(
(config: CustomAxiosRequestConfig) => {
getSetData();
config.baseURL = window.electron
? `http://127.0.0.1:${setData?.musicApiPort}`
: import.meta.env.VITE_API;
// 只在retryCount未定义时初始化为0
if (config.retryCount === undefined) {
config.retryCount = 0;
@@ -75,7 +83,7 @@ request.interceptors.response.use(
return response;
},
async (error) => {
console.log('error', error);
console.error('error', error);
const config = error.config as CustomAxiosRequestConfig;
// 如果没有配置,直接返回错误
@@ -84,9 +92,10 @@ request.interceptors.response.use(
}
// 处理 301 状态码
if (error.response?.status === 301) {
if (error.response?.status === 301 && config.params.noLogin !== true) {
// 使用 store mutation 清除用户信息
store.commit('logout');
const userStore = useUserStore();
userStore.handleLogout();
console.log(`301 状态码,清除登录信息后重试第 ${config.retryCount}`);
config.retryCount = 3;
}
@@ -98,7 +107,7 @@ request.interceptors.response.use(
!NO_RETRY_URLS.includes(config.url as string)
) {
config.retryCount++;
console.log(`请求重试第 ${config.retryCount}`);
console.error(`请求重试第 ${config.retryCount}`);
// 延迟重试
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
@@ -107,7 +116,7 @@ request.interceptors.response.use(
return request(config);
}
console.log(`重试${MAX_RETRIES}次后仍然失败`);
console.error(`重试${MAX_RETRIES}次后仍然失败`);
return Promise.reject(error);
}
);

View File

@@ -88,18 +88,18 @@ import { useMessage } from 'naive-ui';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { getSongUrl } from '@/hooks/MusicListHook';
import { usePlayerStore } from '@/store';
import type { SongResult } from '@/type/music';
import { isElectron, setAnimationClass, setAnimationDelay } from '@/utils';
const { t } = useI18n();
const store = useStore();
const playerStore = usePlayerStore();
const message = useMessage();
const favoriteList = computed(() => store.state.favoriteList);
const favoriteList = computed(() => playerStore.favoriteList);
const favoriteSongs = ref<SongResult[]>([]);
const loading = ref(false);
const noMore = ref(false);
@@ -277,7 +277,7 @@ const handleScroll = (e: any) => {
};
onMounted(async () => {
await store.dispatch('initializeFavoriteList');
await playerStore.initializeFavoriteList();
await getFavoriteSongs();
});
@@ -293,7 +293,7 @@ watch(
);
const handlePlay = () => {
store.commit('setPlayList', favoriteSongs.value);
playerStore.setPlayList(favoriteSongs.value);
};
const getItemAnimationDelay = (index: number) => {

View File

@@ -34,11 +34,11 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { getMusicDetail } from '@/api/music';
import SongItem from '@/components/common/SongItem.vue';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { usePlayerStore } from '@/store/modules/player';
import type { SongResult } from '@/type/music';
import { setAnimationClass, setAnimationDelay } from '@/utils';
@@ -47,12 +47,12 @@ defineOptions({
});
const { t } = useI18n();
const store = useStore();
const { delMusic, musicList } = useMusicHistory();
const scrollbarRef = ref();
const loading = ref(false);
const noMore = ref(false);
const displayList = ref<SongResult[]>([]);
const playerStore = usePlayerStore();
// 无限滚动相关配置
const pageSize = 20;
@@ -112,7 +112,7 @@ const handleScroll = (e: any) => {
// 播放全部
const handlePlay = () => {
store.commit('setPlayList', displayList.value);
playerStore.setPlayList(displayList.value);
};
onMounted(() => {

View File

@@ -2,7 +2,7 @@
<n-scrollbar :size="100" :x-scrollable="false">
<div class="main-page">
<!-- 推荐歌手 -->
<recommend-singer />
<top-banner />
<div class="main-content">
<!-- 歌单分类列表 -->
<playlist-type v-if="!isMobile" />
@@ -19,10 +19,10 @@
</template>
<script lang="ts" setup>
import PlaylistType from '@/components/PlaylistType.vue';
import RecommendAlbum from '@/components/RecommendAlbum.vue';
import RecommendSinger from '@/components/RecommendSinger.vue';
import RecommendSonglist from '@/components/RecommendSonglist.vue';
import PlaylistType from '@/components/home/PlaylistType.vue';
import RecommendAlbum from '@/components/home/RecommendAlbum.vue';
import RecommendSonglist from '@/components/home/RecommendSonglist.vue';
import TopBanner from '@/components/home/TopBanner.vue';
import { isMobile } from '@/utils';
import FavoriteList from '@/views/favorite/index.vue';

View File

@@ -3,9 +3,9 @@ import { useMessage } from 'naive-ui';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
import { useUserStore } from '@/store/modules/user';
import { setAnimationClass } from '@/utils';
defineOptions({
@@ -14,11 +14,12 @@ defineOptions({
const { t } = useI18n();
const message = useMessage();
const store = useStore();
const router = useRouter();
const isQr = ref(true);
const qrUrl = ref<string>();
const userStore = useUserStore();
onMounted(() => {
loadLogin();
});
@@ -56,7 +57,7 @@ const timerIsQr = (key: string) => {
if (data.code === 803) {
localStorage.setItem('token', data.cookie);
const user = await getUserDetail();
store.state.user = user.data.profile;
userStore.user = user.data.profile;
localStorage.setItem('user', JSON.stringify(user.data.profile));
message.success(t('login.message.loginSuccess'));
@@ -83,10 +84,10 @@ onBeforeUnmount(() => {
});
// 是否扫码登陆
const chooseQr = () => {
isQr.value = !isQr.value;
loadLogin();
};
// const chooseQr = () => {
// isQr.value = !isQr.value;
// loadLogin();
// };
// 手机号登录
const phone = ref('');
@@ -95,7 +96,7 @@ const loginPhone = async () => {
const { data } = await loginByCellphone(phone.value, password.value);
if (data.code === 200) {
message.success(t('login.message.loginSuccess'));
store.state.user = data.profile;
userStore.user = data.profile;
localStorage.setItem('token', data.cookie);
setTimeout(() => {
router.push('/user');
@@ -135,9 +136,9 @@ const loginPhone = async () => {
</div>
</div>
<div class="bottom">
<div class="title" @click="chooseQr()">
<!-- <div class="title" @click="chooseQr()">
{{ isQr ? t('login.button.switchToPhone') : t('login.button.switchToQr') }}
</div>
</div> -->
</div>
</div>
</div>

View File

@@ -494,6 +494,8 @@ watch(
// 添加拖动相关变量
const isDragging = ref(false);
const startPosition = ref({ x: 0, y: 0 });
const lastMoveTime = ref(0);
const moveThrottleMs = 10; // 限制拖动事件发送频率,提高性能
// 处理鼠标按下事件
const handleMouseDown = (e: MouseEvent) => {
@@ -501,7 +503,8 @@ const handleMouseDown = (e: MouseEvent) => {
if (
lyricSetting.value.isLock ||
(e.target as HTMLElement).closest('.control-buttons') ||
(e.target as HTMLElement).closest('.font-size-controls')
(e.target as HTMLElement).closest('.font-size-controls') ||
(e.target as HTMLElement).closest('.play-controls')
) {
return;
}
@@ -511,23 +514,38 @@ const handleMouseDown = (e: MouseEvent) => {
isDragging.value = true;
startPosition.value = { x: e.screenX, y: e.screenY };
lastMoveTime.value = performance.now();
// 发送拖动开始信号到主进程
windowData.electron.ipcRenderer.send('lyric-drag-start');
// 添加全局鼠标事件监听
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.value) return;
// 时间节流,避免过于频繁的更新
const now = performance.now();
if (now - lastMoveTime.value < moveThrottleMs) return;
lastMoveTime.value = now;
const deltaX = e.screenX - startPosition.value.x;
const deltaY = e.screenY - startPosition.value.y;
// 发送移动事件到主进程
windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY });
startPosition.value = { x: e.screenX, y: e.screenY };
// 只有在实际移动时才发送事件
if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {
// 发送移动事件到主进程
windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY });
startPosition.value = { x: e.screenX, y: e.screenY };
}
};
const handleMouseUp = () => {
if (!isDragging.value) return;
isDragging.value = false;
// 发送拖动结束信号到主进程
windowData.electron.ipcRenderer.send('lyric-drag-end');
// 移除事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
@@ -573,7 +591,7 @@ const handleNext = () => {
};
</script>
<style>
<style scoped>
body {
background-color: transparent !important;
}
@@ -589,9 +607,10 @@ body {
user-select: none;
transition: background-color 0.2s ease;
cursor: default;
border-radius: 14px;
&:hover {
background: rgba(0, 0, 0, 0.5);
background: rgba(44, 44, 44, 0.466);
.control-bar {
&-show {
opacity: 1;
@@ -750,16 +769,15 @@ body {
color: var(--text-color);
white-space: pre-wrap;
word-break: break-all;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
line-height: 1.4;
-webkit-text-stroke: 0.5px #0000008a;
}
.lyric-translation {
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transition: font-size 0.2s ease;
line-height: 1.4; // 添加行高比例
}
@@ -774,8 +792,6 @@ body {
body {
background-color: transparent !important;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
}
.lyric-content {

View File

@@ -64,11 +64,11 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useStore } from 'vuex';
import { getAllMv, getTopMv } from '@/api/mv';
import MvPlayer from '@/components/MvPlayer.vue';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { IMvItem } from '@/type/mv';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
@@ -79,7 +79,6 @@ defineOptions({
const showMv = ref(false);
const mvList = ref<Array<IMvItem>>([]);
const playMvItem = ref<IMvItem>();
const store = useStore();
const initLoading = ref(false);
const loadingMore = ref(false);
const currentIndex = ref(0);
@@ -97,6 +96,8 @@ const categories = [
];
const selectedCategory = ref('全部');
const playerStore = usePlayerStore();
watch(selectedCategory, async () => {
offset.value = 0;
mvList.value = [];
@@ -114,8 +115,8 @@ onMounted(async () => {
});
const handleShowMv = async (item: IMvItem, index: number) => {
store.commit('setIsPlay', false);
store.commit('setPlayMusic', false);
playerStore.setIsPlay(false);
playerStore.setPlayMusic(false);
audioService.getCurrentSound()?.pause();
showMv.value = true;
currentIndex.value = index;

View File

@@ -107,12 +107,13 @@ import { useDateFormat } from '@vueuse/core';
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import { getHotSearch } from '@/api/home';
import { getSearch } from '@/api/search';
import SearchItem from '@/components/common/SearchItem.vue';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore } from '@/store/modules/player';
import { useSearchStore } from '@/store/modules/search';
import type { IHotSearch } from '@/type/search';
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
@@ -122,10 +123,11 @@ defineOptions({
const { t } = useI18n();
const route = useRoute();
const store = useStore();
const playerStore = usePlayerStore();
const searchStore = useSearchStore();
const searchDetail = ref<any>();
const searchType = computed(() => store.state.searchType as number);
const searchType = computed(() => searchStore.searchType as number);
const searchDetailLoading = ref(false);
const searchHistory = ref<string[]>([]);
@@ -188,23 +190,6 @@ onMounted(() => {
const hotKeyword = ref(route.query.keyword || t('search.title.searchList'));
watch(
() => store.state.searchValue,
(value) => {
loadSearch(value);
}
);
watch(
() => searchType.value,
() => {
if (store.state.searchValue) {
loadSearch(store.state.searchValue);
}
}
);
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
const loadSearch = async (keywords: any, type: any = null, isLoadMore = false) => {
if (!keywords) return;
@@ -295,6 +280,31 @@ const loadSearch = async (keywords: any, type: any = null, isLoadMore = false) =
}
};
watch(
() => searchStore.searchValue,
(value) => {
loadSearch(value);
}
);
watch(
() => searchType.value,
() => {
if (searchStore.searchValue) {
loadSearch(searchStore.searchValue);
}
}
);
// 修改 store.state 的访问
if (searchStore.searchValue) {
loadSearch(searchStore.searchValue);
}
// 修改 store.state 的设置
searchStore.searchValue = route.query.keyword as string;
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
// 添加滚动处理函数
const handleScroll = (e: any) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -308,14 +318,14 @@ watch(
() => route.path,
async (path) => {
if (path === '/search') {
store.state.searchValue = route.query.keyword;
searchStore.searchValue = route.query.keyword as string;
}
}
);
const handlePlay = () => {
const tracks = searchDetail.value?.songs || [];
store.commit('setPlayList', tracks);
playerStore.setPlayList(tracks);
};
// 点击搜索历史

View File

@@ -1,7 +1,7 @@
<template>
<div class="settings-container">
<!-- 左侧导航栏 -->
<div class="settings-nav">
<div v-if="!isMobile" class="settings-nav">
<div
v-for="section in settingSections"
:key="section.id"
@@ -205,7 +205,7 @@
</div>
</div>
<div class="flex items-center gap-2">
<n-button size="small" @click="store.commit('setShowDownloadDrawer', true)">
<n-button size="small" @click="settingsStore.showDownloadDrawer = true">
{{ t('settings.application.download') }}
</n-button>
</div>
@@ -461,11 +461,11 @@
</template>
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core';
import type { FormRules } from 'naive-ui';
import { useMessage } from 'naive-ui';
import { computed, h, nextTick, onMounted, ref, watch } from 'vue';
import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import localData from '@/../main/set.json';
import Coffee from '@/components/Coffee.vue';
@@ -473,13 +473,26 @@ import DonationList from '@/components/common/DonationList.vue';
import PlayBottom from '@/components/common/PlayBottom.vue';
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
import ShortcutSettings from '@/components/settings/ShortcutSettings.vue';
import { isElectron } from '@/utils';
import { useSettingsStore } from '@/store/modules/settings';
import { useUserStore } from '@/store/modules/user';
import { isElectron, isMobile } from '@/utils';
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
import { checkUpdate, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
const store = useStore();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
// 创建一个本地缓存的setData避免频繁更新
const localSetData = ref({ ...settingsStore.setData });
// 在组件卸载时保存设置
onUnmounted(() => {
// 确保最终设置被保存
settingsStore.setSetData(localSetData.value);
});
const checking = ref(false);
const updateInfo = ref<UpdateResult>({
hasUpdate: false,
@@ -490,35 +503,47 @@ const updateInfo = ref<UpdateResult>({
const { t } = useI18n();
const setData = computed(() => {
const data = store.state.setData;
// 确保代理配置存在
if (!data.proxyConfig) {
data.proxyConfig = {
enable: false,
protocol: 'http',
host: '127.0.0.1',
port: 7890
};
// 创建一个防抖的保存函数
// const debouncedSaveSettings = debounce((newData) => {
// settingsStore.setSetData(newData);
// }, 500);
const saveSettings = useDebounceFn((data) => {
settingsStore.setSetData(data);
}, 500);
// 使用计算属性来管理设置数据
const setData = computed({
get: () => localSetData.value,
set: (newData) => {
localSetData.value = newData;
}
// 确保音质设置存在
if (!data.musicQuality) {
data.musicQuality = 'higher';
}
return data;
});
// 监听localSetData变化保存设置
watch(
() => setData.value,
(newVal) => {
store.commit('setSetData', newVal);
() => localSetData.value,
(newValue) => {
saveSettings(newValue);
},
{ deep: true }
);
// 监听store中setData的变化同步到本地
watch(
() => settingsStore.setData,
(newValue) => {
// 只在初始加载时更新本地数据,避免循环更新
if (JSON.stringify(localSetData.value) !== JSON.stringify(newValue)) {
localSetData.value = { ...newValue };
}
},
{ deep: true, immediate: true }
);
const isDarkTheme = computed({
get: () => store.state.theme === 'dark',
set: () => store.commit('toggleTheme')
get: () => settingsStore.theme === 'dark',
set: () => settingsStore.toggleTheme()
});
const openAuthor = () => {
@@ -552,16 +577,16 @@ const checkForUpdates = async (isClick = false) => {
};
const openReleasePage = () => {
store.commit('setShowUpdateModal', true);
settingsStore.showUpdateModal = true;
};
const selectDownloadPath = async () => {
const path = await selectDirectory(message);
if (path) {
store.commit('setSetData', {
setData.value = {
...setData.value,
downloadPath: path
});
};
}
};
@@ -606,7 +631,7 @@ const proxyRules: FormRules = {
};
// 使用 store 中的字体列表
const systemFonts = computed(() => store.state.systemFonts);
const systemFonts = computed(() => settingsStore.systemFonts);
// 已选择的字体列表
const selectedFonts = ref<string[]>([]);
@@ -622,17 +647,17 @@ watch(
(newFonts) => {
// 如果没有选择任何字体,使用系统默认字体
if (newFonts.length === 0) {
store.commit('setSetData', {
setData.value = {
...setData.value,
fontFamily: 'system-ui'
});
};
return;
}
// 将选择的字体组合成字体列表
store.commit('setSetData', {
setData.value = {
...setData.value,
fontFamily: newFonts.join(',')
});
};
},
{ deep: true }
);
@@ -660,10 +685,10 @@ onMounted(async () => {
}
// 确保enableRealIP有默认值
if (setData.value.enableRealIP === undefined) {
store.commit('setSetData', {
setData.value = {
...setData.value,
enableRealIP: false
});
};
}
});
@@ -686,7 +711,7 @@ const handleProxyConfirm = async () => {
try {
await formRef.value?.validate();
// 保存代理配置时保留enable状态
store.commit('setSetData', {
setData.value = {
...setData.value,
proxyConfig: {
enable: setData.value.proxyConfig?.enable || false,
@@ -694,7 +719,7 @@ const handleProxyConfirm = async () => {
host: proxyForm.value.host,
port: proxyForm.value.port
}
});
};
showProxyModal.value = false;
message.success(t('settings.network.messages.proxySuccess'));
} catch (err) {
@@ -705,20 +730,20 @@ const handleProxyConfirm = async () => {
const validateAndSaveRealIP = () => {
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!setData.value.realIP || ipRegex.test(setData.value.realIP)) {
store.commit('setSetData', {
setData.value = {
...setData.value,
realIP: setData.value.realIP,
enableRealIP: true
});
};
if (setData.value.realIP) {
message.success(t('settings.network.messages.realIPSuccess'));
}
} else {
message.error(t('settings.network.messages.realIPError'));
store.commit('setSetData', {
setData.value = {
...setData.value,
realIP: ''
});
};
}
};
@@ -727,11 +752,11 @@ watch(
() => setData.value.enableRealIP,
(newVal) => {
if (!newVal) {
store.commit('setSetData', {
setData.value = {
...setData.value,
realIP: '',
enableRealIP: false
});
};
}
}
);
@@ -795,7 +820,7 @@ const clearCache = async () => {
localStorage.removeItem('favoriteList');
break;
case 'user':
store.commit('logout');
userStore.handleLogout();
break;
case 'settings':
if (window.electron) {

View File

@@ -1,7 +1,7 @@
<template>
<div class="user-page">
<div
v-if="userDetail"
v-if="userDetail && user"
class="left"
:class="setAnimationClass('animate__fadeInLeft')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
@@ -99,7 +99,6 @@ import { useMessage } from 'naive-ui';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getListDetail } from '@/api/list';
import { updatePlaylistTracks } from '@/api/music';
@@ -107,6 +106,8 @@ import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from '@/components/common/SongItem.vue';
import MusicList from '@/components/MusicList.vue';
import { usePlayerStore } from '@/store/modules/player';
import { useUserStore } from '@/store/modules/user';
import type { Playlist } from '@/type/listDetail';
import type { IUserDetail } from '@/type/user';
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
@@ -116,7 +117,8 @@ defineOptions({
});
const { t } = useI18n();
const store = useStore();
const userStore = useUserStore();
const playerStore = usePlayerStore();
const router = useRouter();
const userDetail = ref<IUserDetail>();
const playList = ref<any[]>([]);
@@ -128,7 +130,7 @@ const list = ref<Playlist>();
const listLoading = ref(false);
const message = useMessage();
const user = computed(() => store.state.user);
const user = computed(() => userStore.user);
onBeforeUnmount(() => {
mounted.value = false;
@@ -145,8 +147,8 @@ const checkLoginStatus = () => {
}
// 如果store中没有用户数据但localStorage中有则恢复用户数据
if (!store.state.user && userData) {
store.state.user = JSON.parse(userData);
if (!userStore.user && userData) {
userStore.setUser(JSON.parse(userData));
}
return true;
@@ -165,27 +167,36 @@ const loadData = async () => {
try {
infoLoading.value = true;
const { data: userData } = await getUserDetail(user.value.userId);
if (!mounted.value) return;
userDetail.value = userData;
if (!user.value) {
console.warn('用户数据不存在,尝试重新获取');
// 可以尝试重新获取用户数据
return;
}
const { data: playlistData } = await getUserPlaylist(user.value.userId);
if (!mounted.value) return;
playList.value = playlistData.playlist;
// 使用 Promise.all 并行请求提高效率
const [userDetailRes, playlistRes, recordRes] = await Promise.all([
getUserDetail(user.value.userId),
getUserPlaylist(user.value.userId),
getUserRecord(user.value.userId)
]);
const { data: recordData } = await getUserRecord(user.value.userId);
if (!mounted.value) return;
recordList.value = recordData.allData.map((item: any) => ({
userDetail.value = userDetailRes.data;
playList.value = playlistRes.data.playlist;
recordList.value = recordRes.data.allData.map((item: any) => ({
...item,
...item.song,
picUrl: item.song.al.picUrl
}));
} catch (error: any) {
console.error('加载用户页面失败:', error);
// 如果获取用户数据失败可能是token过期
if (error.response?.status === 401) {
store.commit('logout');
userStore.handleLogout();
router.push('/login');
} else {
// 添加更多错误处理和重试逻辑
message.error(t('user.message.loadFailed'));
}
} finally {
if (mounted.value) {
@@ -208,22 +219,18 @@ watch(
// 监听用户状态变化
watch(
() => store.state.user,
() => userStore.user,
(newUser) => {
if (!mounted.value) return;
if (!newUser) {
router.push('/login');
} else {
if (newUser) {
loadPage();
}
},
{ immediate: true }
}
);
// 页面挂载时检查登录状态
onMounted(() => {
checkLoginStatus();
checkLoginStatus() && loadData();
});
// 展示歌单
@@ -271,7 +278,7 @@ const handleRemoveFromPlaylist = async (songId: number) => {
const handlePlay = () => {
const tracks = recordList.value || [];
store.commit('setPlayList', tracks);
playerStore.setPlayList(tracks);
};
</script>

View File

@@ -1,8 +1,16 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/i18n/**/*"],
"include": [
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"src/i18n/**/*"
],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
"types": [
"electron-vite/node"
],
"moduleResolution": "bundler",
}
}
}