mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:50:50 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e70fed37da | ||
|
|
b749854c5e | ||
|
|
d9210cc50a | ||
|
|
f186d34885 | ||
|
|
ba992b7c33 | ||
|
|
24d7c839c7 | ||
|
|
a4f3df80c9 | ||
|
|
866fec6ee3 | ||
|
|
8f7d6fbb8d | ||
|
|
62e26cae7d | ||
|
|
ddb814da10 | ||
|
|
e266ea8ef8 | ||
|
|
a894954641 | ||
|
|
f640ab9969 | ||
|
|
9eb17fd978 | ||
|
|
020aca7384 | ||
|
|
fcc47dc0ff | ||
|
|
17ce268da6 | ||
|
|
43c64b1b43 | ||
|
|
11ced6b418 |
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.gitignore
|
||||||
136
.eslintrc.cjs
Normal file
136
.eslintrc.cjs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'eslint-config-airbnb-base',
|
||||||
|
'@vue/typescript/recommended',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:vue-scoped-css/base',
|
||||||
|
'@electron-toolkit',
|
||||||
|
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
es6: true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
defineProps: 'readonly',
|
||||||
|
defineEmits: 'readonly'
|
||||||
|
},
|
||||||
|
plugins: ['vue', '@typescript-eslint', 'simple-import-sort'],
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
sourceType: 'module',
|
||||||
|
allowImportExportEverywhere: true,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/extensions': ['.js', '.jsx', '.ts', '.tsx']
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'no-continue': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'no-return-assign': 'off',
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
'no-return-await': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'guard-for-in': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/first': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'vue/first-attribute-linebreak': 0,
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/ban-types': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'simple-import-sort/imports': 'error',
|
||||||
|
'simple-import-sort/exports': 'error'
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.vue'],
|
||||||
|
rules: {
|
||||||
|
'vue/component-name-in-template-casing': [2, 'kebab-case'],
|
||||||
|
'vue/require-default-prop': 0,
|
||||||
|
'vue/multi-word-component-names': 0,
|
||||||
|
'vue/no-reserved-props': 0,
|
||||||
|
'vue/no-v-html': 0,
|
||||||
|
'vue-scoped-css/enforce-style-type': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allows: ['scoped']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
// 需要行尾分号
|
||||||
|
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'dot-notation': 'off',
|
||||||
|
'constructor-super': 'off',
|
||||||
|
'getter-return': 'off',
|
||||||
|
'no-const-assign': 'off',
|
||||||
|
'no-dupe-args': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'no-dupe-keys': 'off',
|
||||||
|
'no-func-assign': 'off',
|
||||||
|
'no-import-assign': 'off',
|
||||||
|
'no-new-symbol': 'off',
|
||||||
|
'no-obj-calls': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-setter-return': 'off',
|
||||||
|
'no-this-before-super': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'no-unreachable': 'off',
|
||||||
|
'no-unsafe-negation': 'off',
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'prefer-rest-params': 'error',
|
||||||
|
'prefer-spread': 'error',
|
||||||
|
'valid-typeof': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'no-promise-executor-return': 'off',
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
5
.prettierrc.yaml
Normal file
5
.prettierrc.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
singleQuote: true
|
||||||
|
semi: true
|
||||||
|
printWidth: 100
|
||||||
|
trailingComma: none
|
||||||
|
endOfLine: auto
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,13 +1,8 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## [v3.2.0] - 2024-01-04
|
## [v3.5.0] - 2025-01-12
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 添加代理功能和 realIP配置功能 解决内网访问问题 和 国外访问问题 (d7e94a3)
|
- 添加下载管理 进度显示 播放下载的音乐
|
||||||
- 关闭应用的提示修改,可存储配置最小化或关闭 (46f8067)
|
- 添加缓存管理 清理缓存
|
||||||
- 解决检查更新请求失败问题 (cdb9524)
|
- 优化下载格式问题 支持下载其他格式 ps:其实之前只是后缀名问题
|
||||||
|
|
||||||
### 🐞 问题修复
|
|
||||||
- 修复歌词页面与底栏冲突问题(#26) (1dc7d0c)
|
|
||||||
- 修复搜索歌曲列表页面显示错误问题(#33) (1dc7d0c)
|
|
||||||
- 修复搜索类型切换没有重新加载搜索的问题(#25) (ba64631)
|
|
||||||
47
README.md
47
README.md
@@ -3,13 +3,15 @@
|
|||||||
|
|
||||||
- 音乐推荐
|
- 音乐推荐
|
||||||
- 网易云登录
|
- 网易云登录
|
||||||
- 播放历史
|
- 播放历史 歌曲收藏
|
||||||
- 桌面歌词
|
- 桌面歌词
|
||||||
- 歌单 mv 搜索 专辑等功能
|
- 歌单 mv 搜索 专辑等功能
|
||||||
- 识别无法播放歌曲 并解析播放
|
- 识别无法播放歌曲 并解析播放
|
||||||
- 主题切换 更新检测
|
- 主题切换 更新检测
|
||||||
- 本地服务 不依赖线上服务
|
- 本地服务 不依赖线上服务
|
||||||
- 可听周杰伦(搜索专辑)
|
- 可听周杰伦(搜索专辑)
|
||||||
|
- 支持歌曲下载(歌曲右键)
|
||||||
|
- 支持音质选择(网易云VIP)
|
||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
||||||
@@ -40,49 +42,6 @@ QQ群:789288579
|
|||||||
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||||
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
|
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
|
||||||
|
|
||||||
## 项目运行
|
|
||||||
```bash
|
|
||||||
# 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 运行项目 web
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 运行项目 electron
|
|
||||||
npm run start
|
|
||||||
|
|
||||||
# 打包项目 web
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 打包项目 electron
|
|
||||||
npm run win ...
|
|
||||||
# 具体看 package.json
|
|
||||||
```
|
|
||||||
#### 注意
|
|
||||||
- 本地运行需要配置 .env.development 文件
|
|
||||||
- 打包需要配置 .env.production 文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.development
|
|
||||||
VITE_API_LOCAL = /api
|
|
||||||
VITE_API_MUSIC_PROXY = /music
|
|
||||||
VITE_API_PROXY_MUSIC = /music_proxy
|
|
||||||
|
|
||||||
# 你的接口地址 (必填)
|
|
||||||
VITE_API = ***
|
|
||||||
# 音乐po接口地址
|
|
||||||
VITE_API_MUSIC = ***
|
|
||||||
VITE_API_PROXY = ***
|
|
||||||
|
|
||||||
|
|
||||||
# .env.production
|
|
||||||
# 你的接口地址 (必填)
|
|
||||||
VITE_API = ***
|
|
||||||
# 音乐po接口地址
|
|
||||||
VITE_API_MUSIC = ***
|
|
||||||
# 代理地址
|
|
||||||
VITE_API_PROXY = ***
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stargazers over time
|
## Stargazers over time
|
||||||
[](https://starchart.cc/algerkong/AlgerMusicPlayer)
|
[](https://starchart.cc/algerkong/AlgerMusicPlayer)
|
||||||
|
|||||||
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "3.2.0",
|
"version": "3.5.0",
|
||||||
"description": "Alger Music Player",
|
"description": "Alger Music Player",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -28,8 +28,7 @@
|
|||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"netease-cloud-music-api-alger": "^4.25.0"
|
"netease-cloud-music-api-alger": "^4.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"marked": "^15.0.4",
|
|
||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
@@ -62,6 +61,7 @@
|
|||||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"marked": "^15.0.4",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.39.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
@@ -78,16 +78,19 @@
|
|||||||
"vue": "^3.4.30",
|
"vue": "^3.4.30",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.4.3",
|
||||||
"vue-tsc": "^2.0.22",
|
"vue-tsc": "^2.0.22",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0",
|
||||||
|
"animate.css": "^4.1.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.alger.music",
|
"appId": "com.alger.music",
|
||||||
"productName": "AlgerMusicPlayer",
|
"productName": "AlgerMusicPlayer",
|
||||||
"publish": [{
|
"publish": [
|
||||||
"provider": "github",
|
{
|
||||||
"owner": "algerkong",
|
"provider": "github",
|
||||||
"repo": "AlgerMusicPlayer"
|
"owner": "algerkong",
|
||||||
}],
|
"repo": "AlgerMusicPlayer"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"icon": "resources/icon.icns",
|
"icon": "resources/icon.icns",
|
||||||
"target": [
|
"target": [
|
||||||
@@ -116,7 +119,10 @@
|
|||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": ["x64", "ia32"]
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"ia32"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
|
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
|
||||||
@@ -127,11 +133,15 @@
|
|||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "AppImage",
|
"target": "AppImage",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "deb",
|
"target": "deb",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { loadLyricWindow } from './lyric';
|
import { loadLyricWindow } from './lyric';
|
||||||
import { startMusicApi } from './server';
|
import { initializeConfig } from './modules/config';
|
||||||
import { initializeFileManager } from './modules/fileManager';
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
import { initializeTray } from './modules/tray';
|
import { initializeTray } from './modules/tray';
|
||||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
import { initializeConfig } from './modules/config';
|
import { startMusicApi } from './server';
|
||||||
|
|
||||||
// 导入所有图标
|
// 导入所有图标
|
||||||
const iconPath = join(__dirname, '../../resources');
|
const iconPath = join(__dirname, '../../resources');
|
||||||
@@ -15,8 +15,8 @@ const icon = nativeImage.createFromPath(
|
|||||||
process.platform === 'darwin'
|
process.platform === 'darwin'
|
||||||
? join(iconPath, 'icon.icns')
|
? join(iconPath, 'icon.icns')
|
||||||
: process.platform === 'win32'
|
: process.platform === 'win32'
|
||||||
? join(iconPath, 'favicon.ico')
|
? join(iconPath, 'favicon.ico')
|
||||||
: join(iconPath, 'icon.png')
|
: join(iconPath, 'icon.png')
|
||||||
);
|
);
|
||||||
|
|
||||||
let mainWindow: Electron.BrowserWindow;
|
let mainWindow: Electron.BrowserWindow;
|
||||||
@@ -26,19 +26,19 @@ function initialize() {
|
|||||||
// 初始化各个模块
|
// 初始化各个模块
|
||||||
initializeConfig();
|
initializeConfig();
|
||||||
initializeFileManager();
|
initializeFileManager();
|
||||||
|
|
||||||
// 创建主窗口
|
// 创建主窗口
|
||||||
mainWindow = createMainWindow(icon);
|
mainWindow = createMainWindow(icon);
|
||||||
|
|
||||||
// 初始化窗口管理
|
// 初始化窗口管理
|
||||||
initializeWindowManager();
|
initializeWindowManager();
|
||||||
|
|
||||||
// 初始化托盘
|
// 初始化托盘
|
||||||
initializeTray(iconPath, mainWindow);
|
initializeTray(iconPath, mainWindow);
|
||||||
|
|
||||||
// 启动音乐API
|
// 启动音乐API
|
||||||
startMusicApi();
|
startMusicApi();
|
||||||
|
|
||||||
// 加载歌词窗口
|
// 加载歌词窗口
|
||||||
loadLyricWindow(ipcMain, mainWindow);
|
loadLyricWindow(ipcMain, mainWindow);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
import set from '../set.json';
|
import set from '../set.json';
|
||||||
|
|
||||||
interface StoreType {
|
interface StoreType {
|
||||||
@@ -22,7 +23,7 @@ export function initializeConfig() {
|
|||||||
store = new Store<StoreType>({
|
store = new Store<StoreType>({
|
||||||
name: 'config',
|
name: 'config',
|
||||||
defaults: {
|
defaults: {
|
||||||
set: set
|
set
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,4 +40,4 @@ export function initializeConfig() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,53 @@
|
|||||||
import { app, dialog, shell, ipcMain } from 'electron';
|
import axios from 'axios';
|
||||||
|
import { app, dialog, ipcMain, protocol, shell } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import axios from 'axios';
|
|
||||||
|
const MAX_CONCURRENT_DOWNLOADS = 3;
|
||||||
|
const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];
|
||||||
|
let activeDownloads = 0;
|
||||||
|
|
||||||
|
// 创建一个store实例用于存储下载历史
|
||||||
|
const downloadStore = new Store({
|
||||||
|
name: 'downloads',
|
||||||
|
defaults: {
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建一个store实例用于存储音频缓存
|
||||||
|
const audioCacheStore = new Store({
|
||||||
|
name: 'audioCache',
|
||||||
|
defaults: {
|
||||||
|
cache: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化文件管理相关的IPC监听
|
* 初始化文件管理相关的IPC监听
|
||||||
*/
|
*/
|
||||||
export function initializeFileManager() {
|
export function initializeFileManager() {
|
||||||
|
// 注册本地文件协议
|
||||||
|
protocol.registerFileProtocol('local', (request, callback) => {
|
||||||
|
try {
|
||||||
|
const decodedUrl = decodeURIComponent(request.url);
|
||||||
|
const filePath = decodedUrl.replace('local://', '');
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error('File not found:', filePath);
|
||||||
|
callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({ path: filePath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling local protocol:', error);
|
||||||
|
callback({ error: -2 }); // net::FAILED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 通用的选择目录处理
|
// 通用的选择目录处理
|
||||||
ipcMain.handle('select-directory', async () => {
|
ipcMain.handle('select-directory', async () => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
@@ -18,27 +58,221 @@ export function initializeFileManager() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 通用的打开目录处理
|
// 通用的打开目录处理
|
||||||
ipcMain.on('open-directory', (_, path) => {
|
ipcMain.on('open-directory', (_, filePath) => {
|
||||||
shell.openPath(path);
|
try {
|
||||||
|
if (fs.statSync(filePath).isDirectory()) {
|
||||||
|
shell.openPath(filePath);
|
||||||
|
} else {
|
||||||
|
shell.showItemInFolder(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening path:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下载音乐处理
|
// 下载音乐处理
|
||||||
ipcMain.on('download-music', downloadMusic);
|
ipcMain.on('download-music', handleDownloadRequest);
|
||||||
|
|
||||||
|
// 检查文件是否已下载
|
||||||
|
ipcMain.handle('check-music-downloaded', (_, filename: string) => {
|
||||||
|
const store = new Store();
|
||||||
|
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||||
|
const filePath = path.join(downloadPath, `${filename}.mp3`);
|
||||||
|
return fs.existsSync(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除已下载的音乐
|
||||||
|
ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
// 先删除文件
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除对应的歌曲信息
|
||||||
|
const store = new Store();
|
||||||
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
delete songInfos[filePath];
|
||||||
|
store.set('downloadedSongs', songInfos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取已下载音乐列表
|
||||||
|
ipcMain.handle('get-downloaded-music', () => {
|
||||||
|
try {
|
||||||
|
const store = new Store();
|
||||||
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
|
||||||
|
// 过滤出实际存在的文件
|
||||||
|
const validSongs = Object.entries(songInfos)
|
||||||
|
.filter(([path]) => fs.existsSync(path))
|
||||||
|
.map(([_, info]) => info)
|
||||||
|
.sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));
|
||||||
|
|
||||||
|
// 更新存储,移除不存在的文件记录
|
||||||
|
const newSongInfos = validSongs.reduce((acc, song) => {
|
||||||
|
acc[song.path] = song;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
store.set('downloadedSongs', newSongInfos);
|
||||||
|
|
||||||
|
return validSongs;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting downloaded music:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查歌曲是否已下载并返回本地路径
|
||||||
|
ipcMain.handle('check-song-downloaded', (_, songId: number) => {
|
||||||
|
const store = new Store();
|
||||||
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
|
||||||
|
// 通过ID查找已下载的歌曲
|
||||||
|
for (const [path, info] of Object.entries(songInfos)) {
|
||||||
|
if (info.id === songId && fs.existsSync(path)) {
|
||||||
|
return {
|
||||||
|
isDownloaded: true,
|
||||||
|
localPath: `local://${path}`,
|
||||||
|
songInfo: info
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDownloaded: false,
|
||||||
|
localPath: '',
|
||||||
|
songInfo: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加清除下载历史的处理函数
|
||||||
|
ipcMain.on('clear-downloads-history', () => {
|
||||||
|
downloadStore.set('history', []);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加清除音频缓存的处理函数
|
||||||
|
ipcMain.on('clear-audio-cache', () => {
|
||||||
|
audioCacheStore.set('cache', {});
|
||||||
|
// 清除临时音频文件目录
|
||||||
|
const tempDir = path.join(app.getPath('userData'), 'AudioCache');
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
try {
|
||||||
|
fs.readdirSync(tempDir).forEach((file) => {
|
||||||
|
const filePath = path.join(tempDir, file);
|
||||||
|
if (file.endsWith('.mp3') || file.endsWith('.m4a')) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除音频缓存文件失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理下载请求
|
||||||
|
*/
|
||||||
|
function handleDownloadRequest(
|
||||||
|
event: Electron.IpcMainEvent,
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
filename,
|
||||||
|
songInfo,
|
||||||
|
type
|
||||||
|
}: { url: string; filename: string; songInfo?: any; type?: string }
|
||||||
|
) {
|
||||||
|
// 检查是否已经在队列中或正在下载
|
||||||
|
if (downloadQueue.some((item) => item.filename === filename)) {
|
||||||
|
event.reply('music-download-error', {
|
||||||
|
filename,
|
||||||
|
error: '该歌曲已在下载队列中'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已下载
|
||||||
|
const store = new Store();
|
||||||
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
|
||||||
|
// 检查是否已下载(通过ID)
|
||||||
|
const isDownloaded =
|
||||||
|
songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id);
|
||||||
|
|
||||||
|
if (isDownloaded) {
|
||||||
|
event.reply('music-download-error', {
|
||||||
|
filename,
|
||||||
|
error: '该歌曲已下载'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到下载队列
|
||||||
|
downloadQueue.push({ url, filename, songInfo, type });
|
||||||
|
event.reply('music-download-queued', {
|
||||||
|
filename,
|
||||||
|
songInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
// 尝试开始下载
|
||||||
|
processDownloadQueue(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理下载队列
|
||||||
|
*/
|
||||||
|
async function processDownloadQueue(event: Electron.IpcMainEvent) {
|
||||||
|
if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, filename, songInfo, type } = downloadQueue.shift()!;
|
||||||
|
activeDownloads++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadMusic(event, { url, filename, songInfo, type });
|
||||||
|
} finally {
|
||||||
|
activeDownloads--;
|
||||||
|
processDownloadQueue(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载音乐功能
|
* 下载音乐功能
|
||||||
*/
|
*/
|
||||||
async function downloadMusic(event: Electron.IpcMainEvent, { url, filename }: { url: string; filename: string }) {
|
async function downloadMusic(
|
||||||
|
event: Electron.IpcMainEvent,
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
filename,
|
||||||
|
songInfo,
|
||||||
|
type = 'mp3'
|
||||||
|
}: { url: string; filename: string; songInfo: any; type?: string }
|
||||||
|
) {
|
||||||
|
let finalFilePath = '';
|
||||||
|
let writer: fs.WriteStream | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
const downloadPath = store.get('set.downloadPath') as string || app.getPath('downloads');
|
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||||
|
|
||||||
// 直接使用配置的下载路径
|
// 从URL中获取文件扩展名,如果没有则使用传入的type或默认mp3
|
||||||
const filePath = path.join(downloadPath, `${filename}.mp3`);
|
const urlExt = type ? `.${type}` : '.mp3';
|
||||||
|
const filePath = path.join(downloadPath, `${filename}${urlExt}`);
|
||||||
|
|
||||||
// 检查文件是否已存在,如果存在则添加序号
|
// 检查文件是否已存在,如果存在则添加序号
|
||||||
let finalFilePath = filePath;
|
finalFilePath = filePath;
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
while (fs.existsSync(finalFilePath)) {
|
while (fs.existsSync(finalFilePath)) {
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
@@ -47,23 +281,115 @@ async function downloadMusic(event: Electron.IpcMainEvent, { url, filename }: {
|
|||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先获取文件大小
|
||||||
|
const headResponse = await axios.head(url);
|
||||||
|
const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10);
|
||||||
|
|
||||||
|
// 开始下载
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream'
|
responseType: 'stream',
|
||||||
|
timeout: 30000 // 30秒超时
|
||||||
});
|
});
|
||||||
|
|
||||||
const writer = fs.createWriteStream(finalFilePath);
|
writer = fs.createWriteStream(finalFilePath);
|
||||||
response.data.pipe(writer);
|
let downloadedSize = 0;
|
||||||
|
|
||||||
writer.on('finish', () => {
|
// 使用 data 事件来跟踪下载进度
|
||||||
event.reply('music-download-complete', { success: true, path: finalFilePath });
|
response.data.on('data', (chunk: Buffer) => {
|
||||||
|
downloadedSize += chunk.length;
|
||||||
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||||
|
event.reply('music-download-progress', {
|
||||||
|
filename,
|
||||||
|
progress,
|
||||||
|
loaded: downloadedSize,
|
||||||
|
total: totalSize,
|
||||||
|
path: finalFilePath,
|
||||||
|
status: progress === 100 ? 'completed' : 'downloading',
|
||||||
|
songInfo: songInfo || {
|
||||||
|
name: filename,
|
||||||
|
ar: [{ name: '本地音乐' }],
|
||||||
|
picUrl: '/images/default_cover.png'
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
writer.on('error', (err) => {
|
// 等待下载完成
|
||||||
event.reply('music-download-complete', { success: false, error: err.message });
|
await new Promise((resolve, reject) => {
|
||||||
|
writer!.on('finish', resolve);
|
||||||
|
writer!.on('error', reject);
|
||||||
|
response.data.pipe(writer!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 验证文件是否完整下载
|
||||||
|
const stats = fs.statSync(finalFilePath);
|
||||||
|
if (stats.size !== totalSize) {
|
||||||
|
throw new Error('文件下载不完整');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存下载信息
|
||||||
|
try {
|
||||||
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
const defaultInfo = {
|
||||||
|
name: filename,
|
||||||
|
ar: [{ name: '本地音乐' }],
|
||||||
|
picUrl: '/images/default_cover.png'
|
||||||
|
};
|
||||||
|
|
||||||
|
const newSongInfo = {
|
||||||
|
id: songInfo?.id || 0,
|
||||||
|
name: songInfo?.name || filename,
|
||||||
|
filename,
|
||||||
|
picUrl: songInfo?.picUrl || defaultInfo.picUrl,
|
||||||
|
ar: songInfo?.ar || defaultInfo.ar,
|
||||||
|
size: totalSize,
|
||||||
|
path: finalFilePath,
|
||||||
|
downloadTime: Date.now(),
|
||||||
|
al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl },
|
||||||
|
type: type || 'mp3'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到下载记录
|
||||||
|
songInfos[finalFilePath] = newSongInfo;
|
||||||
|
store.set('downloadedSongs', songInfos);
|
||||||
|
|
||||||
|
// 添加到下载历史
|
||||||
|
const history = downloadStore.get('history', []) as any[];
|
||||||
|
history.unshift(newSongInfo);
|
||||||
|
downloadStore.set('history', history);
|
||||||
|
|
||||||
|
// 发送下载完成事件
|
||||||
|
event.reply('music-download-complete', {
|
||||||
|
success: true,
|
||||||
|
path: finalFilePath,
|
||||||
|
filename,
|
||||||
|
size: totalSize,
|
||||||
|
songInfo: newSongInfo
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving download info:', error);
|
||||||
|
throw new Error('保存下载信息失败');
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
event.reply('music-download-complete', { success: false, error: error.message });
|
console.error('Download error:', error);
|
||||||
|
|
||||||
|
// 清理未完成的下载
|
||||||
|
if (writer) {
|
||||||
|
writer.end();
|
||||||
|
}
|
||||||
|
if (finalFilePath && fs.existsSync(finalFilePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(finalFilePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete incomplete download:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.reply('music-download-complete', {
|
||||||
|
success: false,
|
||||||
|
error: error.message || '下载失败',
|
||||||
|
filename
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { app, Menu, nativeImage, Tray, BrowserWindow } from 'electron';
|
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
@@ -7,7 +7,9 @@ let tray: Tray | null = null;
|
|||||||
* 初始化系统托盘
|
* 初始化系统托盘
|
||||||
*/
|
*/
|
||||||
export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
||||||
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
|
const trayIcon = nativeImage
|
||||||
|
.createFromPath(join(iconPath, 'icon_16x16.png'))
|
||||||
|
.resize({ width: 16, height: 16 });
|
||||||
tray = new Tray(trayIcon);
|
tray = new Tray(trayIcon);
|
||||||
|
|
||||||
// 创建一个上下文菜单
|
// 创建一个上下文菜单
|
||||||
@@ -16,15 +18,15 @@ export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
|||||||
label: '显示',
|
label: '显示',
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '退出',
|
label: '退出',
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow.destroy();
|
mainWindow.destroy();
|
||||||
app.quit();
|
app.quit();
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 设置系统托盘图标的上下文菜单
|
// 设置系统托盘图标的上下文菜单
|
||||||
@@ -40,4 +42,4 @@ export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return tray;
|
return tray;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BrowserWindow, shell, ipcMain, app, session } from 'electron';
|
|
||||||
import { is } from '@electron-toolkit/utils';
|
import { is } from '@electron-toolkit/utils';
|
||||||
import { join } from 'path';
|
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
@@ -116,4 +116,4 @@ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import server from 'netease-cloud-music-api-alger/server';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -12,13 +13,10 @@ if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理解锁音乐请求
|
// 处理解锁音乐请求
|
||||||
ipcMain.handle('unblock-music', async (_, id) => {
|
ipcMain.handle('unblock-music', async (_, id, data) => {
|
||||||
return unblockMusic(id);
|
return unblockMusic(id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
import server from 'netease-cloud-music-api-alger/server';
|
|
||||||
|
|
||||||
|
|
||||||
async function startMusicApi(): Promise<void> {
|
async function startMusicApi(): Promise<void> {
|
||||||
console.log('MUSIC API STARTED');
|
console.log('MUSIC API STARTED');
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 7890
|
"port": 7890
|
||||||
},
|
},
|
||||||
"enableRealIP":false,
|
"enableRealIP": false,
|
||||||
"realIP":"",
|
"realIP": "",
|
||||||
"noAnimate": false,
|
"noAnimate": false,
|
||||||
"animationSpeed": 1,
|
"animationSpeed": 1,
|
||||||
"author": "Alger",
|
"author": "Alger",
|
||||||
"authorUrl": "https://github.com/algerkong",
|
"authorUrl": "https://github.com/algerkong",
|
||||||
"musicApiPort": 30488,
|
"musicApiPort": 30488,
|
||||||
"closeAction": "ask"
|
"closeAction": "ask",
|
||||||
|
"musicQuality": "higher"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import match from '@unblockneteasemusic/server';
|
import match from '@unblockneteasemusic/server';
|
||||||
|
|
||||||
const unblockMusic = async (id: any): Promise<any> => {
|
const unblockMusic = async (id: any, songData: any): Promise<any> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'])
|
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'], songData)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
resolve({
|
resolve({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -12,7 +12,8 @@ declare global {
|
|||||||
dragStart: (data: string) => void;
|
dragStart: (data: string) => void;
|
||||||
miniTray: () => void;
|
miniTray: () => void;
|
||||||
restart: () => void;
|
restart: () => void;
|
||||||
unblockMusic: (id: number) => Promise<any>;
|
unblockMusic: (id: number, data: any) => Promise<any>;
|
||||||
};
|
};
|
||||||
|
$message: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { darkTheme, lightTheme } from 'naive-ui';
|
import { darkTheme, lightTheme } from 'naive-ui';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { isElectron } from '@/utils';
|
|
||||||
|
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
import { isMobile } from './utils';
|
import { isMobile } from './utils';
|
||||||
|
|
||||||
@@ -32,10 +32,6 @@ onMounted(() => {
|
|||||||
'setMenus',
|
'setMenus',
|
||||||
homeRouter.filter((item) => item.meta.isMobile)
|
homeRouter.filter((item) => item.meta.isMobile)
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
'qqq ',
|
|
||||||
homeRouter.filter((item) => item.meta.isMobile)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
|
import store from '@/store';
|
||||||
import { ILyric } from '@/type/lyric';
|
import { ILyric } from '@/type/lyric';
|
||||||
import { IPlayMusicUrl } from '@/type/music';
|
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import requestMusic from '@/utils/request_music';
|
import requestMusic from '@/utils/request_music';
|
||||||
|
|
||||||
|
// 获取音乐音质详情
|
||||||
|
export const getMusicQualityDetail = (id: number) => {
|
||||||
|
return request.get('/song/music/detail', { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
// 根据音乐Id获取音乐播放URl
|
// 根据音乐Id获取音乐播放URl
|
||||||
export const getMusicUrl = (id: number) => {
|
export const getMusicUrl = async (id: number) => {
|
||||||
return request.get<IPlayMusicUrl>('/song/url', { params: { id } });
|
const res = await request.get('/song/download/url/v1', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
level: store.state.setData.musicQuality || 'higher'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.data.url) {
|
||||||
|
return { data: { data: [{ ...res.data.data }] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await request.get('/song/url/v1', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
level: store.state.setData.musicQuality || 'higher'
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取歌曲详情
|
// 获取歌曲详情
|
||||||
@@ -18,9 +40,19 @@ export const getMusicLrc = (id: number) => {
|
|||||||
return request.get<ILyric>('/lyric', { params: { id } });
|
return request.get<ILyric>('/lyric', { params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number) => {
|
export const getParsingMusicUrl = (id: number, data: any) => {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
return window.api.unblockMusic(id);
|
return window.api.unblockMusic(id, data);
|
||||||
}
|
}
|
||||||
return requestMusic.get<any>('/music', { params: { id } });
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 收藏歌曲
|
||||||
|
export const likeSong = (id: number, like: boolean = true) => {
|
||||||
|
return request.get('/like', { params: { id, like } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户喜欢的音乐列表
|
||||||
|
export const getLikedList = () => {
|
||||||
|
return request.get('/likelist');
|
||||||
|
};
|
||||||
|
|||||||
8
src/renderer/components.d.ts
vendored
8
src/renderer/components.d.ts
vendored
@@ -8,12 +8,16 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
|
NBadge: typeof import('naive-ui')['NBadge']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
|
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
|
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
@@ -26,11 +30,15 @@ declare module 'vue' {
|
|||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||||
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||||
|
|||||||
@@ -49,8 +49,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NImage, NPopover } from 'naive-ui';
|
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||||
|
|
||||||
import alipay from '@/assets/alipay.png';
|
import alipay from '@/assets/alipay.png';
|
||||||
import wechat from '@/assets/wechat.png';
|
import wechat from '@/assets/wechat.png';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const copyQQ = () => {
|
const copyQQ = () => {
|
||||||
navigator.clipboard.writeText('789288579');
|
navigator.clipboard.writeText('789288579');
|
||||||
|
|||||||
@@ -38,11 +38,6 @@
|
|||||||
:step="0.1"
|
:step="0.1"
|
||||||
@update:value="handleProgressChange"
|
@update:value="handleProgressChange"
|
||||||
>
|
>
|
||||||
<template #rail>
|
|
||||||
<div class="progress-rail">
|
|
||||||
<div class="progress-buffer" :style="{ width: `${bufferedProgress}%` }"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-slider>
|
</n-slider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -320,7 +315,6 @@ onUnmounted(() => {
|
|||||||
if (cursorTimer) {
|
if (cursorTimer) {
|
||||||
clearTimeout(cursorTimer);
|
clearTimeout(cursorTimer);
|
||||||
}
|
}
|
||||||
unlockScreenOrientation();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 currentMv 的变化
|
// 监听 currentMv 的变化
|
||||||
@@ -421,27 +415,6 @@ const checkFullscreenAPI = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加横屏锁定功能
|
|
||||||
const lockScreenOrientation = async () => {
|
|
||||||
try {
|
|
||||||
if ('orientation' in screen) {
|
|
||||||
await (screen as any).orientation.lock('landscape');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('无法锁定屏幕方向:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unlockScreenOrientation = () => {
|
|
||||||
try {
|
|
||||||
if ('orientation' in screen) {
|
|
||||||
(screen as any).orientation.unlock();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('无法解锁屏幕方向:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 修改切换全屏状态的方法
|
// 修改切换全屏状态的方法
|
||||||
const toggleFullscreen = async () => {
|
const toggleFullscreen = async () => {
|
||||||
const api = checkFullscreenAPI();
|
const api = checkFullscreenAPI();
|
||||||
@@ -455,17 +428,9 @@ const toggleFullscreen = async () => {
|
|||||||
if (!api.fullscreenElement) {
|
if (!api.fullscreenElement) {
|
||||||
await videoContainerRef.value?.requestFullscreen();
|
await videoContainerRef.value?.requestFullscreen();
|
||||||
isFullscreen.value = true;
|
isFullscreen.value = true;
|
||||||
// 在移动端进入全屏时锁定横屏
|
|
||||||
if (window.innerWidth <= 768) {
|
|
||||||
await lockScreenOrientation();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
isFullscreen.value = false;
|
isFullscreen.value = false;
|
||||||
// 退出全屏时解锁屏幕方向
|
|
||||||
if (window.innerWidth <= 768) {
|
|
||||||
unlockScreenOrientation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换全屏失败:', error);
|
console.error('切换全屏失败:', error);
|
||||||
@@ -643,10 +608,44 @@ const isMobile = computed(() => store.state.isMobile);
|
|||||||
.custom-slider {
|
.custom-slider {
|
||||||
:deep(.n-slider) {
|
:deep(.n-slider) {
|
||||||
--n-rail-height: 4px;
|
--n-rail-height: 4px;
|
||||||
--n-rail-color: rgba(255, 255, 255, 0.2);
|
--n-rail-color: theme('colors.gray.200');
|
||||||
--n-fill-color: #10b981;
|
--n-rail-color-dark: theme('colors.gray.700');
|
||||||
|
--n-fill-color: theme('colors.green.500');
|
||||||
--n-handle-size: 12px;
|
--n-handle-size: 12px;
|
||||||
--n-handle-color: #10b981;
|
--n-handle-color: theme('colors.green.500');
|
||||||
|
|
||||||
|
&.n-slider--vertical {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.n-slider-rail {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.n-slider-rail {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-handle {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-rail {
|
||||||
|
@apply overflow-hidden transition-all duration-200;
|
||||||
|
@apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-handle {
|
||||||
|
@apply transition-all duration-200;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .n-slider-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<MusicList
|
<music-list
|
||||||
v-model:show="showMusic"
|
v-model:show="showMusic"
|
||||||
:name="albumName"
|
:name="albumName"
|
||||||
:song-list="songList"
|
:song-list="songList"
|
||||||
@@ -36,9 +36,9 @@ import { onMounted, ref } from 'vue';
|
|||||||
|
|
||||||
import { getNewAlbum } from '@/api/home';
|
import { getNewAlbum } from '@/api/home';
|
||||||
import { getAlbum } from '@/api/list';
|
import { getAlbum } from '@/api/list';
|
||||||
|
import MusicList from '@/components/MusicList.vue';
|
||||||
import type { IAlbumNew } from '@/type/album';
|
import type { IAlbumNew } from '@/type/album';
|
||||||
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
import MusicList from '@/components/MusicList.vue';
|
|
||||||
|
|
||||||
const albumData = ref<IAlbumNew>();
|
const albumData = ref<IAlbumNew>();
|
||||||
const loadAlbumList = async () => {
|
const loadAlbumList = async () => {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
|
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
|
||||||
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,11 +74,11 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { getDayRecommend, getHotSinger } from '@/api/home';
|
import { getDayRecommend, getHotSinger } from '@/api/home';
|
||||||
|
import MusicList from '@/components/MusicList.vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { IDayRecommend } from '@/type/day_recommend';
|
import { IDayRecommend } from '@/type/day_recommend';
|
||||||
import type { IHotSinger } from '@/type/singer';
|
import type { IHotSinger } from '@/type/singer';
|
||||||
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
||||||
import MusicList from '@/components/MusicList.vue';
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
|
|||||||
386
src/renderer/components/common/DonationList.vue
Normal file
386
src/renderer/components/common/DonationList.vue
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<template>
|
||||||
|
<div class="donation-container">
|
||||||
|
<div class="refresh-container">
|
||||||
|
<n-button secondary round size="small" :loading="isLoading" @click="fetchDonors">
|
||||||
|
<template #icon>
|
||||||
|
<i class="ri-refresh-line"></i>
|
||||||
|
</template>
|
||||||
|
刷新列表
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="donation-grid" :class="{ 'grid-expanded': isExpanded }">
|
||||||
|
<div
|
||||||
|
v-for="(donor, index) in displayDonors"
|
||||||
|
:key="donor.id"
|
||||||
|
class="donation-card animate__animated"
|
||||||
|
:class="getAnimationClass(index)"
|
||||||
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="donor-avatar">
|
||||||
|
<n-avatar
|
||||||
|
:src="donor.avatar"
|
||||||
|
:fallback-src="defaultAvatar"
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
class="animate__animated animate__pulse animate__infinite avatar-img"
|
||||||
|
/>
|
||||||
|
<div class="donor-badge" :class="getBadgeClass(donor.badge)">
|
||||||
|
{{ donor.badge }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="donor-info">
|
||||||
|
<div class="donor-name">{{ donor.name }}</div>
|
||||||
|
<div class="donation-meta">
|
||||||
|
<n-tag
|
||||||
|
:type="getAmountTagType(donor.amount)"
|
||||||
|
size="small"
|
||||||
|
class="donation-amount animate__animated"
|
||||||
|
round
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
¥{{ donor.amount }}
|
||||||
|
</n-tag>
|
||||||
|
<span class="donation-date">{{ donor.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="donor.message" class="donation-message">
|
||||||
|
<n-popover trigger="hover" placement="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="message-content">
|
||||||
|
<i class="ri-double-quotes-l quote-icon"></i>
|
||||||
|
<div class="message-text">{{ donor.message }}</div>
|
||||||
|
<i class="ri-double-quotes-r quote-icon"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="message-popup">
|
||||||
|
<i class="ri-double-quotes-l quote-icon"></i>
|
||||||
|
{{ donor.message }}
|
||||||
|
<i class="ri-double-quotes-r quote-icon"></i>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</div>
|
||||||
|
<div class="card-sparkles"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sortedDonors.length > 8" class="expand-button">
|
||||||
|
<n-button text @click="toggleExpand">
|
||||||
|
<template #icon>
|
||||||
|
<i :class="isExpanded ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"></i>
|
||||||
|
</template>
|
||||||
|
{{ isExpanded ? '收起' : '展开更多' }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<n-image
|
||||||
|
:src="alipay"
|
||||||
|
alt="支付宝收款码"
|
||||||
|
class="w-60 h-60 rounded-lg cursor-none"
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<n-image
|
||||||
|
:src="wechat"
|
||||||
|
alt="微信收款码"
|
||||||
|
class="w-60 h-60 rounded-lg cursor-none"
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import 'animate.css';
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import alipay from '@/assets/alipay.png';
|
||||||
|
import wechat from '@/assets/wechat.png';
|
||||||
|
|
||||||
|
// 默认头像
|
||||||
|
const defaultAvatar = 'https://avatars.githubusercontent.com/u/0?v=4';
|
||||||
|
|
||||||
|
// 捐赠者数据
|
||||||
|
interface Donor {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
date: string;
|
||||||
|
message?: string;
|
||||||
|
avatar?: string;
|
||||||
|
badge: string;
|
||||||
|
badgeColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const donors = ref<Donor[]>([]);
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const fetchDonors = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
'https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/data/main/donors.json'
|
||||||
|
);
|
||||||
|
donors.value = response.data.map((donor: Donor, index: number) => ({
|
||||||
|
...donor,
|
||||||
|
avatar: `https://api.dicebear.com/7.x/micah/svg?seed=${index}`
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch donors:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDonors();
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchDonors();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动画类名列表
|
||||||
|
const animationClasses = [
|
||||||
|
'animate__fadeInUp',
|
||||||
|
'animate__fadeInLeft',
|
||||||
|
'animate__fadeInRight',
|
||||||
|
'animate__zoomIn'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取随机动画类名
|
||||||
|
const getAnimationClass = (index: number) => {
|
||||||
|
return animationClasses[index % animationClasses.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据金额获取标签类型
|
||||||
|
const getAmountTagType = (amount: number): 'success' | 'warning' | 'error' | 'info' => {
|
||||||
|
if (amount >= 5) return 'warning';
|
||||||
|
if (amount >= 2) return 'success';
|
||||||
|
return 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取徽章样式类名
|
||||||
|
const getBadgeClass = (badge: string): string => {
|
||||||
|
if (badge.includes('金牌')) return 'badge-gold';
|
||||||
|
if (badge.includes('银牌')) return 'badge-silver';
|
||||||
|
return 'badge-bronze';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标悬停效果
|
||||||
|
const handleMouseEnter = (event: MouseEvent) => {
|
||||||
|
const card = event.currentTarget as HTMLElement;
|
||||||
|
card.style.transform = 'translateY(-2px)';
|
||||||
|
card.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.12)';
|
||||||
|
|
||||||
|
// 添加金额标签动画
|
||||||
|
const amountTag = card.querySelector('.donation-amount');
|
||||||
|
if (amountTag) {
|
||||||
|
amountTag.classList.add('animate__tada');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (event: MouseEvent) => {
|
||||||
|
const card = event.currentTarget as HTMLElement;
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
card.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.08)';
|
||||||
|
|
||||||
|
// 移除金额标签动画
|
||||||
|
const amountTag = card.querySelector('.donation-amount');
|
||||||
|
if (amountTag) {
|
||||||
|
amountTag.classList.remove('animate__tada');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按金额和留言排序的捐赠列表
|
||||||
|
const sortedDonors = computed(() => {
|
||||||
|
return [...donors.value].sort((a, b) => {
|
||||||
|
// 如果一个有留言一个没有,有留言的排在前面
|
||||||
|
if (a.message && !b.message) return -1;
|
||||||
|
if (!a.message && b.message) return 1;
|
||||||
|
|
||||||
|
// 都有留言或都没有留言时,按金额从大到小排序
|
||||||
|
const amountDiff = b.amount - a.amount;
|
||||||
|
if (amountDiff !== 0) return amountDiff;
|
||||||
|
|
||||||
|
// 金额相同时,按日期从新到旧排序
|
||||||
|
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
|
const displayDonors = computed(() => {
|
||||||
|
if (isExpanded.value) {
|
||||||
|
return sortedDonors.value;
|
||||||
|
}
|
||||||
|
return sortedDonors.value.slice(0, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleExpand = () => {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.donation-container {
|
||||||
|
@apply w-full overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-grid {
|
||||||
|
@apply grid gap-3 px-2 py-3 transition-all duration-300 overflow-hidden;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
max-height: 280px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid-expanded {
|
||||||
|
@apply max-h-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-card {
|
||||||
|
@apply relative rounded-lg p-3 min-w-0 w-full transition-all duration-500 shadow-md backdrop-blur-md;
|
||||||
|
@apply bg-gradient-to-br from-white/[0.03] to-white/[0.08] border border-white/[0.08];
|
||||||
|
@apply hover:shadow-lg;
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
@apply relative z-10 flex items-start gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donor-avatar {
|
||||||
|
@apply relative flex-shrink-0 w-10 h-9 transition-transform duration-300;
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
@apply border border-white/20 dark:border-gray-800/50 shadow-sm;
|
||||||
|
@apply w-10 h-9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donor-badge {
|
||||||
|
@apply absolute -bottom-2 -right-1 px-1.5 py-0.5 text-xs font-medium text-white/90 rounded-full whitespace-nowrap;
|
||||||
|
@apply bg-gradient-to-r from-pink-400 to-pink-500 shadow-sm opacity-90 scale-90;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donor-info {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
|
||||||
|
.donor-name {
|
||||||
|
@apply text-sm font-medium mb-0.5 truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-meta {
|
||||||
|
@apply flex items-center gap-2 mb-1;
|
||||||
|
|
||||||
|
.donation-date {
|
||||||
|
@apply text-xs text-gray-400/80 dark:text-gray-500/80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-message {
|
||||||
|
@apply text-sm text-gray-600 dark:text-gray-300 leading-relaxed mt-3 w-full;
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
@apply relative p-2 rounded-lg transition-all duration-300 cursor-pointer;
|
||||||
|
@apply bg-white/[0.02] hover:bg-[var(--n-color)];
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
@apply px-6 italic line-clamp-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-icon {
|
||||||
|
@apply absolute text-gray-400/60 dark:text-gray-500/60 text-sm;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
@apply left-0.5 top-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
@apply right-0.5 bottom-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.donor-avatar {
|
||||||
|
@apply scale-105 rotate-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donor-badge {
|
||||||
|
@apply scale-95 -translate-y-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sparkles {
|
||||||
|
@apply opacity-60 scale-110;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sparkles {
|
||||||
|
@apply absolute inset-0 pointer-events-none opacity-0 transition-all duration-500;
|
||||||
|
background-image: radial-gradient(2px 2px at 20px 30px, rgba(255, 255, 255, 0.95), transparent),
|
||||||
|
radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.95), transparent),
|
||||||
|
radial-gradient(2.5px 2.5px at 50px 160px, rgba(255, 255, 255, 0.95), transparent),
|
||||||
|
radial-gradient(2px 2px at 90px 40px, rgba(255, 255, 255, 0.95), transparent),
|
||||||
|
radial-gradient(2.5px 2.5px at 130px 80px, rgba(255, 255, 255, 0.95), transparent);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: sparkle 8s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
@apply bg-[0%_0%] opacity-40 scale-100;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
@apply bg-[100%_100%] opacity-80 scale-110;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-container {
|
||||||
|
@apply flex justify-end px-2 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button {
|
||||||
|
@apply flex justify-center items-center py-2;
|
||||||
|
|
||||||
|
:deep(.n-button) {
|
||||||
|
@apply transition-all duration-300 hover:-translate-y-0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-popup {
|
||||||
|
@apply relative px-4 py-2 text-sm;
|
||||||
|
max-width: 300px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
.quote-icon {
|
||||||
|
@apply text-gray-400/60 dark:text-gray-500/60;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
590
src/renderer/components/common/DownloadDrawer.vue
Normal file
590
src/renderer/components/common/DownloadDrawer.vue
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
<template>
|
||||||
|
<div class="download-drawer-trigger">
|
||||||
|
<n-badge :value="downloadingCount" :max="99" :show="downloadingCount > 0">
|
||||||
|
<n-button circle @click="showDrawer = true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-download-cloud-2-line"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</n-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-drawer v-model:show="showDrawer" :height="'80%'" placement="bottom">
|
||||||
|
<n-drawer-content title="下载管理" closable :native-scrollbar="false">
|
||||||
|
<div class="drawer-container">
|
||||||
|
<n-tabs type="line" animated class="h-full">
|
||||||
|
<!-- 下载列表 -->
|
||||||
|
<n-tab-pane name="downloading" tab="下载中" class="h-full">
|
||||||
|
<div class="download-list">
|
||||||
|
<div v-if="downloadList.length === 0" class="empty-tip">
|
||||||
|
<n-empty description="暂无下载任务" />
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="total-progress">
|
||||||
|
<div class="total-progress-text">总进度: {{ totalProgress.toFixed(1) }}%</div>
|
||||||
|
<n-progress
|
||||||
|
type="line"
|
||||||
|
:percentage="Number(totalProgress.toFixed(1))"
|
||||||
|
:height="12"
|
||||||
|
:border-radius="6"
|
||||||
|
:indicator-placement="'inside'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="download-content">
|
||||||
|
<div class="download-items">
|
||||||
|
<div v-for="item in downloadList" :key="item.path" class="download-item">
|
||||||
|
<div class="download-item-content">
|
||||||
|
<div class="download-item-cover">
|
||||||
|
<n-image
|
||||||
|
:src="getImgUrl(item.songInfo?.picUrl, '200y200')"
|
||||||
|
preview-disabled
|
||||||
|
:object-fit="'cover'"
|
||||||
|
class="cover-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="download-item-info">
|
||||||
|
<div class="download-item-name" :title="item.filename">
|
||||||
|
{{ item.filename }}
|
||||||
|
</div>
|
||||||
|
<div class="download-item-artist">
|
||||||
|
{{ item.songInfo?.ar?.map((a) => a.name).join(', ') || '未知歌手' }}
|
||||||
|
</div>
|
||||||
|
<div class="download-item-progress">
|
||||||
|
<n-progress
|
||||||
|
type="line"
|
||||||
|
:percentage="item.progress"
|
||||||
|
:processing="item.status === 'downloading'"
|
||||||
|
:status="getProgressStatus(item)"
|
||||||
|
:height="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="download-item-size">
|
||||||
|
<span
|
||||||
|
>{{ formatSize(item.loaded) }} / {{ formatSize(item.total) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="download-item-status">
|
||||||
|
<n-tag :type="getStatusType(item)" size="small">
|
||||||
|
{{ getStatusText(item) }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 已下载列表 -->
|
||||||
|
<n-tab-pane name="downloaded" tab="已下载" class="h-full">
|
||||||
|
<div class="downloaded-list">
|
||||||
|
<div v-if="downloadedList.length === 0" class="empty-tip">
|
||||||
|
<n-empty description="暂无已下载歌曲" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="downloaded-content">
|
||||||
|
<div class="downloaded-items">
|
||||||
|
<div v-for="item in downloadedList" :key="item.path" class="downloaded-item">
|
||||||
|
<div class="downloaded-item-content">
|
||||||
|
<div class="downloaded-item-cover">
|
||||||
|
<n-image
|
||||||
|
:src="getImgUrl(item.picUrl, '200y200')"
|
||||||
|
preview-disabled
|
||||||
|
:object-fit="'cover'"
|
||||||
|
class="cover-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="downloaded-item-info">
|
||||||
|
<div class="downloaded-item-name" :title="item.filename">
|
||||||
|
{{ item.filename }}
|
||||||
|
</div>
|
||||||
|
<div class="downloaded-item-artist">
|
||||||
|
{{ item.ar?.map((a) => a.name).join(', ') }}
|
||||||
|
</div>
|
||||||
|
<div class="downloaded-item-size">{{ formatSize(item.size) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="downloaded-item-actions">
|
||||||
|
<n-button text type="primary" size="large" @click="handlePlayMusic(item)">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-play-circle-line text-xl"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="openDirectory(item.path)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-folder-open-line text-xl"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button text type="error" size="large" @click="handleDelete(item)">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-delete-bin-line text-xl"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<n-modal v-model:show="showDeleteConfirm" preset="dialog" type="warning" title="删除确认">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="iconfont ri-error-warning-line mr-2 text-xl"></i>
|
||||||
|
<span>删除确认</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="delete-confirm-content">
|
||||||
|
确定要删除歌曲 "{{ itemToDelete?.filename }}" 吗?此操作不可恢复。
|
||||||
|
</div>
|
||||||
|
<template #action>
|
||||||
|
<n-button size="small" @click="showDeleteConfirm = false">取消</n-button>
|
||||||
|
<n-button size="small" type="warning" @click="confirmDelete">确定删除</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ProgressStatus } from 'naive-ui';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import { getMusicDetail } from '@/api/music';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
|
interface DownloadItem {
|
||||||
|
filename: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
path: string;
|
||||||
|
status: 'downloading' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
songInfo?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadedItem {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
id: number;
|
||||||
|
picUrl: string;
|
||||||
|
ar: { name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const message = useMessage();
|
||||||
|
const showDrawer = ref(false);
|
||||||
|
const downloadList = ref<DownloadItem[]>([]);
|
||||||
|
const downloadedList = ref<DownloadedItem[]>([]);
|
||||||
|
|
||||||
|
// 获取播放状态
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算总进度
|
||||||
|
const totalProgress = computed(() => {
|
||||||
|
if (downloadList.value.length === 0) return 0;
|
||||||
|
const total = downloadList.value.reduce((sum, item) => sum + item.progress, 0);
|
||||||
|
return total / downloadList.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(totalProgress, (newVal) => {
|
||||||
|
if (newVal === 100) {
|
||||||
|
refreshDownloadedList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (item: DownloadItem) => {
|
||||||
|
switch (item.status) {
|
||||||
|
case 'downloading':
|
||||||
|
return 'info';
|
||||||
|
case 'completed':
|
||||||
|
return 'success';
|
||||||
|
case 'error':
|
||||||
|
return 'error';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (item: DownloadItem) => {
|
||||||
|
switch (item.status) {
|
||||||
|
case 'downloading':
|
||||||
|
return '下载中';
|
||||||
|
case 'completed':
|
||||||
|
return '已完成';
|
||||||
|
case 'error':
|
||||||
|
return '失败';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取进度条状态
|
||||||
|
const getProgressStatus = (item: DownloadItem): ProgressStatus => {
|
||||||
|
switch (item.status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'success';
|
||||||
|
case 'error':
|
||||||
|
return 'error';
|
||||||
|
default:
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number) => {
|
||||||
|
if (!bytes) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开目录
|
||||||
|
const openDirectory = (path: string) => {
|
||||||
|
const directory = path.substring(0, path.lastIndexOf('/'));
|
||||||
|
window.electron.ipcRenderer.send('open-directory', directory);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除相关
|
||||||
|
const showDeleteConfirm = ref(false);
|
||||||
|
const itemToDelete = ref<DownloadedItem | null>(null);
|
||||||
|
|
||||||
|
// 处理删除点击
|
||||||
|
const handleDelete = (item: DownloadedItem) => {
|
||||||
|
itemToDelete.value = item;
|
||||||
|
showDeleteConfirm.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!itemToDelete.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await window.electron.ipcRenderer.invoke(
|
||||||
|
'delete-downloaded-music',
|
||||||
|
itemToDelete.value.path
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
await refreshDownloadedList();
|
||||||
|
message.success('删除成功');
|
||||||
|
} else {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete music:', error);
|
||||||
|
message.error('删除失败');
|
||||||
|
} finally {
|
||||||
|
showDeleteConfirm.value = false;
|
||||||
|
itemToDelete.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 播放音乐
|
||||||
|
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 refreshDownloadedList = async () => {
|
||||||
|
try {
|
||||||
|
const list = await window.electron.ipcRenderer.invoke('get-downloaded-music');
|
||||||
|
if (!Array.isArray(list) || list.length === 0) {
|
||||||
|
downloadedList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const songIds = list.filter((item) => item.id).map((item) => item.id);
|
||||||
|
|
||||||
|
// 如果有歌曲ID,获取详细信息
|
||||||
|
if (songIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const detailRes = await getMusicDetail(songIds);
|
||||||
|
const songDetails = detailRes.data.songs.reduce((acc, song) => {
|
||||||
|
acc[song.id] = song;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
downloadedList.value = list.map((item) => {
|
||||||
|
const songDetail = songDetails[item.id];
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
picUrl: songDetail?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
||||||
|
ar: songDetail?.ar || item.ar || [{ name: '本地音乐' }]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (detailError) {
|
||||||
|
console.error('Failed to get music details:', detailError);
|
||||||
|
downloadedList.value = list;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadedList.value = list;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get downloaded music list:', error);
|
||||||
|
downloadedList.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听抽屉显示状态
|
||||||
|
watch(
|
||||||
|
() => showDrawer.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
refreshDownloadedList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听下载进度
|
||||||
|
onMounted(() => {
|
||||||
|
refreshDownloadedList();
|
||||||
|
|
||||||
|
// 监听下载进度
|
||||||
|
window.electron.ipcRenderer.on('music-download-progress', (_, data) => {
|
||||||
|
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||||
|
if (existingItem) {
|
||||||
|
Object.assign(existingItem, {
|
||||||
|
...data,
|
||||||
|
songInfo: data.songInfo || existingItem.songInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果下载完成,从列表中移除
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadList.value.push({
|
||||||
|
...data,
|
||||||
|
songInfo: data.songInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听下载完成
|
||||||
|
window.electron.ipcRenderer.on('music-download-complete', (_, data) => {
|
||||||
|
if (data.success) {
|
||||||
|
// 从下载列表中移除
|
||||||
|
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||||
|
// 刷新已下载列表
|
||||||
|
refreshDownloadedList();
|
||||||
|
message.success(`${data.filename} 下载完成`);
|
||||||
|
} else {
|
||||||
|
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||||
|
if (existingItem) {
|
||||||
|
Object.assign(existingItem, {
|
||||||
|
status: 'error',
|
||||||
|
error: data.error,
|
||||||
|
progress: 0
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
message.error(`${data.filename} 下载失败: ${data.error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听下载队列
|
||||||
|
window.electron.ipcRenderer.on('music-download-queued', (_, data) => {
|
||||||
|
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||||
|
if (!existingItem) {
|
||||||
|
downloadList.value.push({
|
||||||
|
filename: data.filename,
|
||||||
|
progress: 0,
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
path: '',
|
||||||
|
status: 'downloading',
|
||||||
|
songInfo: data.songInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.download-drawer-trigger {
|
||||||
|
@apply fixed left-6 bottom-24 z-[999];
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
@apply bg-white/80 dark:bg-gray-800/80 shadow-lg backdrop-blur-sm;
|
||||||
|
@apply hover:bg-light dark:hover:bg-dark-200;
|
||||||
|
@apply text-gray-600 dark:text-gray-300;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
@apply w-10 h-10;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-container {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-list,
|
||||||
|
.downloaded-list {
|
||||||
|
@apply flex flex-col h-full;
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
@apply flex-1 flex items-center justify-center;
|
||||||
|
@apply text-gray-400 dark:text-gray-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-content,
|
||||||
|
.downloaded-content {
|
||||||
|
@apply flex-1 overflow-hidden pb-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-items,
|
||||||
|
.downloaded-items {
|
||||||
|
@apply space-y-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-progress {
|
||||||
|
@apply px-4 py-3 bg-light-100 dark:bg-dark-200 backdrop-blur-sm;
|
||||||
|
@apply border-b border-gray-100 dark:border-gray-800;
|
||||||
|
@apply sticky top-0 z-10;
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
@apply mb-2 text-sm font-medium text-gray-600 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item,
|
||||||
|
.downloaded-item {
|
||||||
|
@apply p-3 rounded-lg;
|
||||||
|
@apply bg-light-100 dark:bg-dark-200 backdrop-blur-sm;
|
||||||
|
@apply border border-gray-100 dark:border-gray-700;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
@apply hover:bg-light-300 dark:hover:bg-dark-300;
|
||||||
|
@apply hover:shadow-md;
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-cover {
|
||||||
|
@apply w-10 h-10 flex-shrink-0 rounded-lg overflow-hidden;
|
||||||
|
@apply shadow-md;
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-name {
|
||||||
|
@apply text-sm font-medium truncate;
|
||||||
|
@apply text-gray-900 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-artist {
|
||||||
|
@apply text-xs text-gray-500 dark:text-gray-400 truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-progress {
|
||||||
|
@apply mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-size {
|
||||||
|
@apply text-xs text-gray-500 dark:text-gray-400 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-status {
|
||||||
|
@apply flex-shrink-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloaded-item {
|
||||||
|
&-actions {
|
||||||
|
@apply flex items-center gap-1;
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
@apply p-2;
|
||||||
|
@apply hover:bg-gray-200/80 dark:hover:bg-gray-600/80;
|
||||||
|
@apply rounded-lg;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-confirm-content {
|
||||||
|
@apply py-6 px-4;
|
||||||
|
@apply text-base text-gray-600 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,10 +39,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { isElectron, isMobile } from '@/utils';
|
import { isElectron, isMobile } from '@/utils';
|
||||||
import config from '../../../../package.json';
|
|
||||||
import { getLatestReleaseInfo } from '@/utils/update';
|
import { getLatestReleaseInfo } from '@/utils/update';
|
||||||
|
|
||||||
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const noPrompt = ref(false);
|
const noPrompt = ref(false);
|
||||||
const releaseInfo = ref<any>(null);
|
const releaseInfo = ref<any>(null);
|
||||||
@@ -77,36 +79,33 @@ const handleInstall = async (): Promise<void> => {
|
|||||||
const isMac = userAgent.toLowerCase().includes('mac');
|
const isMac = userAgent.toLowerCase().includes('mac');
|
||||||
const isWindows = userAgent.toLowerCase().includes('win');
|
const isWindows = userAgent.toLowerCase().includes('win');
|
||||||
const isLinux = userAgent.toLowerCase().includes('linux');
|
const isLinux = userAgent.toLowerCase().includes('linux');
|
||||||
const isX64 = userAgent.includes('x86_64') ||
|
const isX64 =
|
||||||
userAgent.includes('Win64') ||
|
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
||||||
userAgent.includes('WOW64');
|
|
||||||
|
|
||||||
let downloadUrl = '';
|
let downloadUrl = '';
|
||||||
|
|
||||||
// 根据平台和架构选择对应的安装包
|
// 根据平台和架构选择对应的安装包
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
// macOS
|
// macOS
|
||||||
const macAsset = assets.find(asset =>
|
const macAsset = assets.find((asset) => asset.name.includes('mac'));
|
||||||
asset.name.includes('mac')
|
|
||||||
);
|
|
||||||
downloadUrl = macAsset?.browser_download_url || '';
|
downloadUrl = macAsset?.browser_download_url || '';
|
||||||
} else if (isWindows) {
|
} else if (isWindows) {
|
||||||
// Windows
|
// Windows
|
||||||
let winAsset = assets.find(asset =>
|
let winAsset = assets.find(
|
||||||
asset.name.includes('win') &&
|
(asset) =>
|
||||||
(isX64 ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
asset.name.includes('win') &&
|
||||||
|
(isX64 ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
||||||
);
|
);
|
||||||
if(!winAsset){
|
if (!winAsset) {
|
||||||
winAsset = assets.find(asset =>
|
winAsset = assets.find((asset) => asset.name.includes('win.exe'));
|
||||||
asset.name.includes('win.exe')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
downloadUrl = winAsset?.browser_download_url || '';
|
downloadUrl = winAsset?.browser_download_url || '';
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
// Linux
|
// Linux
|
||||||
const linuxAsset = assets.find(asset =>
|
const linuxAsset = assets.find(
|
||||||
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
(asset) =>
|
||||||
asset.name.includes('x64')
|
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
||||||
|
asset.name.includes('x64')
|
||||||
);
|
);
|
||||||
downloadUrl = linuxAsset?.browser_download_url || '';
|
downloadUrl = linuxAsset?.browser_download_url || '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<p class="search-item-artist">{{ item.desc }}</p>
|
<p class="search-item-artist">{{ item.desc }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MusicList
|
<music-list
|
||||||
v-if="['专辑', 'playlist'].includes(item.type)"
|
v-if="['专辑', 'playlist'].includes(item.type)"
|
||||||
v-model:show="showPop"
|
v-model:show="showPop"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
@@ -40,6 +40,7 @@ import MvPlayer from '@/components/MvPlayer.vue';
|
|||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import { IMvItem } from '@/type/mv';
|
import { IMvItem } from '@/type/mv';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
import MusicList from '../MusicList.vue';
|
import MusicList from '../MusicList.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }" @contextmenu.prevent="handleContextMenu">
|
<div
|
||||||
|
class="song-item"
|
||||||
|
:class="{ 'song-mini': mini, 'song-list': list }"
|
||||||
|
@contextmenu.prevent="handleContextMenu"
|
||||||
|
>
|
||||||
|
<div v-if="selectable" class="song-item-select" @click.stop="toggleSelect">
|
||||||
|
<n-checkbox :checked="selected" />
|
||||||
|
</div>
|
||||||
<n-image
|
<n-image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
ref="songImg"
|
ref="songImg"
|
||||||
@@ -71,16 +78,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import type { MenuOption } from 'naive-ui';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
import { computed, h, ref, useTemplateRef } from 'vue';
|
import { computed, h, ref, useTemplateRef } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useMessage } from 'naive-ui';
|
|
||||||
import type { MenuOption } from 'naive-ui';
|
|
||||||
|
|
||||||
|
import { getSongUrl } from '@/hooks/MusicListHook';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isElectron } from '@/utils';
|
import { getImgUrl, isElectron } from '@/utils';
|
||||||
import { getImageBackground } from '@/utils/linearColor';
|
import { getImageBackground } from '@/utils/linearColor';
|
||||||
import { getSongUrl } from '@/hooks/MusicListHook';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -88,11 +96,15 @@ const props = withDefaults(
|
|||||||
mini?: boolean;
|
mini?: boolean;
|
||||||
list?: boolean;
|
list?: boolean;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
|
selectable?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
mini: false,
|
mini: false,
|
||||||
list: false,
|
list: false,
|
||||||
favorite: true
|
favorite: true,
|
||||||
|
selectable: false,
|
||||||
|
selected: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,7 +128,7 @@ const isDownloading = ref(false);
|
|||||||
|
|
||||||
const dropdownOptions = computed<MenuOption[]>(() => [
|
const dropdownOptions = computed<MenuOption[]>(() => [
|
||||||
{
|
{
|
||||||
label: isDownloading.value ? '下载中...' : '下载 ' + props.item.name,
|
label: isDownloading.value ? '下载中...' : `下载 ${props.item.name}`,
|
||||||
key: 'download',
|
key: 'download',
|
||||||
icon: () => h('i', { class: 'iconfont ri-download-line' }),
|
icon: () => h('i', { class: 'iconfont ri-download-line' }),
|
||||||
disabled: isDownloading.value
|
disabled: isDownloading.value
|
||||||
@@ -146,47 +158,65 @@ const downloadMusic = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isDownloading.value = true;
|
isDownloading.value = true;
|
||||||
const loadingMessage = message.loading('正在下载中...', { duration: 0 });
|
|
||||||
|
const data = (await getSongUrl(props.item.id, cloneDeep(props.item), true)) as any;
|
||||||
const url = await getSongUrl(props.item.id);
|
if (!data || !data.url) {
|
||||||
if (!url) {
|
throw new Error('获取音乐下载地址失败');
|
||||||
loadingMessage.destroy();
|
|
||||||
message.error('获取音乐下载地址失败');
|
|
||||||
isDownloading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先移除可能存在的旧监听器
|
// 构建文件名
|
||||||
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(',');
|
||||||
|
const filename = `${props.item.name} - ${artistNames}`;
|
||||||
|
|
||||||
// 发送下载请求
|
// 发送下载请求
|
||||||
window.electron.ipcRenderer.send('download-music', {
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
url,
|
url: data.url,
|
||||||
filename: `${props.item.name} - ${(props.item.ar || props.item.song?.artists)?.map(a => a.name).join(',')}`
|
type: data.type,
|
||||||
});
|
filename,
|
||||||
|
songInfo: {
|
||||||
// 添加新的一次性监听器
|
...cloneDeep(props.item),
|
||||||
window.electron.ipcRenderer.once('music-download-complete', (_, result) => {
|
downloadTime: Date.now()
|
||||||
isDownloading.value = false;
|
|
||||||
loadingMessage.destroy();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
message.success('下载成功');
|
|
||||||
} else if (result.canceled) {
|
|
||||||
// 用户取消了保存
|
|
||||||
message.info('已取消下载');
|
|
||||||
} else {
|
|
||||||
message.error(`下载失败: ${result.error}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
|
message.success('已加入下载队列');
|
||||||
|
|
||||||
|
// 监听下载完成事件
|
||||||
|
const handleDownloadComplete = (_, result) => {
|
||||||
|
if (result.filename === filename) {
|
||||||
|
isDownloading.value = false;
|
||||||
|
removeListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听下载错误事件
|
||||||
|
const handleDownloadError = (_, result) => {
|
||||||
|
if (result.filename === filename) {
|
||||||
|
isDownloading.value = false;
|
||||||
|
removeListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除监听器函数
|
||||||
|
const removeListeners = () => {
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-complete', handleDownloadComplete);
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-error', handleDownloadError);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加事件监听器
|
||||||
|
window.electron.ipcRenderer.once('music-download-complete', handleDownloadComplete);
|
||||||
|
window.electron.ipcRenderer.once('music-download-error', handleDownloadError);
|
||||||
|
|
||||||
|
// 30秒后自动清理监听器(以防下载过程中出现未知错误)
|
||||||
|
setTimeout(removeListeners, 30000);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Download error:', error);
|
||||||
isDownloading.value = false;
|
isDownloading.value = false;
|
||||||
message.destroyAll();
|
message.error(error.message || '下载失败');
|
||||||
message.error('下载失败');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const emits = defineEmits(['play']);
|
const emits = defineEmits(['play', 'select']);
|
||||||
const songImageRef = useTemplateRef('songImg');
|
const songImageRef = useTemplateRef('songImg');
|
||||||
|
|
||||||
const imageLoad = async () => {
|
const imageLoad = async () => {
|
||||||
@@ -231,6 +261,11 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
store.commit('addToFavorite', props.item.id);
|
store.commit('addToFavorite', props.item.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 切换选择状态
|
||||||
|
const toggleSelect = () => {
|
||||||
|
emits('select', props.item.id, !props.selected);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -297,12 +332,16 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
|
|
||||||
&-download {
|
&-download {
|
||||||
@apply mr-2 cursor-pointer;
|
@apply mr-2 cursor-pointer;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
|
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-select {
|
||||||
|
@apply mr-3 cursor-pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-mini {
|
.song-mini {
|
||||||
|
|||||||
@@ -44,9 +44,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from 'vue';
|
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||||
|
|
||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
// 配置 marked
|
// 配置 marked
|
||||||
@@ -64,6 +67,28 @@ const updateInfo = ref<UpdateResult>({
|
|||||||
releaseInfo: null
|
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
|
// 解析 Markdown
|
||||||
const parsedReleaseNotes = computed(() => {
|
const parsedReleaseNotes = computed(() => {
|
||||||
if (!updateInfo.value.releaseInfo?.body) return '';
|
if (!updateInfo.value.releaseInfo?.body) return '';
|
||||||
@@ -98,43 +123,58 @@ const checkForUpdates = async () => {
|
|||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
const assets = updateInfo.value.releaseInfo?.assets || [];
|
const assets = updateInfo.value.releaseInfo?.assets || [];
|
||||||
const platform = window.electron.process.platform;
|
const { platform } = window.electron.process;
|
||||||
const arch = window.electron.ipcRenderer.sendSync('get-arch');
|
const arch = window.electron.ipcRenderer.sendSync('get-arch');
|
||||||
console.log(arch);
|
console.log('arch', arch);
|
||||||
console.log(platform);
|
console.log('platform', platform);
|
||||||
|
const version = updateInfo.value.latestVersion;
|
||||||
|
const downUrls = {
|
||||||
|
win32: {
|
||||||
|
all: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win.exe`,
|
||||||
|
x64: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win-x64.exe`,
|
||||||
|
ia32: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win-ia32.exe`
|
||||||
|
},
|
||||||
|
darwin: {
|
||||||
|
all: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}AlgerMusicPlayer-${version}-mac-universal.dmg`
|
||||||
|
},
|
||||||
|
linux: {
|
||||||
|
AppImage: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-linux-x64.AppImage`,
|
||||||
|
deb: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-linux-x64.deb`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let downloadUrl = '';
|
let downloadUrl = '';
|
||||||
|
|
||||||
// 根据平台和架构选择对应的安装包
|
// 根据平台和架构选择对应的安装包
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
// macOS
|
// macOS
|
||||||
const macAsset = assets.find(asset =>
|
const macAsset = assets.find((asset) => asset.name.includes('mac'));
|
||||||
asset.name.includes('mac')
|
downloadUrl = macAsset?.browser_download_url || downUrls.darwin.all || '';
|
||||||
);
|
|
||||||
downloadUrl = macAsset?.browser_download_url || '';
|
|
||||||
} else if (platform === 'win32') {
|
} else if (platform === 'win32') {
|
||||||
// Windows
|
// Windows
|
||||||
const winAsset = assets.find(asset =>
|
const winAsset = assets.find(
|
||||||
asset.name.includes('win') &&
|
(asset) =>
|
||||||
(arch === 'x64' ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
asset.name.includes('win') &&
|
||||||
|
(arch === 'x64' ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
||||||
);
|
);
|
||||||
downloadUrl = winAsset?.browser_download_url || '';
|
downloadUrl =
|
||||||
|
winAsset?.browser_download_url || downUrls.win32[arch] || downUrls.win32.all || '';
|
||||||
} else if (platform === 'linux') {
|
} else if (platform === 'linux') {
|
||||||
// Linux
|
// Linux
|
||||||
const linuxAsset = assets.find(asset =>
|
const linuxAsset = assets.find(
|
||||||
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
(asset) =>
|
||||||
asset.name.includes('x64')
|
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
||||||
|
asset.name.includes('x64')
|
||||||
);
|
);
|
||||||
downloadUrl = linuxAsset?.browser_download_url || '';
|
downloadUrl = linuxAsset?.browser_download_url || downUrls.linux[arch] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
window.open(`https://ghproxy.cn/${downloadUrl}`, '_blank');
|
window.open(`https://www.ghproxy.cn/${downloadUrl}`, '_blank');
|
||||||
} else {
|
} else {
|
||||||
// 如果没有找到对应的安装包,跳转到 release 页面
|
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||||
}
|
}
|
||||||
closeModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -174,7 +214,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.update-body {
|
.update-body {
|
||||||
@apply p-4 pt-2 text-gray-600 dark:text-gray-300 rounded-lg overflow-hidden;
|
@apply p-4 pt-2 text-gray-600 dark:text-gray-300 rounded-lg overflow-hidden;
|
||||||
|
|
||||||
:deep(h1) {
|
:deep(h1) {
|
||||||
@apply text-xl font-bold mb-3;
|
@apply text-xl font-bold mb-3;
|
||||||
}
|
}
|
||||||
@@ -216,7 +256,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
:deep(table) {
|
:deep(table) {
|
||||||
@apply w-full mb-3;
|
@apply w-full mb-3;
|
||||||
th, td {
|
th,
|
||||||
|
td {
|
||||||
@apply px-3 py-2 border border-gray-200 dark:border-gray-600;
|
@apply px-3 py-2 border border-gray-200 dark:border-gray-600;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
@@ -245,4 +286,4 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { computed, ref } from 'vue';
|
|||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
import type { ILyricText, SongResult } from '@/type/music';
|
import type { ILyricText, SongResult } from '@/type/music';
|
||||||
import { getTextColors } from '@/utils/linearColor';
|
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
|
import { getTextColors } from '@/utils/linearColor';
|
||||||
|
|
||||||
const windowData = window as any;
|
const windowData = window as any;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
|
|||||||
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
|
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
|
||||||
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
||||||
export const isLyricWindowOpen = ref(false); // 新增状态
|
export const isLyricWindowOpen = ref(false); // 新增状态
|
||||||
export const textColors = ref(getTextColors());
|
export const textColors = ref<any>(getTextColors());
|
||||||
|
|
||||||
document.onkeyup = (e) => {
|
document.onkeyup = (e) => {
|
||||||
// 检查事件目标是否是输入框元素
|
// 检查事件目标是否是输入框元素
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
||||||
import { getImgUrl, getMusicProxyUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
import { getImageLinearBackground } from '@/utils/linearColor';
|
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||||
|
|
||||||
const musicHistory = useMusicHistory();
|
const musicHistory = useMusicHistory();
|
||||||
|
|
||||||
// 获取歌曲url
|
// 获取歌曲url
|
||||||
export const getSongUrl = async (id: number) => {
|
export const getSongUrl = async (id: number, songData: any, isDownloaded: boolean = false) => {
|
||||||
const { data } = await getMusicUrl(id);
|
const { data } = await getMusicUrl(id);
|
||||||
|
console.log('data', data);
|
||||||
let url = '';
|
let url = '';
|
||||||
|
let songDetail = null;
|
||||||
try {
|
try {
|
||||||
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
||||||
const res = await getParsingMusicUrl(id);
|
const res = await getParsingMusicUrl(id, songData);
|
||||||
console.log('res', res);
|
console.log('res', res);
|
||||||
url = res.data.data.url;
|
url = res.data.data.url;
|
||||||
|
songDetail = res.data.data;
|
||||||
|
} else {
|
||||||
|
songDetail = data.data[0] as any;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error);
|
console.error('error', error);
|
||||||
}
|
}
|
||||||
|
if (isDownloaded) {
|
||||||
|
return songDetail;
|
||||||
|
}
|
||||||
url = url || data.data[0].url;
|
url = url || data.data[0].url;
|
||||||
return getMusicProxyUrl(url);
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongDetail = async (playMusic: SongResult) => {
|
const getSongDetail = async (playMusic: SongResult) => {
|
||||||
playMusic.playLoading = true;
|
playMusic.playLoading = true;
|
||||||
const playMusicUrl = await getSongUrl(playMusic.id);
|
const playMusicUrl =
|
||||||
|
playMusic.playMusicUrl || (await getSongUrl(playMusic.id, cloneDeep(playMusic)));
|
||||||
const { backgroundColor, primaryColor } =
|
const { backgroundColor, primaryColor } =
|
||||||
playMusic.backgroundColor && playMusic.primaryColor
|
playMusic.backgroundColor && playMusic.primaryColor
|
||||||
? playMusic
|
? playMusic
|
||||||
@@ -42,6 +52,7 @@ const getSongDetail = async (playMusic: SongResult) => {
|
|||||||
export const useMusicListHook = () => {
|
export const useMusicListHook = () => {
|
||||||
const handlePlayMusic = async (state: any, playMusic: SongResult) => {
|
const handlePlayMusic = async (state: any, playMusic: SongResult) => {
|
||||||
const updatedPlayMusic = await getSongDetail(playMusic);
|
const updatedPlayMusic = await getSongDetail(playMusic);
|
||||||
|
console.log('updatedPlayMusic', updatedPlayMusic);
|
||||||
state.playMusic = updatedPlayMusic;
|
state.playMusic = updatedPlayMusic;
|
||||||
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
|
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
|
||||||
state.play = true;
|
state.play = true;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
|
|
||||||
<!-- 样式表 -->
|
<!-- 样式表 -->
|
||||||
<link rel="stylesheet" href="./assets/icon/iconfont.css" />
|
<link rel="stylesheet" href="./assets/icon/iconfont.css" />
|
||||||
<link rel="stylesheet" href="./assets/css/animate.css" />
|
|
||||||
<link rel="stylesheet" href="./assets/css/base.css" />
|
<link rel="stylesheet" href="./assets/css/base.css" />
|
||||||
<script defer src="https://cn.vercount.one/js"></script>
|
<script defer src="https://cn.vercount.one/js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 底部音乐播放 -->
|
<!-- 底部音乐播放 -->
|
||||||
<play-bar v-if="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
|
<play-bar v-if="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
|
||||||
|
<!-- 下载管理抽屉 -->
|
||||||
|
<download-drawer v-if="isElectron" />
|
||||||
</div>
|
</div>
|
||||||
<install-app-modal v-if="!isElectron"></install-app-modal>
|
<install-app-modal v-if="!isElectron"></install-app-modal>
|
||||||
<update-modal />
|
<update-modal v-if="isElectron" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -37,11 +39,12 @@ import { computed, defineAsyncComponent, onMounted } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import DownloadDrawer from '@/components/common/DownloadDrawer.vue';
|
||||||
import InstallAppModal from '@/components/common/InstallAppModal.vue';
|
import InstallAppModal from '@/components/common/InstallAppModal.vue';
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
|
import UpdateModal from '@/components/common/UpdateModal.vue';
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import { isElectron, isMobile } from '@/utils';
|
import { isElectron, isMobile } from '@/utils';
|
||||||
import UpdateModal from '@/components/common/UpdateModal.vue';
|
|
||||||
|
|
||||||
const keepAliveInclude = computed(() =>
|
const keepAliveInclude = computed(() =>
|
||||||
homeRouter
|
homeRouter
|
||||||
|
|||||||
@@ -116,11 +116,15 @@ const lrcScroll = (behavior = 'smooth') => {
|
|||||||
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
||||||
|
|
||||||
const mouseOverLayout = () => {
|
const mouseOverLayout = () => {
|
||||||
if(isMobile.value) {return}
|
if (isMobile.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
isMouse.value = true;
|
isMouse.value = true;
|
||||||
};
|
};
|
||||||
const mouseLeaveLayout = () => {
|
const mouseLeaveLayout = () => {
|
||||||
if(isMobile.value) {return}
|
if (isMobile.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isMouse.value = false;
|
isMouse.value = false;
|
||||||
lrcScroll();
|
lrcScroll();
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
</n-popover>
|
</n-popover>
|
||||||
</div>
|
</div>
|
||||||
<!-- 播放音乐 -->
|
<!-- 播放音乐 -->
|
||||||
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ import {
|
|||||||
textColors
|
textColors
|
||||||
} from '@/hooks/MusicHook';
|
} from '@/hooks/MusicHook';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isMobile, secondToMinute, setAnimationClass, isElectron } from '@/utils';
|
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
||||||
|
|
||||||
import MusicFull from './MusicFull.vue';
|
import MusicFull from './MusicFull.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
<i class="iconfont ri-login-box-line"></i>
|
<i class="iconfont ri-login-box-line"></i>
|
||||||
<span>去登录</span>
|
<span>去登录</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="store.state.user" class="menu-item" @click="selectItem('logout')">
|
||||||
|
<i class="iconfont ri-logout-box-r-line"></i>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</div>
|
||||||
<!-- 切换主题 -->
|
<!-- 切换主题 -->
|
||||||
<div class="menu-item" @click="selectItem('set')">
|
<div class="menu-item" @click="selectItem('set')">
|
||||||
<i class="iconfont ri-settings-3-line"></i>
|
<i class="iconfont ri-settings-3-line"></i>
|
||||||
@@ -92,7 +96,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, watchEffect, computed } from 'vue';
|
import { computed, onMounted, ref, watchEffect } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
@@ -195,8 +199,7 @@ const selectItem = async (key: string) => {
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
logout().then(() => {
|
logout().then(() => {
|
||||||
store.state.user = null;
|
store.commit('logout');
|
||||||
localStorage.clear();
|
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -237,7 +240,7 @@ const checkForUpdates = async () => {
|
|||||||
|
|
||||||
const toGithubRelease = () => {
|
const toGithubRelease = () => {
|
||||||
if (updateInfo.value.hasUpdate) {
|
if (updateInfo.value.hasUpdate) {
|
||||||
window.open(updateInfo.value.releaseInfo?.html_url || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
store.commit('setShowUpdateModal', true);
|
||||||
} else {
|
} else {
|
||||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
||||||
}
|
}
|
||||||
@@ -314,7 +317,7 @@ const toGithubRelease = () => {
|
|||||||
|
|
||||||
.version-info {
|
.version-info {
|
||||||
@apply ml-auto flex items-center;
|
@apply ml-auto flex items-center;
|
||||||
|
|
||||||
.version-number {
|
.version-number {
|
||||||
@apply text-xs px-2 py-0.5 rounded;
|
@apply text-xs px-2 py-0.5 rounded;
|
||||||
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
|
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -56,7 +57,7 @@ const handleAction = (action: 'minimize' | 'close') => {
|
|||||||
closeAction: action
|
closeAction: action
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'minimize') {
|
if (action === 'minimize') {
|
||||||
window.api.miniTray();
|
window.api.miniTray();
|
||||||
} else {
|
} else {
|
||||||
@@ -70,13 +71,13 @@ const close = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeAction = store.state.setData.closeAction;
|
const { closeAction } = store.state.setData;
|
||||||
|
|
||||||
if (closeAction === 'minimize') {
|
if (closeAction === 'minimize') {
|
||||||
window.api.miniTray();
|
window.api.miniTray();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeAction === 'close') {
|
if (closeAction === 'close') {
|
||||||
window.api.close();
|
window.api.close();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'vfonts/Lato.css';
|
|||||||
import 'vfonts/FiraCode.css';
|
import 'vfonts/FiraCode.css';
|
||||||
// tailwind css
|
// tailwind css
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import 'animate.css';
|
||||||
import 'remixicon/fonts/remixicon.css';
|
import 'remixicon/fonts/remixicon.css';
|
||||||
|
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
|
|
||||||
|
import setData from '@/../main/set.json';
|
||||||
|
import { getLikedList, likeSong } from '@/api/music';
|
||||||
import { useMusicListHook } from '@/hooks/MusicListHook';
|
import { useMusicListHook } from '@/hooks/MusicListHook';
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
|
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
|
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
|
||||||
|
|
||||||
// 默认设置
|
// 默认设置
|
||||||
const defaultSettings = {
|
const defaultSettings = setData;
|
||||||
isProxy: false,
|
|
||||||
noAnimate: false,
|
|
||||||
animationSpeed: 1,
|
|
||||||
author: 'Alger',
|
|
||||||
authorUrl: 'https://github.com/algerkong'
|
|
||||||
};
|
|
||||||
|
|
||||||
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||||
const item = localStorage.getItem(key);
|
const item = localStorage.getItem(key);
|
||||||
@@ -38,6 +34,7 @@ interface State {
|
|||||||
playMode: number;
|
playMode: number;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
musicFull: boolean;
|
musicFull: boolean;
|
||||||
|
showUpdateModal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@@ -57,7 +54,8 @@ const state: State = {
|
|||||||
favoriteList: getLocalStorageItem('favoriteList', []),
|
favoriteList: getLocalStorageItem('favoriteList', []),
|
||||||
playMode: getLocalStorageItem('playMode', 0),
|
playMode: getLocalStorageItem('playMode', 0),
|
||||||
theme: getCurrentTheme(),
|
theme: getCurrentTheme(),
|
||||||
musicFull: false
|
musicFull: false,
|
||||||
|
showUpdateModal: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||||
@@ -95,20 +93,44 @@ const mutations = {
|
|||||||
// 'set',
|
// 'set',
|
||||||
// JSON.parse(JSON.stringify(setData))
|
// JSON.parse(JSON.stringify(setData))
|
||||||
// );
|
// );
|
||||||
window.electron.ipcRenderer.send('set-store-value', 'set', JSON.parse(JSON.stringify(setData)));
|
window.electron.ipcRenderer.send(
|
||||||
|
'set-store-value',
|
||||||
|
'set',
|
||||||
|
JSON.parse(JSON.stringify(setData))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('appSettings', JSON.stringify(setData));
|
localStorage.setItem('appSettings', JSON.stringify(setData));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addToFavorite(state: State, songId: number) {
|
async addToFavorite(state: State, songId: number) {
|
||||||
|
// 先添加到本地
|
||||||
if (!state.favoriteList.includes(songId)) {
|
if (!state.favoriteList.includes(songId)) {
|
||||||
state.favoriteList = [songId, ...state.favoriteList];
|
state.favoriteList = [songId, ...state.favoriteList];
|
||||||
localStorage.setItem('favoriteList', JSON.stringify(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeFromFavorite(state: State, songId: number) {
|
async removeFromFavorite(state: State, songId: number) {
|
||||||
|
// 先从本地移除
|
||||||
state.favoriteList = state.favoriteList.filter((id) => id !== songId);
|
state.favoriteList = state.favoriteList.filter((id) => id !== songId);
|
||||||
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
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) {
|
togglePlayMode(state: State) {
|
||||||
state.playMode = (state.playMode + 1) % 3;
|
state.playMode = (state.playMode + 1) % 3;
|
||||||
@@ -117,13 +139,20 @@ const mutations = {
|
|||||||
toggleTheme(state: State) {
|
toggleTheme(state: State) {
|
||||||
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
||||||
applyTheme(state.theme);
|
applyTheme(state.theme);
|
||||||
|
},
|
||||||
|
setShowUpdateModal(state, value) {
|
||||||
|
state.showUpdateModal = value;
|
||||||
|
},
|
||||||
|
logout(state: State) {
|
||||||
|
state.user = null;
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
initializeSettings({ commit }: { commit: any }) {
|
initializeSettings({ commit }: { commit: any }) {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
// const setData = (window as any).electron.ipcRenderer.getStoreValue('set');
|
|
||||||
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
||||||
commit('setSetData', setData || defaultSettings);
|
commit('setSetData', setData || defaultSettings);
|
||||||
} else {
|
} else {
|
||||||
@@ -140,6 +169,34 @@ const actions = {
|
|||||||
},
|
},
|
||||||
initializeTheme({ state }: { state: State }) {
|
initializeTheme({ state }: { state: State }) {
|
||||||
applyTheme(state.theme);
|
applyTheme(state.theme);
|
||||||
|
},
|
||||||
|
async initializeFavoriteList({ state }: { state: State }) {
|
||||||
|
// 先获取本地收藏列表
|
||||||
|
const localFavoriteList = localStorage.getItem('favoriteList');
|
||||||
|
const localList: number[] = localFavoriteList ? JSON.parse(localFavoriteList) : [];
|
||||||
|
|
||||||
|
// 如果用户已登录,尝试获取服务器收藏列表并合并
|
||||||
|
if (state.user && localStorage.getItem('token')) {
|
||||||
|
try {
|
||||||
|
const res = await getLikedList();
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ interface Profile {
|
|||||||
createTime: number;
|
createTime: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
experts: Experts;
|
experts: any;
|
||||||
expertTags?: any;
|
expertTags?: any;
|
||||||
djStatus: number;
|
djStatus: number;
|
||||||
accountStatus: number;
|
accountStatus: number;
|
||||||
@@ -79,8 +79,6 @@ interface Profile {
|
|||||||
newFollows: number;
|
newFollows: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Experts {}
|
|
||||||
|
|
||||||
interface UserPoint {
|
interface UserPoint {
|
||||||
userId: number;
|
userId: number;
|
||||||
balance: number;
|
balance: number;
|
||||||
|
|||||||
2
src/renderer/types/electron.d.ts
vendored
2
src/renderer/types/electron.d.ts
vendored
@@ -19,4 +19,4 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
api: IElectronAPI;
|
api: IElectronAPI;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ export const openDirectory = (path: string | undefined, message: MessageApi, sho
|
|||||||
} else if (showTip) {
|
} else if (showTip) {
|
||||||
message.info('目录不存在');
|
message.info('目录不存在');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
|
|
||||||
// 设置歌手背景图片
|
// 设置歌手背景图片
|
||||||
@@ -54,34 +55,9 @@ export const formatNumber = (num: string | number) => {
|
|||||||
return num.toString();
|
return num.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const windowData = window as any;
|
|
||||||
export const getIsMc = () => {
|
|
||||||
if (!windowData.electron) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
|
||||||
if (setData.isProxy) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
const ProxyUrl = import.meta.env.VITE_API_PROXY;
|
|
||||||
|
|
||||||
export const getMusicProxyUrl = (url: string) => {
|
|
||||||
if (!getIsMc()) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
const PUrl = url.split('').join('+');
|
|
||||||
return `${ProxyUrl}/mc?url=${PUrl}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getImgUrl = (url: string | undefined, size: string = '') => {
|
export const getImgUrl = (url: string | undefined, size: string = '') => {
|
||||||
const bdUrl = 'https://image.baidu.com/search/down?url=';
|
|
||||||
const imgUrl = `${url}?param=${size}`;
|
const imgUrl = `${url}?param=${size}`;
|
||||||
if (!getIsMc()) {
|
return imgUrl;
|
||||||
return imgUrl;
|
|
||||||
}
|
|
||||||
return `${bdUrl}${encodeURIComponent(imgUrl)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMobile = computed(() => {
|
export const isMobile = computed(() => {
|
||||||
@@ -96,4 +72,4 @@ export const isMobile = computed(() => {
|
|||||||
return !!flag;
|
return !!flag;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isElectron = (window as any).electron !== undefined;
|
export const isElectron = (window as any).electron !== undefined;
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import axios, { InternalAxiosRequestConfig } from 'axios';
|
import axios, { InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { createDiscreteApi } from 'naive-ui';
|
||||||
|
|
||||||
|
import store from '@/store';
|
||||||
|
|
||||||
import { isElectron } from '.';
|
import { isElectron } from '.';
|
||||||
|
|
||||||
let setData: any = null;
|
const { notification } = createDiscreteApi(['notification']);
|
||||||
|
|
||||||
const getSetData = ()=>{
|
let setData: any = null;
|
||||||
|
const getSetData = () => {
|
||||||
if (window.electron) {
|
if (window.electron) {
|
||||||
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
getSetData()
|
getSetData();
|
||||||
// 扩展请求配置接口
|
// 扩展请求配置接口
|
||||||
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||||
retryCount?: number;
|
retryCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = window.electron ? `http://127.0.0.1:${setData?.musicApiPort}` : import.meta.env.VITE_API;
|
const baseURL = window.electron
|
||||||
|
? `http://127.0.0.1:${setData?.musicApiPort}`
|
||||||
|
: import.meta.env.VITE_API;
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
@@ -40,21 +47,23 @@ request.interceptors.request.use(
|
|||||||
if (config.method === 'get') {
|
if (config.method === 'get') {
|
||||||
config.params = {
|
config.params = {
|
||||||
...config.params,
|
...config.params,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.params.cookie = token;
|
config.params.cookie = `${token} os=pc;`;
|
||||||
|
} else {
|
||||||
|
config.params.cookie = 'os=pc;';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isElectron){
|
if (isElectron) {
|
||||||
const proxyConfig = setData?.proxyConfig
|
const proxyConfig = setData?.proxyConfig;
|
||||||
if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) {
|
if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) {
|
||||||
config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`
|
config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;
|
||||||
}
|
}
|
||||||
if(setData.enableRealIP && setData.realIP){
|
if (setData.enableRealIP && setData.realIP) {
|
||||||
config.params.realIP = setData.realIP
|
config.params.realIP = setData.realIP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +81,7 @@ request.interceptors.response.use(
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
|
console.log('error', error);
|
||||||
const config = error.config as CustomAxiosRequestConfig;
|
const config = error.config as CustomAxiosRequestConfig;
|
||||||
|
|
||||||
// 如果没有配置,直接返回错误
|
// 如果没有配置,直接返回错误
|
||||||
@@ -79,6 +89,30 @@ request.interceptors.response.use(
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 301 状态码
|
||||||
|
if (error.response?.status === 301) {
|
||||||
|
// 使用 store mutation 清除用户信息
|
||||||
|
store.commit('logout');
|
||||||
|
|
||||||
|
// 如果还可以重试,则重新发起请求
|
||||||
|
if (config.retryCount === undefined || config.retryCount < MAX_RETRIES) {
|
||||||
|
config.retryCount = (config.retryCount || 1) + 1;
|
||||||
|
console.log(`301 状态码,清除登录信息后重试第 ${config.retryCount} 次`);
|
||||||
|
notification.error({
|
||||||
|
content: '登录状态失效,请重新登录',
|
||||||
|
meta: '请重新登录',
|
||||||
|
duration: 2500,
|
||||||
|
keepAliveOnHover: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟重试
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
||||||
|
|
||||||
|
// 重新发起请求
|
||||||
|
return request(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否还可以重试
|
// 检查是否还可以重试
|
||||||
if (config.retryCount !== undefined && config.retryCount < MAX_RETRIES) {
|
if (config.retryCount !== undefined && config.retryCount < MAX_RETRIES) {
|
||||||
config.retryCount++;
|
config.retryCount++;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import config from '../../../package.json';
|
|
||||||
import { useDateFormat } from '@vueuse/core';
|
import { useDateFormat } from '@vueuse/core';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import config from '../../../package.json';
|
||||||
|
|
||||||
interface GithubReleaseInfo {
|
interface GithubReleaseInfo {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -36,20 +37,45 @@ export const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null>
|
|||||||
try {
|
try {
|
||||||
const token = import.meta.env.VITE_GITHUB_TOKEN;
|
const token = import.meta.env.VITE_GITHUB_TOKEN;
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|
||||||
|
const apiUrls = [
|
||||||
|
// 原始地址
|
||||||
|
'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest',
|
||||||
|
|
||||||
|
// 使用 ghproxy.com 代理
|
||||||
|
'https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/package.json'
|
||||||
|
|
||||||
|
// 使用 gitee 镜像(如果有的话)
|
||||||
|
// 'https://gitee.com/api/v5/repos/[用户名]/AlgerMusicPlayer/releases/latest'
|
||||||
|
];
|
||||||
if (token) {
|
if (token) {
|
||||||
headers['Authorization'] = `token ${token}`;
|
headers['Authorization'] = `token ${token}`;
|
||||||
}
|
}
|
||||||
const response = await axios.get(
|
|
||||||
'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest',
|
for (const url of apiUrls) {
|
||||||
{
|
try {
|
||||||
headers
|
const response = await axios.get(url, { headers });
|
||||||
|
|
||||||
|
if (url.includes('package.json')) {
|
||||||
|
// 如果是 package.json,直接读取版本号
|
||||||
|
return {
|
||||||
|
tag_name: response.data.version,
|
||||||
|
body: (
|
||||||
|
await axios.get(
|
||||||
|
'https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/CHANGELOG.md'
|
||||||
|
)
|
||||||
|
).data,
|
||||||
|
html_url: 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest',
|
||||||
|
assets: []
|
||||||
|
} as unknown as GithubReleaseInfo;
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`尝试访问 ${url} 失败:`, err);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data) {
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
return null;
|
throw new Error('所有 API 地址均无法访问');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取 GitHub Release 信息失败:', error);
|
console.error('获取 GitHub Release 信息失败:', error);
|
||||||
return null;
|
return null;
|
||||||
@@ -59,16 +85,19 @@ export const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null>
|
|||||||
/**
|
/**
|
||||||
* 格式化时间
|
* 格式化时间
|
||||||
*/
|
*/
|
||||||
const formatDate = (dateStr: string): string => {
|
export const formatDate = (dateStr: string): string => {
|
||||||
return useDateFormat(new Date(dateStr), 'YYYY-MM-DD HH:mm').value;
|
return useDateFormat(new Date(dateStr), 'YYYY-MM-DD HH:mm').value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查更新
|
* 检查更新
|
||||||
*/
|
*/
|
||||||
export const checkUpdate = async (currentVersion: string = config.version): Promise<UpdateResult | null> => {
|
export const checkUpdate = async (
|
||||||
|
currentVersion: string = config.version
|
||||||
|
): Promise<UpdateResult | null> => {
|
||||||
try {
|
try {
|
||||||
const releaseInfo = await getLatestReleaseInfo();
|
const releaseInfo = await getLatestReleaseInfo();
|
||||||
|
console.log('releaseInfo', releaseInfo);
|
||||||
if (!releaseInfo) {
|
if (!releaseInfo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -77,6 +106,8 @@ export const checkUpdate = async (currentVersion: string = config.version): Prom
|
|||||||
if (latestVersion === currentVersion) {
|
if (latestVersion === currentVersion) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
console.log('latestVersion', latestVersion);
|
||||||
|
console.log('currentVersion', currentVersion);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasUpdate: true,
|
hasUpdate: true,
|
||||||
@@ -84,9 +115,9 @@ export const checkUpdate = async (currentVersion: string = config.version): Prom
|
|||||||
currentVersion,
|
currentVersion,
|
||||||
releaseInfo: {
|
releaseInfo: {
|
||||||
tag_name: latestVersion,
|
tag_name: latestVersion,
|
||||||
body: `## 更新内容\n\n- 版本: ${latestVersion}\n- 发布时间: ${formatDate(releaseInfo.published_at)}\n\n${releaseInfo.body}`,
|
body: `## 更新内容\n\n- 版本: ${latestVersion}\n${releaseInfo.body}`,
|
||||||
html_url: releaseInfo.html_url,
|
html_url: releaseInfo.html_url,
|
||||||
assets: releaseInfo.assets.map(asset => ({
|
assets: releaseInfo.assets.map((asset) => ({
|
||||||
browser_download_url: asset.browser_download_url,
|
browser_download_url: asset.browser_download_url,
|
||||||
name: asset.name
|
name: asset.name
|
||||||
}))
|
}))
|
||||||
@@ -96,4 +127,4 @@ export const checkUpdate = async (currentVersion: string = config.version): Prom
|
|||||||
console.error('检查更新失败:', error);
|
console.error('检查更新失败:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isComponent ? favoriteSongs.length : true" class="favorite-page">
|
<div v-if="isComponent ? favoriteSongs.length : true" class="favorite-page">
|
||||||
<div class="favorite-header" :class="setAnimationClass('animate__fadeInLeft')">
|
<div class="favorite-header" :class="setAnimationClass('animate__fadeInLeft')">
|
||||||
<h2>我的收藏</h2>
|
<div class="favorite-header-left">
|
||||||
<div class="favorite-count">共 {{ favoriteList.length }} 首</div>
|
<h2>我的收藏</h2>
|
||||||
|
<div class="favorite-count">共 {{ favoriteList.length }} 首</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isComponent && isElectron" class="favorite-header-right">
|
||||||
|
<n-button
|
||||||
|
v-if="!isSelecting"
|
||||||
|
secondary
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="select-btn"
|
||||||
|
@click="startSelect"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-checkbox-multiple-line"></i>
|
||||||
|
</template>
|
||||||
|
批量下载
|
||||||
|
</n-button>
|
||||||
|
<div v-else class="select-controls">
|
||||||
|
<n-checkbox
|
||||||
|
class="select-all-checkbox"
|
||||||
|
:checked="isAllSelected"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@update:checked="handleSelectAll"
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</n-checkbox>
|
||||||
|
<n-button-group class="operation-btns">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="isDownloading"
|
||||||
|
:disabled="selectedSongs.length === 0"
|
||||||
|
class="download-btn"
|
||||||
|
@click="handleBatchDownload"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-download-line"></i>
|
||||||
|
</template>
|
||||||
|
下载 ({{ selectedSongs.length }})
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" class="cancel-btn" @click="cancelSelect"> 取消 </n-button>
|
||||||
|
</n-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="favorite-main" :class="setAnimationClass('animate__bounceInRight')">
|
<div class="favorite-main" :class="setAnimationClass('animate__bounceInRight')">
|
||||||
<n-scrollbar ref="scrollbarRef" class="favorite-content" @scroll="handleScroll">
|
<n-scrollbar ref="scrollbarRef" class="favorite-content" @scroll="handleScroll">
|
||||||
@@ -17,7 +60,10 @@
|
|||||||
:favorite="!isComponent"
|
:favorite="!isComponent"
|
||||||
:class="setAnimationClass('animate__bounceInLeft')"
|
:class="setAnimationClass('animate__bounceInLeft')"
|
||||||
:style="getItemAnimationDelay(index)"
|
:style="getItemAnimationDelay(index)"
|
||||||
|
:selectable="isSelecting"
|
||||||
|
:selected="selectedSongs.includes(song.id)"
|
||||||
@play="handlePlay"
|
@play="handlePlay"
|
||||||
|
@select="handleSelect"
|
||||||
/>
|
/>
|
||||||
<div v-if="isComponent" class="favorite-list-more text-center">
|
<div v-if="isComponent" class="favorite-list-more text-center">
|
||||||
<n-button text type="primary" @click="handleMore">查看更多</n-button>
|
<n-button text type="primary" @click="handleMore">查看更多</n-button>
|
||||||
@@ -35,22 +81,130 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { getMusicDetail } from '@/api/music';
|
import { getMusicDetail } from '@/api/music';
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
|
import { getSongUrl } from '@/hooks/MusicListHook';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
import { isElectron, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const message = useMessage();
|
||||||
const favoriteList = computed(() => store.state.favoriteList);
|
const favoriteList = computed(() => store.state.favoriteList);
|
||||||
const favoriteSongs = ref<SongResult[]>([]);
|
const favoriteSongs = ref<SongResult[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const noMore = ref(false);
|
const noMore = ref(false);
|
||||||
const scrollbarRef = ref();
|
const scrollbarRef = ref();
|
||||||
|
|
||||||
|
// 多选相关
|
||||||
|
const isSelecting = ref(false);
|
||||||
|
const selectedSongs = ref<number[]>([]);
|
||||||
|
const isDownloading = ref(false);
|
||||||
|
|
||||||
|
// 开始多选
|
||||||
|
const startSelect = () => {
|
||||||
|
isSelecting.value = true;
|
||||||
|
selectedSongs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消多选
|
||||||
|
const cancelSelect = () => {
|
||||||
|
isSelecting.value = false;
|
||||||
|
selectedSongs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理选择
|
||||||
|
const handleSelect = (songId: number, selected: boolean) => {
|
||||||
|
if (selected) {
|
||||||
|
selectedSongs.value.push(songId);
|
||||||
|
} else {
|
||||||
|
selectedSongs.value = selectedSongs.value.filter((id) => id !== songId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量下载
|
||||||
|
const handleBatchDownload = async () => {
|
||||||
|
if (isDownloading.value) {
|
||||||
|
message.warning('正在下载中,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSongs.value.length === 0) {
|
||||||
|
message.warning('请先选择要下载的歌曲');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isDownloading.value = true;
|
||||||
|
message.success('开始下载...');
|
||||||
|
|
||||||
|
// 移除旧的监听器
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
// 添加新的监听器
|
||||||
|
window.electron.ipcRenderer.on('music-download-complete', (_, result) => {
|
||||||
|
if (result.success) {
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当所有下载完成时
|
||||||
|
if (successCount + failCount === selectedSongs.value.length) {
|
||||||
|
isDownloading.value = false;
|
||||||
|
message.success(`下载完成`);
|
||||||
|
cancelSelect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取选中歌曲的信息
|
||||||
|
const selectedSongsList = selectedSongs.value
|
||||||
|
.map((songId) => favoriteSongs.value.find((s) => s.id === songId))
|
||||||
|
.filter((song) => song) as SongResult[];
|
||||||
|
|
||||||
|
// 并行获取所有歌曲的下载链接
|
||||||
|
const downloadUrls = await Promise.all(
|
||||||
|
selectedSongsList.map(async (song) => {
|
||||||
|
try {
|
||||||
|
const data = (await getSongUrl(song.id, song, true)) as any;
|
||||||
|
return { song, ...data };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取歌曲 ${song.name} 下载链接失败:`, error);
|
||||||
|
return { song, url: null };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 开始下载有效的链接
|
||||||
|
downloadUrls.forEach(({ song, url, type }) => {
|
||||||
|
if (!url) {
|
||||||
|
failCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
|
url,
|
||||||
|
filename: `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`,
|
||||||
|
songInfo: cloneDeep(song),
|
||||||
|
type
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
isDownloading.value = false;
|
||||||
|
message.destroyAll();
|
||||||
|
message.error('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 无限滚动相关
|
// 无限滚动相关
|
||||||
const pageSize = 16;
|
const pageSize = 16;
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
@@ -64,10 +218,9 @@ const props = defineProps({
|
|||||||
|
|
||||||
// 获取当前页的收藏歌曲ID
|
// 获取当前页的收藏歌曲ID
|
||||||
const getCurrentPageIds = () => {
|
const getCurrentPageIds = () => {
|
||||||
const reversedList = [...favoriteList.value];
|
|
||||||
const startIndex = (currentPage.value - 1) * pageSize;
|
const startIndex = (currentPage.value - 1) * pageSize;
|
||||||
const endIndex = startIndex + pageSize;
|
const endIndex = startIndex + pageSize;
|
||||||
return reversedList.slice(startIndex, endIndex);
|
return favoriteList.value.slice(startIndex, endIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取收藏歌曲详情
|
// 获取收藏歌曲详情
|
||||||
@@ -120,6 +273,7 @@ const handleScroll = (e: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
store.dispatch('initializeFavoriteList');
|
||||||
getFavoriteSongs();
|
getFavoriteSongs();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,6 +300,26 @@ const router = useRouter();
|
|||||||
const handleMore = () => {
|
const handleMore = () => {
|
||||||
router.push('/favorite');
|
router.push('/favorite');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 全选相关
|
||||||
|
const isAllSelected = computed(() => {
|
||||||
|
return (
|
||||||
|
favoriteSongs.value.length > 0 && selectedSongs.value.length === favoriteSongs.value.length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isIndeterminate = computed(() => {
|
||||||
|
return selectedSongs.value.length > 0 && selectedSongs.value.length < favoriteSongs.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理全选/取消全选
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedSongs.value = favoriteSongs.value.map((song) => song.id);
|
||||||
|
} else {
|
||||||
|
selectedSongs.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -154,15 +328,68 @@ const handleMore = () => {
|
|||||||
@apply bg-light dark:bg-black;
|
@apply bg-light dark:bg-black;
|
||||||
|
|
||||||
.favorite-header {
|
.favorite-header {
|
||||||
@apply flex items-center justify-between flex-shrink-0 px-4;
|
@apply flex items-center justify-between flex-shrink-0 px-4 pb-2;
|
||||||
|
|
||||||
h2 {
|
&-left {
|
||||||
@apply text-xl font-bold pb-2;
|
@apply flex items-center gap-4;
|
||||||
@apply text-gray-900 dark:text-white;
|
|
||||||
|
h2 {
|
||||||
|
@apply text-xl font-bold;
|
||||||
|
@apply text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-count {
|
||||||
|
@apply text-gray-500 dark:text-gray-400 text-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.favorite-count {
|
&-right {
|
||||||
@apply text-gray-500 dark:text-gray-400 text-sm;
|
@apply flex items-center;
|
||||||
|
|
||||||
|
.select-btn {
|
||||||
|
@apply rounded-full px-4 h-8;
|
||||||
|
@apply transition-all duration-300 ease-in-out;
|
||||||
|
@apply hover:bg-primary hover:text-white;
|
||||||
|
@apply dark:border-gray-600;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply mr-1 text-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-controls {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
@apply bg-gray-50 dark:bg-gray-800;
|
||||||
|
@apply rounded-full px-3 py-1;
|
||||||
|
@apply border border-gray-200 dark:border-gray-700;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
|
||||||
|
.select-all-checkbox {
|
||||||
|
@apply text-sm text-gray-900 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-btns {
|
||||||
|
@apply flex items-center gap-2 ml-2;
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
@apply rounded-full px-4 h-7;
|
||||||
|
@apply bg-primary text-white;
|
||||||
|
@apply hover:bg-primary-dark;
|
||||||
|
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply mr-1 text-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
@apply rounded-full px-4 h-7;
|
||||||
|
@apply text-gray-600 dark:text-gray-300;
|
||||||
|
@apply hover:bg-gray-100 dark:hover:bg-gray-700;
|
||||||
|
@apply border-gray-300 dark:border-gray-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { getMusicDetail } from '@/api/music';
|
import { getMusicDetail } from '@/api/music';
|
||||||
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'History'
|
name: 'History'
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
<!-- 歌单列表 -->
|
<!-- 歌单列表 -->
|
||||||
<n-scrollbar class="recommend" style="height: calc(100% - 55px)" :size="100" @scroll="handleScroll">
|
<n-scrollbar
|
||||||
|
class="recommend"
|
||||||
|
style="height: calc(100% - 55px)"
|
||||||
|
:size="100"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
<div v-loading="loading" class="recommend-list">
|
<div v-loading="loading" class="recommend-list">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in recommendList"
|
v-for="(item, index) in recommendList"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineOptions({
|
|||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isQr = ref(false);
|
const isQr = ref(true);
|
||||||
|
|
||||||
const qrUrl = ref<string>();
|
const qrUrl = ref<string>();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
:class="setAnimationClass('animate__bounceInRight')"
|
:class="setAnimationClass('animate__bounceInRight')"
|
||||||
:style="setAnimationDelay(index, 50)"
|
:style="setAnimationDelay(index, 50)"
|
||||||
>
|
>
|
||||||
<SearchItem :item="item" />
|
<search-item :item="item" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,10 +67,10 @@ import { useStore } from 'vuex';
|
|||||||
|
|
||||||
import { getHotSearch } from '@/api/home';
|
import { getHotSearch } from '@/api/home';
|
||||||
import { getSearch } from '@/api/search';
|
import { getSearch } from '@/api/search';
|
||||||
|
import SearchItem from '@/components/common/SearchItem.vue';
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import type { IHotSearch } from '@/type/search';
|
import type { IHotSearch } from '@/type/search';
|
||||||
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
import SearchItem from '@/components/common/SearchItem.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Search'
|
name: 'Search'
|
||||||
|
|||||||
@@ -22,12 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<n-switch v-model:value="setData.isProxy" />
|
<n-switch v-model:value="setData.isProxy" />
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="set-item" v-if="isElectron">
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">音乐API端口</div>
|
<div class="set-item-title">音乐API端口</div>
|
||||||
<div class="set-item-content">
|
<div class="set-item-content">修改后需要重启应用</div>
|
||||||
修改后需要重启应用
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<n-input-number v-model:value="setData.musicApiPort" />
|
<n-input-number v-model:value="setData.musicApiPort" />
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item" v-if="isElectron">
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">下载目录</div>
|
<div class="set-item-title">下载目录</div>
|
||||||
<div class="set-item-content">
|
<div class="set-item-content">
|
||||||
@@ -101,18 +99,41 @@
|
|||||||
@click="openAuthor"
|
@click="openAuthor"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Coffee>
|
<coffee>
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">作者</div>
|
<div class="set-item-title">作者</div>
|
||||||
<div class="set-item-content">algerkong 点个star🌟呗</div>
|
<div class="set-item-content">algerkong 点个star🌟呗</div>
|
||||||
</div>
|
</div>
|
||||||
</Coffee>
|
</coffee>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<n-button size="small" type="primary" @click="openAuthor"><i class="ri-github-line"></i>前往github</n-button>
|
<n-button size="small" type="primary" @click="openAuthor"
|
||||||
|
><i class="ri-github-line"></i>前往github</n-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item" v-if="isElectron">
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">音质设置</div>
|
||||||
|
<div class="set-item-content">选择音乐播放音质(VIP)</div>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="setData.musicQuality"
|
||||||
|
:options="[
|
||||||
|
{ label: '标准', value: 'standard' },
|
||||||
|
{ label: '较高', value: 'higher' },
|
||||||
|
{ label: '极高', value: 'exhigh' },
|
||||||
|
{ label: '无损', value: 'lossless' },
|
||||||
|
{ label: 'Hi-Res', value: 'hires' },
|
||||||
|
{ label: '高清环绕声', value: 'jyeffect' },
|
||||||
|
{ label: '沉浸环绕声', value: 'sky' },
|
||||||
|
{ label: '杜比全景声', value: 'dolby' },
|
||||||
|
{ label: '超清母带', value: 'jymaster' }
|
||||||
|
]"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">关闭行为</div>
|
<div class="set-item-title">关闭行为</div>
|
||||||
<div class="set-item-content">
|
<div class="set-item-content">
|
||||||
@@ -129,14 +150,30 @@
|
|||||||
style="width: 160px"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item" v-if="isElectron">
|
|
||||||
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">重启</div>
|
<div class="set-item-title">重启</div>
|
||||||
<div class="set-item-content">重启应用</div>
|
<div class="set-item-content">重启应用</div>
|
||||||
</div>
|
</div>
|
||||||
<n-button type="primary" @click="restartApp">重启</n-button>
|
<n-button type="primary" size="small" @click="restartApp">重启</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item" v-if="isElectron">
|
<!-- 缓存管理 -->
|
||||||
|
<!-- <n-card class="set-card" title="缓存管理">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-button type="primary" @click="showClearCacheModal = true"> 清除缓存 </n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card> -->
|
||||||
|
<div v-if="isElectron" class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">缓存管理</div>
|
||||||
|
<div class="set-item-content">清除缓存</div>
|
||||||
|
</div>
|
||||||
|
<n-button type="primary" size="small" @click="showClearCacheModal = true">
|
||||||
|
清除缓存
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">代理设置</div>
|
<div class="set-item-title">代理设置</div>
|
||||||
<div class="set-item-content">无法访问音乐时可以开启代理</div>
|
<div class="set-item-content">无法访问音乐时可以开启代理</div>
|
||||||
@@ -149,10 +186,13 @@
|
|||||||
<n-button size="small" @click="showProxyModal = true">配置</n-button>
|
<n-button size="small" @click="showProxyModal = true">配置</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item" v-if="isElectron">
|
<div v-if="isElectron" class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">realIP</div>
|
<div class="set-item-title">realIP</div>
|
||||||
<div class="set-item-content">由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决,如:116.25.146.177 即可解决</div>
|
<div class="set-item-content">
|
||||||
|
由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决,如:116.25.146.177
|
||||||
|
即可解决
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<n-switch v-model:value="setData.enableRealIP">
|
<n-switch v-model:value="setData.enableRealIP">
|
||||||
@@ -163,22 +203,75 @@
|
|||||||
v-if="setData.enableRealIP"
|
v-if="setData.enableRealIP"
|
||||||
v-model:value="setData.realIP"
|
v-model:value="setData.realIP"
|
||||||
placeholder="realIP"
|
placeholder="realIP"
|
||||||
@blur="validateAndSaveRealIP"
|
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
|
@blur="validateAndSaveRealIP"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="p-4 bg-light dark:bg-dark rounded-lg mb-4 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">捐赠支持</div>
|
||||||
|
<div class="set-item-content">感谢您的支持,让我有动力能够持续改进</div>
|
||||||
|
</div>
|
||||||
|
<n-button text @click="toggleDonationList">
|
||||||
|
<template #icon>
|
||||||
|
<i :class="isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'" />
|
||||||
|
</template>
|
||||||
|
{{ isDonationListVisible ? '隐藏列表' : '显示列表' }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<donation-list v-if="isDonationListVisible" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 清除缓存弹窗 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showClearCacheModal"
|
||||||
|
preset="dialog"
|
||||||
|
title="清除缓存"
|
||||||
|
positive-text="确认"
|
||||||
|
negative-text="取消"
|
||||||
|
@positive-click="clearCache"
|
||||||
|
@negative-click="
|
||||||
|
() => {
|
||||||
|
selectedCacheTypes = [];
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<n-space vertical>
|
||||||
|
<p>请选择要清除的缓存类型:</p>
|
||||||
|
<n-checkbox-group v-model:value="selectedCacheTypes">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-checkbox
|
||||||
|
v-for="option in clearCacheOptions"
|
||||||
|
:key="option.key"
|
||||||
|
:value="option.key"
|
||||||
|
:label="option.label"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div>
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-gray-400 text-sm">{{ option.description }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-checkbox>
|
||||||
|
</n-space>
|
||||||
|
</n-checkbox-group>
|
||||||
|
</n-space>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
<PlayBottom/>
|
<play-bottom />
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="showProxyModal"
|
v-model:show="showProxyModal"
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
title="代理设置"
|
title="代理设置"
|
||||||
positive-text="确认"
|
positive-text="确认"
|
||||||
negative-text="取消"
|
negative-text="取消"
|
||||||
|
:show-icon="false"
|
||||||
@positive-click="handleProxyConfirm"
|
@positive-click="handleProxyConfirm"
|
||||||
@negative-click="showProxyModal = false"
|
@negative-click="showProxyModal = false"
|
||||||
:show-icon="false"
|
|
||||||
>
|
>
|
||||||
<n-form
|
<n-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@@ -202,7 +295,12 @@
|
|||||||
<n-input v-model:value="proxyForm.host" placeholder="请输入代理地址" />
|
<n-input v-model:value="proxyForm.host" placeholder="请输入代理地址" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="代理端口" path="port">
|
<n-form-item label="代理端口" path="port">
|
||||||
<n-input-number v-model:value="proxyForm.port" placeholder="请输入代理端口" :min="1" :max="65535" />
|
<n-input-number
|
||||||
|
v-model:value="proxyForm.port"
|
||||||
|
placeholder="请输入代理端口"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -210,16 +308,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, watch } from 'vue';
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
import { useMessage } from 'naive-ui';
|
|
||||||
import type { FormRules } from 'naive-ui';
|
import type { FormRules } from 'naive-ui';
|
||||||
import { isElectron } from '@/utils';
|
import { useMessage } from 'naive-ui';
|
||||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { selectDirectory, openDirectory } from '@/utils/fileOperation';
|
import { useStore } from 'vuex';
|
||||||
import config from '../../../../package.json';
|
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
import localData from '@/../main/set.json';
|
||||||
import Coffee from '@/components/Coffee.vue';
|
import Coffee from '@/components/Coffee.vue';
|
||||||
|
import DonationList from '@/components/common/DonationList.vue';
|
||||||
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
|
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
||||||
|
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||||
|
|
||||||
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const checking = ref(false);
|
const checking = ref(false);
|
||||||
@@ -247,12 +349,20 @@ const setData = computed(() => {
|
|||||||
port: 7890
|
port: 7890
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// 确保音质设置存在
|
||||||
|
if (!data.musicQuality) {
|
||||||
|
data.musicQuality = 'higher';
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => setData.value, (newVal) => {
|
watch(
|
||||||
store.commit('setSetData', newVal)
|
() => setData.value,
|
||||||
}, { deep: true });
|
(newVal) => {
|
||||||
|
store.commit('setSetData', newVal);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const isDarkTheme = computed({
|
const isDarkTheme = computed({
|
||||||
get: () => store.state.theme === 'dark',
|
get: () => store.state.theme === 'dark',
|
||||||
@@ -290,7 +400,7 @@ const checkForUpdates = async (isClick = false) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openReleasePage = () => {
|
const openReleasePage = () => {
|
||||||
window.open(updateInfo.value.releaseInfo?.html_url || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
store.commit('setShowUpdateModal', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDownloadPath = async () => {
|
const selectDownloadPath = async () => {
|
||||||
@@ -328,7 +438,8 @@ const proxyRules: FormRules = {
|
|||||||
validator: (_rule, value) => {
|
validator: (_rule, value) => {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
// 简单的IP或域名验证
|
// 简单的IP或域名验证
|
||||||
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$|^localhost$|^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
|
const ipRegex =
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}$|^localhost$|^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
|
||||||
return ipRegex.test(value);
|
return ipRegex.test(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -358,15 +469,19 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听代理配置变化
|
// 监听代理配置变化
|
||||||
watch(() => setData.value.proxyConfig, (newVal) => {
|
watch(
|
||||||
if (newVal) {
|
() => setData.value.proxyConfig,
|
||||||
proxyForm.value = {
|
(newVal) => {
|
||||||
protocol: newVal.protocol || 'http',
|
if (newVal) {
|
||||||
host: newVal.host || '127.0.0.1',
|
proxyForm.value = {
|
||||||
port: newVal.port || 7890
|
protocol: newVal.protocol || 'http',
|
||||||
};
|
host: newVal.host || '127.0.0.1',
|
||||||
}
|
port: newVal.port || 7890
|
||||||
}, { immediate: true, deep: true });
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const handleProxyConfirm = async () => {
|
const handleProxyConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -409,15 +524,101 @@ const validateAndSaveRealIP = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 监听enableRealIP变化,当关闭时清空realIP
|
// 监听enableRealIP变化,当关闭时清空realIP
|
||||||
watch(() => setData.value.enableRealIP, (newVal) => {
|
watch(
|
||||||
if (!newVal) {
|
() => setData.value.enableRealIP,
|
||||||
store.commit('setSetData', {
|
(newVal) => {
|
||||||
...setData.value,
|
if (!newVal) {
|
||||||
realIP: '',
|
store.commit('setSetData', {
|
||||||
enableRealIP: false
|
...setData.value,
|
||||||
});
|
realIP: '',
|
||||||
|
enableRealIP: false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const isDonationListVisible = ref(localStorage.getItem('donationListVisible') !== 'false');
|
||||||
|
|
||||||
|
const toggleDonationList = () => {
|
||||||
|
isDonationListVisible.value = !isDonationListVisible.value;
|
||||||
|
localStorage.setItem('donationListVisible', isDonationListVisible.value.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除缓存相关
|
||||||
|
const showClearCacheModal = ref(false);
|
||||||
|
const clearCacheOptions = ref([
|
||||||
|
{ label: '播放历史', key: 'history', description: '清除播放过的歌曲记录' },
|
||||||
|
{ label: '收藏记录', key: 'favorite', description: '清除本地收藏的歌曲记录(不会影响云端收藏)' },
|
||||||
|
{ label: '用户数据', key: 'user', description: '清除登录信息和用户相关数据' },
|
||||||
|
{ label: '应用设置', key: 'settings', description: '清除应用的所有自定义设置' },
|
||||||
|
{ label: '下载记录', key: 'downloads', description: '清除下载历史记录(不会删除已下载的文件)' },
|
||||||
|
{ label: '音乐资源', key: 'resources', description: '清除已加载的音乐文件、歌词等资源缓存' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedCacheTypes = ref<string[]>([]);
|
||||||
|
|
||||||
|
const clearCache = async () => {
|
||||||
|
const clearTasks = selectedCacheTypes.value.map(async (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'history':
|
||||||
|
localStorage.removeItem('musicHistory');
|
||||||
|
break;
|
||||||
|
case 'favorite':
|
||||||
|
localStorage.removeItem('favoriteList');
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
store.commit('logout');
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
if (window.electron) {
|
||||||
|
window.electron.ipcRenderer.send('set-store-value', 'set', localData);
|
||||||
|
}
|
||||||
|
localStorage.removeItem('appSettings');
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
localStorage.removeItem('lyricData');
|
||||||
|
localStorage.removeItem('lyricFontSize');
|
||||||
|
localStorage.removeItem('playMode');
|
||||||
|
break;
|
||||||
|
case 'downloads':
|
||||||
|
if (window.electron) {
|
||||||
|
window.electron.ipcRenderer.send('clear-downloads-history');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'resources':
|
||||||
|
// 清除音频资源缓存
|
||||||
|
if (window.electron) {
|
||||||
|
window.electron.ipcRenderer.send('clear-audio-cache');
|
||||||
|
}
|
||||||
|
// 清除歌词缓存
|
||||||
|
localStorage.removeItem('lyricCache');
|
||||||
|
// 清除音乐URL缓存
|
||||||
|
localStorage.removeItem('musicUrlCache');
|
||||||
|
// 清除图片缓存
|
||||||
|
if (window.caches) {
|
||||||
|
try {
|
||||||
|
const cache = await window.caches.open('music-images');
|
||||||
|
await cache.keys().then((keys) => {
|
||||||
|
keys.forEach((key) => {
|
||||||
|
cache.delete(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除图片缓存失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(clearTasks);
|
||||||
|
message.success('清除成功,部分设置在重启后生效');
|
||||||
|
showClearCacheModal.value = false;
|
||||||
|
selectedCacheTypes.value = [];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
@@ -112,42 +112,104 @@ const userDetail = ref<IUserDetail>();
|
|||||||
const playList = ref<any[]>([]);
|
const playList = ref<any[]>([]);
|
||||||
const recordList = ref();
|
const recordList = ref();
|
||||||
const infoLoading = ref(false);
|
const infoLoading = ref(false);
|
||||||
|
const mounted = ref(true);
|
||||||
const user = computed(() => store.state.user);
|
|
||||||
|
|
||||||
const loadPage = async () => {
|
|
||||||
if (!user.value) {
|
|
||||||
router.push('/login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
infoLoading.value = true;
|
|
||||||
|
|
||||||
const { data: userData } = await getUserDetail(user.value.userId);
|
|
||||||
userDetail.value = userData;
|
|
||||||
|
|
||||||
const { data: playlistData } = await getUserPlaylist(user.value.userId);
|
|
||||||
playList.value = playlistData.playlist;
|
|
||||||
|
|
||||||
const { data: recordData } = await getUserRecord(user.value.userId);
|
|
||||||
recordList.value = recordData.allData.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
...item.song,
|
|
||||||
picUrl: item.song.al.picUrl
|
|
||||||
}));
|
|
||||||
infoLoading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
if (!user.value) {
|
|
||||||
router.push('/login');
|
|
||||||
} else {
|
|
||||||
loadPage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isShowList = ref(false);
|
const isShowList = ref(false);
|
||||||
const list = ref<Playlist>();
|
const list = ref<Playlist>();
|
||||||
const listLoading = ref(false);
|
const listLoading = ref(false);
|
||||||
|
|
||||||
|
const user = computed(() => store.state.user);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
mounted.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const userData = localStorage.getItem('user');
|
||||||
|
|
||||||
|
if (!token || !userData) {
|
||||||
|
router.push('/login');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果store中没有用户数据,但localStorage中有,则恢复用户数据
|
||||||
|
if (!store.state.user && userData) {
|
||||||
|
store.state.user = JSON.parse(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async () => {
|
||||||
|
if (!mounted.value) return;
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (!checkLoginStatus()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
infoLoading.value = true;
|
||||||
|
|
||||||
|
const { data: userData } = await getUserDetail(user.value.userId);
|
||||||
|
if (!mounted.value) return;
|
||||||
|
userDetail.value = userData;
|
||||||
|
|
||||||
|
const { data: playlistData } = await getUserPlaylist(user.value.userId);
|
||||||
|
if (!mounted.value) return;
|
||||||
|
playList.value = playlistData.playlist;
|
||||||
|
|
||||||
|
const { data: recordData } = await getUserRecord(user.value.userId);
|
||||||
|
if (!mounted.value) return;
|
||||||
|
recordList.value = recordData.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');
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted.value) {
|
||||||
|
infoLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value.path,
|
||||||
|
(newPath) => {
|
||||||
|
if (newPath === '/user') {
|
||||||
|
checkLoginStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听用户状态变化
|
||||||
|
watch(
|
||||||
|
() => store.state.user,
|
||||||
|
(newUser) => {
|
||||||
|
if (!mounted.value) return;
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
router.push('/login');
|
||||||
|
} else {
|
||||||
|
loadPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 页面挂载时检查登录状态
|
||||||
|
onMounted(() => {
|
||||||
|
checkLoginStatus();
|
||||||
|
loadPage();
|
||||||
|
});
|
||||||
|
|
||||||
// 展示歌单
|
// 展示歌单
|
||||||
const showPlaylist = async (id: number, name: string) => {
|
const showPlaylist = async (id: number, name: string) => {
|
||||||
isShowList.value = true;
|
isShowList.value = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user