feat: 新增歌手页 & fix: 修复一些小问题
29
README.md
@@ -29,7 +29,7 @@
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 封面主题色自适应
|
||||
- 本地歌曲管理及分类 ~~以及音乐标签编辑~~
|
||||
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲(客户端独占功能)
|
||||
- **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
@@ -48,11 +48,13 @@
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- ~~移动端基础适配~~
|
||||
- `i18n` 支持(暂时去除,感觉不需要)
|
||||
- ~~`i18n` 支持~~
|
||||
|
||||
#### 待办
|
||||
|
||||
- [ ] 电台节目支持
|
||||
- [ ] 完善音乐频谱
|
||||
- [ ] 添加桌面歌词
|
||||
- [ ] 多种布局方式
|
||||
- [ ] 发表评论
|
||||
|
||||
## 😍 Screenshots
|
||||
@@ -62,35 +64,42 @@
|
||||
<details>
|
||||
<summary>主页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>播放页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>发现页面</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>歌单页面</summary>
|
||||
|
||||
> 待装修
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>评论页面</summary>
|
||||
|
||||
> 待装修
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>本地音乐</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
@@ -140,6 +149,10 @@ npm build
|
||||
|
||||
构建完成后可将生成的 `out/renderer` 文件夹内的文件上传至服务器
|
||||
|
||||
若使用的为第三方部署平台,比如 `Vercel`,请将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||

