Compare commits

...

54 Commits

Author SHA1 Message Date
imsyy
ecdb3b75ba fix: 修复歌单为空时的提示 2023-04-20 17:00:44 +08:00
imsyy
415cf3b3c9 feat: 播放列表重构 & fix: 修复一些样式问题 2023-04-20 16:48:28 +08:00
imsyy
5425288e16 feat: 设置页面重构 2023-04-19 17:59:12 +08:00
imsyy
ae8f3696f3 fix: 修复一些样式问题 2023-04-19 16:39:57 +08:00
imsyy
80aea0826b fix: 区分网页端解灰种类 2023-04-18 17:26:34 +08:00
imsyy
a65a7224ae feat: 尝试支持 UnblockNeteaseMusic 2023-04-18 15:05:39 +08:00
imsyy
7592296124 fix: 修复逐字歌词情况下歌词滚动异常 2023-04-17 14:26:34 +08:00
imsyy
0922735b2d fix: 修复逐字歌词导致的一系列问题 2023-04-17 11:57:15 +08:00
imsyy
7495e7af2d feat: 支持逐字歌词显示 2023-04-14 17:26:12 +08:00
imsyy
db5aebbf89 feat: 新增歌曲前奏等待提醒 2023-04-14 14:56:05 +08:00
imsyy
7415f591b3 feat: 更换歌词解析方式 2023-04-13 16:50:12 +08:00
imsyy
e138d06e6f feat: 歌词滚动组件抽离 2023-04-13 14:06:08 +08:00
imsyy
45b374a0cb fix: 歌单无法展示 2023-04-12 16:28:56 +08:00
imsyy
dd66725d9c feat: 站点标题跟随页面内容 2023-04-12 14:40:01 +08:00
imsyy
55df8f05dc feat: 新增收藏专辑页面 & 部分变量调整 2023-04-11 15:21:18 +08:00
imsyy
18359d69d2 feat: 新增收藏/取消收藏专辑 & 歌手页优化 2023-04-10 18:29:40 +08:00
imsyy
d291e86998 feat: 免责声明 2023-04-10 14:09:29 +08:00
imsyy
6a7ee27075 fix: 修复部分歌词滚动异常 2023-04-10 10:19:38 +08:00
imsyy
041990d702 fix: 解决重复调用 2023-04-08 17:29:22 +08:00
imsyy
af63ba2e9c feat: 部分体验优化 2023-04-08 15:39:47 +08:00
imsyy
fa1133b726 fix: 修复私人 FM 无法获取下一首 2023-04-08 11:31:04 +08:00
imsyy
9d5d5a9fc9 fix: 私人 FM 暂停异常 2023-04-08 09:27:11 +08:00
imsyy
05e57ab143 feat: 完善部分页面 & 更换部分图标 2023-04-07 18:14:45 +08:00
imsyy
649052c9a9 fix: 修复评论区 VIP 用户显示异常 2023-04-07 11:57:25 +08:00
imsyy
bd1a515031 feat: 弹窗抽离 & fix: 部分弹窗无法滚动 2023-04-06 18:04:59 +08:00
imsyy
0411f5dc71 fix: 修复部分歌曲无法下载 2023-04-04 18:21:50 +08:00
imsyy
f43d8f4641 feat: 新增导航栏下拉菜单 2023-04-04 14:16:54 +08:00
imsyy
3909b2ff34 fix: 修复歌手页的小问题 2023-04-03 16:19:00 +08:00
imsyy
58e2925bed feat: 优化云盘上传操作 2023-04-03 15:37:38 +08:00
imsyy
3ff5be1f0d fix: 去除开发环境 PWA & 修复一些问题 2023-03-29 16:33:01 +08:00
imsyy
33831ee861 fix: 歌词滚动异常
- 这屎山代码快要看不懂了 😂
2023-03-28 15:52:02 +08:00
imsyy
85ec29d874 fix: 音乐频谱无法显示 2023-03-28 15:32:10 +08:00
imsyy
e08c6ab7ae fix: 优化歌词模糊效果 2023-03-28 15:18:53 +08:00
imsyy
db7aaa424a fix: 修复歌词异常空行 2023-03-28 14:14:25 +08:00
imsyy
e2f2e1292b fix: 一些小问题 2023-03-28 13:48:44 +08:00
imsyy
c9176c9e02 feat: 新增歌曲下载功能 2023-03-28 11:35:49 +08:00
imsyy
b14831f8ab feat: 新增云盘上传进度条 2023-03-27 16:23:51 +08:00
imsyy
49da3a602e feat: 新增歌词模糊
- 新增歌词模糊设置项,默认关闭
2023-03-27 11:50:47 +08:00
imsyy
bca9227be1 fix: 分页参数丢失 2023-03-25 16:02:32 +08:00
imsyy
decd628dc8 更新图片 2023-03-25 13:52:48 +08:00
imsyy
d1c3376a57 更新说明 2023-03-25 13:34:50 +08:00
imsyy
3425a7f77e 更新说明 2023-03-25 13:32:56 +08:00
imsyy
c5b51401d6 支持每日签到及云贝签到 2023-03-25 10:53:30 +08:00
imsyy
e06ac034ff 修复 pnpm-lock 2023-03-23 17:24:33 +08:00
imsyy
fb90e3f93e 修复一些问题 2023-03-23 16:34:48 +08:00
imsyy
43d0d6cffc 修复 路径错误 2023-03-23 16:02:59 +08:00
imsyy
aaa96ef691 API 路径重构 2023-03-23 15:59:28 +08:00
imsyy
aa21948bef 修复专辑图片错误 2023-03-23 11:47:59 +08:00
imsyy
a43aae0a96 新增 歌手全部歌曲 2023-03-23 11:18:14 +08:00
imsyy
86483108e7 新增 全部新碟 & 修复分页 2023-03-22 17:20:41 +08:00
imsyy
cbd241f7c6 新增 删除搜索历史 2023-03-22 16:25:29 +08:00
imsyy
c7fa6efa40 样式调整 2023-03-22 14:54:41 +08:00
imsyy
b0b96f15d9 样式调整 2023-03-22 14:54:14 +08:00
imsyy
e36dc462f9 新增 搜索历史 2023-03-22 14:17:51 +08:00
113 changed files with 6715 additions and 3430 deletions

15
.env
View File

@@ -1,5 +1,20 @@
# 全局 API 地址
## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi
VITE_MUSIC_API = "https://api-music.imsyy.top/"
# 网易云解灰 API 地址
## 需部署 API详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C
VITE_UNM_API = "https://api-unm.imsyy.top/"
# ICP 备案号
## 若不需要,请设为空即可
VITE_ICP = "豫ICP备2022018134号-1"
# 公告配置
## 若无需公告,请将任意一项设为空即可
## 公告标题
VITE_ANN_TITLE = ""
## 公告内容
VITE_ANN_CONTENT = ""
## 公告时长(毫秒)不可超过 999999
VITE_ANN_DURATION = 3000

18
.hintrc Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": [
"development"
],
"hints": {
"detect-css-reflows/composite": "off",
"detect-css-reflows/layout": "off",
"detect-css-reflows/paint": "off",
"compat-api/css": [
"default",
{
"ignore": [
"backdrop-filter"
]
}
]
}
}

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@@ -1,9 +1,13 @@
# SPlayer
<div align="center">
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
<h2>SPlayer</h2>
<p>一个简约的在线音乐播放器</p>
<img alt="main" src="./screenshots/main.png" />
</div>
<br />
> 一个简约的在线音乐播放器,**项目尚未完成**,不保证可用性
本项目采用 Vue 3 全家桶及 SCSS 开发
目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
> 本项目采用 Vue 3 全家桶及 SCSS 开发
> 目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
## 👀 Demo
@@ -11,80 +15,84 @@
## 🎉 功能
- 账号登录
- 扫码登录
- 更多登录方式待添加
- 管理
- 新建歌单
- 歌单编辑
- 收藏 / 取消收藏歌单
- 推荐
- 每日推荐歌曲
- 私人 FM
- 音乐云盘
- 云盘音乐上传
- 云盘内歌曲播放
- 云盘歌曲纠正
- 云盘歌曲删除
- 播放
- 歌词滚动以及歌词翻译
- MV 与视频播放
- 音乐频谱显示( 实验性功能,需在设置中开启
- 音乐渐入渐出
- 其他
- 支持 PWA
- 支持评论区功能
- 明暗模式自动 / 手动切换
- 对移动端简单适配
- 支持扫码登录
- 支持手机号登录(目前暂时无法使用)
- 自动进行每日签到及云贝签到
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
- 由于酷我音源不支持 `https`,故网页端替换可能不全面
- 下载歌曲(最高支持 Hi-Res
- 新建歌单及歌单编辑
- 收藏 / 取消收藏歌单或歌手
- 每日推荐歌曲
- 私人 FM
- 云盘音乐上传
- 云盘内歌曲播放
- 云盘内歌曲纠正
- 云盘歌曲删除
- 支持逐字歌词
- 歌词滚动以及歌词翻译
- MV 与视频播放
- 音乐频谱显示( 实验性功能,需在设置中开启
- 音乐渐入渐出
- 支持 PWA
- 支持评论区及评论点赞
- 明暗模式自动 / 手动切换
- 移动端基础适配
- [ ] 主题换肤
#### 待办
- [ ] 发表评论
## 😍 Screenshots
<details>
<summary>主页面</summary>
![主页面](/screenshots/SPlayer%20-%20%E4%B8%BB%E9%A1%B5%E9%9D%A2.jpg)
![主页面](/screenshots/SPlayer%20-%20%E4%B8%BB%E9%A1%B5%E9%9D%A2.png)
</details>
<details>
<summary>播放页面</summary>
![播放页面](/screenshots/SPlayer%20-%20%E6%92%AD%E6%94%BE%E9%A1%B5%E9%9D%A2.jpg)
![播放页面](/screenshots/SPlayer%20-%20%E6%92%AD%E6%94%BE%E9%A1%B5%E9%9D%A2.png)
</details>
<details>
<summary>发现页面</summary>
![发现页面](/screenshots/SPlayer%20-%20%E5%8F%91%E7%8E%B0%E9%A1%B5%E9%9D%A2.jpg)
![发现页面](/screenshots/SPlayer%20-%20%E5%8F%91%E7%8E%B0%E9%A1%B5%E9%9D%A2.png)
</details>
<details>
<summary>歌单页面</summary>
![歌单页面](/screenshots/SPlayer%20-%20%E6%AD%8C%E5%8D%95%E9%A1%B5%E9%9D%A2.jpg)
![歌单页面](/screenshots/SPlayer%20-%20%E6%AD%8C%E5%8D%95%E9%A1%B5%E9%9D%A2.png)
</details>
<details>
<summary>评论页面</summary>
![评论页面](/screenshots/SPlayer%20-%20%E8%AF%84%E8%AE%BA%E9%A1%B5%E9%9D%A2.jpg)
![评论页面](/screenshots/SPlayer%20-%20%E8%AF%84%E8%AE%BA%E9%A1%B5%E9%9D%A2.png)
</details>
## ⚙️ 部署
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
### API 服务
### API 服务(必需)
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址(必需)
```js
VITE_MUSIC_API = "your api url"
```
### 网易云解灰 API可选
如需使用网易云解灰服务,请前往 [UNM-Server](https://github.com/imsyy/UNM-Server) 部署在线 API 服务并将 `API` 地址填入 `.env` 环境变量中,该服务用于网页端替换无法播放或无版权的歌曲。如不需要该服务,请前往站点的 `全局设置` 中关闭
### 安装依赖
```bash
@@ -123,4 +131,16 @@ npm build
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
## 📜 开源许可
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
- 本项目基于 [MIT license](https://opensource.org/license/mit/) 许可进行开源
## 📢 免责声明
本项目使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途。** 本项目旨在提供一个前端练手的实战项目,用于帮助开发者提升技能水平和对前端技术的理解
同时,本项目开发者承诺 **严格遵守相关法律法规和网易云音乐 API 使用协议,不会利用本项目进行任何违法活动。** 如因使用本项目而引起的任何纠纷或责任,均由使用者自行承担。**本项目开发者不承担任何因使用本项目而导致的任何直接或间接责任,并保留追究使用者违法行为的权利**
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证

View File

@@ -1 +0,0 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })

View File

@@ -1 +0,0 @@
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise((i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()})).then((()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didnt register its module`);return e})));self.define=(n,s)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let r={};const l=e=>t(e,o),c={module:{uri:o},exports:r,require:l};i[o]=Promise.all(n.map((e=>c[e]||l(e)))).then((e=>(s(...e),r)))}}define(["./workbox-d4ada07d"],(function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"3ca0b8505b4bec776b69afdba2768812"},{revision:null,url:"index.html"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"),{allowlist:[/^\/$/]})),e.registerRoute(/(.*?)\.(woff2|woff|ttf)/,new e.CacheFirst({cacheName:"file-cache",plugins:[]}),"GET"),e.registerRoute(/(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,new e.CacheFirst({cacheName:"image-cache",plugins:[]}),"GET")}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/images/logo/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
<title>SPlayer</title>
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" />
<meta name="description" content="一个简约的在线音乐播放器具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" />

View File

@@ -1,7 +1,6 @@
{
"name": "splayer",
"version": "0.5.0",
"private": true,
"version": "1.1.3",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",
@@ -13,8 +12,6 @@
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@ivanv/vue-collapse-transition": "^1.0.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-terser": "^0.4.0",
"artplayer": "^4.5.12",
"axios": "^1.2.0",
"pinia": "^2.0.26",
@@ -27,12 +24,13 @@
"vue-router": "^4.1.6"
},
"devDependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-terser": "^0.4.0",
"@vicons/material": "^0.12.0",
"@vitejs/plugin-vue": "^3.2.0",
"naive-ui": "^2.34.2",
"unplugin-auto-import": "^0.12.0",
"unplugin-vue-components": "^0.22.11",
"vfonts": "^0.0.3",
"vite": "^3.2.4",
"vite-plugin-pwa": "^0.14.1"
}

1327
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
screenshots/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

View File

@@ -10,7 +10,12 @@
:native-scrollbar="false"
embedded
>
<main ref="mainContent" class="main">
<main
ref="mainContent"
class="main"
id="main"
:class="[music.showPlayList ? 'playlist' : null]"
>
<n-back-top
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
style="transition: all 0.3s"
@@ -30,9 +35,10 @@
</template>
<script setup>
import { musicStore, userStore } from "@/store";
import { musicStore, userStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
import { getLoginState } from "@/api";
import { getLoginState, refreshLogin } from "@/api/login";
import { userDailySignin, userYunbeiSign } from "@/api/user";
import Provider from "@/components/Provider/index.vue";
import Nav from "@/components/Nav/index.vue";
import Player from "@/components/Player/index.vue";
@@ -40,14 +46,24 @@ import packageJson from "@/../package.json";
const music = musicStore();
const user = userStore();
const setting = settingStore();
const router = useRouter();
const mainContent = ref(null);
// 公告数据
const annShow =
import.meta.env.VITE_ANN_TITLE && import.meta.env.VITE_ANN_CONTENT
? true
: false;
const annTitle = import.meta.env.VITE_ANN_TITLE;
const annContene = import.meta.env.VITE_ANN_CONTENT;
const annDuration = Number(import.meta.env.VITE_ANN_DURATION);
// 空格暂停与播放
const spacePlayOrPause = (e) => {
if (e.code === "Space") {
console.log(e.target.tagName);
if (router.currentRoute.value.name == "video") return false;
if (router.currentRoute.value.name === "video") return false;
if (e.target.tagName === "BODY") {
e.preventDefault();
music.setPlayState(!music.getPlayState);
@@ -57,16 +73,91 @@ const spacePlayOrPause = (e) => {
}
};
onMounted(() => {
// 挂载主窗口至全局
window.$mainContent = mainContent.value;
// 更改页面标题
const setSiteTitle = (val) => {
const title = val
? val === "SPlayer"
? val
: val + " - SPlayer"
: user.siteTitle;
user.setSiteTitle(title);
sessionStorage.setItem("siteTitle", title);
if (!music.getPlayState) {
window.document.title = title;
}
};
// 初始化
$notification["info"]({
content: "项目未完成",
meta: "最近更新:音乐库页面完善",
duration: 8000,
});
// 刷新登录
const toRefreshLogin = () => {
const today = Date.now();
const threeDays = 3 * 24 * 60 * 60 * 1000;
const lastRefreshDate = new Date(
localStorage.getItem("lastRefreshDate")
).getTime();
if (today - lastRefreshDate >= threeDays || !lastRefreshDate) {
refreshLogin().then((res) => {
if (res.code === 200) {
localStorage.setItem(
"lastRefreshDate",
new Date(today).toLocaleDateString()
);
console.log("刷新登录成功");
} else {
console.error("刷新登录失败");
}
});
}
};
// 用户签到
const signIn = () => {
const today = new Date().toLocaleDateString();
const lastSignInDate = localStorage.getItem("lastSignInDate");
if (lastSignInDate !== today) {
const signInPromises = [userDailySignin(0), userYunbeiSign()];
Promise.all(signInPromises)
.then((results) => {
localStorage.setItem("lastSignInDate", today);
console.log("签到成功!");
console.log("userDailySignin:", results[0]);
console.log("userYunbeiSign:", results[1]);
$notification["success"]({
content: "签到成功",
meta: "每日签到及云贝签到成功",
duration: 3000,
});
})
.catch((error) => {
console.error("签到失败:", error);
$message.error("每日签到失败");
});
} else {
console.log("今天已经签到过了!");
}
};
// 系统重置
const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功");
localStorage.clear();
window.location.href = "/";
};
onMounted(() => {
// 挂载至全局
window.$mainContent = mainContent.value;
window.$cleanAll = cleanAll;
window.$signIn = signIn;
window.$setSiteTitle = setSiteTitle;
// 公告
if (annShow) {
$notification["info"]({
content: annTitle,
meta: annContene,
duration: annDuration,
});
}
// 版权声明
const logoText = "SPlayer";
@@ -76,12 +167,23 @@ onMounted(() => {
"color:#f55e55;font-size:26px;font-weight:bold;",
"font-size:16px"
);
console.info(
"若站点出现异常,可尝试在下方输入 %c$cleanAll()%c 然后按回车来重置",
"background: #eaeffd;color:#f55e55;padding: 4px 6px;border-radius:8px;",
"background:unset;color:unset;"
);
// 检查账号登录状态
getLoginState()
.then((res) => {
if (res.data.profile && user.userLogin) {
// 签到
if (setting.autoSignIn) signIn();
// 刷新登录
toRefreshLogin();
// 保存登录信息
user.userLogin = true;
user.setUserData(res.data.profile);
user.setUserOtherData();
} else {
user.userLogOut();
@@ -92,7 +194,9 @@ onMounted(() => {
}
})
.catch((err) => {
$message.error("遇到错误" + err);
$message.error("请求发生错误");
console.error("请求发生错误" + err);
router.push("/500");
return false;
});
@@ -121,6 +225,18 @@ onMounted(() => {
:deep(.n-scrollbar-rail--vertical) {
right: 0;
}
.main {
max-width: 1400px;
margin: 0 auto;
div:nth-of-type(2) {
transition: all 0.3s;
}
&.playlist {
div:nth-of-type(2) {
transform: scale(0.98);
}
}
}
}
// 路由跳转动画

67
src/api/album.js Normal file
View File

@@ -0,0 +1,67 @@
import axios from "@/utils/request";
/**
* 专辑部分
*/
/**
* 获取专辑内容
* @param {number} id - 专辑id
*/
export const getAlbum = (id) => {
return axios({
method: "GET",
url: "/album",
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取全部新碟
* @param {string} area - 地区码ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getAlbumNew = (area, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/album/new",
params: {
area,
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取专辑排行榜数据
* @param {boolean} [detail=true] 是否获取详情数据默认为true
*/
export const getToplist = (detail = true) => {
return axios({
method: "GET",
url: `/toplist${detail ? "/detail" : null}`,
});
};
/**
* 收藏/取消收藏专辑
* @param {number} t - 操作类型1为收藏2为取消收藏
* @param {number} id - 专辑id
*/
export const likeAlbum = (t, id) => {
return axios({
method: "GET",
url: "/album/sub",
params: {
t,
id,
timestamp: new Date().getTime(),
},
});
};

142
src/api/artist.js Normal file
View File

@@ -0,0 +1,142 @@
import axios from "@/utils/request";
/**
* 歌手部分
*/
/**
* 歌手分类列表
* @param {number} type - 歌手类型(-1:全部 1:男歌手 2:女歌手 3:乐队)
* @param {number} area - 歌手区域(-1:全部 7:华语 96:欧美 8:日本 16:韩国 0:其他)
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
* @param {number} initial - 首字母索引查找参数
*/
export const getArtistList = (
type = -1,
area = -1,
limit = 30,
offset = 0,
initial = -1
) => {
return axios({
method: "GET",
url: "/artist/list",
params: {
type,
area,
limit,
offset,
initial,
},
});
};
/**
* 获取歌手详情
* @param {number} id - 歌手id
*/
export const getArtistDetail = (id) => {
return axios({
method: "GET",
url: "/artist/detail",
params: {
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=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
* @param {string} order - hot: 热门, time: 时间
*/
export const getArtistAllSongs = (
id,
limit = 30,
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=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getArtistAblums = (id, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/artist/album",
params: {
id,
limit,
offset,
},
});
};
/**
* 获取歌手视频
* @param {number} id - 歌手id
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getArtistVideos = (id, limit = 30, 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",
hiddenBar: true,
url: "/artist/sub",
params: {
t,
id,
timestamp: new Date().getTime(),
},
});
};

71
src/api/comment.js Normal file
View File

@@ -0,0 +1,71 @@
import axios from "@/utils/request";
/**
* 评论部分
*/
/**
* 获取评论
* @param {number} id - 对应资源的id
* @param {number} [offset=0] - 分页的偏移量默认为0
* @param {number|null} [before=null] - 获取早于某一条评论的评论默认为null
* @param {string} [type="music"] - 对应资源的类型,默认为"music"
*/
export const getComment = (id, offset = 0, before = null, type = "music") => {
return axios({
method: "GET",
url: `/comment/${type}`,
params: {
id,
offset,
before,
timestamp: new Date().getTime(),
},
});
};
/**
* 评论点赞
* @param {number} id - 对应资源的id
* @param {number} cid - 评论的id
* @param {number} t - 是否点赞0为取消点赞1为点赞
* @param {number} [type=0] - 对应资源的类型默认为0歌曲
*/
export const likeComment = (id, cid, t, type = 0) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/comment/like",
params: {
id,
cid,
t,
type,
timestamp: new Date().getTime(),
},
});
};
/**
* 发送/删除评论
* @param {number} id - 对应资源的id
* @param {number} commentId - 回复的评论 id回复评论时必填
* @param {string} content - 要发送的内容
* @param {number} t - 是否点赞0为删除1为发送2为回复
* @param {number} [type=0] - 对应资源的类型默认为0歌曲
*/
export const sendComment = (id, commentId = null, content, t, type = 0) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/comment",
params: {
id,
commentId,
content,
t,
type,
timestamp: new Date().getTime(),
},
});
};

125
src/api/home.js Normal file
View File

@@ -0,0 +1,125 @@
import axios from "@/utils/request";
/**
* 首页推荐部分
*/
/**
* 获取网站首页轮播图列表
*/
export const getBanner = () => {
return axios({
method: "GET",
url: "/banner",
});
};
/**
* 获取每日推荐歌曲
*/
export const getDailySongs = () => {
return axios({
method: "GET",
url: "/recommend/songs",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 获取历史日推可用日期列表
*/
export const getDailySongsHistory = () => {
return axios({
method: "GET",
url: "/history/recommend/songs",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 获取历史日推详情数据
* @param {string} date - 日期
*/
export const getDailySongsHistoryDetail = (date) => {
return axios({
method: "GET",
url: "/history/recommend/songs/detail",
params: {
date,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取私人FM数据
*/
export const getPersonalFm = () => {
return axios({
method: "GET",
hiddenBar: true,
url: "/personal_fm",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 将指定歌曲加入垃圾桶
* @param {number} id 歌曲ID
*/
export const setFmTrash = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/fm_trash",
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取首页推荐内容列表
* @param {string} [type=null] - 推荐类型,可选值包括"null"(默认歌单),"mv"MV"newsong"(新音乐),"djprogram"(电台)和"privatecontent"(独家放送)
* @param {number} [limit=10] - 返回结果的数量默认为10
*/
export const getPersonalized = (type = null, limit = 10) => {
return axios({
method: "GET",
url: `/personalized/${type}`,
params: {
limit,
},
});
};
/**
* 获取首页热门歌手列表
* @param {number} [limit=6] - 可选参数要返回的歌手数量。默认为6个
*/
export const getTopArtists = (limit = 6) => {
return axios({
method: "GET",
url: "/top/artists",
params: {
limit,
},
});
};
/**
* 获取首页最新专辑列表
*/
export const getNewAlbum = () => {
return axios({
method: "GET",
url: "/album/newest",
});
};

View File

@@ -1,769 +0,0 @@
import axios from "@/api/request";
/*
* 用户部分
*/
// 二维码 key 生成
export const getQrKey = () => {
return axios({
method: "GET",
loadingBar: "Hidden",
url: "/login/qr/key",
params: {
time: new Date().getTime(),
},
});
};
// 二维码生成
export const qrCreate = (key, qrimg = true) => {
return axios({
method: "GET",
loadingBar: "Hidden",
url: "/login/qr/create",
params: {
key,
qrimg,
time: new Date().getTime(),
},
});
};
// 二维码状态接口
export const checkQr = (key) => {
return axios({
method: "GET",
url: "/login/qr/check",
loadingBar: "Hidden",
params: {
key,
time: new Date().getTime(),
},
});
};
// 手机登录
export const toLogin = (phone, captcha, type = "phone") => {
return axios({
method: "GET",
url: `/login/${type == "phone" ? "cellphone" : null}`,
params: {
phone,
captcha,
time: new Date().getTime(),
},
});
};
// 发送验证码
export const sentCaptcha = (phone) => {
return axios({
method: "GET",
url: "/captcha/sent",
params: {
phone,
time: new Date().getTime(),
},
});
};
// 验证验证码
export const verifyCaptcha = (phone, captcha) => {
return axios({
method: "GET",
url: "/captcha/verify",
params: {
phone,
captcha,
time: new Date().getTime(),
},
});
};
// 获取登录状态
export const getLoginState = () => {
return axios({
method: "GET",
url: "/login/status",
params: {
time: new Date().getTime(),
},
});
};
// 获取用户信息 , 歌单收藏mv, dj 数量
export const getUserSubcount = () => {
return axios({
method: "GET",
url: "/user/subcount",
params: {
time: new Date().getTime(),
},
});
};
// 获取用户等级信息
export const getUserLevel = () => {
return axios({
method: "GET",
url: "/user/level",
params: {
time: new Date().getTime(),
},
});
};
// 获取用户歌单
export const getUserPlaylist = (uid, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/user/playlist",
params: {
uid,
limit,
offset,
time: new Date().getTime(),
},
});
};
// 新建歌单
export const createPlaylist = (name, privacy = false) => {
return axios({
method: "GET",
url: "/playlist/create",
params: {
name,
privacy,
time: new Date().getTime(),
},
});
};
// 获取用户收藏的歌手列表
export const getUserArtistlist = () => {
return axios({
method: "GET",
url: "/artist/sublist",
params: {
time: new Date().getTime(),
},
});
};
// 退出登录
export const userLogOut = () => {
return axios({
method: "GET",
url: "/logout",
params: {
time: new Date().getTime(),
},
});
};
/*
* 首页部分
*/
// 轮播图
export const getBanner = () => {
return axios({
method: "GET",
url: "/banner",
});
};
// 热搜内容
export const getSearchHot = () => {
return axios({
method: "GET",
loadingBar: "Hidden",
url: "/search/hot/detail",
});
};
// 推荐内容
// 默认歌单, mv, newsong新音乐, djprogram电台, privatecontent独家放送
export const getPersonalized = (type = null, limit = 10) => {
return axios({
method: "GET",
url: `/personalized/${type}`,
params: {
limit,
},
});
};
// 热门歌手
export const getTopArtists = (limit = 6) => {
return axios({
method: "GET",
url: "/top/artists",
params: {
limit,
},
});
};
/*
* 搜索部分
*/
// 搜索建议
export const getSearchSuggest = (keywords) => {
return axios({
method: "GET",
url: "/search/suggest",
loadingBar: "Hidden",
params: {
keywords,
},
});
};
// 搜索结果
// 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合, 2000:声音
export const getSearchData = (keywords, limit = 30, offset = 0, type = 1) => {
return axios({
method: "GET",
url: "/search",
params: {
keywords,
limit,
offset,
type,
},
});
};
/*
* 歌曲部分
*/
// 获取音乐是否可用
export const checkMusicCanUse = (id) => {
return axios({
method: "GET",
url: "/check/music",
loadingBar: "Hidden",
params: {
id,
time: new Date().getTime(),
},
});
};
// 获取音乐 url
export const getMusicUrl = (id, level = "exhigh") => {
return axios({
method: "GET",
url: "/song/url/v1",
loadingBar: "Hidden",
params: {
id,
level,
},
});
};
// 获取音乐歌词
export const getMusicLyric = (id) => {
return axios({
method: "GET",
url: "/lyric",
loadingBar: "Hidden",
params: {
id,
},
});
};
// 获取歌曲详情
export const getMusicDetail = (ids) => {
return axios({
method: "GET",
url: "/song/detail",
params: {
ids,
},
});
};
// 获取包含这首歌的歌单
export const getSimiPlayList = (id) => {
return axios({
method: "GET",
url: "/simi/playlist",
params: {
id,
},
});
};
// 获取相似音乐
export const getSimiSong = (id) => {
return axios({
method: "GET",
url: "/simi/song",
params: {
id,
},
});
};
/*
* 视频部分
*/
// 获取 mv 数据
export const getVideoDetail = (mvid) => {
return axios({
method: "GET",
url: "/mv/detail",
params: {
mvid,
},
});
};
// 获取 mv 播放地址
export const getVideoUrl = (id, r = null) => {
return axios({
method: "GET",
url: "/mv/url",
loadingBar: "Hidden",
params: {
id,
r,
},
});
};
// 获取相似 mv
export const getSimiVideo = (mvid) => {
return axios({
method: "GET",
url: "/simi/mv",
params: {
mvid,
},
});
};
/*
* 评论部分
*/
// 获取评论
export const getComment = (id, offset = 0, before = null, type = "music") => {
return axios({
method: "GET",
url: `/comment/${type}`,
params: {
id,
offset,
before,
time: new Date().getTime(),
},
});
};
// 评论点赞
// 0: 歌曲 1: mv 2: 歌单 3: 专辑 4: 电台节目 5: 视频 6: 动态 7: 电台
export const likeComment = (id, cid, t, type = 0) => {
return axios({
method: "GET",
loadingBar: "Hidden",
url: "/comment/like",
params: {
id,
cid,
t,
type,
time: new Date().getTime(),
},
});
};
/*
* 云盘部分
*/
// 获取云盘数据
export const getCloud = (limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/user/cloud",
params: {
limit,
offset,
time: new Date().getTime(),
},
});
};
// 云盘歌曲删除
export const setCloudDel = (id) => {
return axios({
method: "GET",
url: "/user/cloud/del",
params: {
id,
time: new Date().getTime(),
},
});
};
// 云盘歌曲信息匹配纠正
export const setCloudMatch = (uid, sid, asid) => {
return axios({
method: "GET",
url: "/cloud/match",
params: {
uid,
sid,
asid,
time: new Date().getTime(),
},
});
};
// 云盘上传
export const upCloudSong = (file) => {
let formData = new FormData();
formData.append("songFile", file);
return axios({
url: "/cloud",
method: "POST",
loadingBar: "Hidden",
params: {
time: new Date().getTime(),
},
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
timeout: 200000,
});
};
/*
* 歌手部分
*/
// 歌手分类列表
export const getArtistList = (
type = -1,
area = -1,
limit = 30,
offset = 0,
initial = -1
) => {
return axios({
method: "GET",
url: "/artist/list",
params: {
type,
area,
limit,
offset,
initial,
},
});
};
// 获取歌手详情
export const getArtistDetail = (id) => {
return axios({
method: "GET",
url: "/artist/detail",
params: {
id,
},
});
};
// 获取歌手单曲
export const getArtistSongs = (id) => {
return axios({
method: "GET",
url: "/artists",
params: {
id,
},
});
};
// 获取歌手专辑
export const getArtistAblums = (id, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/artist/album",
params: {
id,
limit,
offset,
},
});
};
// 获取歌手视频
export const getArtistVideos = (id, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/artist/mv",
params: {
id,
limit,
offset,
},
});
};
// 收藏/取消收藏歌手
export const likeArtist = (t, id) => {
return axios({
method: "GET",
url: "/artist/sub",
params: {
t,
id,
time: new Date().getTime(),
},
});
};
/*
* 歌单与专辑部分
*/
// 歌单分类
export const getPlayListCatlist = (highquality = false) => {
return axios({
method: "GET",
url: `/playlist/${highquality ? "highquality/tags" : "catlist"}`,
});
};
// 获取歌单列表
export const getTopPlaylist = (cat = "全部", limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/top/playlist",
params: {
cat,
limit,
offset,
},
});
};
// 获取精品歌单列表
export const getHighqualityPlaylist = (cat = "全部", limit = 30, before) => {
return axios({
method: "GET",
url: "/top/playlist/highquality",
params: {
cat,
limit,
before,
},
});
};
// 获取歌单详情
export const getPlayListDetail = (id) => {
return axios({
method: "GET",
url: "/playlist/detail",
params: {
id,
},
});
};
// 获取歌单所有歌曲
export const getAllPlayList = (id, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/playlist/track/all",
params: {
id,
limit,
offset,
},
});
};
// 删除歌单
export const delPlayList = (id) => {
return axios({
method: "GET",
url: "/playlist/delete",
params: {
id,
time: new Date().getTime(),
},
});
};
// 更新歌单
export const playlistUpdate = (id, name, desc = null, tags = null) => {
return axios({
method: "GET",
url: "/playlist/update",
params: {
id,
name,
desc,
tags,
time: new Date().getTime(),
},
});
};
// 对歌单添加或删除歌曲
export const addSongToPlayList = (pid, tracks, op = "add") => {
return axios({
method: "GET",
url: "/playlist/tracks",
params: {
op,
pid,
tracks,
time: new Date().getTime(),
},
});
};
// 收藏/取消收藏歌单
export const likePlaylist = (t, id) => {
return axios({
method: "GET",
url: "/playlist/subscribe",
params: {
t,
id,
time: new Date().getTime(),
},
});
};
// 获取专辑内容
export const getAlbum = (id) => {
return axios({
method: "GET",
url: "/album",
params: {
id,
},
});
};
// 最新专辑
export const getNewAlbum = () => {
return axios({
method: "GET",
url: "/album/newest",
});
};
// 获取排行榜数据
export const getToplist = (detail = true) => {
return axios({
method: "GET",
url: `/toplist${detail ? "/detail" : null}`,
});
};
/*
* 登录后部分
*/
// 获取每日推荐歌曲
export const getDailySongs = () => {
return axios({
method: "GET",
url: "/recommend/songs",
params: {
time: new Date().getTime(),
},
});
};
// 获取历史日推可用日期列表
export const getDailySongsHistory = () => {
return axios({
method: "GET",
url: "/history/recommend/songs",
params: {
time: new Date().getTime(),
},
});
};
// 获取历史日推详情数据
export const getDailySongsHistoryDetail = (date) => {
return axios({
method: "GET",
url: "/history/recommend/songs/detail",
params: {
date,
time: new Date().getTime(),
},
});
};
// 私人 FM
export const getPersonalFm = () => {
return axios({
method: "GET",
url: "/personal_fm",
loadingBar: "Hidden",
params: {
time: new Date().getTime(),
},
});
};
// 垃圾桶
export const setFmTrash = (id) => {
return axios({
method: "GET",
url: "/fm_trash",
loadingBar: "Hidden",
params: {
id,
time: new Date().getTime(),
},
});
};
// 喜欢音乐列表
export const getLikelist = (uid) => {
return axios({
method: "GET",
url: "/likelist",
loadingBar: "Hidden",
params: {
uid,
time: new Date().getTime(),
},
});
};
// 喜欢音乐
export const setLikeSong = (id, like = true) => {
return axios({
method: "GET",
url: "/like",
loadingBar: "Hidden",
params: {
id,
like,
time: new Date().getTime(),
},
});
};

128
src/api/login.js Normal file
View File

@@ -0,0 +1,128 @@
import axios from "@/utils/request";
/**
* 登录部分
*/
/**
* 生成二维码 key
*/
export const getQrKey = () => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/qr/key",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 生成二维码
* @param {string} key 二维码key
* @param {boolean} qrimg 是否生成二维码图片默认为true
*/
export const qrCreate = (key, qrimg = true) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/qr/create",
params: {
key,
qrimg,
timestamp: new Date().getTime(),
},
});
};
/**
* 检查二维码状态
* @param {string} key 二维码key
*/
export const checkQr = (key) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/qr/check",
params: {
key,
timestamp: new Date().getTime(),
},
});
};
/**
* 手机号登录
* @param {string} phone 手机号码
* @param {string} captcha 验证码
*/
export const toLogin = (phone, captcha) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/cellphone",
params: {
phone,
captcha,
timestamp: new Date().getTime(),
},
});
};
/**
* 发送验证码
* @param {string} phone 手机号码
*/
export const sentCaptcha = (phone) => {
return axios({
method: "GET",
url: "/captcha/sent",
params: {
phone,
timestamp: new Date().getTime(),
},
});
};
/**
* 验证验证码是否正确
* @param {string} phone 手机号码
* @param {string} captcha 验证码
*/
export const verifyCaptcha = (phone, captcha) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/captcha/verify",
params: {
phone,
captcha,
timestamp: new Date().getTime(),
},
});
};
// 获取登录状态
export const getLoginState = () => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/status",
params: {
timestamp: new Date().getTime(),
},
});
};
// 刷新登录
export const refreshLogin = () => {
return axios({
method: "GET",
hiddenBar: true,
url: "/login/refresh",
params: {
timestamp: new Date().getTime(),
},
});
};

176
src/api/playlist.js Normal file
View File

@@ -0,0 +1,176 @@
import axios from "@/utils/request";
/**
* 歌单部分
*/
/**
* 获取歌单分类信息
* @param {boolean} [highquality=false] - 是否为精品歌单标签
*/
export const getPlayListCatlist = (highquality = false) => {
return axios({
method: "GET",
url: `/playlist/${highquality ? "highquality/tags" : "catlist"}`,
});
};
/**
* 获取歌单分类列表
* @param {string} [cat='全部'] - 歌单分类
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getTopPlaylist = (cat = "全部", limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/top/playlist",
params: {
cat,
limit,
offset,
},
});
};
/**
* 获取精品歌单列表
* @param {string} [cat='全部'] - 歌单分类
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [before] - 上一页最后一个歌单的updateTime用于翻页
*/
export const getHighqualityPlaylist = (cat = "全部", limit = 30, before) => {
return axios({
method: "GET",
url: "/top/playlist/highquality",
params: {
cat,
limit,
before,
},
});
};
/**
* 获取歌单详情
* @param {number} id - 歌单id
*/
export const getPlayListDetail = (id) => {
return axios({
method: "GET",
url: "/playlist/detail",
withCredentials: false,
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取歌单中所有歌曲信息
* @param {number} id - 歌单id
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getAllPlayList = (id, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/playlist/track/all",
params: {
id,
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 新建歌单(登录后调用)
* @param {string} name 歌单名称
* @param {boolean} privacy 是否设置为隐私默认为false公开
*/
export const createPlaylist = (name, privacy = false) => {
return axios({
method: "GET",
url: "/playlist/create",
params: {
name,
privacy,
timestamp: new Date().getTime(),
},
});
};
/**
* 删除歌单(登录后调用)
* @param {number} id - 歌单id
*/
export const delPlayList = (id) => {
return axios({
method: "GET",
url: "/playlist/delete",
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 更新歌单信息(登录后调用)
* @param {number} id - 歌单id
* @param {string} name - 歌单名称
* @param {string|null} [desc=null] - 歌单描述,可选
* @param {string|null} [tags=null] - 歌单标签,可选,多个用 `;` 隔开,只能用官方规定标签
*/
export const playlistUpdate = (id, name, desc = null, tags = null) => {
return axios({
method: "GET",
url: "/playlist/update",
params: {
id,
name,
desc,
tags,
timestamp: new Date().getTime(),
},
});
};
/**
* 向歌单中添加或删除歌曲
* @param {number} pid - 歌单id
* @param {Array<number>} tracks - 要添加或删除的歌曲id数组
* @param {string} [op='add'] - 操作类型,可选,默认为添加
*/
export const addSongToPlayList = (pid, tracks, op = "add") => {
return axios({
method: "GET",
url: "/playlist/tracks",
params: {
op,
pid,
tracks,
timestamp: new Date().getTime(),
},
});
};
/**
* 收藏/取消收藏歌单
* @param {number} t - 操作类型1为收藏2为取消收藏
* @param {number} id - 歌单id
*/
export const likePlaylist = (t, id) => {
return axios({
method: "GET",
url: "/playlist/subscribe",
params: {
t,
id,
timestamp: new Date().getTime(),
},
});
};

51
src/api/search.js Normal file
View File

@@ -0,0 +1,51 @@
import axios from "@/utils/request";
/**
* 搜索部分
*/
/**
* 获取热门搜索列表
*/
export const getSearchHot = () => {
return axios({
method: "GET",
hiddenBar: true,
url: "/search/hot/detail",
});
};
/**
* 搜索建议
* @param {string} keywords - 搜索关键词
*/
export const getSearchSuggest = (keywords) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/search/suggest",
params: {
keywords,
},
});
};
/**
* 搜索结果
* @param {string} keywords - 搜索关键词
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
* @param {number} [type=1] - 可选参数搜索类型。1表示单曲10表示专辑100表示歌手1000表示歌单1002表示用户1004表示MV1006表示歌词1009表示电台1014表示视频1018表示综合2000表示声音。默认为1
*/
export const getSearchData = (keywords, limit = 30, offset = 0, type = 1) => {
return axios({
method: "GET",
url: "/cloudsearch",
params: {
keywords,
limit,
offset,
type,
},
});
};

150
src/api/song.js Normal file
View File

@@ -0,0 +1,150 @@
import axios from "@/utils/request";
/**
* 歌曲部分
*/
/**
* 检查指定音乐是否可用
* @param {number} id - 要检查的音乐ID
*/
export const checkMusicCanUse = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/check/music",
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取指定音乐的播放链接
* @param {number} id - 要获取播放链接的音乐ID
* @param {string} [level='exhigh'] - 可选参数,音质等级。可选值包括'low'、'medium'、'high'、'exhigh'。默认为'exhigh'
*/
export const getMusicUrl = (id, level = "exhigh") => {
return axios({
method: "GET",
hiddenBar: true,
url: "/song/url/v1",
params: {
id,
level,
timestamp: new Date().getTime(),
},
});
};
/**
* 网易云解灰
* @param {number} id - 要替换播放链接的音乐ID
*/
export const getMusicNumUrl = async (id) => {
const server =
process.env.NODE_ENV === "development"
? "kuwo,qq,pyncmd,kugou"
: "qq,pyncmd,kugou";
const url = `${import.meta.env.VITE_UNM_API}match?id=${id}&server=${server}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return Promise.reject(new Error());
}
return await response.json();
};
/**
* 获取指定音乐的歌词
* @param {number} id - 要获取歌词的音乐ID
*/
export const getMusicLyric = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/lyric",
params: {
id,
},
});
};
/**
* 获取指定音乐的逐字歌词
* @param {number} id - 要获取逐字歌词的音乐ID
*/
export const getMusicNewLyric = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/lyric/new",
params: {
id,
},
});
};
/**
* 获取指定音乐的详情。
* @param {string} ids - 要获取详情的音乐ID多个ID用逗号分隔
*/
export const getMusicDetail = (ids) => {
return axios({
method: "GET",
url: "/song/detail",
params: {
ids,
},
});
};
/**
* 获取包含指定音乐的相似歌单
* @param {number} id - 要查询的音乐ID
*/
export const getSimiPlayList = (id) => {
return axios({
method: "GET",
url: "/simi/playlist",
params: {
id,
},
});
};
/**
* 获取与指定音乐相似的音乐列表
* @param {number} id - 要查询的音乐ID
*/
export const getSimiSong = (id) => {
return axios({
method: "GET",
url: "/simi/song",
params: {
id,
},
});
};
/**
* 获取客户端歌曲下载
* @param {number} id - 要下载的音乐ID
* @param {number} br - 码率, 默认设置了 999000 即最大码率, 如果要 320k 则可设置为 320000, 其他类推
*/
export const getSongDownload = (id, br = 999000) => {
return axios({
method: "GET",
url: "/song/download/url",
params: {
id,
br,
timestamp: new Date().getTime(),
},
});
};

229
src/api/user.js Normal file
View File

@@ -0,0 +1,229 @@
import axios from "@/utils/request";
/**
* 用户部分
*/
/**
* 获取用户等级信息
*/
export const getUserLevel = () => {
return axios({
method: "GET",
url: "/user/level",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户订阅信息包括歌单、收藏、MV和DJ数量
*/
export const getUserSubcount = () => {
return axios({
method: "GET",
url: "/user/subcount",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户的歌单列表
* @param {string} uid 用户的id
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getUserPlaylist = (uid, limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/user/playlist",
params: {
uid,
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户的专辑列表
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getUserAlbum = (limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/album/sublist",
params: {
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户收藏的歌手列表
*/
export const getUserArtistlist = () => {
return axios({
method: "GET",
url: "/artist/sublist",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户喜欢的音乐列表
* @param {string} uid 用户的id
*/
export const getLikelist = (uid) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/likelist",
params: {
uid,
timestamp: new Date().getTime(),
},
});
};
/**
* 将指定音乐添加或移除喜欢列表
* @param {number} id 音乐ID
* @param {boolean} [like=true] 是否添加到喜欢列表默认为true
*/
export const setLikeSong = (id, like = true) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/like",
params: {
id,
like,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户云盘数据
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getCloud = (limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/user/cloud",
params: {
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 用户云盘歌曲删除
* @param {string} id - 歌曲的id
*/
export const setCloudDel = (id) => {
return axios({
method: "GET",
url: "/user/cloud/del",
params: {
id,
timestamp: new Date().getTime(),
},
});
};
/**
* 云盘歌曲信息匹配纠正
* @param {string} uid - 用户id
* @param {string} sid - 原歌曲id
* @param {string} asid - 要匹配的歌曲id
*/
export const setCloudMatch = (uid, sid, asid) => {
return axios({
method: "GET",
url: "/cloud/match",
params: {
uid,
sid,
asid,
timestamp: new Date().getTime(),
},
});
};
/**
* 用户云盘上传
* @param {File} file - 要上传的文件
*/
export const upCloudSong = (file, onUploadProgress) => {
const formData = new FormData();
formData.append("songFile", file);
return axios({
url: "/cloud",
method: "POST",
hiddenBar: true,
params: {
timestamp: new Date().getTime(),
},
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
timeout: 200000,
onUploadProgress,
});
};
/**
* 退出登录
*/
export const userLogOut = () => {
return axios({
method: "GET",
url: "/logout",
params: {
timestamp: new Date().getTime(),
},
});
};
/**
* 每日签到
* @param {type} type - 签到类型 , 默认 0, 其中 0 为安卓端签到 ,1 为 web/PC 签到
*/
export const userDailySignin = (type = 0) => {
return axios({
method: "GET",
url: "/daily_signin",
params: {
type,
timestamp: new Date().getTime(),
},
});
};
/**
* 云贝签到
*/
export const userYunbeiSign = () => {
return axios({
method: "GET",
url: "/yunbei/sign",
params: {
timestamp: new Date().getTime(),
},
});
};

50
src/api/video.js Normal file
View File

@@ -0,0 +1,50 @@
import axios from "@/utils/request";
/**
* 视频部分
*/
/**
* 获取指定MV的详细信息
* @param {number} mvid - 要查询的MV ID
*/
export const getVideoDetail = (mvid) => {
return axios({
method: "GET",
url: "/mv/detail",
params: {
mvid,
},
});
};
/**
* 获取指定MV的播放地址
* @param {number} id - 要查询的MV ID
* @param {string} [r=null] - 分辨率。默认值为null
*/
export const getVideoUrl = (id, r = null) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/mv/url",
params: {
id,
r,
},
});
};
/**
* 获取与指定MV相似的MV列表
* @param {number} mvid - 要查询的MV ID
*/
export const getSimiVideo = (mvid) => {
return axios({
method: "GET",
url: "/simi/mv",
params: {
mvid,
},
});
};

View File

@@ -35,7 +35,7 @@
<script setup>
import { useRouter } from "vue-router";
import { getBanner } from "@/api";
import { getBanner } from "@/api/home";
const router = useRouter();
@@ -67,7 +67,7 @@ const bannerJump = (type, id, url) => {
break;
case 1000:
// 歌单页
router.push(`/playlist?id=${id}`);
router.push(`/playlist?id=${id}&page=1`);
break;
case 1004:
// MV页
@@ -95,10 +95,12 @@ const bannerJump = (type, id, url) => {
const getBannerHeight = () => {
if (window.innerWidth > 680) {
bannerType.value = "card";
if (window.innerWidth >= 1200) {
if (window.innerWidth >= 1200 && window.innerWidth <= 1500) {
bannerHeight.value = window.innerWidth / 5.5;
} else {
} else if (window.innerWidth <= 1500) {
bannerHeight.value = window.innerWidth / 5;
} else {
bannerHeight.value = 300;
}
} else {
bannerType.value = "slide";
@@ -142,4 +144,4 @@ onBeforeUnmount(() => {
.v-leave-to {
opacity: 0;
}
</style>
</style>

View File

@@ -1,77 +1,88 @@
<template>
<n-card class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.musicPackage"
:src="commentData.user.vipRights.musicPackage.iconUrl"
alt="vip"
/>
</div>
<div class="associator" v-if="commentData.user?.redVipLevel">
<img
v-if="commentData.user.vipRights.associator"
:src="commentData.user.vipRights.associator.iconUrl"
alt="associator"
/>
</div>
</div>
<div class="review">
<div class="content">
<span class="name">{{ commentData.user.nickname }}</span>
<span class="text">{{ commentData.content }}</span>
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<span class="name">
@{{ commentData.beReplied[0].user.nickname }}
</span>
<span class="text">{{ commentData.beReplied[0].content }}</span>
</div>
<div class="num">
<span class="time">
<n-icon :component="AccessTimeFilled" />
{{ getCommentTime(commentData.time) }}
</span>
<span class="ip" v-if="commentData.ipLocation.location">
<n-icon :component="FmdGoodOutlined" />
{{ commentData.ipLocation.location }}
</span>
<span
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
>
<n-icon
:component="
commentData.liked ? ThumbUpOffAltRound : ThumbUpAltOutlined
<Transition mode="out-in">
<n-card v-if="Object.keys(commentData).length" class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
{{ formatNumber(commentData.likedCount) }}
</span>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="
commentData.user.vipRights.musicPackage.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="redVipAnnualCount"
title="网易音乐人"
/>
</div>
<div
class="associator"
v-if="commentData.user.vipRights?.redVipLevel > 0"
>
<img
v-if="commentData.user.vipRights.associator"
:src="
commentData.user.vipRights.associator.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="associator"
title="黑胶会员"
/>
</div>
</div>
</div>
</n-card>
<div class="review">
<div class="content">
<n-text class="name">{{ commentData.user.nickname }}</n-text>
<n-text class="text" v-html="commentData.content" />
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<n-text class="name">
@{{ commentData.beReplied[0].user.nickname }}
</n-text>
<n-text class="text">{{ commentData.beReplied[0].content }}</n-text>
</div>
<div class="thing">
<div class="item">
<n-icon size="14" :depth="3" :component="Time" />
<n-text :depth="3" v-html="getCommentTime(commentData.time)" />
</div>
<div class="item" v-if="commentData.ipLocation?.location">
<n-icon size="14" :depth="3" :component="Local" />
<n-text :depth="3" v-html="commentData.ipLocation.location" />
</div>
<div
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
>
<n-icon>
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
</n-icon>
{{ formatNumber(commentData.likedCount) }}
</div>
</div>
</div>
</n-card>
<n-skeleton v-else class="skeleton" />
</Transition>
</template>
<script setup>
import { getCommentTime, formatNumber } from "@/utils/timeTools.js";
import { Local, Time, ThumbsUp } from "@icon-park/vue-next";
import { userStore } from "@/store";
import { useRouter } from "vue-router";
import { likeComment } from "@/api";
import {
AccessTimeFilled,
FmdGoodOutlined,
ThumbUpAltOutlined,
ThumbUpOffAltRound,
} from "@vicons/material";
import { likeComment } from "@/api/comment";
const user = userStore();
const router = useRouter();
@@ -79,7 +90,7 @@ const props = defineProps({
// 评论 数据
commentData: {
type: Object,
default: [],
default: {},
},
});
@@ -107,6 +118,15 @@ const toLikeComment = () => {
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.comment {
margin-bottom: 12px;
border-radius: 8px;
@@ -153,6 +173,17 @@ const toLikeComment = () => {
}
}
}
@media (max-width: 578px) {
min-width: 48px;
width: 48px;
.avatar {
width: 40px;
height: 40px;
.musicPackage {
height: 12px;
}
}
}
}
.review {
width: 100%;
@@ -186,16 +217,20 @@ const toLikeComment = () => {
}
}
}
.num {
.thing {
margin-top: 12px;
display: flex;
align-items: center;
.time {
.item {
margin-right: 12px;
opacity: 0.6;
}
.ip {
opacity: 0.6;
font-size: 13px;
.n-icon {
margin-right: 4px;
}
.n-text {
transform: translateY(0.5px);
display: inline-block;
}
}
.like {
margin-left: auto;
@@ -222,4 +257,10 @@ const toLikeComment = () => {
}
}
}
.skeleton {
width: 100%;
height: 120px;
border-radius: 8px;
margin-bottom: 12px;
}
</style>

View File

@@ -14,7 +14,7 @@
class="item"
v-for="item in listData"
:key="item"
@click="router.push(`/artist?id=${item.id}`)"
@click="router.push(`/artist/songs?id=${item.id}&page=1`)"
@contextmenu="openRightMenu($event, item)"
>
<div class="cover">
@@ -24,7 +24,7 @@
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
fallback-src="/images/pic/default.png"
/>
<n-icon :component="PersonSearchFilled" />
<n-icon size="40" :component="PeopleSearchOne" />
</div>
<n-text class="name text-hidden">{{ item.name }}</n-text>
<n-text class="size" :depth="3" v-if="item.size">
@@ -65,12 +65,11 @@
</template>
<script setup>
import { PersonSearchFilled } from "@vicons/material";
import { likeArtist } from "@/api";
import { PeopleSearchOne } from "@icon-park/vue-next";
import { likeArtist } from "@/api/artist";
import { useRouter } from "vue-router";
import { musicStore, userStore } from "@/store";
import { userStore } from "@/store";
const music = musicStore();
const user = userStore();
const router = useRouter();
const props = defineProps({
@@ -111,7 +110,7 @@ const openRightMenu = (e, data) => {
{
key: "like",
label: isLikeOrDislike(data.id) ? "收藏歌手" : "取消收藏歌手",
show: user.userLogin && music.getUserArtistlists.has ? true : false,
show: user.userLogin && user.getUserArtistLists.has ? true : false,
props: {
onClick: () => {
toLikeArtist(data);
@@ -158,7 +157,7 @@ const toLikeArtist = (data) => {
$message.success(
`${data.name}${type == 1 ? "收藏成功" : "取消收藏成功"}`
);
music.setUserArtistLists();
user.setUserArtistLists();
} else {
$message.error(`${data.name}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
@@ -167,22 +166,19 @@ const toLikeArtist = (data) => {
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (music.getUserArtistlists.list[0]) {
const index = music.getUserArtistlists.list.findIndex(
(item) => item.id === id
);
if (index !== -1) {
return false;
}
return true;
} else {
if (!user.getUserArtistLists.list[0]) {
return true;
}
return !user.getUserArtistLists.list.some((item) => item.id === id);
};
onMounted(() => {
if (user.userLogin && !music.getUserArtistlists.has)
music.setUserArtistLists();
if (
user.userLogin &&
!user.getUserArtistLists.has &&
!user.getUserArtistLists.isLoading
)
user.setUserArtistLists();
});
</script>
@@ -221,7 +217,6 @@ onMounted(() => {
transform: scale(0.8);
position: absolute;
color: #fff;
font-size: 5vh;
transition: all 0.3s;
}
&:hover {

View File

@@ -23,10 +23,14 @@
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'"
fallback-src="/images/pic/default.png"
/>
<n-icon class="play" :component="PlayArrowRound" />
<n-icon class="play" size="40">
<PlayOne theme="filled" />
</n-icon>
<div class="description" v-if="listType != 'topList'">
<div class="num" v-if="listType == 'playList'">
<n-icon :component="HeadsetFilled" />
<div class="num" v-if="listType == 'playlist'">
<n-icon>
<Headset theme="filled" />
</n-icon>
<span class="des">{{ item.playCount }}</span>
</div>
<div class="num" v-else>
@@ -36,7 +40,7 @@
</div>
<div class="title">
<span class="name text-hidden">{{ item.name }}</span>
<span v-if="listType == 'playList' && item.artist" class="by">
<span v-if="listType == 'playlist' && item.artist" class="by">
By {{ item.artist.nickname }}
</span>
<span v-else-if="listType == 'topList' && item.update" class="by">
@@ -77,69 +81,22 @@
@select="rightMenuShow = false"
/>
<!-- 更新歌单弹窗 -->
<n-modal
style="width: 60vw; min-width: min(24rem, 100vw)"
v-model:show="playlistUpdateModel"
preset="card"
title="歌单编辑"
:bordered="false"
:on-after-leave="closeUpdateModel"
>
<n-form
ref="playlistUpdateRef"
:rules="playlistUpdateRules"
:label-width="80"
:model="playlistUpdateValue"
>
<n-form-item label="歌单名称" path="name">
<n-input
v-model:value="playlistUpdateValue.name"
placeholder="请输入歌单名称"
/>
</n-form-item>
<n-form-item label="歌单描述" path="desc">
<n-input
v-model:value="playlistUpdateValue.desc"
placeholder="请输入歌单描述"
type="textarea"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</n-form-item>
<n-form-item label="歌单标签" path="tags">
<n-select
multiple
v-model:value="playlistUpdateValue.tags"
placeholder="请输入歌单标签"
:options="playlistTags"
@click="openSelect"
/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="closeUpdateModel"> 取消 </n-button>
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button>
</n-space>
</template>
</n-modal>
<PlaylistUpdate ref="playlistUpdateRef" />
</div>
</template>
<script setup>
import { PlayArrowRound, HeadsetFilled } from "@vicons/material";
import { delPlayList, playlistUpdate, likePlaylist } from "@/api";
import { PlayOne, Headset } from "@icon-park/vue-next";
import { delPlayList, likePlaylist } from "@/api/playlist";
import { likeAlbum } from "@/api/album";
import { musicStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import { formRules } from "@/utils/formRules.js";
import AllArtists from "./AllArtists.vue";
import PlaylistUpdate from "@/components/DataModel/PlaylistUpdate.vue";
const router = useRouter();
const music = musicStore();
const user = userStore();
const { textRule } = formRules();
const props = defineProps({
// 列表数据
listData: {
@@ -149,7 +106,7 @@ const props = defineProps({
// 列表类型
listType: {
type: String,
default: "playList",
default: "playlist",
},
// 自定义列数
columns: {
@@ -172,26 +129,14 @@ const props = defineProps({
default: 30,
},
});
const playlistUpdateRef = ref(null);
// 弹窗数据
// 右键菜单数据
const rightMenuX = ref(0);
const rightMenuY = ref(0);
const rightMenuShow = ref(false);
const rightMenuOptions = ref(null);
// 更新歌单数据
const playlistUpdateId = ref(null);
const playlistUpdateRef = ref(null);
const playlistUpdateModel = ref(false);
const playlistUpdateRules = {
name: textRule,
};
const playlistUpdateValue = ref({
name: null,
desc: null,
tags: null,
});
// 打开右键菜单
const openRightMenu = (e, data) => {
e.preventDefault();
@@ -201,23 +146,19 @@ const openRightMenu = (e, data) => {
{
key: "update",
label: "编辑歌单",
show: router.currentRoute.value.name === "playlists" ? true : false,
show:
router.currentRoute.value.name === "user-playlists" ? true : false,
props: {
onClick: () => {
playlistUpdateId.value = data.id;
playlistUpdateModel.value = true;
playlistUpdateValue.value = {
name: data.name,
desc: data.desc,
tags: data.tags,
};
playlistUpdateRef.value.openUpdateModel(data);
},
},
},
{
key: "del",
label: "删除歌单",
show: router.currentRoute.value.name === "playlists" ? true : false,
show:
router.currentRoute.value.name === "user-playlists" ? true : false,
props: {
onClick: () => {
toDelPlayList(data);
@@ -225,31 +166,53 @@ const openRightMenu = (e, data) => {
},
},
{
key: "like",
key: "likePlaylist",
label: isLikeOrDislike(data.id) ? "收藏歌单" : "取消收藏歌单",
show:
user.userLogin &&
music.getUserPlayLists.has &&
router.currentRoute.value.name != "playlists"
user.getUserPlayLists.has &&
props.listType === "playlist" &&
router.currentRoute.value.name !== "user-playlists"
? true
: false,
props: {
onClick: () => {
toLikePlaylist(data.id);
toChangeLike(data.id);
},
},
},
{
key: "likeAlbum",
label: isLikeOrDislike(data.id) ? "收藏专辑" : "取消收藏专辑",
show:
user.userLogin &&
user.getUserAlbumLists.has &&
props.listType === "album"
? true
: false,
props: {
onClick: () => {
toChangeLike(data.id);
},
},
},
{
key: "copy",
label: "复制歌单链接",
label: `复制${props.listType === "playlist" ? "歌单" : "专辑"}链接`,
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${data.id}`
`https://music.163.com/#/${
props.listType === "playlist" ? "playlist" : "album"
}?id=${data.id}`
);
$message.success(
`${
props.listType === "playlist" ? "歌单" : "专辑"
}链接复制成功`
);
$message.success("歌单链接复制成功");
} catch (err) {
$message.error("复制失败:", err);
}
@@ -271,44 +234,17 @@ const onClickoutside = () => {
rightMenuShow.value = false;
};
// 更新歌单
const toUpdatePlayList = (e) => {
e.preventDefault();
playlistUpdateRef.value?.validate((errors) => {
if (!errors) {
console.log("通过");
playlistUpdate(
playlistUpdateId.value,
playlistUpdateValue._value.name,
playlistUpdateValue._value.desc,
playlistUpdateValue._value.tags.join(";")
).then((res) => {
console.log(res);
if (res.code === 200) {
$message.success("编辑成功");
closeUpdateModel();
music.setUserPlayLists();
} else {
$message.error("编辑失败,请重试");
}
});
} else {
$loadingBar.error();
$message.error("请检查您的输入");
}
});
};
// 链接跳转
const toLink = (id) => {
if (props.listType == "playList" || props.listType == "topList") {
if (props.listType === "playlist" || props.listType === "topList") {
router.push({
path: "/playlist",
query: {
id,
page: 1,
},
});
} else if (props.listType == "album") {
} else if (props.listType === "album") {
router.push({
path: "/album",
query: {
@@ -318,76 +254,86 @@ const toLink = (id) => {
}
};
// 关闭更新歌单弹窗
const closeUpdateModel = () => {
playlistUpdateModel.value = false;
playlistUpdateId.value = null;
};
// 删除歌单
const toDelPlayList = (data) => {
$dialog.warning({
class: "s-dialog",
title: "删除歌单",
content: "确认删除歌单 " + data.name + "",
content: "确认删除歌单 " + data.name + "删除后将不可恢复!",
positiveText: "删除",
negativeText: "取消",
onPositiveClick: () => {
delPlayList(data.id).then((res) => {
if (res.code === 200) {
$message.success("删除成功");
music.setUserPlayLists();
user.setUserPlayLists();
}
});
},
});
};
// 歌单分类标签
const playlistTags = ref([]);
const openSelect = () => {
if (music.catList.sub) {
playlistTags.value = music.catList.sub.map((v) => ({
label: v.name,
value: v.name,
}));
} else {
music.setCatList();
}
};
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (music.getUserPlayLists.like[0]) {
const index = music.getUserPlayLists.like.findIndex(
(item) => item.id === id
);
if (index !== -1) {
return false;
}
return true;
} else {
return true;
const listType = props.listType;
const playlists = user.getUserPlayLists.like;
const albums = user.getUserAlbumLists.list;
if (listType === "playlist" && playlists.length) {
return !playlists.some((item) => item.id === id);
}
if (listType === "album" && albums.length) {
return !albums.some((item) => item.id === id);
}
return true;
};
// 收藏/取消收藏歌单
const toLikePlaylist = (id) => {
// 收藏/取消收藏
const toChangeLike = async (id) => {
const listType = props.listType;
const type = isLikeOrDislike(id) ? 1 : 2;
likePlaylist(type, id).then((res) => {
const likeFn = listType === "playlist" ? likePlaylist : likeAlbum;
const likeMsg = listType === "playlist" ? "歌单" : "专辑";
try {
const res = await likeFn(type, id);
if (res.code === 200) {
$message.success(`歌单${type == 1 ? "收藏成功" : "取消收藏成功"}`);
music.setUserPlayLists();
$message.success(`${likeMsg}${type == 1 ? "收藏成功" : "取消收藏成功"}`);
listType === "playlist"
? user.setUserPlayLists()
: user.setUserAlbumLists();
} else {
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
});
} catch (err) {
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
console.error(
`${likeMsg}${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err
);
}
};
onMounted(() => {
if (router.currentRoute.value.name === "playlists" && !music.catList.sub)
if (
router.currentRoute.value.name === "user-playlists" &&
!music.catList.sub
) {
music.setCatList();
if (user.userLogin && !music.getUserPlayLists.has) music.setUserPlayLists();
}
if (
user.userLogin &&
!user.getUserPlayLists.has &&
!user.getUserPlayLists.isLoading &&
props.listType === "playlist"
) {
user.setUserPlayLists();
}
if (
user.userLogin &&
!user.getUserAlbumLists.has &&
!user.getUserAlbumLists.isLoading &&
props.listType === "album"
) {
user.setUserAlbumLists();
}
});
</script>
@@ -424,10 +370,10 @@ onMounted(() => {
.play {
opacity: 0;
position: absolute;
font-size: 5vh;
color: #fff;
padding: 0.5vw;
background-color: #00000010;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 50%;
transform: scale(0.8);
@@ -440,6 +386,7 @@ onMounted(() => {
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 6px;
border-top-left-radius: 8px;
@@ -449,7 +396,7 @@ onMounted(() => {
flex-direction: row;
align-items: center;
.n-icon {
margin-right: 2px;
margin-right: 4px;
}
.des {
line-height: normal;

View File

@@ -26,14 +26,14 @@
@contextmenu="openRightMenu($event, item)"
>
<n-avatar
v-if="item.album?.picUrl"
class="pic"
:src="
item.album && item.album.picUrl
? item.album.picUrl.replace(/^http:/, 'https:') + '?param=60y60'
: '/images/pic/default.png'
"
:src="item.album.picUrl.replace(/^http:/, 'https:') + '?param=60y60'"
fallback-src="/images/pic/default.png"
/>
<div class="num" v-else-if="item.num">
<n-text :depth="2" v-html="item.num" />
</div>
<div class="name">
<div class="title">
<n-text
@@ -97,19 +97,27 @@
<n-icon
class="like"
size="20"
:component="
music.getSongIsLike(item.id) ? FavoriteRound : FavoriteBorderRound
"
@click.stop="
music.getSongIsLike(item.id)
? music.changeLikeList(item.id, false)
: music.changeLikeList(item.id, true)
"
/>
>
<Like
:theme="music.getSongIsLike(item.id) ? 'filled' : 'outline'"
/>
</n-icon>
<n-icon
class="download"
size="20"
@click.stop="downloadSongRef.openDownloadModel(item)"
>
<DownloadFour theme="filled" />
</n-icon>
<n-icon
class="more"
size="20"
:component="MoreHorizRound"
:component="More"
@click.stop="openDrawer(item)"
/>
</div>
@@ -155,10 +163,16 @@
}
"
>
<n-icon size="20" :component="PlayArrowRound" />
<n-icon size="20">
<PlayOne theme="filled" />
</n-icon>
<n-text>立即播放</n-text>
</div>
<div
v-if="
!music.getPersonalFmMode &&
music.getPlaySongData.id != drawerData.id
"
class="item"
@click="
() => {
@@ -167,26 +181,46 @@
}
"
>
<n-icon size="20" :component="SlowMotionVideoRound" />
<n-icon size="20">
<AddMusic theme="filled" />
</n-icon>
<n-text>下一首播放</n-text>
</div>
<div
class="item"
@click="
() => {
openAddToPlaylist(drawerData.id);
addPlayListRef.openAddToPlaylist(drawerData.id);
drawerShow = false;
}
"
>
<n-icon size="20" :component="AddCircleRound" />
<n-text>收藏到歌单</n-text>
<n-icon size="20">
<ListAdd theme="filled" />
</n-icon>
<n-text>添加到歌单</n-text>
</div>
<div
class="item"
@click="
() => {
downloadSongRef.openDownloadModel(drawerData);
drawerShow = false;
}
"
>
<n-icon size="20">
<DownloadFour theme="filled" />
</n-icon>
<n-text>歌曲下载</n-text>
</div>
<div
class="item"
@click="router.push(`/comment?id=${drawerData.id}`)"
>
<n-icon size="20" :component="MessageFilled" />
<n-icon size="20">
<Comments theme="filled" />
</n-icon>
<n-text>前往评论区</n-text>
</div>
<div
@@ -194,7 +228,9 @@
v-if="drawerData.mv"
@click="router.push(`/video?id=${drawerData.mv}`)"
>
<n-icon size="20" :component="OndemandVideoRound" />
<n-icon size="20">
<Video theme="filled" />
</n-icon>
<n-text>观看 MV</n-text>
</div>
<div
@@ -206,13 +242,17 @@
}
"
>
<n-icon size="20" :component="InsertLinkRound" />
<n-icon size="20">
<LinkTwo theme="filled" />
</n-icon>
<n-text>复制歌曲链接</n-text>
</div>
<div class="item">
<n-icon size="20" :component="AccountCircleRound" />
<n-text
>歌手
<n-icon size="20">
<Voice theme="filled" />
</n-icon>
<n-text>
歌手:
<AllArtists
class="text-hidden"
:artistsData="drawerData.artist"
@@ -223,26 +263,28 @@
class="item"
@click="router.push(`/album?id=${drawerData.album.id}`)"
>
<n-icon size="20" :component="AlbumRound" />
<n-icon size="20">
<RecordDisc theme="filled" />
</n-icon>
<n-text>专辑:{{ drawerData.album.name }}</n-text>
</div>
<div
v-if="router.currentRoute.value.name == 'cloud'"
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@click="
() => {
cloudMatchValue.sid = drawerData.id;
cloudMatchBeforeData = drawerData;
cloudMatchModel = true;
cloudMatchRef.openCloudMatch(drawerData);
drawerShow = false;
}
"
>
<n-icon size="20" :component="InsertPageBreakRound" />
<n-icon size="20">
<FileMusic theme="filled" />
</n-icon>
<n-text>歌曲信息纠正</n-text>
</div>
<div
v-if="router.currentRoute.value.name == 'cloud'"
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@click="
() => {
@@ -251,69 +293,20 @@
}
"
>
<n-icon size="20" :component="DeleteRound" />
<n-icon size="20">
<DeleteFour theme="filled" />
</n-icon>
<n-text>从云盘中删除</n-text>
</div>
</div>
</n-drawer-content>
</n-drawer>
<!-- 歌曲信息纠正 -->
<n-modal
style="width: 60vw; min-width: min(24rem, 100vw)"
v-model:show="cloudMatchModel"
preset="card"
title="歌曲信息纠正"
:bordered="false"
:on-after-leave="closeCloudMatch"
>
<n-form class="cloud-match" :label-width="80" :model="cloudMatchValue">
<n-form-item label="原歌曲信息">
<n-card content-style="padding: 16px" :bordered="false" embedded>
<SmallSongData :songData="cloudMatchBeforeData" notJump />
</n-card>
</n-form-item>
<n-form-item label="匹配 ID" path="asid">
<n-input-number
v-model:value="cloudMatchValue.asid"
placeholder="请输入要匹配的歌曲 ID"
:show-button="false"
/>
<n-button
style="margin-left: 12px"
:disabled="!cloudMatchValue.asid"
@click="cloudMatchId = cloudMatchValue.asid.toString()"
>
检查
</n-button>
</n-form-item>
</n-form>
<n-card
v-if="cloudMatchId"
content-style="padding: 16px"
:bordered="false"
embedded
>
<SmallSongData
ref="smallSongDataRef"
:getDataByID="cloudMatchId"
notJump
/>
</n-card>
<template #footer>
<n-space justify="end">
<n-button @click="closeCloudMatch"> 取消 </n-button>
<n-button
type="primary"
@click="setCloudMatchBtn(cloudMatchValue)"
:disabled="!cloudMatchValue.asid"
>
纠正歌曲
</n-button>
</n-space>
</template>
</n-modal>
<CloudMatch ref="cloudMatchRef" />
<!-- 收藏到歌单 -->
<AddPlaylist ref="addPlayListRef" />
<!-- 歌曲下载 -->
<DownloadSong ref="downloadSongRef" />
</div>
<n-spin class="loading" size="small" v-else />
</Transition>
@@ -321,25 +314,29 @@
<script setup>
import {
PlayArrowRound,
SlowMotionVideoRound,
FavoriteBorderRound,
FavoriteRound,
MoreHorizRound,
MessageFilled,
InsertLinkRound,
AccountCircleRound,
AlbumRound,
OndemandVideoRound,
InsertPageBreakRound,
DeleteRound,
AddCircleRound,
} from "@vicons/material";
PlayOne,
AddMusic,
ListAdd,
DownloadFour,
Comments,
Video,
LinkTwo,
Voice,
RecordDisc,
FileMusic,
DeleteFour,
Like,
More,
Search,
} from "@icon-park/vue-next";
import { musicStore, settingStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import { setCloudDel, setCloudMatch, addSongToPlayList } from "@/api";
import { setCloudDel } from "@/api/user";
import { NIcon } from "naive-ui";
import AllArtists from "./AllArtists.vue";
import AddPlaylist from "./AddPlaylist.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import CloudMatch from "@/components/DataModel/CloudMatch.vue";
import DownloadSong from "@/components/DataModel/DownloadSong.vue";
import SmallSongData from "./SmallSongData.vue";
const router = useRouter();
@@ -347,12 +344,11 @@ const music = musicStore();
const setting = settingStore();
const user = userStore();
const addPlayListRef = ref(null);
// 父组件方法
const emit = defineEmits(["cloudDataLoad"]);
const cloudMatchRef = ref(null);
const downloadSongRef = ref(null);
const props = defineProps({
// 表数据
// 表数据
listData: {
type: Object,
default: [],
@@ -364,7 +360,7 @@ const props = defineProps({
},
});
// 弹窗数据
// 右键菜单数据
const rightMenuX = ref(0);
const rightMenuY = ref(0);
const rightMenuShow = ref(false);
@@ -374,17 +370,6 @@ const rightMenuOptions = ref(null);
const drawerShow = ref(false);
const drawerData = ref(null);
// 歌曲信息纠正数据
const smallSongDataRef = ref(null);
const cloudMatchModel = ref(false);
const cloudMatchBeforeData = ref(null);
const cloudMatchId = ref(null);
const cloudMatchValue = ref({
uid: user.getUserData.userId,
sid: null,
asid: null,
});
// 复制歌曲链接或ID
const copySongData = (id, url = true) => {
if (navigator.clipboard) {
@@ -401,6 +386,19 @@ const copySongData = (id, url = true) => {
}
};
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ depth: 2, style: { transform: "translateX(2px)" } },
{
default: () => h(icon, { theme: "filled" }),
}
);
};
};
// 打开右键菜单
const openRightMenu = (e, data) => {
e.preventDefault();
@@ -410,6 +408,7 @@ const openRightMenu = (e, data) => {
{
key: "play",
label: "立即播放",
icon: renderIcon(PlayOne),
props: {
onClick: () => {
playSong(props.listData, data);
@@ -419,7 +418,11 @@ const openRightMenu = (e, data) => {
{
key: "nextPlay",
label: "下一首播放",
disabled: music.getPersonalFmMode ? true : false,
icon: renderIcon(AddMusic),
show:
music.getPersonalFmMode || music.getPlaySongData.id == data.id
? false
: true,
props: {
onClick: () => {
music.addSongToNext(data);
@@ -429,6 +432,7 @@ const openRightMenu = (e, data) => {
{
key: "add",
label: "添加到歌单",
icon: renderIcon(ListAdd),
show: user.userLogin ? true : false,
props: {
onClick: () => {
@@ -436,9 +440,20 @@ const openRightMenu = (e, data) => {
},
},
},
{
key: "download",
label: "歌曲下载",
icon: renderIcon(DownloadFour),
props: {
onClick: () => {
downloadSongRef.value.openDownloadModel(data);
},
},
},
{
key: "comment",
label: "前往评论区",
icon: renderIcon(Comments),
props: {
onClick: () => {
router.push(`/comment?id=${data.id}`);
@@ -448,6 +463,7 @@ const openRightMenu = (e, data) => {
{
key: "mv",
label: "观看 MV",
icon: renderIcon(Video),
show: data.mv && data.mv != 0 ? true : false,
props: {
onClick: () => {
@@ -458,12 +474,13 @@ const openRightMenu = (e, data) => {
{
key: "line1",
type: "divider",
show: router.currentRoute.value.name == "cloud" ? true : false,
show: router.currentRoute.value.name === "user-cloud" ? true : false,
},
{
key: "delete",
label: "从云盘中删除",
show: router.currentRoute.value.name == "cloud" ? true : false,
icon: renderIcon(DeleteFour),
show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: {
onClick: () => {
delCloudSong(data);
@@ -473,12 +490,11 @@ const openRightMenu = (e, data) => {
{
key: "match",
label: "歌曲信息纠正",
show: router.currentRoute.value.name == "cloud" ? true : false,
icon: renderIcon(FileMusic),
show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: {
onClick: () => {
cloudMatchValue.value.sid = data.id;
cloudMatchBeforeData.value = data;
cloudMatchModel.value = true;
cloudMatchRef.value.openCloudMatch(data);
},
},
},
@@ -489,15 +505,23 @@ const openRightMenu = (e, data) => {
{
key: "search",
label: "同名搜索",
icon: renderIcon(Search),
props: {
onClick: () => {
router.push(`/search/songs?keywords=${data.name}`);
router.push({
path: "/search/songs",
query: {
keywords: data.name,
page: 1,
},
});
},
},
},
{
key: "copyId",
label: "复制歌曲 ID",
icon: renderIcon(FileMusic),
props: {
onClick: () => {
copySongData(data.id, false);
@@ -506,22 +530,14 @@ const openRightMenu = (e, data) => {
},
{
key: "copy",
label: "复制链接",
label: "复制歌曲链接",
icon: renderIcon(LinkTwo),
props: {
onClick: () => {
copySongData(data.id);
},
},
},
{
key: "line2",
type: "divider",
},
{
label: data.name,
key: "name",
disabled: true,
},
];
rightMenuShow.value = true;
rightMenuX.value = e.clientX;
@@ -557,38 +573,6 @@ const delCloudSong = (data) => {
});
};
// 歌曲纠正
const setCloudMatchBtn = (data) => {
if (data.sid == data.asid) {
$message.error("与原歌曲 ID 一致,无需纠正");
} else {
if (!smallSongDataRef.value) {
$message.error("请先检查");
} else if (smallSongDataRef.value.checkSongData()) {
setCloudMatch(data.uid, data.sid, data.asid).then((res) => {
console.log(res);
if (res.data) {
closeCloudMatch();
$message.success("歌曲纠正成功");
emit("cloudDataLoad");
} else {
$message.error("歌曲纠正失败,请重试");
}
});
} else {
$message.error("非正常歌曲 ID无法匹配");
}
}
};
// 关闭歌曲纠正
const closeCloudMatch = () => {
cloudMatchBeforeData.value = null;
cloudMatchId.value = null;
cloudMatchValue.value.asid = null;
cloudMatchModel.value = false;
};
// 开启抽屉
const openDrawer = (data) => {
console.log(data);
@@ -602,7 +586,7 @@ const playSong = (data, song) => {
music.setPersonalFmMode(false);
if (router.currentRoute.value.name !== "history") music.setPlaylists(data);
// 检查是否为云盘歌曲
if (router.currentRoute.value.name === "cloud") {
if (router.currentRoute.value.name === "user-cloud") {
music.setPlayListMode("cloud");
} else {
music.setPlayListMode("list");
@@ -657,7 +641,8 @@ const jumpLink = (id, type) => {
box-shadow: 0 1px 2px -2px #f55e5526, 0 3px 6px 0 #f55e5530,
0 5px 12px 4px #f55e5505;
.action {
.like {
.like,
.download {
opacity: 1;
transform: scale(1);
}
@@ -689,12 +674,18 @@ const jumpLink = (id, type) => {
display: none;
}
}
.pic {
.pic,
.num {
width: 50px;
height: 50px;
min-width: 50px;
border-radius: 8px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
}
.name {
flex: 1;
@@ -758,12 +749,14 @@ const jumpLink = (id, type) => {
}
}
.action {
width: 40px;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
justify-content: space-evenly;
@media (max-width: 768px) {
.like {
width: 40px;
.like,
.download {
display: none;
}
}
@@ -772,14 +765,15 @@ const jumpLink = (id, type) => {
display: none;
}
}
.like {
.like,
.download {
cursor: pointer;
opacity: 0;
transform: scale(0.8);
color: $mainColor;
transition: all 0.3s;
&:hover {
transform: scale(1.2);
transform: scale(1.1);
}
&:active {
transform: scale(1);
@@ -813,6 +807,7 @@ const jumpLink = (id, type) => {
}
.n-icon {
margin-right: 8px;
transform: translateY(1.5px);
}
.n-text {
transform: translateY(1px);
@@ -822,9 +817,4 @@ const jumpLink = (id, type) => {
}
}
}
.cloud-match {
:deep(.n-input-number) {
width: 100%;
}
}
</style>

View File

@@ -1,179 +0,0 @@
<template>
<CollapseTransition easing="ease-in-out">
<n-card
title="播放列表"
closable
class="playlistModel"
v-show="music.showPlayList && music.getPlaylists.length"
:header-style="{
padding: '12px 16px',
fontSize: '16px',
backgroundColor: 'var(--n-border-color)',
borderRadius: '8px',
}"
:content-style="{
padding: '0',
display: 'flex',
flexDirection: 'column',
}"
@close="music.showPlayList = false"
@click.stop
>
<n-scrollbar>
<n-card
hoverable
:class="
index == music.persistData.playSongIndex ? 'songs play' : 'songs'
"
:id="'playlist' + index"
:content-style="{
padding: '8px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}"
v-for="(item, index) in music.getPlaylists"
:key="item"
@click="changeIndex(index)"
>
<div class="left">
<div class="num">{{ index + 1 }}</div>
</div>
<div class="right">
<div class="name text-hidden">{{ item.name }}</div>
<AllArtists class="text-hidden" :artistsData="item.artist" />
<n-icon
class="remove"
:component="RemoveCircleOutlineFilled"
@click.stop="music.removeSong(index)"
/>
</div>
</n-card>
</n-scrollbar>
</n-card>
</CollapseTransition>
</template>
<script setup>
import { musicStore } from "@/store";
import { RemoveCircleOutlineFilled } from "@vicons/material";
import AllArtists from "./AllArtists.vue";
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import { nextTick } from "vue";
const music = musicStore();
onMounted(() => {
// 点击外部区域关闭播放列表
document.addEventListener("click", () => {
music.showPlayList = false;
});
});
// 改变播放索引
const changeIndex = (index) => {
music.persistData.playSongIndex = index;
music.setPlayState(true);
};
</script>
<style lang="scss" scoped>
.playlistModel {
position: absolute;
bottom: 76px;
min-width: 300px;
right: 0;
border-radius: 8px;
border-top: none;
box-shadow: var(--n-box-shadow);
:deep(.n-card__content) {
.n-scrollbar {
max-height: 70vh;
.n-scrollbar-content {
padding: 12px;
.songs {
border-radius: 8px;
cursor: pointer;
margin-bottom: 12px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
&:active {
transform: scale(0.98);
}
&:hover {
.n-card__content {
.right {
.remove {
opacity: 1;
}
}
}
}
&.play {
background-color: $mainSecondaryColor;
border-color: $mainColor;
a,
span,
div,
.n-icon {
color: $mainColor;
}
.right {
.remove {
color: $mainColor;
&:hover {
background-color: var(--n-action-color);
}
}
}
}
.left {
width: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.right {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding-right: 42px;
.name {
pointer-events: none;
}
.artists {
opacity: 0.8;
font-size: 13px;
pointer-events: none;
}
.remove {
position: absolute;
border-radius: 8px;
right: 0;
font-size: 22px;
opacity: 0;
transition: all 0.3s;
color: #999;
padding: 6px;
&:hover {
color: $mainColor;
background-color: var(--n-border-color);
}
}
}
}
}
}
.n-scrollbar-rail {
width: 0;
}
}
}
</style>

View File

@@ -14,7 +14,7 @@
<n-text
class="text-hidden"
depth="2"
v-html="songDetail ? songDetail.name : '获取失败'"
v-html="songDetail ? songDetail.name : '未知歌曲'"
@click.stop="router.push(`/song?id=${songDetail.id}`)"
/>
<AllArtists
@@ -28,7 +28,7 @@
<script setup>
import { useRouter } from "vue-router";
import { getMusicDetail } from "@/api";
import { getMusicDetail } from "@/api/song";
import AllArtists from "./AllArtists.vue";
const router = useRouter();
@@ -77,15 +77,22 @@ defineExpose({
checkSongData,
});
// 监听路由参数变化
// 监听参数变化
watch(
() => props.getDataByID,
(val) => {
getMusicDetailData(val);
}
);
watch(
() => props.songData,
(val) => {
songDetail.value = val;
}
);
onMounted(() => {
if (props.songData) songDetail.value = props.songData;
if (props.getDataByID) getMusicDetailData(props.getDataByID);
});
</script>
@@ -102,7 +109,7 @@ onMounted(() => {
.name {
line-height: 1.6;
.n-text {
font-size: 16px;
font-size: 18px;
transition: all 0.3s;
cursor: pointer;
&:hover {
@@ -114,4 +121,4 @@ onMounted(() => {
}
}
}
</style>
</style>

View File

@@ -25,9 +25,11 @@
:src="item.cover.replace(/^http:/, 'https:') + '?param=464y260'"
fallback-src="/images/pic/default.png"
/>
<n-icon class="play" :component="PlayArrowRound" />
<n-icon class="play" size="40">
<PlayOne theme="filled" />
</n-icon>
<div class="num">
<n-icon :component="OndemandVideoFilled" />
<n-icon size="14" :component="Youtube" />
<span>{{ item.playCount }}</span>
</div>
<div class="time">
@@ -61,8 +63,9 @@
</Transition>
</div>
</template>
<script setup>
import { PlayOne, Youtube } from "@icon-park/vue-next";
import { PlayArrowRound, OndemandVideoFilled } from "@vicons/material";
import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue";
@@ -76,7 +79,7 @@ const props = defineProps({
},
});
</script>
<style lang="scss" scoped>
.videolists {
.v-enter-active,
@@ -109,10 +112,10 @@ const props = defineProps({
.play {
opacity: 0;
position: absolute;
font-size: 5vh;
color: #fff;
padding: 0.5vw;
background-color: #00000010;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 50%;
transform: scale(0.8);
@@ -124,6 +127,7 @@ const props = defineProps({
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 4px 8px;
transition: all 0.3s;
@@ -136,7 +140,6 @@ const props = defineProps({
border-bottom-left-radius: 8px;
.n-icon {
margin-right: 4px;
transform: translateY(-1px);
}
}
.time {
@@ -201,4 +204,4 @@ const props = defineProps({
}
}
}
</style>
</style>

View File

@@ -0,0 +1,105 @@
<template>
<!-- 关于本站 -->
<n-modal
class="s-modal"
v-model:show="showAboutModal"
preset="card"
title="关于本站"
:bordered="false"
transform-origin="center"
>
<div class="copyright">
<div class="desc">
<n-text class="name">SPlayer</n-text>
<n-text class="version" :depth="3">
v&nbsp;{{ packageJson.version }}
</n-text>
</div>
<n-blockquote>
<n-text class="power">
Copyright&nbsp;©&nbsp;2020 - {{ new Date().getFullYear() }}
<n-a
:href="packageJson.home"
target="_blank"
v-html="packageJson.author"
/>
</n-text>
<n-text class="point" v-html="'·'" />
<n-a
v-if="icp"
class="beian"
href="https://beian.miit.gov.cn/"
target="_blank"
v-html="icp"
/>
</n-blockquote>
<n-button
class="github"
secondary
strong
@click="jumpUrl(packageJson.github)"
>
<template #icon>
<n-icon :component="GithubOne" />
</template>
Github
</n-button>
</div>
</n-modal>
</template>
<script setup>
import { GithubOne } from "@icon-park/vue-next";
import packageJson from "@/../package.json";
// 关于本站数据
const showAboutModal = ref(false);
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);
// 链接跳转
const jumpUrl = (url) => {
window.open(url);
};
// 开启本站数据弹窗
const openAboutSite = () => {
showAboutModal.value = true;
};
// 暴露方法
defineExpose({
openAboutSite,
});
</script>
<style lang="scss" scoped>
.copyright {
display: flex;
flex-direction: column;
a {
text-decoration: none;
}
.name {
font-size: 30px;
font-weight: bold;
}
.version {
margin-left: 6px;
}
.n-blockquote {
@media (max-width: 768px) {
display: flex;
flex-direction: column;
.point {
display: none;
}
}
.point {
margin: 0 4px;
}
}
.github {
margin-top: 8px;
}
}
</style>

View File

@@ -1,17 +1,16 @@
<template>
<n-modal
class="add-playlist"
style="width: 60vw; min-width: min(24rem, 100vw)"
class="add-playlist s-modal"
v-model:show="addToPlaylistModel"
preset="card"
title="收藏到歌单"
title="添加到歌单"
:bordered="false"
:on-after-leave="closeAddToPlaylist"
>
<n-space vertical class="list" v-if="music.getUserPlayLists.own[0]">
<n-space vertical class="list" v-if="user.getUserPlayLists.own[0]">
<div
class="item"
v-for="item in music.getUserPlayLists.own"
v-for="item in user.getUserPlayLists.own"
:key="item"
@click="addToPlayList(item.id, addToPlaylistId)"
>
@@ -30,14 +29,14 @@
</div>
</div>
</n-space>
<n-text v-else>歌单列表加载中</n-text>
</n-modal>
</template>
<script setup>
import { addSongToPlayList } from "@/api";
import { musicStore, userStore } from "@/store";
import { addSongToPlayList } from "@/api/playlist";
import { userStore } from "@/store";
const music = musicStore();
const user = userStore();
//
@@ -50,9 +49,9 @@ const addToPlayList = (pid, tracks) => {
addSongToPlayList(pid, tracks).then((res) => {
console.log(res);
if (res.status === 200) {
$message.success("收藏歌曲至歌单成功");
$message.success("添加歌曲至歌单成功");
closeAddToPlaylist();
music.setUserPlayLists();
user.setUserPlayLists();
} else {
$message.error("添加失败,请重试");
}
@@ -65,7 +64,9 @@ const openAddToPlaylist = (id) => {
$message.error("请登录账号后使用");
return false;
}
if (!music.getUserPlayLists.has) music.setUserPlayLists();
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
user.setUserPlayLists();
}
addToPlaylistModel.value = true;
addToPlaylistId.value = id;
console.log("开启", addToPlaylistModel.value, addToPlaylistId.value);

View File

@@ -0,0 +1,128 @@
<template>
<n-modal
class="s-modal"
v-model:show="cloudMatchModel"
preset="card"
title="歌曲信息纠正"
:bordered="false"
:on-after-leave="closeCloudMatch"
>
<n-form class="cloud-match" :label-width="80" :model="cloudMatchValue">
<n-form-item label="原歌曲信息">
<n-card content-style="padding: 16px" :bordered="false" embedded>
<SmallSongData :songData="cloudMatchBeforeData" notJump />
</n-card>
</n-form-item>
<n-form-item label="匹配 ID" path="asid">
<n-input-number
v-model:value="cloudMatchValue.asid"
placeholder="请输入要匹配的歌曲 ID"
:show-button="false"
/>
<n-button
style="margin-left: 12px"
:disabled="!cloudMatchValue.asid"
@click="cloudMatchId = cloudMatchValue.asid.toString()"
>
检查
</n-button>
</n-form-item>
</n-form>
<n-card
v-if="cloudMatchId"
content-style="padding: 16px"
:bordered="false"
embedded
>
<SmallSongData
ref="smallSongDataRef"
:getDataByID="cloudMatchId"
notJump
/>
</n-card>
<template #footer>
<n-space justify="end">
<n-button @click="closeCloudMatch"> 取消 </n-button>
<n-button
type="primary"
@click="setCloudMatchBtn(cloudMatchValue)"
:disabled="!cloudMatchValue.asid"
>
纠正歌曲
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { setCloudMatch } from "@/api/user";
import { userStore } from "@/store";
import SmallSongData from "@/components/DataList/SmallSongData.vue";
const user = userStore();
// 歌曲信息纠正数据
const cloudDataLoad = inject("cloudDataLoad", null);
const smallSongDataRef = ref(null);
const cloudMatchModel = ref(false);
const cloudMatchBeforeData = ref(null);
const cloudMatchId = ref(null);
const cloudMatchValue = ref({
uid: user.getUserData.userId,
sid: null,
asid: null,
});
// 歌曲纠正
const setCloudMatchBtn = (data) => {
if (data.sid == data.asid) {
$message.error("与原歌曲 ID 一致,无需纠正");
} else {
if (!smallSongDataRef.value) {
$message.error("请先检查");
} else if (smallSongDataRef.value.checkSongData()) {
setCloudMatch(data.uid, data.sid, data.asid).then((res) => {
console.log(res);
if (res.data) {
closeCloudMatch();
$message.success("歌曲纠正成功");
cloudDataLoad();
} else {
$message.error("歌曲纠正失败,请重试");
}
});
} else {
$message.error("非正常歌曲 ID无法匹配");
}
}
};
// 开启歌曲纠正
const openCloudMatch = (data) => {
cloudMatchValue.value.sid = data.id;
cloudMatchBeforeData.value = data;
cloudMatchModel.value = true;
};
// 关闭歌曲纠正
const closeCloudMatch = () => {
cloudMatchBeforeData.value = null;
cloudMatchId.value = null;
cloudMatchValue.value.asid = null;
cloudMatchModel.value = false;
};
// 暴露方法
defineExpose({
openCloudMatch,
});
</script>
<style lang="scss" scoped>
.cloud-match {
:deep(.n-input-number) {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<n-modal
class="s-modal downloadModel"
v-model:show="downloadModel"
preset="card"
title="歌曲下载"
:bordered="false"
:on-after-leave="closeDownloadModel"
>
<Transition mode="out-in">
<div v-if="songData">
<SmallSongData ref="smallSongDataRef" :songData="songData" notJump />
<n-alert v-if="songData.pc" class="tip" type="info" :show-icon="false">
当前为云盘歌曲下载的文件均为最高音质
</n-alert>
<n-radio-group
class="downloadGroup"
v-model:value="downloadChoose"
name="downloadGroup"
>
<n-space vertical>
<n-radio
v-for="item in downloadLevel"
:key="item"
:value="item.value"
:disabled="item.disabled"
>
<div :class="item.disabled ? 'text disabled' : 'text'">
<n-text class="name">{{ item.label }}</n-text>
<n-text v-if="item.size" class="size" :depth="3">
{{ item.size }}
</n-text>
<n-text v-else-if="!item.disabled" class="error" :depth="3">
无法获取
</n-text>
</div>
</n-radio>
</n-space>
</n-radio-group>
</div>
<n-text v-else>正在获取歌曲下载数据</n-text>
</Transition>
<template #footer>
<n-space justify="end">
<n-button @click="closeDownloadModel"> 取消 </n-button>
<n-button
:disabled="!downloadChoose"
:loading="downloadStatus"
type="primary"
@click="
toSongDownload(
songId,
downloadChoose,
songData.artist[0].name + '-' + songData.name
)
"
>
{{ downloadStatus ? "正在下载" : "下载" }}
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { userStore } from "@/store";
import { useRouter } from "vue-router";
import { getMusicDetail, getSongDownload } from "@/api/song";
import SmallSongData from "@/components/DataList/SmallSongData.vue";
const user = userStore();
const router = useRouter();
// 歌曲下载数据
const songId = ref(null);
const songData = ref(null);
const downloadStatus = ref(false);
const downloadModel = ref(false);
const downloadChoose = ref(null);
const downloadLevel = ref(null);
// 歌曲下载
const toSongDownload = (id, br, name) => {
downloadStatus.value = true;
getSongDownload(id, br)
.then((res) => {
console.log(name, res);
if (res.data.url) {
const type = res.data.type.toLowerCase();
const songName = name ? name : "未知曲目";
fetch(res.data.url)
.then((response) => response.blob())
.then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${songName}.${type}`;
document.body.appendChild(a);
a.click();
a.remove();
closeDownloadModel();
downloadStatus.value = false;
$message.success(name + " 下载完成");
});
} else {
downloadStatus.value = false;
$message.error("下载失败,请尝试其他音质");
}
})
.catch((err) => {
closeDownloadModel();
console.error("下载出现错误:" + err);
$message.error("下载出现错误,请重试");
});
};
// 获取歌曲详情
const getMusicDetailData = (id) => {
getMusicDetail(id)
.then((res) => {
if (res.songs[0] && res.privileges[0]) {
songData.value = {
album: res.songs[0].al,
artist: res.songs[0].ar,
name: res.songs[0].name,
id: res.songs[0].id,
pc: res.songs[0]?.pc,
};
// 生成音质列表
generateLists(res);
} else {
$message.error("歌曲信息获取失败");
}
})
.catch((err) => {
closeDownloadModel();
console.error("歌曲信息获取出现错误:" + err);
$message.error("歌曲信息获取出现错误,请重试");
});
};
// 生成可下载列表
const generateLists = (data) => {
const br = data.privileges[0].downloadMaxbr;
downloadLevel.value = [
{
value: "128000",
label: "标准音质",
disabled: br >= 128000 ? false : true,
size: getSongSize(data, "l"),
},
{
value: "192000",
label: "较高音质",
disabled: br >= 192000 ? false : true,
size: getSongSize(data, "m"),
},
{
value: "320000",
label: "极高音质",
disabled: br >= 320000 ? false : true,
size: getSongSize(data, "h"),
},
{
value: "420000",
label: "无损音质",
disabled: [128000, 192000, 320000].includes(parseInt(br)),
size: getSongSize(data, "sq"),
},
{
value: "999000",
label: "Hi-Res",
disabled: br >= 999000 ? false : true,
size: getSongSize(data, "hr"),
},
];
console.log(downloadLevel.value);
};
// 获取下载大小
const getSongSize = (data, type) => {
let fileSize = 0;
// 转换文件大小
const convertSize = (num) => {
if (!num) return 0;
return (num / (1024 * 1024)).toFixed(2);
};
if (type === "l") {
fileSize = convertSize(data.songs[0]?.l?.size);
} else if (type === "m") {
fileSize = convertSize(data.songs[0]?.m?.size);
} else if (type === "h") {
fileSize = convertSize(data.songs[0]?.h?.size);
} else if (type === "sq") {
fileSize = convertSize(data.songs[0]?.sq?.size);
} else if (type === "hr") {
fileSize = convertSize(data.songs[0]?.hr?.size);
}
return fileSize;
};
// 开启歌曲下载
const openDownloadModel = (data) => {
if (user.userLogin) {
if (
router.currentRoute.value.name === "user-cloud" ||
user.userData?.vipType ||
data?.fee === 0 ||
data?.pc
) {
songId.value = data.id;
downloadModel.value = true;
getMusicDetailData(data.id);
} else {
$message.error("该歌曲需使用黑胶会员下载");
}
} else {
$message.error("请登录后使用该功能");
}
};
// 关闭歌曲下载
const closeDownloadModel = () => {
songId.value = null;
songData.value = null;
downloadStatus.value = false;
downloadModel.value = false;
downloadChoose.value = null;
};
// 暴露方法
defineExpose({
openDownloadModel,
});
</script>
<style lang="scss" scoped>
.downloadModel {
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.tip {
margin-top: 20px;
}
.downloadGroup {
margin-top: 20px;
.text {
&.disabled {
span {
color: var(--n-text-color-disabled);
}
}
.size {
font-size: 13px;
&::before {
content: "-";
margin: 0 4px;
}
&::after {
content: "Mb";
margin-left: 4px;
}
}
.error {
font-size: 13px;
&::before {
content: "-";
transform: translateY(-1.5px);
display: inline-block;
margin: 0 4px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,239 @@
<template>
<n-drawer
class="playlist-drawer"
v-model:show="playListShow"
:z-index="2000"
:width="400"
:trap-focus="false"
:block-scroll="false"
placement="right"
to="#main"
@after-leave="music.showPlayList = false"
@mask-click="music.showPlayList = false"
>
<n-drawer-content title="播放列表" :native-scrollbar="false" closable>
<Transition mode="out-in">
<div v-if="music.getPlaylists[0]">
<n-card
hoverable
:class="
index === music.persistData.playSongIndex ? 'songs play' : 'songs'
"
:id="'playlist' + index"
:content-style="{
padding: '8px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}"
v-for="(item, index) in music.getPlaylists"
:key="item"
@click="changeIndex(index)"
>
<div class="left">
<div v-if="index !== music.persistData.playSongIndex" class="num">
{{ index + 1 }}
</div>
<div v-else class="bar">
<div
v-for="item in 3"
:key="item"
class="line"
:style="{
animationDelay: `0.${item * item}s`,
animationPlayState: music.getPlayState
? 'running'
: 'paused',
height: `${Math.floor(Math.random() * 7) + 10}px`,
}"
/>
</div>
</div>
<div class="right">
<div class="name text-hidden">{{ item.name }}</div>
<AllArtists class="text-hidden" :artistsData="item.artist" />
<n-icon
class="remove"
size="18"
:component="DeleteFour"
@click.stop="music.removeSong(index)"
/>
</div>
</n-card>
</div>
<n-text v-else>暂无歌曲请前往列表添加</n-text>
</Transition>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { musicStore } from "@/store";
import { DeleteFour } from "@icon-park/vue-next";
import AllArtists from "@/components/DataList/AllArtists.vue";
const music = musicStore();
// 播放列表显隐
const playListShow = ref(false);
// 改变播放索引
const changeIndex = (index) => {
music.persistData.playSongIndex = index;
music.setPlayState(true);
};
// 监听播放列表显隐
const timeOut = ref(null);
watch(
() => music.showPlayList,
(val) => {
playListShow.value = val;
nextTick(() => {
if (val && music.getPlaylists[0]) {
const el = document.getElementById(
`playlist${music.persistData.playSongIndex}`
);
if (el) {
timeOut.value = setTimeout(() => {
el.scrollIntoView({
behavior: "smooth",
block: "center",
});
}, 300);
}
} else {
clearTimeout(timeOut.value);
}
});
}
);
onMounted(() => {
playListShow.value = music.showPlayList;
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
</script>
<style lang="scss" scoped>
.playlist-drawer {
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.songs {
border-radius: 8px;
cursor: pointer;
margin-bottom: 12px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
&:active {
transform: scale(0.98);
}
&:hover {
.n-card__content {
.right {
.remove {
opacity: 1;
}
}
}
}
&.play {
background-color: $mainSecondaryColor;
border-color: $mainColor;
a,
span,
div,
.n-icon {
color: $mainColor;
}
:deep(span) {
color: $mainColor;
}
.right {
.remove {
color: $mainColor;
&:hover {
background-color: var(--n-action-color);
}
}
}
}
.left {
width: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.bar {
display: flex;
justify-content: space-evenly;
align-items: flex-end;
width: 20px;
height: 20px;
.line {
width: 3px;
height: 16px;
background-color: $mainColor;
border-radius: 4px;
transition: all 0.3s;
animation: lineMove 1s ease-in-out infinite;
}
@keyframes lineMove {
0% {
height: 16px;
}
50% {
height: 10px;
}
100% {
height: 16px;
}
}
}
}
.right {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding-right: 42px;
.name {
pointer-events: none;
}
.artists {
opacity: 0.8;
font-size: 13px;
pointer-events: none;
}
.remove {
position: absolute;
border-radius: 8px;
right: 0;
opacity: 0;
transition: all 0.3s;
color: #999;
padding: 6px;
&:hover {
color: $mainColor;
background-color: var(--n-border-color);
}
}
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<n-modal
class="s-modal"
v-model:show="playlistUpdateModel"
preset="card"
title="歌单编辑"
:bordered="false"
:on-after-leave="closeUpdateModel"
>
<n-form
ref="playlistUpdateRef"
:rules="playlistUpdateRules"
:label-width="80"
:model="playlistUpdateValue"
>
<n-form-item label="歌单名称" path="name">
<n-input
v-model:value="playlistUpdateValue.name"
placeholder="请输入歌单名称"
/>
</n-form-item>
<n-form-item label="歌单描述" path="desc">
<n-input
v-model:value="playlistUpdateValue.desc"
placeholder="请输入歌单描述"
type="textarea"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</n-form-item>
<n-form-item label="歌单标签" path="tags">
<n-select
multiple
v-model:value="playlistUpdateValue.tags"
placeholder="请输入歌单标签"
:options="playlistTags"
@click="openSelect"
/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="closeUpdateModel"> 取消 </n-button>
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { playlistUpdate } from "@/api/playlist";
import { formRules } from "@/utils/formRules.js";
import { musicStore, userStore } from "@/store";
const { textRule } = formRules();
const music = musicStore();
const user = userStore();
// 更新歌单数据
const playlistUpdateId = ref(null);
const playlistUpdateRef = ref(null);
const playlistUpdateModel = ref(false);
const playlistUpdateRules = {
name: textRule,
};
const playlistUpdateValue = ref({
name: null,
desc: null,
tags: null,
});
// 更新歌单
const toUpdatePlayList = (e) => {
e.preventDefault();
playlistUpdateRef.value?.validate((errors) => {
if (!errors) {
console.log("通过");
playlistUpdate(
playlistUpdateId.value,
playlistUpdateValue._value.name,
playlistUpdateValue._value.desc,
playlistUpdateValue._value.tags.join(";")
).then((res) => {
console.log(res);
if (res.code === 200) {
$message.success("编辑成功");
closeUpdateModel();
user.setUserPlayLists();
} else {
$message.error("编辑失败,请重试");
}
});
} else {
$loadingBar.error();
$message.error("请检查您的输入");
}
});
};
// 歌单分类标签
const playlistTags = ref([]);
const openSelect = () => {
if (music.catList.sub) {
playlistTags.value = music.catList.sub.map((v) => ({
label: v.name,
value: v.name,
}));
} else {
music.setCatList();
}
};
// 开启编辑歌单
const openUpdateModel = (data) => {
playlistUpdateValue.value = {
name: data.name,
desc: data.desc,
tags: data.tags,
};
playlistUpdateId.value = data.id;
playlistUpdateModel.value = true;
};
// 关闭更新歌单弹窗
const closeUpdateModel = () => {
playlistUpdateModel.value = false;
playlistUpdateId.value = null;
};
// 暴露方法
defineExpose({
openUpdateModel,
});
</script>

View File

@@ -5,22 +5,22 @@
<img src="/images/logo/favicon.svg" alt="logo" />
</div>
<div class="controls">
<n-icon
size="26"
:component="NavigateBeforeFilled"
@click="router.go(-1)"
/>
<n-icon
size="26"
:component="NavigateNextFilled"
@click="router.go(1)"
/>
<n-icon size="22" :component="Left" @click="router.go(-1)" />
<n-icon size="22" :component="Right" @click="router.go(1)" />
</div>
</div>
<div class="center">
<router-link class="link" to="/">首页</router-link>
<router-link class="link" to="/discover">发现</router-link>
<router-link class="link" to="/user">我的</router-link>
<n-dropdown
trigger="hover"
:options="discoverOptions"
@select="menuSelect"
>
<router-link class="link" to="/discover">发现</router-link>
</n-dropdown>
<n-dropdown trigger="hover" :options="userOptions" @select="menuSelect">
<router-link class="link" to="/user">我的</router-link>
</n-dropdown>
</div>
<div class="right">
<SearchInp />
@@ -40,7 +40,8 @@
size="small"
:src="
user.getUserData.avatarUrl
? user.getUserData.avatarUrl
? user.getUserData.avatarUrl.replace(/^http:/, 'https:') +
'?param=60y60'
: '/images/ico/user-filling.svg'
"
:img-props="{ class: 'avatarImg' }"
@@ -49,52 +50,7 @@
/>
</n-dropdown>
<!-- 关于本站 -->
<n-modal
class="main-model"
v-model:show="showAboutModal"
style="width: 60vw; min-width: min(24rem, 100vw)"
preset="card"
title="关于本站"
:bordered="false"
>
<div class="copyright">
<div class="desc">
<n-text class="name">SPlayer</n-text>
<n-text class="version" :depth="3">
v&nbsp;{{ packageJson.version }}
</n-text>
</div>
<n-blockquote>
<n-text class="power">
Copyright&nbsp;©&nbsp;2020 - {{ new Date().getFullYear() }}
<n-a
:href="packageJson.home"
target="_blank"
v-html="packageJson.author"
/>
</n-text>
<n-text class="point" v-html="'·'" />
<n-a
v-if="icp"
class="beian"
href="https://beian.miit.gov.cn/"
target="_blank"
v-html="icp"
/>
</n-blockquote>
<n-button
class="github"
secondary
strong
@click="jumpUrl(packageJson.github)"
>
<template #icon>
<n-icon :component="GithubOne" />
</template>
Github
</n-button>
</div>
</n-modal>
<AboutSite ref="aboutSiteRef" />
</div>
</nav>
</template>
@@ -102,24 +58,25 @@
<script setup>
import { NIcon, NAvatar, NText, NProgress } from "naive-ui";
import {
NavigateBeforeFilled,
NavigateNextFilled,
LogInFilled,
LogOutFilled,
SettingsRound,
WbSunnyFilled,
DarkModeFilled,
InfoRound,
HistoryRound,
} from "@vicons/material";
import { GithubOne, Copyright } from "@icon-park/vue-next";
Left,
Right,
Login,
Logout,
Info,
SettingTwo,
History,
SunOne,
Moon,
} from "@icon-park/vue-next";
import { userStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
import AboutSite from "@/components/DataModel/AboutSite.vue";
import SearchInp from "@/components/SearchInp/index.vue";
import packageJson from "@/../package.json";
const router = useRouter();
const user = userStore();
const setting = settingStore();
const aboutSiteRef = ref(null);
// 下拉菜单显隐
const showDropdown = ref(false);
@@ -132,15 +89,6 @@ const closeDropdown = (event) => {
}
};
// 关于本站弹窗
const showAboutModal = ref(false);
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);
// 链接跳转
const jumpUrl = (url) => {
window.open(url);
};
// 用户数据模块
const userDataRender = () => {
return h(
@@ -158,7 +106,8 @@ const userDataRender = () => {
round: true,
style: "margin-right: 12px",
src: user.userLogin
? user.getUserData.avatarUrl
? user.getUserData.avatarUrl.replace(/^http:/, "https:") +
"?param=60y60"
: "/images/ico/user-filling.svg",
fallbackSrc: "/images/ico/user-filling.svg",
}),
@@ -180,17 +129,19 @@ const userDataRender = () => {
{
default: () =>
user.userLogin
? user.getUserData.level
? Object.keys(user.getUserOtherData).length
? h(
NProgress,
{
height: 4,
type: "line",
percentage: user.getUserData.level.progress * 100,
percentage:
user.getUserOtherData.level.progress * 100,
color: "#f55e55",
},
{
default: () => "Lv." + user.getUserData.level.level,
default: () =>
"Lv." + user.getUserOtherData.level.level,
}
)
: "等级信息获取失败"
@@ -204,6 +155,51 @@ const userDataRender = () => {
};
// 下拉框数据
const discoverOptions = ref([
{
label: "歌单",
key: "/discover/playlists",
},
{
label: "排行榜",
key: "/discover/toplists",
},
{
label: "歌手",
key: "/discover/artists",
},
]);
const userOptions = ref(
user.userLogin
? [
{
label: "我的歌单",
key: "/user/playlists",
},
{
label: "收藏的歌单",
key: "/user/like",
},
{
label: "收藏的专辑",
key: "/user/album",
},
{
label: "收藏的歌手",
key: "/user/artists",
},
{
label: "音乐云盘",
key: "/user/cloud",
},
]
: [
{
label: "登录账号",
key: "/login",
},
]
);
const dropdownOptions = ref([
{
key: "header",
@@ -216,21 +212,23 @@ const dropdownOptions = ref([
},
{
label: () => {
return h(NText, null, {
default: () =>
setting.getSiteTheme == "light" ? "深色模式" : "浅色模式",
});
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{
default: () =>
setting.getSiteTheme == "light" ? "深色模式" : "浅色模式",
}
);
},
key: "changeTheme",
icon: () => {
return h(
NIcon,
{ style: "transform: translateY(-1px)" },
{ style: { transform: "translateX(2px)" } },
{
default: () =>
setting.getSiteTheme == "light"
? h(DarkModeFilled)
: h(WbSunnyFilled),
setting.getSiteTheme == "light" ? h(Moon) : h(SunOne),
}
);
},
@@ -241,9 +239,9 @@ const dropdownOptions = ref([
icon: () => {
return h(
NIcon,
{ style: "transform: translateY(-1px)" },
{ style: { transform: "translateX(2px)" } },
{
default: () => h(HistoryRound),
default: () => h(History),
}
);
},
@@ -254,26 +252,30 @@ const dropdownOptions = ref([
icon: () => {
return h(
NIcon,
{ style: "transform: translateY(-0.5px)" },
{ style: { transform: "translateX(2px)" } },
{
default: () => h(SettingsRound),
default: () => h(SettingTwo),
}
);
},
},
{
label: () => {
return h(NText, null, {
default: () => (user.userLogin ? "退出登录" : "登录账号"),
});
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{
default: () => (user.userLogin ? "退出登录" : "登录账号"),
}
);
},
key: "user",
icon: () => {
return h(
NIcon,
{ style: "transform: translateY(-0.5px)" },
{ style: { transform: "translateX(2px)" } },
{
default: () => (user.userLogin ? h(LogOutFilled) : h(LogInFilled)),
default: () => (user.userLogin ? h(Logout) : h(Login)),
}
);
},
@@ -284,9 +286,9 @@ const dropdownOptions = ref([
icon: () => {
return h(
NIcon,
{ style: "transform: translateY(-2px)" },
{ style: { transform: "translateX(2px)" } },
{
default: () => h(InfoRound),
default: () => h(Info),
}
);
},
@@ -294,6 +296,9 @@ const dropdownOptions = ref([
]);
// 下拉框事件
const menuSelect = (key) => {
router.push(key);
};
const dropdownSelect = (key) => {
showDropdown.value = false;
switch (key) {
@@ -319,7 +324,7 @@ const dropdownSelect = (key) => {
class: "s-dialog",
title: "退出登录",
content: "确认退出当前用户登录?",
positiveText: "退出",
positiveText: "退出登录",
negativeText: "取消",
onPositiveClick: () => {
user.userLogOut();
@@ -333,7 +338,7 @@ const dropdownSelect = (key) => {
break;
// 关于
case "about":
showAboutModal.value = true;
aboutSiteRef.value.openAboutSite();
break;
default:
break;
@@ -349,6 +354,8 @@ nav {
flex-direction: row;
justify-content: space-between;
align-items: center;
max-width: 1400px;
margin: 0 auto;
.left {
flex: 1;
max-width: 300px;
@@ -439,33 +446,4 @@ nav {
}
}
}
.copyright {
display: flex;
flex-direction: column;
a {
text-decoration: none;
}
.name {
font-size: 30px;
font-weight: bold;
}
.version {
margin-left: 6px;
}
.n-blockquote {
@media (max-width: 768px) {
display: flex;
flex-direction: column;
.point {
display: none;
}
}
.point {
margin: 0 4px;
}
}
.github {
margin-top: 8px;
}
}
</style>

View File

@@ -85,6 +85,12 @@ watch(
pageNumberChange(val);
}
);
watch(
() => props.pageNumber,
(val) => {
currentPageNumber.value = val;
}
);
onMounted(() => {
// 更改当前页数

View File

@@ -2,7 +2,7 @@
<div class="paalbum">
<n-h3 class="title" prefix="bar">
新碟上架
<span class="more">更多</span>
<span class="more" @click="router.push('/new-album?page=1')">更多</span>
</n-h3>
<CoverLists
listType="album"
@@ -14,7 +14,7 @@
</template>
<script setup>
import { getNewAlbum } from "@/api";
import { getNewAlbum } from "@/api/home";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -71,4 +71,4 @@ onMounted(() => {
}
}
}
</style>
</style>

View File

@@ -14,14 +14,16 @@
<n-tab :name="8"> 日本 </n-tab>
<n-tab :name="16"> 韩国 </n-tab>
</n-tabs>
<span class="more" @click="router.push('/discover/artists')">更多</span>
<span class="more" @click="router.push('/discover/artists?page=1')">
更多
</span>
</n-h3>
<ArtistLists :listData="artistsData" :gridCollapsed="true" />
</div>
</template>
<script setup>
import { getArtistList } from "@/api";
import { getArtistList } from "@/api/artist";
import { useRouter } from "vue-router";
import ArtistLists from "@/components/DataList/ArtistLists.vue";
@@ -95,4 +97,4 @@ onMounted(() => {
}
}
}
</style>
</style>

View File

@@ -19,7 +19,7 @@
</template>
<script setup>
import { getDailySongs } from "@/api";
import { getDailySongs } from "@/api/home";
import { useRouter } from "vue-router";
import { musicStore, userStore } from "@/store";

View File

@@ -22,8 +22,9 @@
<span
class="name text-hidden"
@click="router.push(`/song?id=${music.getPersonalFmData.id}`)"
>{{ music.getPersonalFmData.name }}</span
>
{{ music.getPersonalFmData.name }}
</span>
<AllArtists
class="text-hidden"
:artistsData="music.getPersonalFmData.artist"
@@ -126,6 +127,7 @@ onMounted(() => {
width: 100%;
height: 100%;
background-color: #00000040;
-webkit-backdrop-filter: blur(80px);
backdrop-filter: blur(80px);
z-index: -1;
}
@@ -169,9 +171,10 @@ onMounted(() => {
flex-direction: row;
align-items: center;
.state {
cursor: pointer;
margin-right: 2px;
transform: scale(1);
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.1);
}

View File

@@ -2,7 +2,9 @@
<div class="paplaylists">
<n-h3 class="title" prefix="bar">
推荐歌单
<span class="more" @click="router.push('/discover/playlists')">更多</span>
<span class="more" @click="router.push('/discover/playlists?page=1')">
更多
</span>
</n-h3>
<CoverLists
:listData="personalizedData"
@@ -13,7 +15,7 @@
</template>
<script setup>
import { getPersonalized } from "@/api";
import { getPersonalized } from "@/api/home";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -69,4 +71,4 @@ onMounted(() => {
}
}
}
</style>
</style>

View File

@@ -12,25 +12,53 @@
"
>
<div class="gray" />
<n-icon
class="close"
size="40"
:component="KeyboardArrowDownFilled"
@click="music.setBigPlayerState(false)"
/>
<n-icon
class="screenfull"
size="36"
:component="screenfullIcon"
@click="screenfullChange"
/>
<div class="icon-menu">
<div class="menu-left">
<div class="icon">
<n-icon
class="setting"
size="30"
:component="SettingsRound"
@click="
() => {
music.setBigPlayerState(false);
router.push('/setting/player');
}
"
/>
</div>
</div>
<div class="menu-right">
<div class="icon">
<n-icon
class="screenfull"
:component="screenfullIcon"
@click="screenfullChange"
/>
</div>
<div class="icon">
<n-icon
class="close"
:component="KeyboardArrowDownFilled"
@click="music.setBigPlayerState(false)"
/>
</div>
</div>
</div>
<div
:class="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4
? 'all'
: 'all noLrc'
"
>
<!-- 提示文本 -->
<Transition name="lrc">
<div class="tip" v-show="lrcMouseStatus">
<n-text>点击选中的歌词以调整播放进度</n-text>
</div>
</Transition>
<div class="left">
<PlayerCover v-if="setting.playerStyle === 'cover'" />
<PlayerRecord v-else />
@@ -44,7 +72,8 @@
<div
class="lrcShow"
v-if="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] &&
music.getPlaySongLyric.lrc.length > 4
"
>
<div class="data" v-show="setting.playerStyle === 'record'">
@@ -76,56 +105,13 @@
</span>
</div>
</div>
<div
:class="
setting.playerStyle === 'cover'
? 'lrc-all cover'
: 'lrc-all record'
<RollingLyrics
@mouseenter="
lrcMouseStatus = setting.lrcMousePause ? true : false
"
v-if="music.getPlaySongLyric[0]"
:style="
setting.lyricsPosition === 'center'
? { textAlign: 'center', paddingRight: '0' }
: null
"
>
<div class="placeholder"></div>
<div
:class="
music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'
"
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
v-for="(item, index) in music.getPlaySongLyric"
:key="item"
:id="'lrc' + index"
@click="jumpTime(item.time)"
>
<div
class="lrc-text"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
}"
>
<span
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>{{ item.lyric }}
</span>
<span
v-show="
music.getPlaySongTransl &&
setting.getShowTransl &&
item.lyricFy
"
:style="{ fontSize: setting.lyricsFontSize - 0.4 + 'vh' }"
class="lyric-fy"
>{{ item.lyricFy }}</span
>
</div>
</div>
<div class="placeholder"></div>
</div>
@mouseleave="lrcAllLeave"
@lrcTextClick="lrcTextClick"
/>
<div
:class="menuShow ? 'menu show' : 'menu'"
v-show="setting.playerStyle === 'record'"
@@ -160,12 +146,14 @@ import {
MessageFilled,
FullscreenRound,
FullscreenExitRound,
SettingsRound,
} from "@vicons/material";
import { musicStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
import MusicFrequency from "@/utils/MusicFrequency.js";
import PlayerRecord from "./PlayerRecord.vue";
import PlayerCover from "./PlayerCover.vue";
import RollingLyrics from "./RollingLyrics.vue";
import screenfull from "screenfull";
const router = useRouter();
@@ -179,12 +167,21 @@ const menuShow = ref(false);
const avBars = ref(null);
const musicFrequency = ref(null);
// 点击歌词跳转
const jumpTime = (time) => {
// 歌词文本点击事件
const lrcTextClick = (time) => {
if ($player) $player.currentTime = time;
lrcMouseStatus.value = false;
};
// 鼠标移出歌词区域
const lrcMouseStatus = ref(false);
const lrcAllLeave = () => {
lrcMouseStatus.value = false;
lyricsScroll(music.getPlaySongLyricIndex);
};
// 全屏切换
const timeOut = ref(null);
const screenfullIcon = shallowRef(FullscreenRound);
const screenfullChange = () => {
if (screenfull.isEnabled) {
@@ -192,6 +189,11 @@ const screenfullChange = () => {
screenfullIcon.value = screenfull.isFullscreen
? FullscreenRound
: FullscreenExitRound;
// 延迟一段时间执行列表滚动
timeOut.value = setTimeout(() => {
lrcMouseStatus.value = false;
lyricsScroll(music.getPlaySongLyricIndex);
}, 500);
}
};
@@ -209,13 +211,20 @@ const toComment = () => {
// 歌词滚动
const lyricsScroll = (index) => {
const type = setting.lyricsBlock;
const el = document.getElementById(
`lrc${type === "center" ? index : index - 2}`
);
if (el) {
el.scrollIntoView({
const lrcType =
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? "lrc" : "yrc";
const el = document.getElementById(lrcType + index);
if (el && !lrcMouseStatus.value) {
const container = el.parentElement;
const containerHeight = container.clientHeight;
// 调整滚动的距离
const scrollDistance =
el.offsetTop -
container.offsetTop -
(type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 100);
container.scrollTo({
top: scrollDistance,
behavior: "smooth",
block: type,
});
}
};
@@ -235,9 +244,15 @@ onMounted(() => {
);
musicFrequency.value.drawSpectrum();
}
// 滚动歌词
lyricsScroll(music.getPlaySongLyricIndex);
});
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
// 监听页面是否打开
watch(
() => music.showBigPlayer,
@@ -246,6 +261,7 @@ watch(
console.log("开启播放器", music.getPlaySongLyricIndex);
nextTick(() => {
lyricsScroll(music.getPlaySongLyricIndex);
music.showPlayList = false;
});
}
}
@@ -309,11 +325,63 @@ watch(
width: 100%;
height: 100%;
background-color: #00000060;
-webkit-backdrop-filter: blur(80px);
backdrop-filter: blur(80px);
z-index: -1;
}
.icon-menu {
padding: 20px;
width: 100%;
height: 80px;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 2;
box-sizing: border-box;
.menu-left,
.menu-right {
display: flex;
align-items: center;
.icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
opacity: 0.3;
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
&:hover {
background-color: #ffffff20;
transform: scale(1.05);
opacity: 1;
}
&:active {
transform: scale(1);
}
.screenfull,
.setting {
@media (max-width: 768px) {
display: none;
}
}
}
}
.menu-right {
.icon {
margin-left: 12px;
}
}
}
/*
.close,
.screenfull {
.screenfull,
.setting {
position: absolute;
top: 24px;
right: 24px;
@@ -322,6 +390,7 @@ watch(
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
z-index: 2;
&:hover {
background-color: #ffffff20;
transform: scale(1.05);
@@ -338,6 +407,9 @@ watch(
display: none;
}
}
.setting {
left: 24px;
}*/
.all {
width: 100%;
height: 100%;
@@ -345,6 +417,7 @@ watch(
flex-direction: row;
align-items: center;
transition: all 0.3s ease-in-out;
position: relative;
&.noLrc {
.left {
padding-right: 0;
@@ -367,11 +440,12 @@ watch(
display: none !important;
}
.right {
padding: 0 5vw;
padding: 0 2vw;
.lrcShow {
.lrc-all {
height: 70vh !important;
padding-right: 16% !important;
// padding-right: 16% !important;
margin-right: 0 !important;
}
.data,
.menu {
@@ -381,6 +455,23 @@ watch(
}
}
}
.tip {
position: absolute;
top: 24px;
left: calc(50% - 150px);
width: 300px;
height: 40px;
border-radius: 25px;
background-color: #ffffff20;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
display: flex;
align-items: center;
justify-content: center;
span {
color: #ffffffc7;
}
}
.left {
// flex: 1;
// padding: 0 4vw;
@@ -430,104 +521,6 @@ watch(
}
}
}
.lrc-all {
padding-right: 20%;
scrollbar-width: none;
// max-width: 460px;
max-width: 52vh;
overflow: auto;
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
&::-webkit-scrollbar {
display: none;
}
&.cover {
height: 80vh;
}
&.record {
height: 60vh;
}
.placeholder {
width: 100%;
height: 50%;
}
.lrc {
opacity: 0.5;
transition: all 0.3s;
// display: flex;
// flex-direction: column;
// margin-bottom: 4px;
// padding: 12px 20px;
margin-bottom: 0.8vh;
padding: 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: center left;
cursor: pointer;
.lrc-text {
display: flex;
flex-direction: column;
transition: all 0.3s;
transform: scale(0.95);
transform-origin: center left;
.lyric {
transition: all 0.3s;
// font-size: 2.4vh;
transform-origin: center left;
}
.lyric-fy {
margin-top: 2px;
transition: all 0.3s;
opacity: 0.8;
// font-size: 2vh;
transform-origin: center left;
}
}
&.on {
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
}
}
// .lyric {
// font-weight: bold;
// // font-size: 3vh;
// transform: scale(1.3);
// }
// .lyric-fy {
// // font-weight: normal;
// // font-size: 2.3vh;
// transform: scale(1.1);
// }
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
}
}
&:active {
transform: scale(0.95);
}
}
}
.menu {
opacity: 0;
padding: 0 20px;

View File

@@ -0,0 +1,91 @@
<template>
<Transition mode="out-in" appear>
<div
class="countdown"
:style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }"
v-if="
remainingPoint <= 2 &&
totalDuration > 3 &&
music.getPlaySongLyric.lrc[0]
"
>
<span class="point" :class="remainingPoint > 2 ? 'hidden' : null">●</span>
<span class="point" :class="remainingPoint > 1 ? 'hidden' : null">●</span>
<span class="point" :class="remainingPoint > 0 ? 'hidden' : null">●</span>
</div>
</Transition>
</template>
<script setup>
import { musicStore } from "@/store";
const music = musicStore();
// 剩余点数
const remainingPoint = ref(0);
// 总时长
const totalDuration = ref(
music.getPlaySongLyric.hasYrc
? music.getPlaySongLyric?.yrc[0].time
: music.getPlaySongLyric?.lrc[0].time
);
// 监听歌曲时长变化
watch(
() => music.getPlaySongTime.currentTime,
(val) => {
if (music.getPlaySongLyric.lrc[0]) {
const remainingTime = totalDuration.value - val - 0.5;
const progress = 1 - remainingTime / totalDuration.value;
remainingPoint.value = Number(Math.floor(3 * progress));
}
}
);
// 监听歌曲改变
watch(
() => music.getPlaySongLyric?.lrc,
(val) => {
if (music.getPlaySongLyric.lrc[0]) {
totalDuration.value = music.getPlaySongLyric.hasYrc
? music.getPlaySongLyric?.yrc[0].time
: val[0].time;
remainingPoint.value = 0;
}
}
);
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: all 0.3s ease-in-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: scale(0);
}
.countdown {
animation: breathe 5s ease-in-out infinite;
.point {
margin-right: 4px;
transition: all 0.3s;
&.hidden {
opacity: 0.2;
}
}
}
@keyframes breathe {
0% {
transform: scale(0.95);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(0.95);
}
}
</style>

View File

@@ -149,8 +149,6 @@ const router = useRouter();
const music = musicStore();
const user = userStore();
const canvas = ref(null);
// 歌曲进度条更新
const songTimeSliderUpdate = (val) => {
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
@@ -206,10 +204,12 @@ const routerJump = (url, query) => {
width: 100%;
padding-right: 4px;
.name {
// font-size: 3vh;
font-size: 23px;
font-weight: bold;
-webkit-line-clamp: 2;
@media (max-width: 1200px) {
font-size: 20px;
}
}
.message {
cursor: pointer;
@@ -220,6 +220,9 @@ const routerJump = (url, query) => {
font-size: 15px;
width: 100%;
color: #ffffffcc;
@media (max-width: 1200px) {
font-size: 14px;
}
.ablum {
transition: all 0.3s;
&:hover {
@@ -315,4 +318,4 @@ const routerJump = (url, query) => {
}
}
}
</style>
</style>

View File

@@ -24,11 +24,19 @@
</div>
<div class="control">
<n-icon
v-if="!music.getPersonalFmMode"
class="prev"
size="36"
:component="SkipPreviousRound"
@click.stop="music.setPlaySongIndex('prev')"
/>
<n-icon
v-else
class="dislike"
size="24"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
<div class="play-state">
<n-icon
v-show="!music.getPlayState"
@@ -59,6 +67,7 @@ import {
PauseCircleFilled,
SkipNextRound,
SkipPreviousRound,
ThumbDownRound,
} from "@vicons/material";
import { musicStore } from "@/store";
const music = musicStore();
@@ -181,16 +190,17 @@ const music = musicStore();
height: 68%;
border-radius: 50%;
background-color: #00000050;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
transition: all 0.5s;
.play-state {
display: flex;
align-items: center;
justify-content: center;
}
.n-icon {
cursor: pointer;
transition: all 0.3s;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: scale(1.1);
}
@@ -198,6 +208,15 @@ const music = musicStore();
transform: scale(1);
}
}
.play-state {
display: flex;
align-items: center;
justify-content: center;
.n-icon {
width: 60px;
height: 60px;
}
}
}
}
@@ -210,4 +229,4 @@ const music = musicStore();
transform: rotate(360deg);
}
}
</style>
</style>

View File

@@ -0,0 +1,302 @@
<template>
<!-- 歌词滚动 -->
<div
v-if="music.getPlaySongLyric.lrc[0]"
:class="[
setting.playerStyle === 'cover' ? 'lrc-all cover' : 'lrc-all record',
setting.lyricsBlock === 'center' ? 'center' : 'top',
]"
:style="
setting.lyricsPosition === 'center'
? { textAlign: 'center', paddingRight: '0' }
: null
"
>
<div
class="placeholder"
:id="
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? 'lrc-1' : 'yrc-1'
"
>
<CountDown :style="{ fontSize: setting.lyricsFontSize + 'vh' }" />
</div>
<template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc">
<div
v-for="(item, index) in music.getPlaySongLyric.lrc"
:class="music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'"
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
:key="item"
:id="'lrc' + index"
@click="lrcTextClick(item.time)"
>
<div
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null,
}"
>
<span
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
{{ item.content }}
</span>
<span
v-show="
music.getPlaySongLyric.hasTran &&
setting.getShowTransl &&
item.tran
"
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy"
>
{{ item.tran }}</span
>
</div>
</div>
</template>
<template v-else>
<div
v-for="(item, index) in music.getPlaySongLyric.yrc"
:class="music.getPlaySongLyricIndex == index ? 'yrc on' : 'yrc'"
:key="item"
:id="'yrc' + index"
@click="lrcTextClick(item.time)"
>
<div
:class="setting.lyricsBlur ? 'yrc-text blur' : 'yrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null,
}"
>
<div
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
<span
v-for="(v, i) in item.content"
:key="i"
:style="{
'--dur': v.duration - 0.15 + 's',
}"
:class="
music.getPlaySongLyricIndex == index &&
music.getPlaySongTime.currentTime + 0.15 >= v.time
? 'text fill'
: 'text'
"
>
{{ v.content }}
</span>
</div>
<span
v-show="
music.getPlaySongLyric.hasTran &&
setting.getShowTransl &&
item.tran
"
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy"
>
{{ item.tran }}</span
>
</div>
</div>
</template>
<div class="placeholder"></div>
</div>
</template>
<script setup>
import { musicStore, settingStore } from "@/store";
import CountDown from "./CountDown.vue";
const music = musicStore();
const setting = settingStore();
// 发送方法
const emit = defineEmits(["lrcTextClick"]);
// 歌词模糊数值
const getFilter = (lrcIndex, index) => {
if (lrcIndex >= index) {
return lrcIndex - index;
} else {
return index - lrcIndex;
}
};
// 歌词文本点击
const lrcTextClick = (time) => {
emit("lrcTextClick", time);
};
</script>
<style lang="scss" scoped>
.lrc-all {
margin-right: 20%;
scrollbar-width: none;
// max-width: 460px;
max-width: 52vh;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
&.cover {
height: 80vh;
}
&.record {
height: 60vh;
}
&.center {
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
}
&.top {
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 5%,
#fff 10%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 5%,
#fff 10%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
.placeholder {
&:nth-of-type(1) {
height: 16%;
}
}
}
&:hover {
.lrc-text {
&.blur {
filter: blur(0) !important;
}
}
}
@media (max-width: 768px) {
height: 70vh;
margin-right: 0;
}
.placeholder {
width: 100%;
&:nth-of-type(1) {
height: 50%;
display: flex;
align-items: flex-end;
padding: 0 0 0.8vh 3vh;
}
&:nth-last-of-type(1) {
height: 80%;
}
}
.lrc,
.yrc {
opacity: 0.2;
transition: all 0.3s;
margin-bottom: 0.8vh;
padding: 1.8vh 4vh 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: left bottom;
cursor: pointer;
.lrc-text,
.yrc-text {
display: flex;
flex-direction: column;
transition: all 0.35s ease-in-out;
transform: scale(0.95);
transform-origin: left bottom;
.lyric {
font-weight: bold;
transition: all 0.3s;
.text {
transition: all var(--dur);
color: #ffffff66;
&.fill {
text-shadow: 0px 0px 30px #ffffff40;
background-image: linear-gradient(to right, #fff 0%, #fff 0%);
background-repeat: no-repeat;
background-size: 0% 100%;
background-clip: text;
-webkit-background-clip: text;
color: #ffffff66;
animation: toRight var(--dur) forwards linear;
}
@keyframes toRight {
to {
background-size: 100% 100%;
}
}
}
}
.lyric-fy {
margin-top: 4px;
transition: all 0.3s;
opacity: 0.6;
}
}
&.on {
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
text-shadow: 0px 0px 30px #ffffff40;
}
}
.yrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
}
}
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
opacity: 1;
}
}
&:active {
transform: scale(0.95);
}
}
.yrc {
opacity: 0.6;
}
}
</style>

View File

@@ -17,157 +17,188 @@
/>
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div>
<div class="data">
<div class="pic" @click.stop="music.setBigPlayerState(true)">
<img
:src="
music.getPlaySongData
? music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
'?param=50y50'
: '/images/pic/default.png'
"
alt="pic"
/>
<n-icon class="open" size="30" :component="KeyboardArrowUpFilled" />
</div>
<div class="name">
<div
class="song text-hidden"
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
>
{{ music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲" }}
<div class="all">
<div class="data">
<div class="pic" @click.stop="music.setBigPlayerState(true)">
<img
:src="
music.getPlaySongData
? music.getPlaySongData.album.picUrl.replace(
/^http:/,
'https:'
) + '?param=50y50'
: '/images/pic/default.png'
"
alt="pic"
/>
<n-icon class="open" size="30" :component="KeyboardArrowUpFilled" />
</div>
<div class="artisrOrLrc" v-if="music.getPlaySongData">
<template v-if="setting.bottomLyricShow">
<Transition mode="out-in">
<div class="name">
<div
class="song text-hidden"
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
>
{{
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲"
}}
</div>
<div class="artisrOrLrc" v-if="music.getPlaySongData">
<template v-if="setting.bottomLyricShow">
<Transition mode="out-in">
<AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric.lrc[0]"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
<n-text
v-else-if="
setting.showYrc &&
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.hasYrc
"
class="lrc text-hidden"
>
<n-text
v-for="item in music.getPlaySongLyric.yrc[
music.getPlaySongLyricIndex
].content"
:key="item"
:depth="3"
>
{{ item.content }}
</n-text>
</n-text>
<n-text
v-else-if="
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.lrc[0]
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric.lrc[music.getPlaySongLyricIndex]
.content
"
/>
<AllArtists
v-else
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</Transition>
</template>
<template v-else>
<AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric[0]"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
<n-text
v-else-if="
music.getPlaySongLyric[0] && music.getPlaySongLyricIndex != -1
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric[music.getPlaySongLyricIndex].lyric
"
/>
</Transition>
</template>
<template v-else>
<AllArtists
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</template>
</template>
</div>
</div>
</div>
</div>
<div class="control">
<n-icon
v-if="!music.getPersonalFmMode"
title="上一曲"
class="prev"
size="30"
:component="SkipPreviousRound"
@click.stop="music.setPlaySongIndex('prev')"
/>
<n-icon
v-else
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
<div class="play-state">
<div class="control">
<n-icon
size="46"
:title="music.getPlayState ? '暂停' : '播放'"
:component="music.getPlayState ? PauseCircleFilled : PlayCircleFilled"
@click.stop="music.setPlayState(!music.getPlayState)"
/>
</div>
<n-icon
class="next"
size="30"
:component="SkipNextRound"
@click.stop="music.setPlaySongIndex('next')"
/>
</div>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<div class="like" v-if="music.getPlaySongData">
<n-icon
class="like-icon"
size="24"
:component="
music.getSongIsLike(music.getPlaySongData.id)
? FavoriteRound
: FavoriteBorderRound
"
@click.stop="
music.getSongIsLike(music.getPlaySongData.id)
? music.changeLikeList(music.getPlaySongData.id, false)
: music.changeLikeList(music.getPlaySongData.id, true)
"
/>
</div>
<div class="add-playlist">
<n-icon
class="add-icon"
v-if="!music.getPersonalFmMode"
title="上一曲"
class="prev"
size="30"
:component="PlaylistAddRound"
@click.stop="
addPlayListRef.openAddToPlaylist(music.getPlaySongData.id)
"
:component="SkipPreviousRound"
@click.stop="music.setPlaySongIndex('prev')"
/>
</div>
<div class="pattern">
<n-icon
:component="
persistData.playSongMode === 'normal'
? PlayCycle
: persistData.playSongMode === 'random'
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
v-else
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
</div>
<div class="playlist">
<PlayList />
<div class="play-state">
<n-icon
size="46"
:title="music.getPlayState ? '暂停' : '播放'"
:component="
music.getPlayState ? PauseCircleFilled : PlayCircleFilled
"
@click.stop="music.setPlayState(!music.getPlayState)"
/>
</div>
<n-icon
class="next"
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
:component="SkipNextRound"
@click.stop="music.setPlaySongIndex('next')"
/>
</div>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"
:tooltip="false"
:min="0"
:max="1"
:step="0.01"
@click.stop
/>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<div class="like" v-if="music.getPlaySongData">
<n-icon
class="like-icon"
size="24"
:component="
music.getSongIsLike(music.getPlaySongData.id)
? FavoriteRound
: FavoriteBorderRound
"
@click.stop="
music.getSongIsLike(music.getPlaySongData.id)
? music.changeLikeList(music.getPlaySongData.id, false)
: music.changeLikeList(music.getPlaySongData.id, true)
"
/>
</div>
<div class="add-playlist">
<n-icon
class="add-icon"
size="30"
:component="PlaylistAddRound"
@click.stop="
addPlayListRef.openAddToPlaylist(music.getPlaySongData.id)
"
/>
</div>
<div class="pattern">
<n-icon
:component="
persistData.playSongMode === 'normal'
? PlayCycle
: persistData.playSongMode === 'random'
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
/>
</div>
<div :class="music.showPlayList ? 'playlist open' : 'playlist'">
<n-icon
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
/>
</div>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"
:tooltip="false"
:min="0"
:max="1"
:step="0.01"
@click.stop
/>
</div>
</div>
</div>
<audio
@@ -182,12 +213,21 @@
:src="music.getPlaySongLink"
></audio>
</n-card>
<!-- 播放列表 -->
<PlayListDrawer ref="PlayListDrawerRef" />
<!-- 添加到歌单 -->
<AddPlaylist ref="addPlayListRef" />
<!-- 播放器 -->
<BigPlayer />
</template>
<script setup>
import { checkMusicCanUse, getMusicUrl, getMusicLyric } from "@/api";
import {
checkMusicCanUse,
getMusicUrl,
getMusicNumUrl,
getMusicNewLyric,
} from "@/api/song";
import { NIcon } from "naive-ui";
import {
KeyboardArrowUpFilled,
@@ -201,8 +241,6 @@ import {
VolumeMuteRound,
VolumeDownRound,
VolumeUpRound,
RepeatRound,
RepeatOneRound,
ThumbDownRound,
FavoriteBorderRound,
FavoriteRound,
@@ -212,9 +250,9 @@ import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { storeToRefs } from "pinia";
import { musicStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
import AddPlaylist from "@/components/DataList/AddPlaylist.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import PlayListDrawer from "@/components/DataModel/PlayListDrawer.vue";
import AllArtists from "@/components/DataList/AllArtists.vue";
import PlayList from "@/components/DataList/PlayList.vue";
import BigPlayer from "./BigPlayer.vue";
import debounce from "@/utils/debounce";
@@ -223,32 +261,79 @@ const setting = settingStore();
const music = musicStore();
const { persistData } = storeToRefs(music);
const addPlayListRef = ref(null);
const PlayListDrawerRef = ref(null);
// 重试次数
const testNumber = ref(0);
// UNM 是否存在
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
// 音频标签
const player = ref(null);
// 获取歌曲播放数据
const getPlaySongData = (id, level = setting.songLevel) => {
checkMusicCanUse(id).then((res) => {
if (res.success) {
console.log("音乐可用");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
if (res.data[0].fee == 1) {
$message.warning("当前歌曲为 VIP 专享,仅可试听");
const getPlaySongData = (data, level = setting.songLevel) => {
try {
const { id, fee, pc } = data;
// VIP 歌曲或需要购买专辑
if (
useUnmServerHas &&
setting.useUnmServer &&
!pc &&
(fee === 1 || fee === 4)
) {
console.log("网易云解灰");
getMusicNumUrlData(id);
}
// 免费或无版权
else {
checkMusicCanUse(id).then((res) => {
if (res.success) {
console.log("当前歌曲可用");
if (!pc && (fee === 1 || fee === 4))
$message.info("当前歌曲为 VIP 专享,仅可试听");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
});
} else {
if (useUnmServerHas && setting.useUnmServer) {
getMusicNumUrlData(id);
} else {
$message.warning("当前歌曲播放失败,跳至下一首");
music.setPlaySongIndex("next");
}
}
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
});
// 获取歌词
getMusicLyric(id).then((res) => {
music.setPlaySongLyric(res);
});
} else {
console.log("无法播放");
$message.error("当前歌曲无法播放,已跳至下一首");
}
// 获取歌词
getMusicNewLyric(id).then((res) => {
music.setPlaySongLyric(res);
});
} catch (err) {
console.log("当前歌曲所有音源匹配失败:" + err);
if (music.getPlayState && $player) {
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
music.setPlaySongIndex("next");
}
});
}
};
// 网易云解灰
const getMusicNumUrlData = (id) => {
getMusicNumUrl(id)
.then((res) => {
if (res.code === 200) {
console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
music.setPlaySongLink(res.data.url.replace(/^http:/, ""));
}
})
.catch((err) => {
console.log("解灰失败:" + err);
$message.warning("当前歌曲解灰失败,跳至下一首");
music.setPlaySongIndex("next");
});
};
// 歌曲进度更新事件
@@ -269,9 +354,13 @@ const songCanplay = () => {
// 歌曲开始播放
const songPlay = () => {
testNumber.value = 0;
if (!music.getPlaySongData) {
$message.error("音乐数据获取失败");
return false;
}
music.setPlayState(true);
// 兼容 mediaSession
console.log(music.getPlaySongData.album.picUrl);
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: music.getPlaySongData.name,
@@ -317,11 +406,24 @@ const songPlay = () => {
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 更改页面标题
window.document.title = music.getPlaySongData.name + " - SPlayer";
// $setSiteTitle(
// music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name
// );
window.document.title =
music.getPlaySongData.name +
" - " +
music.getPlaySongData.artist[0].name +
" - SPlayer";
};
// 音乐渐入渐出
const isFading = ref(false);
const songInOrOut = (type) => {
if (isFading.value) {
return;
}
isFading.value = true;
if (type === "play") {
let volume = 0;
$player.play();
@@ -329,6 +431,7 @@ const songInOrOut = (type) => {
// 如果音量已经到达当前音量,则停止渐入
if (volume >= persistData.value.playVolume) {
clearInterval(interval);
isFading.value = false;
return;
}
// 增加音量
@@ -345,6 +448,7 @@ const songInOrOut = (type) => {
if (volume <= 0) {
clearInterval(interval);
$player.pause();
isFading.value = false;
return;
}
// 减小音量
@@ -362,7 +466,8 @@ const songPause = () => {
console.log("音乐暂停");
if (!$player.ended) music.setPlayState(false);
// 更改页面标题
window.document.title = "SPlayer";
// window.document.title = "SPlayer";
$setSiteTitle();
};
// 歌曲进度条更新
@@ -371,16 +476,16 @@ const songTimeSliderUpdate = (val) => {
$player.currentTime = (music.getPlaySongTime.duration / 100) * val;
};
// 进度条弹出提示
// const sliderTooltip = (val) => {
// if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
// return getSongPlayingTime((music.getPlaySongTime.duration / 100) * val);
// };
// 歌曲播放失败事件
const songError = () => {
// $message.error("播放失败,请重试");
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData.id);
console.error("歌曲播放失败");
$message.error("歌曲播放失败");
if (testNumber.value < 4) {
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData);
testNumber.value++;
} else {
$message.error("歌曲重试次数过多,请刷新后重试");
}
if (music.getPlayState) songInOrOut("play");
};
@@ -397,7 +502,7 @@ const volumeMute = () => {
onMounted(() => {
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData)
getPlaySongData(music.getPlaySongData.id);
getPlaySongData(music.getPlaySongData);
// 挂载播放器
window.$player = player.value;
// 恢复上次播放进度
@@ -413,7 +518,7 @@ watch(
() => music.getPlaySongData,
(val) => {
debounce(() => {
getPlaySongData(val.id);
getPlaySongData(val);
}, 500);
}
);
@@ -459,11 +564,10 @@ watch(
position: fixed;
bottom: -90px;
left: 0;
transition: 0.3s;
z-index: 2;
transition: all 0.3s;
z-index: 2004;
&.show {
bottom: 0;
transition: 0.5s;
}
.slider {
position: absolute;
@@ -498,27 +602,13 @@ watch(
}
}
:deep(.n-card__content) {
.all {
height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
@media (max-width: 620px) {
display: flex;
flex-direction: row;
justify-content: space-between;
.data {
.time {
display: none;
}
}
.control {
margin-left: auto;
.prev,
.next {
display: none;
}
}
}
max-width: 1400px;
margin: 0 auto;
.data {
display: flex;
flex-direction: row;
@@ -690,6 +780,12 @@ watch(
display: flex;
align-items: center;
justify-content: center;
&.open {
.n-icon {
background-color: $mainColor;
color: var(--n-color-embedded);
}
}
}
.volume {
display: flex;
@@ -706,6 +802,26 @@ watch(
}
}
}
@media (max-width: 620px) {
display: flex;
flex-direction: row;
justify-content: space-between;
.data {
.time {
display: none;
}
}
.control {
margin-left: auto;
.prev,
.next {
display: none;
}
.play-state {
margin: 0;
}
}
}
}
}
</style>

View File

@@ -72,9 +72,11 @@ watch(
watch(
() => osThemeRef.value,
(value) => {
value == "dark"
? setting.setSiteTheme("dark")
: setting.setSiteTheme("light");
if (setting.themeAuto) {
value == "dark"
? setting.setSiteTheme("dark")
: setting.setSiteTheme("light");
}
}
);
@@ -108,4 +110,4 @@ const NaiveProviderContent = defineComponent({
});
},
});
</script>
</script>

View File

@@ -2,9 +2,10 @@
<div class="searchInp">
<n-input
:class="inputActive ? 'input focus' : 'input'"
:input-props="{ autoComplete: false }"
round
clearable
placeholder="音乐/视频/电台/用户"
placeholder="搜索音乐/视频"
v-model:value="inputValue"
@focus="inputFocus"
@keydown="inputkeydown($event)"
@@ -12,41 +13,78 @@
>
<template #prefix>
<n-icon
size="20"
size="16"
:color="inputActive ? '#f55e55' : ''"
:component="SearchFilled"
:component="Search"
/>
</template>
</n-input>
<CollapseTransition easing="ease-in-out">
<n-card
class="list"
v-show="inputActive && !inputValue && searchData.hot[0]"
v-show="
inputActive &&
!inputValue &&
(music.getSearchHistory[0] || searchData.hot[0])
"
content-style="padding: 0"
>
<n-scrollbar>
<div
class="hot-item"
v-for="(item, index) in searchData.hot"
:key="item"
@click="toSearch(item.searchWord, 0)"
class="history-list"
v-if="music.getSearchHistory[0] && setting.searchHistory"
>
<div :class="index < 3 ? 'num hot' : 'num'">{{ index + 1 }}</div>
<div class="title">
<span class="name">
{{ item.searchWord }}
<!-- <img :src="item.iconUrl" alt="icon" /> -->
<n-tag
v-if="item.iconUrl"
class="tag"
round
:bordered="false"
size="small"
>
{{ item.iconType == 1 ? "HOT" : "UP" }}
</n-tag>
</span>
<n-text class="tip" depth="3" v-html="item.content" />
<div class="list-title">
<n-icon size="16" :component="History" />
<n-text>搜索历史</n-text>
</div>
<n-space>
<n-tag
v-for="item in music.getSearchHistory"
:key="item"
:bordered="false"
round
v-html="item"
@click="toSearch(item, 0)"
/>
</n-space>
<div class="del" @click="delHistory">
<n-icon size="16" :depth="3">
<DeleteFour theme="filled" />
</n-icon>
<n-text :depth="3">删除搜索历史</n-text>
</div>
</div>
<div class="hot-list" v-if="searchData.hot[0]">
<div class="list-title">
<n-icon size="16">
<Fire theme="filled" />
</n-icon>
<n-text>热搜榜</n-text>
</div>
<div
class="hot-item"
v-for="(item, index) in searchData.hot"
:key="item"
@click="toSearch(item.searchWord, 0)"
>
<div :class="index < 3 ? 'num hot' : 'num'">{{ index + 1 }}</div>
<div class="title">
<span class="name">
{{ item.searchWord }}
<!-- <img :src="item.iconUrl" alt="icon" /> -->
<n-tag
v-if="item.iconUrl"
class="tag"
round
:bordered="false"
size="small"
>
{{ item.iconType == 1 ? "HOT" : "UP" }}
</n-tag>
</span>
<n-text class="tip" depth="3" v-html="item.content" />
</div>
</div>
</div>
</n-scrollbar>
@@ -61,19 +99,21 @@
<n-scrollbar>
<div
class="suggest-tip"
v-if="JSON.stringify(searchData.suggest) == '{}'"
v-if="Object.keys(searchData.suggest).length === 0"
>
<n-icon size="22" :component="SearchOffFilled" />
<n-icon size="16" :component="Find" />
<span>暂无搜索结果</span>
</div>
<div class="suggest-all" v-else>
<div class="loading" v-show="!searchData.suggest.order">
<n-icon size="22" :component="ManageSearchFilled" />
<n-icon size="16" :component="Find" />
<span>努力搜索中</span>
</div>
<div class="suggest-item" v-if="searchData.suggest.songs">
<div class="type">
<n-icon size="18" :component="MusicNoteFilled" />
<n-icon size="18">
<MusicOne theme="filled" />
</n-icon>
<span class="name">单曲</span>
</div>
<span
@@ -81,12 +121,15 @@
v-for="songs in searchData.suggest.songs"
:key="songs"
@click="toSearch(songs.id, 1)"
>{{ songs.name }} - {{ songs.artists[0].name }}</span
>
{{ songs.name }} - {{ songs.artists[0].name }}</span
>
</div>
<div class="suggest-item" v-if="searchData.suggest.artists">
<div class="type">
<n-icon size="18" :component="MicFilled" />
<n-icon size="18">
<Voice theme="filled" />
</n-icon>
<span class="name">歌手</span>
</div>
<span
@@ -94,12 +137,14 @@
v-for="artists in searchData.suggest.artists"
:key="artists"
@click="toSearch(artists.id, 100)"
>{{ artists.name }}</span
>
v-html="artists.name"
/>
</div>
<div class="suggest-item" v-if="searchData.suggest.albums">
<div class="type">
<n-icon size="18" :component="AlbumSharp" />
<n-icon size="18">
<RecordDisc theme="filled" />
</n-icon>
<span class="name">专辑</span>
</div>
<span
@@ -107,12 +152,15 @@
v-for="albums in searchData.suggest.albums"
:key="albums"
@click="toSearch(albums.id, 10)"
>{{ albums.name }} - {{ albums.artist.name }}</span
>
{{ albums.name }} - {{ albums.artist.name }}
</span>
</div>
<div class="suggest-item" v-if="searchData.suggest.playlists">
<div class="type">
<n-icon size="18" :component="PlaylistPlayFilled" />
<n-icon size="18">
<Record theme="filled" />
</n-icon>
<span class="name">歌单</span>
</div>
<span
@@ -120,8 +168,9 @@
v-for="playlists in searchData.suggest.playlists"
:key="playlists"
@click="toSearch(playlists.id, 1000)"
>{{ playlists.name }}</span
>
{{ playlists.name }}
</span>
</div>
</div>
</n-scrollbar>
@@ -131,22 +180,25 @@
</template>
<script setup>
import { getSearchHot, getSearchSuggest } from "@/api";
import { getSearchHot, getSearchSuggest } from "@/api/search";
import {
SearchFilled,
MusicNoteFilled,
MicFilled,
AlbumSharp,
PlaylistPlayFilled,
SearchOffFilled,
ManageSearchFilled,
} from "@vicons/material";
Search,
MusicOne,
Voice,
RecordDisc,
Record,
Find,
Fire,
History,
DeleteFour,
} from "@icon-park/vue-next";
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import debounce from "@/utils/debounce";
import { useRouter } from "vue-router";
import { musicStore } from "@/store";
import { musicStore, settingStore } from "@/store";
const router = useRouter();
const music = musicStore();
const setting = settingStore();
// 输入框内容
const inputValue = ref(null);
@@ -187,7 +239,15 @@ const toSearch = (val, type) => {
case 0:
// 直接搜索
inputValue.value = val;
router.push(`/search/songs?keywords=${val}`);
// 写入搜索历史
music.setSearchHistory(inputValue.value.trim());
router.push({
path: "/search/songs",
query: {
keywords: val,
page: 1,
},
});
break;
case 1:
// 歌曲页
@@ -215,6 +275,8 @@ const inputkeydown = (e) => {
if (e.key === "Enter" && inputValue.value != null) {
console.log("执行搜索" + inputValue.value.trim());
inputActive.value = false;
// 写入搜索历史
music.setSearchHistory(inputValue.value.trim());
router.push({
path: "/search/songs",
query: {
@@ -224,6 +286,21 @@ const inputkeydown = (e) => {
}
};
// 删除搜索历史
const delHistory = () => {
$dialog.warning({
class: "s-dialog",
title: "删除历史",
content: "确认删除全部的搜索历史记录?",
positiveText: "删除",
negativeText: "取消",
onPositiveClick: () => {
music.setSearchHistory(null, true);
$message.success("删除成功");
},
});
};
onMounted(() => {
// 获取热搜
getSearchHotData();
@@ -251,6 +328,14 @@ watch(
}
}
);
// 监听播放列表显隐
watch(
() => music.showPlayList,
(val) => {
if (val) inputActive.value = false;
}
);
</script>
<style lang="scss" scoped>
@@ -262,11 +347,17 @@ watch(
.input {
width: 200px;
transition: all 0.3s;
@media (max-width: 450px) {
width: 40px;
}
&.focus {
width: 280px;
:deep(input) {
color: $mainColor;
}
@media (max-width: 450px) {
width: 60vw;
}
}
}
.list {
@@ -276,68 +367,135 @@ watch(
border-radius: 8px;
width: 280px;
z-index: 3;
@media (max-width: 450px) {
padding-top: 12px;
position: fixed;
width: 100%;
top: 58px;
right: 0;
left: 0;
border-radius: 0 0 8px 8px;
&::after {
position: absolute;
right: 0;
top: 0;
content: "收起";
padding: 4px 12px;
font-size: 12px;
background-color: #efefef;
border-radius: 0 0 0 14px;
}
}
:deep(.n-scrollbar) {
max-height: 80vh;
@media (max-width: 450px) {
max-height: calc(100vh - 130px);
min-height: calc(100vh - 130px);
}
.n-scrollbar-rail {
width: 0;
width: 4px;
}
.n-scrollbar-content {
padding: 12px;
.hot-item {
.list-title {
color: $mainColor;
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
border-radius: 8px;
padding: 6px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
.n-text {
margin-left: 4px;
font-size: 14px;
color: $mainColor;
line-height: 0;
}
&:hover {
background-color: var(--n-border-color);
}
.num {
width: 30px;
height: 30px;
min-width: 30px;
text-align: center;
line-height: 30px;
font-size: 16px;
font-weight: bold;
margin-right: 8px;
&.hot {
color: #ff5656;
}
}
.title {
display: flex;
flex-direction: column;
.name {
font-size: 16px;
display: flex;
flex-direction: row;
align-items: center;
img {
height: 16px;
width: auto;
margin-left: 6px;
margin-bottom: 2px;
}
.tag {
transform: translateY(-1px);
margin-left: 6px;
height: 18px;
color: $mainColor;
}
.history-list {
margin-bottom: 18px;
.n-space {
margin: 12px 0;
.n-tag {
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
border-color: $mainColor;
color: $mainColor;
}
&:active {
transform: scale(0.95);
}
}
.tip {
font-size: 12px;
}
.del {
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
cursor: pointer;
.n-icon {
margin-right: 4px;
}
}
}
.hot-list {
margin-top: 6px;
.hot-item {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
border-radius: 8px;
padding: 6px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
&:hover {
background-color: var(--n-border-color);
}
.num {
width: 30px;
height: 30px;
min-width: 30px;
text-align: center;
line-height: 30px;
font-size: 16px;
font-weight: bold;
margin-right: 8px;
&.hot {
color: #ff5656;
}
}
.title {
display: flex;
flex-direction: column;
.name {
font-size: 16px;
display: flex;
flex-direction: row;
align-items: center;
img {
height: 16px;
width: auto;
margin-left: 6px;
margin-bottom: 2px;
}
.tag {
transform: scale(0.9);
margin-left: 6px;
height: 18px;
color: $mainColor;
background-color: $mainSecondaryColor;
border-color: $mainColor;
}
}
.tip {
font-size: 12px;
}
}
}
}
@@ -346,6 +504,9 @@ watch(
flex-direction: row;
justify-content: center;
align-items: center;
.n-icon {
margin-right: 6px;
}
}
.suggest-all {
.loading {
@@ -354,7 +515,7 @@ watch(
justify-content: center;
align-items: center;
.n-icon {
margin-bottom: 4px;
margin-right: 6px;
}
}
.suggest-item {
@@ -391,4 +552,4 @@ watch(
}
}
}
</style>
</style>

View File

@@ -8,10 +8,6 @@ import router from "./router";
// 全局样式
import "@/style/global.scss";
// 字体文件
import "vfonts/Lato.css";
import "vfonts/FiraCode.css";
const app = createApp(App);
const pinia = createPinia();

View File

@@ -1,21 +1,11 @@
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes";
import { getLoginState } from "@/api";
import { getLoginState } from "@/api/login";
import { userStore } from "@/store";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return {
x: 0,
y: 0,
};
}
},
});
// 路由守卫
@@ -26,21 +16,30 @@ router.beforeEach((to, from, next) => {
if (to.meta.needLogin) {
getLoginState()
.then((res) => {
if (res.data.profile && user.userLogin) {
if (user.userLogin && !user.userData.level) user.setUserOtherData();
if (res.data?.profile && user.userLogin) {
user.setUserData(res.data.profile);
if (!Object.keys(user.getUserOtherData).length) {
user.setUserOtherData();
}
next();
} else {
$message.error("请登录账号后使用");
$message.error(
localStorage.getItem("cookie")
? "登录过期,请重新登录"
: "请登录账号后使用"
);
user.userLogOut();
next("/login");
}
})
.catch((err) => {
$message.error("遇到错误" + err);
$message.error("请求发生错误");
console.error("请求发生错误" + err);
next("/500");
return false;
});
} else {
if (user.userLogin && !user.userData.level) user.setUserOtherData();
if (!Object.keys(user.getUserOtherData).length) user.setUserOtherData();
next();
}
});

View File

@@ -84,22 +84,27 @@ const routes = [
children: [
{
path: "playlists",
name: "playlists",
name: "user-playlists",
component: () => import("@/views/User/playlists.vue"),
},
{
path: "like",
name: "like",
name: "user-like",
component: () => import("@/views/User/like.vue"),
},
{
path: "album",
name: "user-album",
component: () => import("@/views/User/album.vue"),
},
{
path: "artists",
name: "artists",
name: "user-artists",
component: () => import("@/views/User/artists.vue"),
},
{
path: "cloud",
name: "cloud",
name: "user-cloud",
component: () => import("@/views/User/cloud.vue"),
},
],
@@ -120,7 +125,25 @@ const routes = [
meta: {
title: "全局设置",
},
component: () => import("@/views/Setting/SettingView.vue"),
component: () => import("@/views/Setting/index.vue"),
redirect: "/setting/main",
children: [
{
path: "main",
name: "setting-main",
component: () => import("@/views/Setting/main.vue"),
},
{
path: "player",
name: "setting-player",
component: () => import("@/views/Setting/player.vue"),
},
{
path: "other",
name: "setting-other",
component: () => import("@/views/Setting/other.vue"),
},
],
},
// 登录页
{
@@ -204,6 +227,15 @@ const routes = [
},
],
},
// 歌手全部歌曲
{
path: "/all-songs",
name: "all-songs",
meta: {
title: "全部歌曲",
},
component: () => import("@/views/Artist/all-songs.vue"),
},
// 历史记录
{
path: "/history",
@@ -213,6 +245,15 @@ const routes = [
},
component: () => import("@/views/History/HistoryView.vue"),
},
// 全部新碟
{
path: "/new-album",
name: "new-album",
meta: {
title: "全部新碟",
},
component: () => import("@/views/NewAlbum/NewAlbumView.vue"),
},
// 状态页
// 404
{
@@ -223,10 +264,6 @@ const routes = [
},
component: () => import("@/views/State/404.vue"),
},
{
path: "/:pathMatch(.*)",
redirect: "/404",
},
// 403
{
path: "/403",
@@ -245,6 +282,10 @@ const routes = [
},
component: () => import("@/views/State/500.vue"),
},
{
path: "/:pathMatch(.*)",
redirect: "/404",
},
];
export default routes;

View File

@@ -1,4 +1,3 @@
import { defineStore } from "pinia";
import useSettingDataStore from "./settingData";
import useMusicDataStore from "./musicData";
import useUserDataStore from "./userData";

View File

@@ -1,20 +1,12 @@
import { defineStore } from "pinia";
import {
getSongTime,
formatNumber,
getSongPlayingTime,
} from "@/utils/timeTools.js";
import {
getPersonalFm,
setFmTrash,
getLikelist,
setLikeSong,
getUserPlaylist,
getPlayListCatlist,
getUserArtistlist,
} from "@/api";
import { userStore } from "@/store";
import lyricFormat from "@/utils/lyricFormat";
import { getSongTime, getSongPlayingTime } from "@/utils/timeTools.js";
import { getPersonalFm, setFmTrash } from "@/api/home";
import { getLikelist, setLikeSong } from "@/api/user";
import { getPlayListCatlist } from "@/api/playlist";
import { userStore, settingStore } from "@/store";
import { NIcon } from "naive-ui";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import parseLyric from "@/utils/parseLyric";
const useMusicDataStore = defineStore("musicData", {
state: () => {
@@ -27,25 +19,28 @@ const useMusicDataStore = defineStore("musicData", {
showPlayList: false,
// 播放状态
playState: false,
// 当前歌曲播放链接
playSongLink: null,
// 当前歌曲歌词数据
playSongLyric: {
lrc: [],
yrc: [],
hasTran: false,
hasTran: false,
hasYrc: false,
},
// 当前歌曲歌词播放索引
playSongLyricIndex: 0,
// 每日推荐
dailySongsData: [],
// 歌单分类
catList: {},
// 精品歌单分类
highqualityCatList: [],
// 用户歌单
userPlayLists: {
has: false,
own: [], // 创建歌单
like: [], // 收藏歌单
},
// 用户收藏歌手
userArtistLists: {
has: false,
list: [],
},
// 持久化数据
persistData: {
// 搜索历史
searchHistory: [],
// 是否处于私人 FM 模式
personalFmMode: false,
// 私人 FM 数据
@@ -61,12 +56,6 @@ const useMusicDataStore = defineStore("musicData", {
// 当前播放模式
// normal-顺序播放 random-随机播放 single-单曲循环
playSongMode: "normal",
// 当前歌曲歌词
playSongLyric: [],
// 当前歌曲歌词播放索引
playSongLyricIndex: 0,
// 当前歌曲播放链接
playSongLink: null,
// 当前播放时间
playSongTime: {
currentTime: 0,
@@ -81,8 +70,6 @@ const useMusicDataStore = defineStore("musicData", {
playVolumeMute: 0,
// 列表状态
playlistState: 0, // 0 顺序 1 单曲循环 2 随机
// 是否拥有翻译
playSongTransl: false,
// 播放历史
playHistory: [],
},
@@ -97,10 +84,6 @@ const useMusicDataStore = defineStore("musicData", {
getPersonalFmData(state) {
return state.persistData.personalFmData;
},
// 获取是否拥有翻译
getPlaySongTransl(state) {
return state.persistData.playSongTransl;
},
// 获取每日推荐
getDailySongs(state) {
return state.dailySongsData;
@@ -119,11 +102,11 @@ const useMusicDataStore = defineStore("musicData", {
},
// 获取当前歌词
getPlaySongLyric(state) {
return state.persistData.playSongLyric;
return state.playSongLyric;
},
// 获取当前歌词索引
getPlaySongLyricIndex(state) {
return state.persistData.playSongLyricIndex;
return state.playSongLyricIndex;
},
// 获取当前播放时间
getPlaySongTime(state) {
@@ -135,20 +118,12 @@ const useMusicDataStore = defineStore("musicData", {
},
// 获取播放链接
getPlaySongLink(state) {
return state.persistData.playSongLink;
return state.playSongLink;
},
// 获取喜欢音乐列表
getLikeList(state) {
return state.persistData.likeList;
},
// 获取用户歌单
getUserPlayLists(state) {
return state.userPlayLists;
},
// 获取收藏歌手
getUserArtistlists(state) {
return state.userArtistLists;
},
// 获取播放历史
getPlayHistory(state) {
return state.persistData.playHistory;
@@ -157,13 +132,17 @@ const useMusicDataStore = defineStore("musicData", {
getPlayListMode(state) {
return state.persistData.playListMode;
},
// 获取搜索历史
getSearchHistory(state) {
return state.persistData.searchHistory;
},
},
actions: {
// 更改是否处于私人FM模式
setPersonalFmMode(value) {
this.persistData.personalFmMode = value;
if (value) {
this.persistData.playSongLink = null;
this.playSongLink = null;
if (this.persistData.personalFmData.id) {
this.persistData.playlists = [];
this.persistData.playlists.push(this.persistData.personalFmData);
@@ -175,38 +154,50 @@ const useMusicDataStore = defineStore("musicData", {
},
// 当处于私人fm模式时更改歌单
setPersonalFmData() {
getPersonalFm().then((res) => {
if (res.data[0]) {
let data = res.data[0];
let fmData = {
id: data.id,
name: data.name,
artist: data.artists,
album: data.album,
alia: data.alias,
time: getSongTime(data.duration),
fee: data.fee,
pc: data.pc ? data.pc : null,
mv: data.mvid,
};
this.persistData.personalFmData = fmData;
if (this.persistData.personalFmMode) {
this.persistData.playlists = [];
this.persistData.playlists.push(fmData);
this.persistData.playSongIndex = 0;
try {
const songName = this.getPersonalFmData?.name;
getPersonalFm().then((res) => {
if (res.data[0]) {
const data = res.data[2] || res.data[0];
const fmData = {
id: data.id,
name: data.name,
artist: data.artists,
album: data.album,
alia: data.alias,
time: getSongTime(data.duration),
fee: data.fee,
pc: data.pc ? data.pc : null,
mv: data.mvid,
};
if (songName && songName == fmData.name) {
this.setFmDislike(fmData.id, false);
} else {
this.persistData.personalFmData = fmData;
if (this.persistData.personalFmMode) {
this.playSongLink = null;
this.persistData.playlists = [];
this.persistData.playlists.push(fmData);
this.persistData.playSongIndex = 0;
this.setPlayState(true);
}
}
} else {
$message.error("获取私人 FM 失败");
}
} else {
$message.error("获取私人 FM 失败");
}
});
});
} catch (err) {
console.error("获取私人 FM 失败:" + err);
$message.error("获取私人 FM 失败");
}
},
// 私人fm垃圾桶
setFmDislike(id) {
setFmDislike(id, tip = true) {
const user = userStore();
if (user.userLogin) {
setFmTrash(id).then((res) => {
if (res.code == 200) {
$message.success("已将该歌曲移除至垃圾桶");
if (tip) $message.success("已将该歌曲移除至垃圾桶");
this.persistData.personalFmMode = true;
this.setPlaySongIndex("next");
} else {
@@ -281,7 +272,7 @@ const useMusicDataStore = defineStore("musicData", {
},
// 更改歌曲播放链接
setPlaySongLink(value) {
this.persistData.playSongLink = value;
this.playSongLink = value;
},
// 更改播放列表模式
setPlayListMode(value) {
@@ -291,7 +282,7 @@ const useMusicDataStore = defineStore("musicData", {
// 添加歌单至播放列表
setPlaylists(value) {
this.persistData.playlists = value;
// $message.success(`已添加${value.length}首歌曲至播放列表`);
console.log(`已添加${value.length}首歌曲至播放列表`);
},
// 更改每日推荐数据
setDailySongs(value) {
@@ -307,7 +298,7 @@ const useMusicDataStore = defineStore("musicData", {
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv,
mv: v.mv ? v.mv : null,
});
});
} else {
@@ -317,26 +308,15 @@ const useMusicDataStore = defineStore("musicData", {
// 歌词处理
setPlaySongLyric(value) {
if (value.lrc) {
this.persistData.playSongLyric = lyricFormat(value.lrc.lyric);
if (value.tlyric && value.tlyric.lyric) {
console.log("歌词有翻译");
this.persistData.playSongTransl = true;
let playSongLyric = this.persistData.playSongLyric;
let playSongLyricFy = lyricFormat(value.tlyric.lyric);
playSongLyric.forEach((v) => {
playSongLyricFy.forEach((x) => {
if (v.time === x.time) {
v.lyricFy = x.lyric;
}
});
});
this.persistData.playSongLyric = playSongLyric;
} else {
this.persistData.playSongTransl = false;
try {
this.playSongLyric = parseLyric(value);
} catch (err) {
$message.error("歌词处理出错");
console.error("歌词处理出错:" + err);
}
} else {
console.log("无歌词");
this.persistData.playSongLyric = [];
console.log("该歌曲暂无歌词");
this.playSongLyric = [];
}
},
// 歌曲播放进度
@@ -357,36 +337,35 @@ const useMusicDataStore = defineStore("musicData", {
);
}
// 计算当前歌词播放索引
this.persistData.playSongLyricIndex;
let index = this.persistData.playSongLyric.findIndex(
(v) => v.time > value.currentTime
);
if (index === -1) {
// 如果没有找到合适的歌词,则返回最后一句歌词
this.persistData.playSongLyricIndex =
this.persistData.playSongLyric.length - 1;
} else {
this.persistData.playSongLyricIndex = (index ? index : index + 1) - 1;
}
const setting = settingStore();
const lrcType = !this.playSongLyric.hasYrc || !setting.showYrc;
const lyrics = lrcType ? this.playSongLyric.lrc : this.playSongLyric.yrc;
const index = lyrics.findIndex((v) => v.time >= value.currentTime);
this.playSongLyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
},
// 设置当前播放模式
setPlaySongMode() {
if (this.persistData.playSongMode === "normal") {
this.persistData.playSongMode = "random";
$message.info("随机播放");
$message.info("随机播放", {
icon: () => h(NIcon, null, { default: () => h(ShuffleOne) }),
});
} else if (this.persistData.playSongMode === "random") {
this.persistData.playSongMode = "single";
$message.info("单曲循环");
$message.info("单曲循环", {
icon: () => h(NIcon, null, { default: () => h(PlayOnce) }),
});
} else {
this.persistData.playSongMode = "normal";
$message.info("循环播放");
$message.info("列表循环", {
icon: () => h(NIcon, null, { default: () => h(PlayCycle) }),
});
}
},
// 上下曲调整
setPlaySongIndex(type) {
if (!$player.ended) $player.pause();
this.playState = false;
if (this.persistData.personalFmMode) {
this.persistData.playSongLink = null;
this.setPersonalFmData();
} else {
let listLength = this.persistData.playlists.length;
@@ -410,10 +389,10 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongIndex = 0;
}
if (listMode !== "single") {
this.persistData.playSongLink = null;
this.playSongLink = null;
}
this.playState = true;
}
this.playState = true;
},
// 添加歌曲至播放列表
addSongToPlaylists(value, play = true) {
@@ -424,13 +403,13 @@ const useMusicDataStore = defineStore("musicData", {
try {
if (
value.id !==
this.persistData.playlists[this.persistData.playSongIndex].id
this.persistData.playlists[this.persistData.playSongIndex]?.id
) {
console.log("播放歌曲与上一次不一致");
this.persistData.playSongLink = null;
this.playSongLink = null;
}
} catch (error) {
console.error("出现致命错误:" + error);
console.error("出现错误:" + error);
}
if (index !== -1) {
this.persistData.playSongIndex = index;
@@ -493,77 +472,6 @@ const useMusicDataStore = defineStore("musicData", {
});
}
},
// 更改用户歌单
setUserPlayLists() {
const user = userStore();
if (user.userLogin) {
getUserPlaylist(
user.getUserData.userId,
user.getUserData.subcount.createdPlaylistCount +
user.getUserData.subcount.subPlaylistCount
).then((res) => {
if (res.playlist) {
this.userPlayLists = {
own: [],
like: [],
};
this.userPlayLists.has = true;
res.playlist.forEach((v) => {
if (v.creator.userId === user.getUserData.userId) {
this.userPlayLists.own.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
desc: v.description,
tags: v.tags,
playCount: formatNumber(v.playCount),
trackCount: v.trackCount,
});
} else {
this.userPlayLists.like.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
playCount: formatNumber(v.playCount),
});
}
});
} else {
$message.error("用户歌单为空");
}
});
} else {
$message.error("请登录账号后使用");
}
},
// 更改用户收藏歌手
setUserArtistLists() {
const user = userStore();
if (user.userLogin) {
getUserArtistlist().then((res) => {
if (res.data) {
this.userArtistLists = {
list: [],
};
this.userArtistLists.has = true;
res.data.forEach((v) => {
this.userArtistLists.list.push({
id: v.id,
name: v.name,
cover: v.img1v1Url,
size: v.musicSize,
});
});
} else {
$message.error("用户收藏歌手为空");
}
});
} else {
$message.error("请登录账号后使用");
}
},
// 更改播放历史
setPlayHistory(data, clean = false) {
if (clean) {
@@ -581,6 +489,21 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playHistory.unshift(data);
}
},
// 更改搜索历史
setSearchHistory(name, clean = false) {
if (clean) {
this.persistData.searchHistory = [];
} else {
const index = this.persistData.searchHistory.indexOf(name);
if (index !== -1) {
this.persistData.searchHistory.splice(index, 1);
}
this.persistData.searchHistory.unshift(name);
if (this.persistData.searchHistory.length > 30) {
this.persistData.searchHistory.pop();
}
}
},
},
// 开启数据持久化
persist: [

View File

@@ -7,14 +7,21 @@ const useSettingDataStore = defineStore("settingData", {
return {
// 全局主题
theme: "light",
themeAuto: true,
// 搜索历史
searchHistory: true,
// 轮播图显示
bannerShow: true,
// 自动签到
autoSignIn: true,
// 列表点击方式
listClickMode: "dblclick",
// 播放器样式
playerStyle: "cover",
// 底栏歌词显示
bottomLyricShow: true,
// 是否显示逐字歌词
showYrc: true,
// 是否显示歌词翻译
showTransl: true,
// 歌曲音质
@@ -22,11 +29,17 @@ const useSettingDataStore = defineStore("settingData", {
// 歌词位置
lyricsPosition: "left",
// 歌词滚动位置
lyricsBlock: "center",
lyricsBlock: "start",
// 歌词大小
lyricsFontSize: 2.4,
lyricsFontSize: 3.6,
// 歌词模糊
lyricsBlur: false,
// 音乐频谱
musicFrequency: false,
// 鼠标移入歌词区域暂停滚动
lrcMousePause: true,
// 是否使用网易云解灰
useUnmServer: true,
};
},
getters: {

View File

@@ -1,15 +1,46 @@
import { defineStore } from "pinia";
import { userLogOut, getUserLevel, getUserSubcount } from "@/api";
import {
userLogOut,
getUserLevel,
getUserSubcount,
getUserPlaylist,
getUserArtistlist,
getUserAlbum,
} from "@/api/user";
import { formatNumber, getLongTime } from "@/utils/timeTools.js";
const useUserDataStore = defineStore("userData", {
state: () => {
return {
// 站点标题
siteTitle: "SPlayer",
// 用户登录状态
userLogin: false,
// 用户 cookie
cookie: null,
// 用户数据
// 用户基础数据
userData: {},
// 用户详情数据
userOtherData: {},
// 用户歌单
userPlayLists: {
isLoading: false,
has: false,
own: [], // 创建歌单
like: [], // 收藏歌单
},
// 用户专辑
userAlbum: {
isLoading: false,
has: false,
list: [],
},
// 用户收藏歌手
userArtistLists: {
isLoading: false,
has: false,
list: [],
},
};
},
getters: {
@@ -17,12 +48,32 @@ const useUserDataStore = defineStore("userData", {
getCookie(state) {
return state.cookie;
},
// 获取用户数据
// 获取用户基础数据
getUserData(state) {
return state.userData;
},
// 获取用户详情数据
getUserOtherData(state) {
return state.userOtherData;
},
// 获取用户歌单
getUserPlayLists(state) {
return state.userPlayLists;
},
// 获取用户收藏歌手
getUserArtistLists(state) {
return state.userArtistLists;
},
// 获取用户收藏专辑
getUserAlbumLists(state) {
return state.userAlbum;
},
},
actions: {
// 更改站点标题
setSiteTitle(value) {
this.siteTitle = value;
},
// 更改 cookie
setCookie(value) {
window.localStorage.setItem("cookie", value);
@@ -31,24 +82,21 @@ const useUserDataStore = defineStore("userData", {
// 更改用户数据
setUserData(value) {
this.userData = value;
if (!this.userData.level) this.setUserOtherData();
},
// 更改用户等级信息
setUserOtherData() {
if (this.userLogin) {
getUserLevel()
const getOtherData = [getUserLevel(), getUserSubcount()];
Promise.all(getOtherData)
.then((res) => {
this.userData.level = res.data;
console.log(res);
this.userOtherData.level = res[0].data;
this.userOtherData.subcount = res[1];
this.setUserPlayLists();
})
.catch((err) => {
$message.error("获取用户等级出错 " + err);
});
getUserSubcount()
.then((res) => {
this.userData.subcount = res;
})
.catch((err) => {
$message.error("获取用户详情失败 " + err);
console.error("获取用户详情失败:" + err);
$message.error("获取用户详情失败,请刷新后重试");
});
}
},
@@ -57,14 +105,140 @@ const useUserDataStore = defineStore("userData", {
this.userLogin = false;
this.cookie = null;
this.userData = {};
window.localStorage.removeItem("cookie");
this.userOtherData = {};
localStorage.removeItem("cookie");
userLogOut();
},
// 更改用户歌单
async setUserPlayLists() {
if (this.userLogin) {
try {
if (!Object.keys(this.userOtherData).length) {
this.setUserOtherData();
} else {
this.userPlayLists.isLoading = true;
const { userId } = this.userData;
const { subcount } = this.userOtherData;
const number =
subcount.createdPlaylistCount + subcount.subPlaylistCount;
const res = await getUserPlaylist(userId, number);
if (res.playlist) {
this.userPlayLists = {
has: true,
own: [],
like: [],
};
res.playlist.forEach((v) => {
if (v.creator.userId === this.getUserData.userId) {
this.userPlayLists.own.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
desc: v.description,
tags: v.tags,
playCount: formatNumber(v.playCount),
trackCount: v.trackCount,
});
} else {
this.userPlayLists.like.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
playCount: formatNumber(v.playCount),
});
}
});
this.userPlayLists.isLoading = false;
} else {
this.userPlayLists.isLoading = false;
$message.error("用户歌单为空");
}
}
} catch (err) {
this.userPlayLists.isLoading = false;
console.error("获取用户歌单时出现错误:" + err);
$message.error("获取用户歌单时出现错误,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}
},
// 更改用户收藏歌手
async setUserArtistLists(callback) {
if (this.userLogin) {
try {
this.userArtistLists.isLoading = true;
const res = await getUserArtistlist();
if (res.data) {
this.userArtistLists.list = [];
this.userArtistLists.has = true;
res.data.forEach((v) => {
this.userArtistLists.list.push({
id: v.id,
name: v.name,
cover: v.img1v1Url,
size: v.musicSize,
});
});
if (typeof callback === "function") {
callback();
}
this.userArtistLists.isLoading = false;
} else {
this.userArtistLists.isLoading = false;
$message.error("用户收藏歌手为空");
}
} catch (err) {
this.userArtistLists.isLoading = false;
console.error("用户收藏歌手获取失败:" + err);
$message.error("用户收藏歌手获取失败,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}
},
// 更改用户收藏专辑
async setUserAlbumLists() {
if (this.userLogin) {
try {
let offset = 0;
let totalCount = null;
this.userAlbum.isLoading = true;
this.userAlbum.list = [];
while (totalCount === null || offset < totalCount) {
const res = await getUserAlbum(30, offset);
res.data.forEach((v) => {
this.userAlbum.list.push({
id: v.id,
cover: v.picUrl,
name: v.name,
artist: v.artists,
time: getLongTime(v.subTime),
});
});
totalCount = res.count;
offset += 30;
console.log(totalCount, offset, this.userAlbum.list);
}
this.userAlbum.isLoading = false;
this.userAlbum.has = true;
} catch (err) {
this.userAlbum.isLoading = false;
console.error("用户收藏专辑获取失败:" + err);
$message.error("用户收藏专辑获取失败,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}
},
},
// 开启数据持久化
persist: [
{
storage: localStorage,
paths: ["userLogin", "cookie", "userData", "userOtherData"],
},
],
});

View File

@@ -9,28 +9,78 @@
html,
body,
#app {
-webkit-font-smoothing: subpixel-antialiased;
font-family: "HarmonyOS_Regular", sans-serif;
-webkit-font-smoothing: antialiased;
font-family: "HarmonyOS_Regular", sans-serif !important;
}
// Dialog
.s-dialog {
border-radius: 8px;
.n-dialog__close {
margin: 16px 26px 0 0;
}
.n-dialog__title {
transform: translateX(-8px);
font-size: 16px;
font-weight: bold;
font-size: 18px;
// font-weight: bold;
i {
font-size: 24px;
transform: translateY(-1px);
// transform: translateY(-1px);
}
}
}
// Modal
.s-modal {
width: 60vw;
max-width: 700px;
min-width: min(24rem, 100vw);
border-radius: 8px;
&.close {
.n-card-header {
.n-base-close {
display: none;
}
}
}
.n-card-header {
.n-card-header__main {
font-size: 18px;
}
}
.n-card__content {
padding-right: 28px;
}
.n-scrollbar {
max-height: 60vh;
font-size: 16px;
line-height: 32px;
}
}
.n-modal-container {
z-index: 2006 !important;
.n-modal-body-wrapper {
.n-modal-mask {
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
}
}
}
// Nscrollbar
.n-scrollbar {
.n-scrollbar-rail {
right: 0 !important;
}
}
// Dropdown
.n-dropdown-menu {
border-radius: 8px !important;
}
// Tab
.main-tab {
.n-tabs-tab {
@@ -41,6 +91,28 @@ body,
}
}
// Notification
.n-notification {
border-radius: 8px !important;
}
// Nalert
.n-alert {
border-radius: 8px !important;
}
// Drawer
.n-drawer-container {
.n-drawer {
border-radius: 8px 0 0 8px;
transition: all 0.3s;
@media (max-width: 450px) {
width: 100% !important;
border-radius: 0;
}
}
}
// 文本超出隐藏
.text-hidden {
display: -webkit-box !important;

View File

@@ -5,7 +5,7 @@
*/
const lyricFormat = (lrc) => {
// 匹配时间轴和歌词文本的正则表达式
const regex = /^\[(.*?)\]\s*(.+?)\s*$/;
const regex = /^\[([^\]]+)\]\s*(.+?)\s*$/;
// 将歌词字符串按行分割为数组
const lines = lrc.split("\n");
// 对每一行进行转换
@@ -15,11 +15,13 @@ const lyricFormat = (lrc) => {
// 转换时间轴和歌词文本为对象
.map((line) => {
const [, time, text] = line.match(regex);
const seconds = time
.split(":")
.reduce((acc, cur) => acc * 60 + parseFloat(cur), 0);
return { time: seconds, lyric: text };
});
const parts = time.split(":");
let seconds = parseInt(parts[0]) * 60 + parseFloat(parts[1]);
if (parts.length > 2) {
seconds += parseFloat(parts[2]) / 1000;
}
return { time: seconds, lyric: text.trim() };
}).filter((element) => element.lyric.trim() !== "");
// 检查是否为纯音乐,是则返回空数组
if (result.length && /纯音乐,请欣赏/.test(result[0].lyric)) {
console.log("该歌曲为纯音乐");
@@ -28,4 +30,5 @@ const lyricFormat = (lrc) => {
return result;
};
export default lyricFormat;

193
src/utils/parseLyric.js Normal file
View File

@@ -0,0 +1,193 @@
/**
* 将接口数据解析出对应数据
* @param {string} data 接口数据
* @returns {Array} 对应数据
*/
const parseLyric = (data) => {
// 初始化数据
const { lrc, tlyric, romalrc, yrc, ytlrc } = data;
const lyrics = lrc ? lrc.lyric : null;
const otherLyrics = {
tran: tlyric ? tlyric.lyric : null,
roma: romalrc ? romalrc.lyric : null,
yrc: yrc ? yrc.lyric : null,
ytlrc: ytlrc ? ytlrc.lyric : null,
};
// 初始化输出结果
let result = {
lrc: [], // 歌词数组 {time:时间,content:歌词}
yrc: [], // 逐字歌词数据
// 是否具有翻译
hasTran: tlyric ? (tlyric.lyric ? true : false) : false,
// 是否具有音译
hasRoma: romalrc ? (romalrc.lyric ? true : false) : false,
// 是否具有逐字歌词
hasYrc: yrc ? (yrc.lyric ? true : false) : false,
};
// 普通歌词数据
let lrcData = Lrcsplit(lyrics);
// 翻译歌词数据
let tranLrcData = null;
// 循环遍历 otherLyrics 参数对象
for (let i in otherLyrics) {
const element = otherLyrics[i];
if (element !== null) {
// 若存在逐字歌词
if (i == "yrc" && otherLyrics[i] != null) {
result[i] = parseYrc(otherLyrics[i]);
continue;
}
// 若存在翻译
if (i == "ytlrc" && element != null) {
tranLrcData = Lrcsplit(element);
for (let num in tranLrcData) {
// 翻译文本对齐
let objNum = result["yrc"].findIndex(
(o) => o.time == tranLrcData[num].time
);
if (objNum != -1)
result["yrc"][objNum]["tran"] = tranLrcData[num].content;
}
}
// 若存在其他翻译
tranLrcData = Lrcsplit(element);
if (tranLrcData[0]) {
console.log(`歌曲存在 ${i} 翻译`, tranLrcData);
for (let num in tranLrcData) {
// 翻译文本对齐
let objNum = lrcData.findIndex(
(o) => o.time == tranLrcData[num].time
);
if (objNum != -1) lrcData[objNum][i] = tranLrcData[num].content;
}
}
}
}
// 将歌词按时间排序
result.lrc = lrcData.sort((a, b) => {
return a.t - b.t;
});
return result;
};
/**
* 将歌词字符串解析为歌词对象数组
* @param {string} lrc 歌词字符串
* @returns {Array} 歌词对象数组
*/
const Lrcsplit = (lrc) => {
const lyrics = lrc.split("\n");
const lrcData = [];
lyrics.forEach((lyric) => {
lyric = lyric.replace(/(^\s*)|(\s*$)/g, "");
const time = lyric.substring(lyric.indexOf("[") + 1, lyric.indexOf("]"));
const timeArr = time.split(":");
if (isNaN(parseInt(timeArr[0]))) {
for (let i in lyrics) {
if (i != "lrc" && i == timeArr[0].toLowerCase()) {
lyrics[i] = timeArr[1];
}
}
} else {
const lyricArr = lyric.match(/\[(\d+:.+?)\]/g);
let start = 0;
for (let k in lyricArr) {
start += lyricArr[k].length;
}
const content = lyric.substring(start);
if (content == "") {
return false;
}
lyricArr.forEach((t) => {
let time = t.substring(1, t.length - 1);
let second = time.split(":");
if (
(parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3) == 0
) {
return;
}
lrcData.push({
time: (parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3),
content: content,
});
});
}
});
return lrcData;
};
/**
* 逐字歌词解析
* @param {string} lyrics 歌词字符串
* @returns {Array} 歌词对象数组
*/
const parseYrc = (lyrics) => {
// 若无内容,则返回空数组
if (lyrics == undefined) {
return [];
}
let lines = lyrics.split("\n");
let parsedLyrics = [];
// 解析每一句
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 创建一个空对象,用于存放当前句的信息
let parsedLine = {
time: undefined, // 开始时间
endTime: undefined, // 结束时间
content: undefined, // 歌词内容
};
// 分离出时间信息,并转换为秒
let timeInfo = line
.substring(line.indexOf("[") + 1, line.indexOf("]"))
.split(",");
// 将开始时间转换为秒
parsedLine.time = Number(timeInfo[0]) / 1000;
// 将结束时间转换为秒
parsedLine.endTime = Number(timeInfo[1]) / 1000;
// 若时间信息不合法,将跳过该句
if (isNaN(parsedLine.time) || isNaN(parsedLine.endTime)) {
continue;
}
// 寻找歌词内容
const lyricArr = line.match(/\[[1-9]\d*,[1-9]\d*]/g);
if (!lyricArr) {
continue;
}
// 去除时间信息,获取歌词内容
let contentArray = [];
// 分离成单个字或词,并解析时间信息
let splitContent = line.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g);
for (let j = 0; j < splitContent.length; j++) {
const splitc = splitContent[j];
if (splitc == "") {
continue;
}
// 创建一个对象,用于存放当前字或词的信息,并添加到当前句的歌词内容中
let contentObj = {
time: undefined, // 开始时间
duration: undefined, // 持续时间
content: "", // 字或词的文本内容
};
// 提取时间和文本信息,并转换为秒
let time = splitc.match(/\([1-9]\d*,[1-9]\d*,\d*\)/);
if (!time) {
continue;
}
let timeArray = time[0].slice(1, -1).split(",");
// 将开始时间转换为秒
contentObj.time = Number(timeArray[0]) / 1000;
// 将持续时间转换为秒
contentObj.duration = Number(timeArray[1]) / 1000;
// 获取字或词的文本内容
contentObj.content = splitc.slice(time[0].length);
contentArray.push(contentObj);
}
parsedLine.content = contentArray;
parsedLyrics.push(parsedLine);
}
return parsedLyrics;
};
export default parseLyric;

View File

@@ -19,14 +19,12 @@ axios.defaults.withCredentials = true;
// 请求拦截
axios.interceptors.request.use(
(request) => {
if (request.loadingBar != "Hidden") $loadingBar.start();
// const token = localStorage.getItem("cookie");
// token && (request.headers.Authorization = token);
if (!request.hiddenBar) $loadingBar.start();
return request;
},
(error) => {
$loadingBar.error();
$message.error("请求失败,请稍后重试");
console.error("请求失败,请稍后重试");
return Promise.reject(error);
}
);
@@ -43,23 +41,23 @@ axios.interceptors.response.use(
const data = error.response.data;
switch (error.response.status) {
case 401:
$message.error("您未登录");
console.error("您未登录");
break;
case 301:
$message.error("请求发生重定向");
console.error("请求发生重定向");
break;
case 404:
$message.error("请求资源不存在");
console.error("请求资源不存在");
break;
case 500:
$message.error("内部服务器错误");
console.error("内部服务器错误");
break;
default:
$message.error(data.message ? data.message : "请求失败,请稍后重试");
console.error(data.message ? data.message : "请求失败,请稍后重试");
break;
}
} else {
$message.error("请求失败,请稍后重试");
console.error("请求失败,请稍后重试");
}
return Promise.reject(error);
}

View File

@@ -7,7 +7,7 @@
:src="
albumDetail.picUrl
? albumDetail.picUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
@@ -29,14 +29,14 @@
全部简介
</n-button>
<n-modal
class="s-modal"
v-model:show="albumDescShow"
preset="card"
style="width: 60vw; min-width: min(24rem, 100vw)"
title="歌单简介"
:bordered="false"
>
<n-scrollbar style="max-height: 60vh">
{{ albumDetail.description }}
<n-scrollbar>
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div>
@@ -55,7 +55,12 @@
<div class="right">
<div class="meta">
<span class="name">{{ albumDetail.name }}</span>
<span class="creator">{{ albumDetail.artist.name }}</span>
<span
class="creator"
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
>
{{ albumDetail.artist.name }}
</span>
<div class="time">
<div class="createTime">
<span class="num">发行时间</span>
@@ -71,7 +76,7 @@
</div>
</div>
<div class="title" v-else-if="!albumId">
<span class="key">未提供所需数据</span>
<span class="key">参数不完整</span>
<br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级
@@ -91,7 +96,7 @@
</template>
<script setup>
import { getAlbum } from "@/api";
import { getAlbum } from "@/api/album";
import { useRouter } from "vue-router";
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
import DataLists from "@/components/DataList/DataLists.vue";
@@ -109,6 +114,7 @@ const getAlbumData = (id) => {
console.log(res);
// 专辑信息
albumDetail.value = res.album;
$setSiteTitle(res.album.name + " - 专辑");
// 专辑歌曲
if (res.songs) {
albumData.value = [];
@@ -123,6 +129,7 @@ const getAlbumData = (id) => {
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
} else {
@@ -154,25 +161,6 @@ watch(
.loading {
display: flex;
flex-direction: row;
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static !important;
width: 60vw !important;
max-width: none !important;
.intr {
display: none;
}
}
.right {
.meta {
.name {
font-size: 26px !important;
}
}
}
}
.left {
width: 40vw;
height: 100%;
@@ -245,13 +233,19 @@ watch(
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 1;
color: $mainColor;
}
}
.time {
margin-top: 8px;
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 370px) {
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
}
@@ -274,6 +268,25 @@ watch(
}
}
}
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static;
width: 60vw;
max-width: none;
.intr {
display: none;
}
}
.right {
.meta {
.name {
font-size: 26px;
}
}
}
}
}
.title {
margin-top: 30px;

View File

@@ -11,7 +11,7 @@
</template>
<script setup>
import { getArtistAblums } from "@/api";
import { getArtistAblums } from "@/api/artist";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -90,7 +90,7 @@ watch(
() => router.currentRoute.value,
(val) => {
artistId.value = val.query.id;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "ar-albums") {
getArtistAblumsData(
artistId.value,

View File

@@ -0,0 +1,151 @@
<template>
<div class="all-songs">
<div class="title" v-if="artistId">
<span class="key">{{ artistName ? artistName : "未知歌手" }}</span>
<span>全部歌曲</span>
</div>
<div class="title" v-else>
<span class="key">未提供所需信息</span>
<br />
<n-button
strong
secondary
@click="router.go(-1)"
style="margin-top: 20px"
>
返回上一级
</n-button>
</div>
<div class="songs" v-if="artistId">
<DataLists :listData="artistData" />
<Pagination
v-if="artistData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
/>
</div>
</div>
</template>
<script setup>
import { getArtistAllSongs } from "@/api/artist";
import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
// 歌手信息
const artistId = ref(router.currentRoute.value.query.id);
const artistData = ref([]);
const artistName = ref(null);
const totalCount = ref(0);
const pagelimit = ref(30);
const pageNumber = ref(
router.currentRoute.value.query.page
? Number(router.currentRoute.value.query.page)
: 1
);
// 获取歌手信息
const getArtistAllSongsData = (id, limit = 30, offset = 0, order = "hot") => {
getArtistAllSongs(id, limit, offset, order).then((res) => {
console.log(res);
if (res.songs[0]) {
// 数据总数
totalCount.value = res.total;
// 歌手名称
artistName.value = res.songs[0].ar[0].name;
// 列表数据
const ids = res.songs.map((obj) => obj.id);
getMusicDetail(ids.join(",")).then((res) => {
console.log(res);
artistData.value = [];
res.songs.forEach((v, i) => {
artistData.value.push({
id: v.id,
num: i + 1 + (pageNumber.value - 1) * pagelimit.value,
name: v.name,
artist: v.ar,
album: v.al,
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
});
} else {
$message.error("歌手全部歌曲为空");
}
// 请求后回顶并结束加载条
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
});
};
// 监听路由参数变化
watch(
() => router.currentRoute.value,
(val) => {
artistId.value = val.query.id;
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "all-songs") {
getArtistAllSongsData(
artistId.value,
pagelimit.value,
pageNumber.value ? (pageNumber.value - 1) * pagelimit.value : 0
);
}
}
);
// 每页个数数据变化
const pageSizeChange = (val) => {
console.log(val);
pagelimit.value = val;
getSearchDataList(
artistId.value,
val,
(pageNumber.value - 1) * pagelimit.value
);
};
// 当前页数数据变化
const pageNumberChange = (val) => {
router.push({
path: "/all-songs",
query: {
id: artistId.value,
page: val,
},
});
};
onMounted(() => {
getArtistAllSongsData(
artistId.value,
pagelimit.value,
(pageNumber.value - 1) * pagelimit.value
);
});
</script>
<style lang="scss" scoped>
.all-songs {
.title {
margin-top: 30px;
margin-bottom: 20px;
font-size: 24px;
.key {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="artist">
<div class="artistData" v-if="artistData">
<div class="artistData" v-if="artistId && artistData">
<div class="cover">
<n-avatar
round
@@ -10,47 +10,84 @@
/>
</div>
<div class="data">
<span class="name">{{ artistData.name }}</span>
<span class="occupation">{{ artistData.occupation }}</span>
<n-text class="name">{{ artistData.name }}</n-text>
<n-text class="occupation" :depth="3">
{{ artistData.occupation }}
</n-text>
<div class="num">
<span class="musicSize" @click="tabChange('songs')">
<n-text class="musicSize" @click="tabChange('songs')">
<n-icon :component="MusicNoteFilled" />
{{ artistData.musicSize }} 首歌
</span>
<span class="albumSize" @click="tabChange('albums')">
</n-text>
<n-text class="albumSize" @click="tabChange('albums')">
<n-icon :component="AlbumFilled" />
{{ artistData.albumSize }} 张专辑
</span>
<span class="mvSize" @click="tabChange('videos')">
</n-text>
<n-text class="mvSize" @click="tabChange('videos')">
<n-icon :component="VideocamRound" />
{{ artistData.mvSize }} MV
</span>
</n-text>
</div>
<span class="desc text-hidden" @click="artistDescShow = true">
<n-text class="desc text-hidden" @click="artistDescShow = true">
{{ artistData.desc }}
</span>
</n-text>
<n-space class="button" v-if="user.userLogin">
<!-- <n-button type="primary" strong secondary>
<template #icon>
<n-icon :component="PlayArrowRound" />
</template>
播放热门歌曲
</n-button> -->
<n-button
:type="artistLikeBtn ? 'primary' : 'default'"
strong
secondary
@click="toLikeArtist(artistData)"
>
<template #icon>
<n-icon
:component="
artistLikeBtn ? PersonAddAlt1Round : PersonRemoveAlt1Round
"
/>
</template>
{{ artistLikeBtn ? "收藏歌手" : "取消收藏歌手" }}
</n-button>
</n-space>
<!-- 歌手介绍 -->
<n-modal
class="s-modal"
v-model:show="artistDescShow"
preset="card"
style="width: 60vw; min-width: min(24rem, 100vw)"
title="歌手介绍"
:bordered="false"
>
<n-scrollbar
style="max-height: 60vh"
v-html="artistData.desc.replace(/\n/g, '<br>')"
/>
<n-scrollbar>
<n-text v-html="artistData.desc.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div>
</div>
<div class="error" v-else-if="!artistId">
<n-text>参数不完整</n-text>
<br />
<n-button
strong
secondary
@click="router.go(-1)"
style="margin-top: 20px"
>
返回上一级
</n-button>
</div>
<n-tabs
class="main-tab"
type="segment"
@update:value="tabChange"
v-model:value="tabValue"
v-if="artistId"
v-if="artistData"
>
<n-tab name="songs"> 单曲 </n-tab>
<n-tab name="songs"> 热门单曲 </n-tab>
<n-tab name="albums"> 专辑 </n-tab>
<n-tab name="videos"> MV </n-tab>
</n-tabs>
@@ -71,14 +108,24 @@
<script setup>
import { useRouter } from "vue-router";
import { getArtistDetail } from "@/api";
import { MusicNoteFilled, AlbumFilled, VideocamRound } from "@vicons/material";
import { userStore } from "@/store";
import { getArtistDetail, likeArtist } from "@/api/artist";
import {
MusicNoteFilled,
AlbumFilled,
VideocamRound,
PersonAddAlt1Round,
PersonRemoveAlt1Round,
} from "@vicons/material";
const router = useRouter();
const user = userStore();
// 歌手数据
const artistId = ref(router.currentRoute.value.query.id);
const artistData = ref(null);
const artistDescShow = ref(false);
const artistLikeBtn = ref(false);
// Tab 默认选中
const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
@@ -86,20 +133,30 @@ const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
// 获取歌手数据
const getArtistDetailData = (id) => {
if (id) {
getArtistDetail(id).then((res) => {
console.log(res);
artistData.value = {
name: res.data.artist.name,
occupation: res.data.identify ? res.data.identify.imageDesc : null,
cover: res.data.artist.cover,
desc: res.data.artist.briefDesc,
albumSize: res.data.artist.albumSize,
musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize,
};
});
getArtistDetail(id)
.then((res) => {
console.log(res);
artistData.value = {
id: res.data.artist.id,
name: res.data.artist.name,
occupation: res.data.identify ? res.data.identify.imageDesc : null,
cover: res.data.artist.cover,
desc: res.data.artist.briefDesc,
albumSize: res.data.artist.albumSize,
musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize,
};
$setSiteTitle(res.data.artist.name + " - 歌手");
// 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
})
.catch((err) => {
router.go(-1);
console.error("歌手信息获取失败:" + err);
$message.error("歌手信息获取失败");
});
} else {
$message.error("请提供歌手id");
$message.error("参数不完整");
}
};
@@ -110,12 +167,56 @@ const tabChange = (value) => {
path: `/artist/${value}`,
query: {
id: artistId.value,
page: 1,
},
});
};
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (user.getUserArtistLists.list[0]) {
const index = user.getUserArtistLists.list.findIndex(
(item) => item.id === Number(id)
);
if (index !== -1) {
return false;
}
return true;
} else {
return true;
}
};
// 收藏/取消收藏歌手
const toLikeArtist = (data) => {
const type = isLikeOrDislike(data.id) ? 1 : 2;
likeArtist(type, data.id).then((res) => {
if (res.code === 200) {
$message.success(
`${data.name}${type == 1 ? "收藏成功" : "取消收藏成功"}`
);
user.setUserArtistLists(() => {
artistLikeBtn.value = isLikeOrDislike(artistId.value);
});
} else {
$message.error(`${data.name}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
});
};
onMounted(() => {
getArtistDetailData(artistId.value);
artistLikeBtn.value = isLikeOrDislike(artistId.value);
if (
user.userLogin &&
!user.getUserArtistLists.has &&
!user.getUserArtistLists.isLoading
) {
user.setUserArtistLists(() => {
console.log("执行回调", artistId.value, isLikeOrDislike(artistId.value));
artistLikeBtn.value = isLikeOrDislike(artistId.value);
});
}
});
// 监听路由参数变化
@@ -124,6 +225,7 @@ watch(
(val) => {
artistId.value = val.query.id;
tabValue.value = val.path.split("/")[2];
artistLikeBtn.value = isLikeOrDislike(artistId.value);
if (val.path.split("/")[1] == "artist") {
getArtistDetailData(artistId.value);
}
@@ -134,6 +236,15 @@ watch(
<style lang="scss" scoped>
.artist {
margin-top: 30px;
.error {
margin-top: 30px;
margin-bottom: 20px;
.n-text {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
}
.artistData {
display: flex;
align-items: center;
@@ -165,6 +276,8 @@ watch(
}
.cover {
margin-right: 40px;
display: flex;
align-items: center;
.n-avatar {
height: 240px;
width: 240px;
@@ -177,12 +290,11 @@ watch(
.name {
font-size: 40px;
font-weight: bold;
margin-bottom: 8px;
margin-bottom: 4px;
margin-left: 2px;
}
.occupation {
font-size: 18px;
opacity: 0.8;
margin-left: 4px;
}
.num {
@@ -208,13 +320,16 @@ watch(
}
.desc {
margin-top: 12px;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
}
.button {
margin-top: 18px;
}
}
}
.content {

View File

@@ -1,11 +1,23 @@
<template>
<div class="songs">
<DataLists :listData="artistData" />
<n-space justify="center" v-if="artistData[0]">
<n-button
class="more"
size="large"
strong
secondary
round
@click="router.push(`/all-songs?id=${artistId}&page=1`)"
>
全部歌曲
</n-button>
</n-space>
</div>
</template>
<script setup>
import { getArtistSongs } from "@/api";
import { getArtistSongs } from "@/api/artist";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import DataLists from "@/components/DataList/DataLists.vue";
@@ -31,6 +43,7 @@ const getArtistSongsData = (id) => {
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
});
@@ -50,4 +63,22 @@ watch(
}
}
);
</script>
</script>
<style lang="scss" scoped>
.songs {
.more {
margin-top: 40px;
width: 140px;
font-size: 16px;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
}
&:active {
transform: scale(0.95);
}
}
}
</style>

View File

@@ -11,7 +11,7 @@
</template>
<script setup>
import { getArtistVideos } from "@/api";
import { getArtistVideos } from "@/api/artist";
import { useRouter } from "vue-router";
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
import VideoLists from "@/components/DataList/VideoLists.vue";
@@ -37,7 +37,7 @@ const pageNumber = ref(
);
const totalCount = ref(0);
// 获取歌手视频(网易云你视频就不返回总数了?)
// 获取歌手视频
const getArtistVideosData = (id, limit = 30, offset = 0) => {
getArtistVideos(id, limit, offset).then((res) => {
console.log(res);
@@ -99,7 +99,7 @@ watch(
() => router.currentRoute.value,
(val) => {
artistId.value = val.query.id;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "ar-videos") {
getArtistVideosData(
artistId.value,
@@ -109,4 +109,4 @@ watch(
}
}
);
</script>
</script>

View File

@@ -1,12 +1,14 @@
<template>
<div class="comment">
<n-card
class="goback"
v-if="music.getPlaySongData && music.getPlaySongData.id != songId"
@click="router.push(`/comment?id=${music.getPlaySongData.id}`)"
content-style="padding: 6px"
>前往当前歌曲评论
</n-card>
<Transition name="up">
<n-card
v-if="music.getPlaySongData && music.getPlaySongData.id != songId"
class="goback"
@click="router.push(`/comment?id=${music.getPlaySongData.id}&page=1`)"
content-style="padding: 6px"
>前往当前播放歌曲
</n-card>
</Transition>
<div class="title" v-if="songId">
<span class="key">全部评论</span>
<n-card class="song">
@@ -70,9 +72,8 @@
<script setup>
import { musicStore } from "@/store";
import { useRouter } from "vue-router";
import { getComment } from "@/api";
import { getComment } from "@/api/comment";
import SmallSongData from "@/components/DataList/SmallSongData.vue";
import AllArtists from "@/components/DataList/AllArtists.vue";
import Comment from "@/components/Comment/index.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
@@ -133,6 +134,7 @@ const pageNumberChange = (val) => {
};
onMounted(() => {
$setSiteTitle("全部评论");
// 获取评论数据
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
});
@@ -141,9 +143,7 @@ onMounted(() => {
watch(
() => router.currentRoute.value,
(val) => {
val.query.page
? (pageNumber.value = Number(val.query.page))
: (pageNumber.value = 1);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "comment") {
songId.value = val.query.id;
getCommentData(val.query.id, (pageNumber.value - 1) * 20);
@@ -154,6 +154,15 @@ watch(
<style lang="scss" scoped>
.comment {
.up-enter-active,
.up-leave-active {
transition: all 0.3s ease;
}
.up-enter-from,
.up-leave-to {
transform: translateY(-34px);
}
.goback {
cursor: pointer;
position: absolute;
@@ -192,4 +201,4 @@ watch(
}
}
}
</style>
</style>

View File

@@ -7,7 +7,9 @@
</div>
<div class="right">
<n-gradient-text class="big" type="danger"> 每日推荐 </n-gradient-text>
<n-text class="tip">根据你的音乐口味生成 · 每天 6:00 更新</n-text>
<n-text class="tip" :depth="3">
根据你的音乐口味生成 · 每天 6:00 更新
</n-text>
</div>
</div>
<DataLists :listData="music.getDailySongs" />
@@ -15,7 +17,7 @@
</template>
<script setup>
import { getDailySongs } from "@/api";
import { getDailySongs } from "@/api/home";
import { musicStore } from "@/store";
import { CalendarTodayFilled } from "@vicons/material";
import DataLists from "@/components/DataList/DataLists.vue";
@@ -34,6 +36,7 @@ const getDailySongsData = () => {
};
onMounted(() => {
$setSiteTitle("每日推荐");
if (music.getDailySongs.length === 0) getDailySongsData();
});
</script>
@@ -57,7 +60,7 @@ onMounted(() => {
transform: translateY(-3px);
}
.num {
margin-top: 12px;
margin-top: 7px;
position: absolute;
font-size: 30px;
font-weight: bold;
@@ -72,10 +75,10 @@ onMounted(() => {
line-height: 50px;
}
.tip {
font-size: 16px;
font-size: 14px;
margin-left: 2px;
}
}
}
}
</style>
</style>

View File

@@ -49,7 +49,7 @@
<script setup>
import { useRouter } from "vue-router";
import { getArtistList } from "@/api";
import { getArtistList } from "@/api/artist";
import ArtistLists from "@/components/DataList/ArtistLists.vue";
const router = useRouter();
@@ -145,6 +145,7 @@ const artistInitialChange = (key) => {
query: {
type: artistTypeNamesChoose.value,
initial: key,
page: 1,
},
});
};
@@ -157,6 +158,7 @@ const artistTypeChange = (index) => {
query: {
type: index,
initial: artistInitialChoose.value,
page: 1,
},
});
};
@@ -179,10 +181,13 @@ const loadingMore = () => {
watch(
() => router.currentRoute.value,
(val) => {
artistTypeNamesChoose.value = Number(val.query.type);
artistInitialChoose.value = val.query.initial;
artistTypeNamesChoose.value = Number(val.query.type ? val.query.type : 0);
artistInitialChoose.value = val.query.initial
? val.query.initial
: artistInitials[0].key;
artistsOffset.value = 0;
if (val.name == "dsc-artists") {
artistsData.value = [];
getArtistListData(
artistType[artistTypeNamesChoose.value],
artistArea[artistTypeNamesChoose.value],
@@ -195,6 +200,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌手");
// 获取歌手数据
getArtistListData(
artistType[artistTypeNamesChoose.value],

View File

@@ -36,6 +36,7 @@ const tabChange = (value) => {
console.log(value);
router.push({
path: `/discover/${value}`,
page: 1,
});
};
@@ -71,4 +72,4 @@ watch(
opacity: 0;
transform: translateX(10px);
}
</style>
</style>

View File

@@ -16,9 +16,8 @@
{{ catName }}
</n-button>
<n-modal
class="cat-model"
class="s-modal"
v-model:show="catModelShow"
style="width: 60vw; min-width: min(24rem, 100vw)"
preset="card"
title="歌单分类"
:bordered="false"
@@ -96,6 +95,7 @@
<Pagination
v-if="playlistsData[0] && !hqPLayListOpen"
:totalCount="totalCount"
:pageNumber="pageNumber"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
/>
@@ -125,11 +125,7 @@
import { ChevronRightRound, LocalFireDepartmentRound } from "@vicons/material";
import { useRouter } from "vue-router";
import { musicStore } from "@/store";
import {
getPlayListCatlist,
getHighqualityPlaylist,
getTopPlaylist,
} from "@/api";
import { getHighqualityPlaylist, getTopPlaylist } from "@/api/playlist";
import { formatNumber } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
@@ -185,6 +181,7 @@ const hqPLayListChange = (val) => {
query: {
cat: catName.value,
hq: val ? true : false,
page: 1,
},
});
};
@@ -294,7 +291,7 @@ const pageNumberChange = (val) => {
watch(
() => router.currentRoute.value,
(val) => {
catName.value = val.query.cat;
catName.value = val.query.cat ? val.query.cat : "全部歌单";
hqPLayListOpen.value = val.query.hq
? val.query.hq == "true"
? true
@@ -316,6 +313,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌单");
// 获取歌单分类
if (!music.catList.sub || !music.highqualityCatList[0])
music.setCatList(true);
@@ -388,4 +386,4 @@ onMounted(() => {
transform: translateY(-1px);
}
}
</style>
</style>

View File

@@ -19,7 +19,7 @@
alignItems: 'center',
}"
hoverable
@click="router.push(`/playlist?id=${item.id}`)"
@click="router.push(`/playlist?id=${item.id}&page=1`)"
>
<div class="cover">
<n-avatar
@@ -55,7 +55,7 @@
</template>
<script setup>
import { getToplist } from "@/api";
import { getToplist } from "@/api/album";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -97,6 +97,7 @@ const getToplistData = () => {
};
onMounted(() => {
$setSiteTitle("发现 - 排行榜");
getToplistData();
});
</script>
@@ -157,6 +158,7 @@ onMounted(() => {
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(40px);
backdrop-filter: blur(40px);
padding: 4px 8px;
border-radius: 8px 0 8px 0;

View File

@@ -32,6 +32,10 @@ import DataLists from "@/components/DataList/DataLists.vue";
const music = musicStore();
const router = useRouter();
onMounted(() => {
$setSiteTitle("播放历史");
});
</script>
<style lang="scss" scoped>

View File

@@ -28,6 +28,10 @@ import PaDailySongs from "@/components/Personalized/PaDailySongs.vue";
import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
const setting = settingStore();
onMounted(() => {
$setSiteTitle("SPlayer");
});
</script>
<style lang="scss" scoped>

View File

@@ -94,7 +94,7 @@
</template>
<script setup>
import { userStore } from "@/store";
import { userStore, musicStore } from "@/store";
import {
getLoginState,
getQrKey,
@@ -102,7 +102,7 @@ import {
toLogin,
sentCaptcha,
verifyCaptcha,
} from "@/api";
} from "@/api/login";
import { useRouter } from "vue-router";
import { PhoneAndroidRound, PasswordRound } from "@vicons/material";
import { formRules } from "@/utils/formRules.js";
@@ -110,6 +110,7 @@ import QrcodeVue from "qrcode.vue";
const router = useRouter();
const user = userStore();
const music = musicStore();
const { numberRule, mobileRule } = formRules();
// 二维码数据
@@ -146,6 +147,8 @@ const saveLoginData = (data) => {
user.userLogin = true;
qrText.value = "登录成功";
$message.success("登录成功");
// 自动签到
if ($signIn) $signIn();
clearInterval(qrCheckInterval.value);
router.go(-1);
} else {
@@ -264,7 +267,7 @@ const phoneLogin = (e) => {
phoneFormData._value.captcha
).then((res) => {
console.log(res);
// 网易接口抽风,等好了再写
// 暂时不支持,等支持了再写
});
}
});
@@ -286,11 +289,16 @@ const tabChange = (val) => {
};
onMounted(() => {
$setSiteTitle("登录");
// 隐藏控制条
music.setPlayBarState(false);
// 获取二维码登录 key
getQrKeyData();
});
onBeforeUnmount(() => {
// 恢复控制条
music.setPlayBarState(true);
// 清除定时器
clearInterval(qrCheckInterval.value);
clearInterval(captchaTimeOut.value);

View File

@@ -0,0 +1,190 @@
<template>
<div class="new-album">
<div class="title">
<span class="key">全部新碟</span>
</div>
<n-space class="category">
<n-tag
class="tag"
round
v-for="item in albumArea"
:key="item"
:bordered="false"
:type="item.value == albumAreaChoose ? 'primary' : 'default'"
@click="changeArea(item.value)"
>
{{ item.label }}
</n-tag>
</n-space>
<CoverLists :listData="newAlbumData" listType="album" />
<Pagination
v-if="newAlbumData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
/>
</div>
</template>
<script setup>
import { getAlbumNew } from "@/api/album";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
// 新碟数据
const newAlbumData = ref([]);
const totalCount = ref(0);
const pagelimit = ref(30);
const pageNumber = ref(
router.currentRoute.value.query.page
? Number(router.currentRoute.value.query.page)
: 1
);
const albumAreaChoose = ref(
router.currentRoute.value.query.area
? router.currentRoute.value.query.area
: "ALL"
);
const albumArea = [
{
label: "全部",
value: "ALL",
},
{
label: "华语",
value: "ZH",
},
{
label: "欧美",
value: "EA",
},
{
label: "韩国",
value: "KR",
},
{
label: "日本",
value: "JP",
},
];
// 获取新碟数据
const getAlbumNewData = (area, limit = 30, offset = 0) => {
getAlbumNew(area, limit, offset).then((res) => {
console.log(res);
// 数据总数
totalCount.value = res.total;
// 列表数据
newAlbumData.value = [];
if (res.albums) {
res.albums.forEach((v) => {
newAlbumData.value.push({
id: v.id,
cover: v.picUrl,
name: v.name,
artist: v.artists,
time: getLongTime(v.publishTime),
});
});
} else {
$message.error("全部新碟为空");
}
// 请求后回顶并结束加载条
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
});
};
// 监听路由参数变化
watch(
() => router.currentRoute.value,
(val) => {
albumAreaChoose.value = val.query.area ? val.query.area : "ALL";
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "new-album") {
getAlbumNewData(
albumAreaChoose.value,
pagelimit.value,
(pageNumber.value - 1) * pagelimit.value
);
}
}
);
// 每页个数数据变化
const pageSizeChange = (val) => {
console.log(val);
pagelimit.value = val;
getAlbumNewData(
albumAreaChoose.value,
val,
(pageNumber.value - 1) * pagelimit.value
);
};
// 当前页数数据变化
const pageNumberChange = (val) => {
router.push({
path: "/new-album",
query: {
area: albumAreaChoose.value,
page: val,
},
});
};
// 切换分类
const changeArea = (area) => {
router.push({
path: "/new-album",
query: {
area,
page: 1,
},
});
};
onMounted(() => {
$setSiteTitle("全部新碟");
getAlbumNewData(
albumAreaChoose.value,
pagelimit.value,
(pageNumber.value - 1) * pagelimit.value
);
});
</script>
<style lang="scss" scoped>
.new-album {
.title {
margin-top: 30px;
margin-bottom: 20px;
.key {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
}
.category {
margin-bottom: 20px;
.tag {
font-size: 13px;
padding: 0 16px;
line-height: 0;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
}
&:active {
transform: scale(0.9);
}
}
}
}
</style>

View File

@@ -7,7 +7,7 @@
:src="
playListDetail.coverImgUrl
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
@@ -27,20 +27,22 @@
block
strong
secondary
v-if="playListDetail?.description.length > 70"
v-if="playListDetail?.description?.length > 70"
@click="playListDescShow = true"
>
全部简介
</n-button>
<n-modal
class="s-modal"
v-model:show="playListDescShow"
preset="card"
style="width: 60vw; min-width: min(24rem, 100vw)"
title="歌单简介"
:bordered="false"
>
<n-scrollbar style="max-height: 60vh">
{{ playListDetail.description }}
<n-scrollbar>
<n-text
v-html="playListDetail.description.replace(/\n/g, '<br>')"
/>
</n-scrollbar>
</n-modal>
</div>
@@ -52,12 +54,12 @@
:bordered="false"
v-for="item in playListDetail.tags"
:key="item"
@click="router.push(`/discover/playlists?cat=${item}`)"
@click="router.push(`/discover/playlists?cat=${item}&page=1`)"
>
{{ item }}
</n-tag>
</n-space>
<!-- <div class="control" v-if="playListControlShow">
<!-- <div class="control" v-if="true">
<n-space>
<n-button strong secondary round>
<template #icon>
@@ -100,7 +102,7 @@
</div>
</div>
<div class="title" v-else-if="!playListId">
<span class="key">未提供所需数据</span>
<span class="key">参数不完整</span>
<br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级
@@ -120,7 +122,7 @@
</template>
<script setup>
import { getPlayListDetail, getAllPlayList } from "@/api";
import { getPlayListDetail, getAllPlayList } from "@/api/playlist";
import { useRouter } from "vue-router";
import { userStore, musicStore } from "@/store";
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
@@ -153,6 +155,7 @@ const getPlayListDetailData = (id) => {
totalCount.value = res.playlist.trackCount;
// 歌单信息
playListDetail.value = res.playlist;
$setSiteTitle(res.playlist.name + " - 歌单");
} else {
$message.error("获取歌单信息失败");
}
@@ -176,6 +179,7 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
} else {
@@ -224,7 +228,7 @@ watch(
() => router.currentRoute.value,
(val, oldVal) => {
playListId.value = val.query.id;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "playlist") {
if (val.query.id != oldVal.query.id) {
getPlayListDetailData(playListId.value);
@@ -250,26 +254,6 @@ watch(
.loading {
display: flex;
flex-direction: row;
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static !important;
width: 60vw !important;
max-width: none !important;
.intr,
.tag {
display: none !important;
}
}
.right {
.meta {
.name {
font-size: 26px !important;
}
}
}
}
.left {
width: 40vw;
height: 100%;
@@ -345,13 +329,19 @@ watch(
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 1;
color: $mainColor;
}
}
.time {
margin-top: 8px;
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 370px) {
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
}
@@ -374,6 +364,26 @@ watch(
}
}
}
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static;
width: 60vw;
max-width: none;
.intr,
.tag {
display: none;
}
}
.right {
.meta {
.name {
font-size: 26px;
}
}
}
}
}
.title {
margin-top: 30px;

View File

@@ -3,6 +3,7 @@
<CoverLists :listData="searchData" listType="album" />
<Pagination
v-if="searchData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
@@ -11,7 +12,7 @@
</template>
<script setup>
import { getSearchData } from "@/api";
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -60,7 +61,7 @@ watch(
() => router.currentRoute.value,
(val) => {
searchKeywords.value = val.query.keywords;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "s-albums") {
getSearchDataList(
searchKeywords.value,

View File

@@ -5,7 +5,7 @@
</template>
<script setup>
import { getSearchData } from "@/api";
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import ArtistLists from "@/components/DataList/ArtistLists.vue";

View File

@@ -55,6 +55,7 @@ const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
watch(
() => router.currentRoute.value,
(val) => {
$setSiteTitle(val.query.keywords + "的搜索结果");
searchKeywords.value = val.query.keywords;
tabValue.value = val.path.split("/")[2];
}
@@ -67,9 +68,14 @@ const tabChange = (value) => {
path: `/search/${value}`,
query: {
keywords: searchKeywords.value,
page: 1,
},
});
};
onMounted(() => {
$setSiteTitle(searchKeywords.value + "的搜索结果");
});
</script>
<style lang="scss" scoped>
@@ -100,4 +106,4 @@ const tabChange = (value) => {
opacity: 0;
transform: translateX(10px);
}
</style>
</style>

View File

@@ -3,6 +3,7 @@
<CoverLists :listData="searchData" />
<Pagination
v-if="searchData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
@@ -11,7 +12,7 @@
</template>
<script setup>
import { getSearchData } from "@/api";
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import CoverLists from "@/components/DataList/CoverLists.vue";
@@ -60,7 +61,7 @@ watch(
() => router.currentRoute.value,
(val) => {
searchKeywords.value = val.query.keywords;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "s-playlists") {
getSearchDataList(
searchKeywords.value,

View File

@@ -3,6 +3,7 @@
<DataLists :listData="searchData" />
<Pagination
v-if="searchData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
@@ -11,7 +12,8 @@
</template>
<script setup>
import { getSearchData, getMusicDetail } from "@/api";
import { getSearchData } from "@/api/search";
import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import DataLists from "@/components/DataList/DataLists.vue";
@@ -33,27 +35,26 @@ const pageNumber = ref(
const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1) => {
getSearchData(keywords, limit, offset, type).then((res) => {
console.log(res);
// 数据总数
totalCount.value = res.result.songCount;
// 列表数据
if (res.result.songs) {
const ids = res.result.songs.map((obj) => obj.id);
getMusicDetail(ids.join(",")).then((res) => {
console.log(res);
searchData.value = [];
res.songs.forEach((v, i) => {
searchData.value.push({
id: v.id,
num: i + 1 + (pageNumber.value - 1) * pagelimit.value,
name: v.name,
artist: v.ar,
album: v.al,
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv,
});
// 数据总数
totalCount.value = res.result.songCount;
// const ids = res.result.songs.map((obj) => obj.id);
// getMusicDetail(ids.join(",")).then((res) => {});
// console.log(res);
searchData.value = [];
res.result.songs.forEach((v, i) => {
searchData.value.push({
id: v.id,
num: i + 1 + (pageNumber.value - 1) * pagelimit.value,
name: v.name,
artist: v.ar,
album: v.al,
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
} else {
@@ -69,7 +70,7 @@ watch(
() => router.currentRoute.value,
(val) => {
searchKeywords.value = val.query.keywords;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "s-songs") {
getSearchDataList(
searchKeywords.value,
@@ -109,4 +110,4 @@ onMounted(() => {
(pageNumber.value - 1) * pagelimit.value
);
});
</script>
</script>

View File

@@ -3,6 +3,7 @@
<VideoLists :listData="searchData" />
<Pagination
v-if="searchData[0]"
:pageNumber="pageNumber"
:totalCount="totalCount"
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
@@ -11,7 +12,7 @@
</template>
<script setup>
import { getSearchData } from "@/api";
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
import VideoLists from "@/components/DataList/VideoLists.vue";
@@ -61,7 +62,7 @@ watch(
() => router.currentRoute.value,
(val) => {
searchKeywords.value = val.query.keywords;
pageNumber.value = Number(val.query.page);
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "s-videos") {
getSearchDataList(
searchKeywords.value,

137
src/views/Setting/index.vue Normal file
View File

@@ -0,0 +1,137 @@
<template>
<div class="setting">
<div class="title">全局设置</div>
<n-tabs
class="main-tab"
type="segment"
@update:value="tabChange"
v-model:value="tabValue"
>
<n-tab name="main"> 基础 </n-tab>
<n-tab name="player"> 播放器 </n-tab>
<n-tab name="other"> 其他 </n-tab>
</n-tabs>
<main class="content">
<router-view v-slot="{ Component }">
<keep-alive>
<Transition name="move" mode="out-in">
<component :is="Component" />
</Transition>
</keep-alive>
</router-view>
</main>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
// Tab 默认选中
const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
// Tab 选项卡变化
const tabChange = (value) => {
console.log(value);
router.push({
path: `/setting/${value}`,
});
};
// 监听路由参数变化
watch(
() => router.currentRoute.value,
(val) => {
tabValue.value = val.path.split("/")[2];
}
);
onMounted(() => {
$setSiteTitle("全局设置");
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
});
</script>
<style lang="scss" scoped>
.setting {
.title {
margin-top: 30px;
margin-bottom: 20px;
font-size: 40px;
font-weight: bold;
display: flex;
align-items: center;
}
.content {
margin-top: 20px;
:deep(.set-item) {
width: 100%;
border-radius: 8px;
margin-bottom: 12px;
.n-card__content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.name {
font-size: 16px;
display: flex;
flex-direction: column;
padding-right: 20px;
.tip {
font-size: 12px;
opacity: 0.8;
}
}
.set {
width: 200px;
@media (max-width: 768px) {
width: 140px;
min-width: 140px;
}
}
.more {
padding: 12px;
border-radius: 8px;
background-color: var(--n-border-color);
width: 100%;
margin-top: 12px;
box-sizing: border-box;
&.blur {
.lrc {
filter: blur(2px);
&.on {
filter: blur(0);
}
}
}
.lrc {
opacity: 0.6;
display: flex;
flex-direction: column;
transform: scale(0.95);
transition: all 0.3s;
&.on {
font-weight: bold;
opacity: 1;
transform: scale(1.05);
}
}
}
}
}
}
}
// 路由跳转动画
.move-enter-active,
.move-leave-active {
transition: all 0.2s ease;
}
.move-enter-from,
.move-leave-to {
opacity: 0;
transform: translateX(10px);
}
</style>

125
src/views/Setting/main.vue Normal file
View File

@@ -0,0 +1,125 @@
<template>
<div class="set-main">
<n-card class="set-item">
<div class="name">明暗模式</div>
<n-select class="set" v-model:value="theme" :options="darkOptions" />
</n-card>
<n-card class="set-item">
<div class="name">明暗模式跟随系统</div>
<n-switch v-model:value="themeAuto" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
每日签到
<span class="tip">是否自动进行每日签到</span>
</div>
<n-switch v-model:value="autoSignIn" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">显示轮播图</div>
<n-switch v-model:value="bannerShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
列表点击方式
<span class="tip">移动端该设置项无效单击同时生效</span>
</div>
<n-select
class="set"
v-model:value="listClickMode"
:options="listClickModeOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">显示搜索历史</div>
<n-switch v-model:value="searchHistory" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示底栏歌词
<span class="tip">是否在播放时显示歌词</span>
</div>
<n-switch v-model:value="bottomLyricShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌曲音质
<span class="tip">无损音质及以上需要您为黑胶会员</span>
</div>
<n-select
class="set"
v-model:value="songLevel"
:options="songLevelOptions"
/>
</n-card>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { settingStore, userStore } from "@/store";
const setting = settingStore();
const user = userStore();
const {
theme,
themeAuto,
listClickMode,
bottomLyricShow,
songLevel,
bannerShow,
autoSignIn,
searchHistory,
} = storeToRefs(setting);
// 深浅模式
const darkOptions = [
{
label: "浅色模式",
value: "light",
},
{
label: "深色模式",
value: "dark",
},
];
// 列表模式
const listClickModeOptions = [
{
label: "双击播放",
value: "dblclick",
},
{
label: "单击播放",
value: "click",
},
];
// 歌曲音质
const songLevelOptions = [
{
label: "标准",
value: "standard",
},
{
label: "较高",
value: "higher",
},
,
{
label: "极高",
value: "exhigh",
},
{
label: "无损",
value: "lossless",
disabled: user.userData?.vipType ? false : true,
},
{
label: "Hi-Res",
value: "hires",
disabled: user.userData?.vipType ? false : true,
},
];
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="set-other">
<n-card class="set-item">
<div class="name">
系统重置
<span class="tip">若程序显示异常或出现问题时可尝试此操作</span>
</div>
<n-button strong secondary type="error" @click="resetApp">
重置
</n-button>
</n-card>
</div>
</template>
<script setup>
// 系统重置
const resetApp = () => {
const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功");
localStorage.clear();
window.location.href = "/";
};
$dialog.warning({
title: "系统重置",
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
positiveText: "重置",
negativeText: "取消",
onPositiveClick: () => {
$cleanAll ? $cleanAll() : cleanAll();
},
});
};
</script>

Some files were not shown because too many files have changed in this diff Show More