|
||||
|
||||
### 构建客户端
|
||||
|
||||
```bash
|
||||
|
||||
1
components.d.ts
vendored
@@ -43,6 +43,7 @@ declare module 'vue' {
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
NH6: typeof import('naive-ui')['NH6']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 应用程序的唯一标识符
|
||||
appId: com.splayer-imsyy.app
|
||||
appId: com.imsyy.splayer
|
||||
# 应用程序的产品名称
|
||||
productName: splayer-desktop-dev
|
||||
productName: splayer-desktop
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
buildResources: build
|
||||
@@ -18,7 +18,7 @@ asarUnpack:
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer-desktop-dev
|
||||
executableName: splayer-desktop
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
# 构建的目标类型
|
||||
@@ -37,7 +37,7 @@ nsis:
|
||||
# 创建桌面图标
|
||||
createDesktopShortcut: always
|
||||
# 是否允许 UAC 提升权限
|
||||
allowElevation: false
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# macOS 平台配置
|
||||
@@ -64,7 +64,7 @@ linux:
|
||||
- snap
|
||||
- deb
|
||||
# 维护者信息
|
||||
maintainer: electronjs.org
|
||||
maintainer: imsyy.top
|
||||
# 应用程序类别
|
||||
category: Utility
|
||||
# AppImage 配置
|
||||
@@ -76,6 +76,8 @@ npmRebuild: false
|
||||
# 自动更新的配置
|
||||
publish:
|
||||
# 更新提供商
|
||||
provider: generic
|
||||
provider: github
|
||||
# 自动更新检查的 URL
|
||||
url: https://example.com/auto-updates
|
||||
# url: https://example.com/auto-updates
|
||||
owner: "imsyy"
|
||||
repo: "splayer"
|
||||
|
||||
@@ -3,9 +3,11 @@ import { app, protocol, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import createSystemInfo from "@main/createSystemInfo";
|
||||
import createGlobalShortcut from "@main/createGlobalShortcut";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import log from "electron-log";
|
||||
|
||||
// 主窗口
|
||||
let mainWindow;
|
||||
@@ -21,6 +23,14 @@ protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "imsyy-splayer", privileges: { standard: true, secure: true } },
|
||||
]);
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/splayer-log.txt");
|
||||
// 设置日志文件的最大大小为 10 MB
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024;
|
||||
// 绑定 console.log
|
||||
console.log = log.log.bind(log);
|
||||
|
||||
// 创建主窗口
|
||||
const createWindow = () => {
|
||||
// 创建浏览器窗口
|
||||
@@ -84,7 +94,7 @@ app.whenReady().then(async () => {
|
||||
if (!gotTheLock) {
|
||||
// 如果获取不到单例锁,表示已经有一个实例在运行
|
||||
app.quit();
|
||||
console.error("已有一个程序正在运行");
|
||||
log.error("已有一个程序正在运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,6 +138,9 @@ app.whenReady().then(async () => {
|
||||
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(mainWindow);
|
||||
|
||||
// 检测更新
|
||||
configureAutoUpdater(process.platform);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { readDirAsync } from "@main/readDirAsync";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { write } from "node-id3";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/getNeteaseMusicUrl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
|
||||
29
electron/main/utils/checkUpdates.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
|
||||
const checkForUpdates = () => {
|
||||
autoUpdater.checkForUpdates();
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
checkForUpdates();
|
||||
|
||||
// 监听检查更新的事件
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("Checking for update...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
console.log("Update available:", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("Update not available.");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
console.log("Update downloaded. Ready to install.");
|
||||
|
||||
// 在需要的时候,触发安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
};
|
||||
14
package.json
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.1",
|
||||
"description": "An Electron application with Vue",
|
||||
"version": "2.0.0-beta.2",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
"homepage": "https://www.electronjs.org",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
@@ -24,6 +28,7 @@
|
||||
"axios": "^1.4.0",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
"howler": "^2.2.3",
|
||||
@@ -46,6 +51,7 @@
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
|
||||
54
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ dependencies:
|
||||
electron-dl:
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1
|
||||
electron-updater:
|
||||
specifier: ^6.1.7
|
||||
version: 6.1.7
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.18.2
|
||||
@@ -88,6 +91,9 @@ devDependencies:
|
||||
electron-builder:
|
||||
specifier: ^24.6.3
|
||||
version: 24.6.3
|
||||
electron-log:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
electron-vite:
|
||||
specifier: ^1.0.27
|
||||
version: 1.0.27(vite@4.4.9)
|
||||
@@ -1208,7 +1214,6 @@ packages:
|
||||
|
||||
/argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/array-flatten@1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
@@ -1416,6 +1421,16 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/builder-util-runtime@9.2.3:
|
||||
resolution: {integrity: sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
sax: 1.2.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/builder-util@24.5.0:
|
||||
resolution: {integrity: sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==}
|
||||
dependencies:
|
||||
@@ -1981,6 +1996,11 @@ packages:
|
||||
unused-filename: 2.1.0
|
||||
dev: false
|
||||
|
||||
/electron-log@5.0.1:
|
||||
resolution: {integrity: sha512-x4wnwHg00h/onWQgjmvcdLV7Mrd9TZjxNs8LmXVpqvANDf4FsSs5wLlzOykWLcaFzR3+5hdVEQ8ctmrUxgHlPA==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/electron-publish@24.5.0:
|
||||
resolution: {integrity: sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==}
|
||||
dependencies:
|
||||
@@ -1999,6 +2019,21 @@ packages:
|
||||
resolution: {integrity: sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==}
|
||||
dev: true
|
||||
|
||||
/electron-updater@6.1.7:
|
||||
resolution: {integrity: sha512-SNOhYizjkm4ET+Y8ilJyUzcVsFJDtINzVN1TyHnZeMidZEG3YoBebMyXc/J6WSiXdUaOjC7ngekN6rNp6ardHA==}
|
||||
dependencies:
|
||||
builder-util-runtime: 9.2.3
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 4.1.0
|
||||
lazy-val: 1.0.5
|
||||
lodash.escaperegexp: 4.1.2
|
||||
lodash.isequal: 4.5.0
|
||||
semver: 7.5.4
|
||||
tiny-typed-emitter: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/electron-vite@1.0.27(vite@4.4.9):
|
||||
resolution: {integrity: sha512-T8UVt9HtMFMMqU78dhv8TsRHYxMkuMTIZBIFYHzfeEoJ1Go3tVemgY/kO6sTTv94jIhkhcZIkvwmq4liABFjmA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -2575,7 +2610,6 @@ packages:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.0
|
||||
dev: true
|
||||
|
||||
/fs-extra@8.1.0:
|
||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||
@@ -3140,7 +3174,6 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/jsbn@0.1.1:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
@@ -3191,7 +3224,6 @@ packages:
|
||||
universalify: 2.0.0
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
dev: true
|
||||
|
||||
/jsprim@1.4.2:
|
||||
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
||||
@@ -3210,7 +3242,6 @@ packages:
|
||||
|
||||
/lazy-val@1.0.5:
|
||||
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
||||
dev: true
|
||||
|
||||
/levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
@@ -3259,6 +3290,14 @@ packages:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
dev: true
|
||||
|
||||
/lodash.escaperegexp@4.1.2:
|
||||
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
|
||||
dev: false
|
||||
|
||||
/lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
dev: false
|
||||
|
||||
/lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
@@ -4473,6 +4512,10 @@ packages:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
dev: false
|
||||
|
||||
/tiny-typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
|
||||
dev: false
|
||||
|
||||
/titleize@3.0.0:
|
||||
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4616,7 +4659,6 @@ packages:
|
||||
/universalify@2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
dev: true
|
||||
|
||||
/unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
|
||||
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 261 KiB |
BIN
screenshots/SPlayer - 本地音乐.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
screenshots/SPlayer - 歌单页面.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
screenshots/SPlayer - 评论页面.jpg
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
screenshots/build.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -39,3 +39,92 @@ export const getArtistDetail = (id) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取歌手部分信息和热门歌曲
|
||||
* @param {number} id - 歌手id
|
||||
*/
|
||||
export const getArtistSongs = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/artists",
|
||||
params: {
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取歌手全部歌曲
|
||||
* @param {number} id - 歌手id
|
||||
* @param {number} [limit=50] - 返回数量,默认50
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
* @param {string} order - hot: 热门, time: 时间
|
||||
*/
|
||||
export const getArtistAllSongs = (id, limit = 50, offset = 0, order = "hot") => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/artist/songs",
|
||||
params: {
|
||||
id,
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取歌手专辑
|
||||
* @param {number} id - 歌手id
|
||||
* @param {number} [limit=50] - 返回数量,默认50
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getArtistAblums = (id, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/artist/album",
|
||||
params: {
|
||||
id,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取歌手视频
|
||||
* @param {number} id - 歌手id
|
||||
* @param {number} [limit=50] - 返回数量,默认50
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getArtistVideos = (id, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/artist/mv",
|
||||
params: {
|
||||
id,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏歌手
|
||||
* @param {number} t - 操作类型,1 为收藏,其他为取消收藏
|
||||
* @param {number} id - 歌手id
|
||||
*/
|
||||
export const likeArtist = (t, id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/artist/sub",
|
||||
params: {
|
||||
t,
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -199,6 +199,7 @@ export const likePlaylist = (t, id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/subscribe",
|
||||
noCookie: true, // 临时解决无法收藏歌单
|
||||
params: {
|
||||
t,
|
||||
id,
|
||||
|
||||
@@ -78,5 +78,7 @@
|
||||
"more": "M6 10c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2z",
|
||||
"more-vert": "M12 8c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2z",
|
||||
"download": "M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z",
|
||||
"radio": "M3.24 6.15C2.51 6.43 2 7.17 2 8v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.89-2-2-2H8.3l8.26-3.34L15.88 1L3.24 6.15zM7 20c-1.66 0-3-1.34-3-3s1.34-3 3-3s3 1.34 3 3s-1.34 3-3 3zm13-8h-2v-2h-2v2H4V8h16v4z"
|
||||
"radio": "M3.24 6.15C2.51 6.43 2 7.17 2 8v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.89-2-2-2H8.3l8.26-3.34L15.88 1L3.24 6.15zM7 20c-1.66 0-3-1.34-3-3s1.34-3 3-3s3 1.34 3 3s-1.34 3-3 3zm13-8h-2v-2h-2v2H4V8h16v4z",
|
||||
"person-add": "M15.39 14.56C13.71 13.7 11.53 13 9 13s-4.71.7-6.39 1.56A2.97 2.97 0 0 0 1 17.22V20h16v-2.78c0-1.12-.61-2.15-1.61-2.66zM9 12c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4zm11-3V7c0-.55-.45-1-1-1s-1 .45-1 1v2h-2c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z",
|
||||
"person-remove": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4s4-1.79 4-4zM2 18v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4s-8 1.34-8 4zm16-8h4c.55 0 1 .45 1 1s-.45 1-1 1h-4c-.55 0-1-.45-1-1s.45-1 1-1z"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<n-text class="add-desc">{{ item.desc }}</n-text>
|
||||
</div>
|
||||
<!-- 播放按钮 -->
|
||||
<n-icon class="play" @click.stop>
|
||||
<n-icon class="play">
|
||||
<SvgIcon :icon="type !== 'artist' ? 'play-circle' : 'account-music'" />
|
||||
</n-icon>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<Transition name="fade" mode="out-in" @after-enter="checkHasPlaying">
|
||||
<div v-if="data !== 'empty' && data?.length && data[0] !== 'empty'" class="song-list">
|
||||
<div class="song-list-header">
|
||||
<div v-if="showTitle" class="song-list-header">
|
||||
<n-text class="num" depth="3"> # </n-text>
|
||||
<n-text :class="{ info: true, 'has-cover': data[0].cover && showCover }" depth="3">
|
||||
歌曲
|
||||
@@ -113,7 +113,12 @@
|
||||
</div>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="Array.isArray(item.artists)" class="artist">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" class="ar">
|
||||
<n-text
|
||||
v-for="ar in item.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
@@ -230,6 +235,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示表头
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 分页数据
|
||||
|
||||
@@ -120,15 +120,25 @@
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
<span v-if="music.getPlaySongData.alia" class="alia">{{
|
||||
music.getPlaySongData.alia
|
||||
}}</span>
|
||||
<span v-if="music.getPlaySongData.alia" class="alia">
|
||||
{{ music.getPlaySongData.alia }}
|
||||
</span>
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div v-if="Array.isArray(music.getPlaySongData.artists)" class="all-ar">
|
||||
<span v-for="ar in music.getPlaySongData.artists" :key="ar.id" class="ar">
|
||||
<span
|
||||
v-for="ar in music.getPlaySongData.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="
|
||||
() => {
|
||||
showFullPlayer = false;
|
||||
router.push(`/artist?id=${ar.id}`);
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,12 @@
|
||||
music.getPlaySongData?.artists && Array.isArray(music.getPlaySongData.artists)
|
||||
"
|
||||
>
|
||||
<n-text v-for="ar in music.getPlaySongData.artists" :key="ar.id" class="ar">
|
||||
<n-text
|
||||
v-for="ar in music.getPlaySongData.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</template>
|
||||
|
||||
@@ -102,7 +102,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
artists: v.artists,
|
||||
desc: v.copywriter,
|
||||
cover,
|
||||
coverSize: getCoverUrl(v.picUrl || v.cover, "464y260"),
|
||||
coverSize: getCoverUrl(v.imgurl16v9 || v.picUrl || v.cover, "464y260"),
|
||||
duration: v.duration,
|
||||
playCount: v.playCount,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="artist-albums">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="artistAblums !== 'empty'" class="list">
|
||||
<!-- 列表 -->
|
||||
<MainCover :data="artistAblums" type="album" />
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="artistAblums?.length"
|
||||
:totalCount="totalCount"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前歌手暂无专辑"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteSettings } from "@/stores";
|
||||
import { getArtistAblums } from "@/api/artist";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const settings = siteSettings();
|
||||
|
||||
// 歌手数据
|
||||
const artistId = ref(router.currentRoute.value.query.id);
|
||||
const artistAblums = ref(null);
|
||||
const totalCount = ref(0);
|
||||
const pageNumber = ref(Number(router.currentRoute.value.query?.page) || 1);
|
||||
|
||||
// 获取歌手全部专辑
|
||||
const getArtistAblumsData = async (id, limit = settings.loadSize, offset = 0) => {
|
||||
try {
|
||||
artistAblums.value = null;
|
||||
const result = await getArtistAblums(id, limit, offset);
|
||||
// 数据总数
|
||||
totalCount.value = result.artist.albumSize;
|
||||
if (totalCount.value === 0) return (artistAblums.value = "empty");
|
||||
// 处理数据
|
||||
artistAblums.value = formatData(result.hotAlbums, "album");
|
||||
} catch (error) {
|
||||
console.error("获取歌手专辑失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 页数变化
|
||||
const pageNumberChange = (page) => {
|
||||
router.push({
|
||||
path: "/artist/albums",
|
||||
query: {
|
||||
id: artistId.value,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
if (val.name === "ar-albums") {
|
||||
// 更改参数
|
||||
artistId.value = val.query.id;
|
||||
pageNumber.value = Number(val.query?.page) || 1;
|
||||
// 调用接口
|
||||
await getArtistAblumsData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getArtistAblumsData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,94 @@
|
||||
<template>
|
||||
<div class="artist-hot">歌手热门</div>
|
||||
<div class="artist-hot">
|
||||
<n-h4 class="title" prefix="bar">
|
||||
<n-text class="name">热门歌曲</n-text>
|
||||
<div class="more" @click="router.push(`/artist/songs?id=${artistId}`)">
|
||||
<n-text depth="3">查看全部</n-text>
|
||||
<n-icon class="more" depth="3">
|
||||
<SvgIcon icon="chevron-right" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-h4>
|
||||
<!-- 列表 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<SongList
|
||||
v-if="artistHotSongs"
|
||||
:data="artistHotSongs"
|
||||
:showPagination="false"
|
||||
:showTitle="false"
|
||||
/>
|
||||
<div v-else class="loading">
|
||||
<n-skeleton :repeat="10" text />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { getArtistSongs } from "@/api/artist";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 歌手数据
|
||||
const artistId = ref(router.currentRoute.value.query.id);
|
||||
const artistHotSongs = ref(null);
|
||||
|
||||
// 获取歌手热门数据
|
||||
const getArtistHotData = async (id) => {
|
||||
try {
|
||||
artistHotSongs.value = null;
|
||||
// 获取热门歌曲
|
||||
const result = await getArtistSongs(id);
|
||||
artistHotSongs.value = formatData(result.hotSongs, "song");
|
||||
} catch (error) {
|
||||
console.error("获取歌手热门数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
artistId.value = val.query.id;
|
||||
if (val.name === "ar-hot") {
|
||||
await getArtistHotData(artistId.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getArtistHotData(artistId.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.artist-hot {
|
||||
.title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 6px;
|
||||
margin-top: 16px;
|
||||
padding-left: 16px;
|
||||
cursor: pointer;
|
||||
.more {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
:deep(.n-skeleton) {
|
||||
&:nth-of-type(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
height: 70px;
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,91 +1,155 @@
|
||||
<template>
|
||||
<div class="artist">
|
||||
<div v-if="artistData && Object.keys(artistData)?.length" class="detail">
|
||||
<div class="cover">
|
||||
<!-- 头像 -->
|
||||
<n-image
|
||||
:src="artistData.coverSize.m"
|
||||
:previewed-img-props="{ style: { borderRadius: '8px' } }"
|
||||
:preview-src="artistData.cover"
|
||||
class="cover-img"
|
||||
show-toolbar-tooltip
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/avatar.jpg?assest" alt="avatar" />
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 头像背板 -->
|
||||
<n-image :src="artistData.coverSize.s" class="cover-shadow" preview-disabled />
|
||||
</div>
|
||||
<div class="data">
|
||||
<!-- 名称 -->
|
||||
<n-text class="name">{{ artistData.name || "未知艺术家" }}</n-text>
|
||||
<!-- 别名 -->
|
||||
<div class="alias">
|
||||
<n-text v-if="artistData?.alias?.length" class="alias-text">
|
||||
{{ artistData.alias[0] }}
|
||||
</n-text>
|
||||
<n-text v-if="artistData?.identify" class="identify">{{ artistData.identify }}</n-text>
|
||||
<div v-if="artistId" class="artist">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="artistData && Object.keys(artistData)?.length" class="detail">
|
||||
<div class="cover">
|
||||
<!-- 头像 -->
|
||||
<n-image
|
||||
:src="artistData.coverSize.m"
|
||||
:previewed-img-props="{ style: { borderRadius: '8px' } }"
|
||||
:preview-src="artistData.cover"
|
||||
class="cover-img"
|
||||
show-toolbar-tooltip
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/avatar.jpg?assest" alt="avatar" />
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 头像背板 -->
|
||||
<n-image :src="artistData.coverSize.s" class="cover-shadow" preview-disabled />
|
||||
</div>
|
||||
<!-- 数量 -->
|
||||
<n-space class="num">
|
||||
<div v-if="artistData.size?.music" class="num-item">
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.music }}</n-text>
|
||||
<div class="data">
|
||||
<!-- 名称 -->
|
||||
<div class="name">
|
||||
<n-text class="name-text">{{ artistData.name || "未知艺术家" }}</n-text>
|
||||
<n-text v-if="artistData?.alias?.length" class="name-alias" depth="3">
|
||||
{{ artistData.alias[0] }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-if="artistData.size?.album" class="num-item">
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="album" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.album }}</n-text>
|
||||
</div>
|
||||
<div v-if="artistData.size?.mv" class="num-item">
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="video" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.mv }}</n-text>
|
||||
</div>
|
||||
</n-space>
|
||||
<!-- 简介 -->
|
||||
<n-ellipsis
|
||||
v-if="artistData?.description"
|
||||
:tooltip="false"
|
||||
class="description"
|
||||
expand-trigger="click"
|
||||
line-clamp="2"
|
||||
>
|
||||
<n-text depth="3">{{ artistData.description }}</n-text>
|
||||
</n-ellipsis>
|
||||
<n-text v-else class="description">哇!竟然没有简介</n-text>
|
||||
<!-- 职业 -->
|
||||
<n-text v-if="artistData?.identify" class="identify" depth="3">
|
||||
{{ artistData.identify }}
|
||||
</n-text>
|
||||
<!-- 数量 -->
|
||||
<n-space class="num">
|
||||
<div
|
||||
v-if="artistData.size?.music"
|
||||
class="num-item"
|
||||
@click="router.push(`/artist/songs?id=${artistId}`)"
|
||||
>
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.music }}</n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="artistData.size?.album"
|
||||
class="num-item"
|
||||
@click="router.push(`/artist/albums?id=${artistId}`)"
|
||||
>
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="album" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.album }}</n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="artistData.size?.mv"
|
||||
class="num-item"
|
||||
@click="router.push(`/artist/videos?id=${artistId}`)"
|
||||
>
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="video" />
|
||||
</n-icon>
|
||||
<n-text depth="3">{{ artistData.size.mv }}</n-text>
|
||||
</div>
|
||||
</n-space>
|
||||
<!-- 简介 -->
|
||||
<n-ellipsis
|
||||
v-if="artistData?.description"
|
||||
:tooltip="false"
|
||||
:line-clamp="artistData?.identify ? 2 : 3"
|
||||
class="description"
|
||||
expand-trigger="click"
|
||||
>
|
||||
<n-text depth="3">{{ artistData.description }}</n-text>
|
||||
</n-ellipsis>
|
||||
<n-text v-else class="description">哇!竟然没有简介</n-text>
|
||||
<!-- 功能区 -->
|
||||
<n-space class="menu" justify="space-between">
|
||||
<n-button size="large" round strong secondary @click="likeOrDislike(artistId)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon :icon="isLikeOrDislike(artistId) ? 'person-add' : 'person-remove'" />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ isLikeOrDislike(artistId) ? "关注歌手" : "取消关注" }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="detail">
|
||||
<n-skeleton class="cover" />
|
||||
<div class="data">
|
||||
<n-skeleton :repeat="4" text />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="tabValue" class="tabs" type="segment" @update:value="tabChange">
|
||||
<n-tab name="ar-hot"> 热门 </n-tab>
|
||||
<n-tab name="ar-songs"> 单曲 </n-tab>
|
||||
<n-tab name="ar-albums"> 专辑 </n-tab>
|
||||
<n-tab name="ar-videos"> 视频 </n-tab>
|
||||
</n-tabs>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }" :mvSize="artistData ? artistData.size?.mv : null">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
<div v-else class="title">
|
||||
<n-text class="key">参数不完整</n-text>
|
||||
<n-button class="back" strong secondary @click="router.go(-1)"> 返回上一页 </n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getArtistDetail } from "@/api/artist";
|
||||
import { getArtistDetail, likeArtist } from "@/api/artist";
|
||||
import { siteData } from "@/stores";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
|
||||
// 歌手数据
|
||||
const artistId = ref(router.currentRoute.value.query.id);
|
||||
const artistData = ref(null);
|
||||
|
||||
// 默认选中
|
||||
const tabValue = ref(router.currentRoute.value?.name ?? "ar-hot");
|
||||
|
||||
// 获取歌手详情
|
||||
const getArtistDetailData = async () => {
|
||||
const getArtistDetailData = async (id) => {
|
||||
try {
|
||||
const result = await getArtistDetail(artistId.value);
|
||||
if (!id) return false;
|
||||
// 清空数据
|
||||
artistData.value = null;
|
||||
const result = await getArtistDetail(id);
|
||||
artistData.value = formatData(result.data.artist, "artist")[0];
|
||||
// 附加身份
|
||||
artistData.value.identify = result.data.identify?.imageDesc;
|
||||
@@ -96,6 +160,57 @@ const getArtistDetailData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 标签页切换
|
||||
const tabChange = (val) => {
|
||||
const routerPath = val.replace(/^ar-/, "");
|
||||
router.push({
|
||||
path: `/artist/${routerPath}`,
|
||||
query: {
|
||||
id: artistId.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 判断收藏还是取消
|
||||
const isLikeOrDislike = (id) => {
|
||||
const artists = userLikeData.value.artists;
|
||||
if (artists.length) {
|
||||
return !artists.some((item) => item.id === Number(id));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 关注 / 取消关注歌手
|
||||
const likeOrDislike = debounce(async (id) => {
|
||||
try {
|
||||
if (!isLogin()) return $message.warning("请登录后使用");
|
||||
const type = isLikeOrDislike(id) ? 1 : 2;
|
||||
const result = await likeArtist(type, id);
|
||||
if (result.code === 200) {
|
||||
$message.success((type === 1 ? "关注" : "取消关注") + "成功");
|
||||
// 更新用户歌单
|
||||
await data.setUserLikeArtists();
|
||||
} else {
|
||||
$message.error((type === 1 ? "关注" : "取消关注") + "失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("关注出错:", error);
|
||||
$message.error("关注操作出现错误");
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
tabValue.value = "ar-" + val.path.split("/")[2];
|
||||
if (val.path.split("/")[1] === "artist" && val.query.id !== artistId.value) {
|
||||
artistId.value = val.query.id;
|
||||
await getArtistDetailData(artistId.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getArtistDetailData(artistId.value);
|
||||
});
|
||||
@@ -111,9 +226,9 @@ onBeforeMount(async () => {
|
||||
.cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
min-width: 260px;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
min-width: 240px;
|
||||
margin-right: 20px;
|
||||
border-radius: 50%;
|
||||
.cover-img {
|
||||
@@ -148,14 +263,34 @@ onBeforeMount(async () => {
|
||||
}
|
||||
}
|
||||
.data {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
.name {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
.name-alias {
|
||||
&::before {
|
||||
content: "(";
|
||||
margin-right: 6px;
|
||||
}
|
||||
&::after {
|
||||
content: ")";
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.identify {
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.num {
|
||||
margin-top: 12px;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
.num-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -173,6 +308,9 @@ onBeforeMount(async () => {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
.menu {
|
||||
margin-top: 12px;
|
||||
}
|
||||
:deep(.n-skeleton) {
|
||||
&:first-child {
|
||||
width: 60%;
|
||||
@@ -185,5 +323,21 @@ onBeforeMount(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.key {
|
||||
margin: 10px 0;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.back {
|
||||
width: 98px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="artist-songs">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="artistAllSongs !== 'empty'" class="list">
|
||||
<!-- 列表 -->
|
||||
<SongList :data="artistAllSongs" :showPagination="false" />
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="artistAllSongs?.length"
|
||||
:totalCount="totalCount"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前歌手暂无歌曲"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteSettings } from "@/stores";
|
||||
import { getSongDetail } from "@/api/song";
|
||||
import { getArtistAllSongs } from "@/api/artist";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const settings = siteSettings();
|
||||
|
||||
// 歌手数据
|
||||
const artistId = ref(router.currentRoute.value.query.id);
|
||||
const artistAllSongs = ref(null);
|
||||
const totalCount = ref(0);
|
||||
const pageNumber = ref(Number(router.currentRoute.value.query?.page) || 1);
|
||||
|
||||
// 获取歌手全部歌曲
|
||||
const getArtistAllSongsData = async (id, limit = settings.loadSize, offset = 0) => {
|
||||
try {
|
||||
const result = await getArtistAllSongs(id, limit, offset);
|
||||
// 数据总数
|
||||
totalCount.value = result.total;
|
||||
if (totalCount.value === 0) return (artistAllSongs.value = "empty");
|
||||
// 处理数据
|
||||
const ids = result.songs.map((song) => song.id).join(",");
|
||||
const songsDetail = await getSongDetail(ids);
|
||||
artistAllSongs.value = formatData(songsDetail.songs, "song");
|
||||
} catch (error) {
|
||||
console.error("获取歌手全部歌曲失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 页数变化
|
||||
const pageNumberChange = (page) => {
|
||||
router.push({
|
||||
path: "/artist/songs",
|
||||
query: {
|
||||
id: artistId.value,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
if (val.name === "ar-songs") {
|
||||
// 更改参数
|
||||
artistId.value = val.query.id;
|
||||
pageNumber.value = Number(val.query?.page) || 1;
|
||||
// 调用接口
|
||||
await getArtistAllSongsData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getArtistAllSongsData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="artist-videos">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="artistVideos !== 'empty'" class="list">
|
||||
<!-- 列表 -->
|
||||
<MainCover :data="artistVideos" columns="1 s:2 m:3 l:4 xl:5" type="mv" />
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="artistVideos?.length"
|
||||
:totalCount="totalCount"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前歌手暂无视频"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteSettings } from "@/stores";
|
||||
import { getArtistVideos } from "@/api/artist";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const settings = siteSettings();
|
||||
const props = defineProps({
|
||||
// 视频总数
|
||||
mvSize: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 歌手数据
|
||||
const artistId = ref(router.currentRoute.value.query.id);
|
||||
const artistVideos = ref(null);
|
||||
const totalCount = ref(0);
|
||||
const pageNumber = ref(Number(router.currentRoute.value.query?.page) || 1);
|
||||
|
||||
// 获取歌手全部视频
|
||||
const getArtistVideosData = async (id, limit = settings.loadSize, offset = 0) => {
|
||||
try {
|
||||
artistVideos.value = null;
|
||||
const result = await getArtistVideos(id, limit, offset);
|
||||
// 数据总数
|
||||
totalCount.value = props.mvSize;
|
||||
if (totalCount.value === 0) return (artistVideos.value = "empty");
|
||||
// 处理数据
|
||||
artistVideos.value = formatData(result.mvs, "mv");
|
||||
} catch (error) {
|
||||
console.error("获取歌手视频失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 页数变化
|
||||
const pageNumberChange = (page) => {
|
||||
router.push({
|
||||
path: "/artist/videos",
|
||||
query: {
|
||||
id: artistId.value,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
async (val) => {
|
||||
if (val.name === "ar-videos") {
|
||||
// 更改参数
|
||||
artistId.value = val.query.id;
|
||||
pageNumber.value = Number(val.query?.page) || 1;
|
||||
// 调用接口
|
||||
await getArtistVideosData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getArtistVideosData(
|
||||
artistId.value,
|
||||
settings.loadSize,
|
||||
(pageNumber.value - 1) * settings.loadSize,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -431,11 +431,11 @@ const likeOrDislike = debounce(async (id) => {
|
||||
const type = isLikeOrDislike(id) ? 1 : 2;
|
||||
const result = await likePlaylist(type, id);
|
||||
if (result.code === 200) {
|
||||
$message.success(type === 1 ? "收藏" : "取消收藏" + "成功");
|
||||
$message.success((type === 1 ? "收藏" : "取消收藏") + "成功");
|
||||
// 更新用户歌单
|
||||
await data.setUserLikePlaylists();
|
||||
} else {
|
||||
$message.error(type === 1 ? "收藏" : "取消收藏" + "失败,请重试");
|
||||
$message.error((type === 1 ? "收藏" : "取消收藏") + "失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("收藏出错:", error);
|
||||
|
||||