mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 11:29:26 +08:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2370d237a8 | ||
|
|
ffbe6229f9 | ||
|
|
bf1312889d | ||
|
|
2bf3d7db5a | ||
|
|
06ccb969e4 | ||
|
|
6867897e02 | ||
|
|
c78f94ae86 | ||
|
|
40ed0dada1 | ||
|
|
f4d2c5f337 | ||
|
|
aace9e97b0 | ||
|
|
b3641801df | ||
|
|
1dd877832c | ||
|
|
660cd33387 | ||
|
|
8caebf65f9 | ||
|
|
3b432dbd8b | ||
|
|
ee934c89f4 | ||
|
|
921b0eed0a | ||
|
|
7142991f7d | ||
|
|
a60e557ba2 | ||
|
|
bc7031ba0c | ||
|
|
82aabc555a | ||
|
|
8149c2dd71 | ||
|
|
02084c6be0 | ||
|
|
ade3ddbe82 | ||
|
|
72e5b11558 | ||
|
|
c8cb4c2c9e | ||
|
|
396a54f646 | ||
|
|
cf8fd6b7fc | ||
|
|
4709ab3910 | ||
|
|
46e6ac3408 | ||
|
|
a9a03e1cc4 | ||
|
|
69a2855f77 | ||
|
|
ecdb3b75ba | ||
|
|
415cf3b3c9 | ||
|
|
bc84e11adf | ||
|
|
5425288e16 | ||
|
|
ae8f3696f3 | ||
|
|
6a102a1bff | ||
|
|
80aea0826b | ||
|
|
a65a7224ae | ||
|
|
ddd12364fe | ||
|
|
7592296124 | ||
|
|
0922735b2d | ||
|
|
144955e7c8 | ||
|
|
7495e7af2d | ||
|
|
43fe04b4fc | ||
|
|
db5aebbf89 | ||
|
|
535d0f7493 | ||
|
|
7415f591b3 | ||
|
|
e138d06e6f | ||
|
|
cd05376e18 | ||
|
|
45b374a0cb | ||
|
|
418da81738 | ||
|
|
dd66725d9c |
22
.env
22
.env
@@ -1,6 +1,22 @@
|
||||
# 全局 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/"
|
||||
|
||||
# 站点标题
|
||||
VITE_SITE_TITLE = "SPlayer"
|
||||
VITE_SITE_ANTHOR = "無名"
|
||||
VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器"
|
||||
VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
VITE_SITE_URL = "imsyy.top"
|
||||
VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
|
||||
# 百度统计(若不需要,请设为空即可)
|
||||
VITE_SITE_BAIDUTONGJI = "c6579e9a33cbc5260fc90231678556ec"
|
||||
|
||||
# ICP 备案号
|
||||
## 若不需要,请设为空即可
|
||||
VITE_ICP = "豫ICP备2022018134号-1"
|
||||
@@ -8,8 +24,8 @@ VITE_ICP = "豫ICP备2022018134号-1"
|
||||
# 公告配置
|
||||
## 若无需公告,请将任意一项设为空即可
|
||||
## 公告标题
|
||||
VITE_ANN_TITLE = "即将完成"
|
||||
VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
VITE_ANN_CONTENT = "进行最后完善"
|
||||
## 公告时长(毫秒)
|
||||
VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
VITE_ANN_DURATION = 3000
|
||||
|
||||
26
.hintrc
Normal file
26
.hintrc
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"compat-api/html": [
|
||||
"default",
|
||||
{
|
||||
"ignore": [
|
||||
"meta[name=theme-color]"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
75
README.md
75
README.md
@@ -6,8 +6,12 @@
|
||||
</div>
|
||||
<br />
|
||||
|
||||
> 本项目采用 Vue 3 全家桶及 SCSS 开发
|
||||
> 目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
|
||||
## 说明
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 `SCSS` 开发
|
||||
- 目前主要以 `Web` 端为主,可能暂时不会考虑使用 `Electron` 构建客户端
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
- 欢迎各位大佬指点和 `Star` 哦 😍
|
||||
|
||||
## 👀 Demo
|
||||
|
||||
@@ -15,37 +19,36 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 账号
|
||||
- 扫码登录
|
||||
- 手机号登录(目前暂时无法使用)
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 管理
|
||||
- 下载歌曲(最高 Hi-Res)
|
||||
- 新建歌单
|
||||
- 歌单编辑
|
||||
- 收藏 / 取消收藏歌单
|
||||
- 收藏 / 取消收藏歌手
|
||||
- 推荐
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 音乐云盘
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 播放
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 实验性功能,需在设置中开启 )
|
||||
- 音乐渐入渐出
|
||||
- 其他
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- 移动端基础适配
|
||||
- 支持扫码登录
|
||||
- 支持手机号登录(上游接口暂时无法使用)
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
|
||||
- 由于酷我音源不支持 `https`,故网页端替换可能不全面
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 支持逐字歌词
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 暂时去除,还待完善 )
|
||||
- 音乐渐入渐出
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- 移动端基础适配
|
||||
|
||||
- [ ] 主题换肤
|
||||
#### 待办
|
||||
|
||||
- [ ] 电台节目支持
|
||||
- [ ] 发表评论
|
||||
- [ ] `i18n` 支持
|
||||
- [ ] 重构(写成屎山了) 🤣
|
||||
|
||||
## 😍 Screenshots
|
||||
|
||||
@@ -83,16 +86,20 @@
|
||||
|
||||
> 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
|
||||
@@ -131,6 +138,8 @@ npm build
|
||||
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
23
index.html
23
index.html
@@ -3,23 +3,34 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/images/logo/favicon.svg">
|
||||
<link rel="icon" href="<%- logo %>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
<title>SPlayer</title>
|
||||
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" />
|
||||
<meta name="description" content="一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" />
|
||||
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
|
||||
<title><%- title %></title>
|
||||
<meta name="author" content="<%- author %>" />
|
||||
<meta name="keywords" content="<%- keywords %>" />
|
||||
<meta name="description" content="<%- description %>" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<!-- HarmonyOS Sans -->
|
||||
<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" />
|
||||
<!-- IE Out -->
|
||||
<script>
|
||||
if ( /*@cc_on!@*/ false || (!!window.MSInputMethodContext && !!document.documentMode))
|
||||
window.location.href =
|
||||
"https://support.dmeng.net/upgrade-your-browser.html?referrer=" + encodeURIComponent(window.location.href)
|
||||
</script>
|
||||
<% if (tongji) { %>
|
||||
<!-- 百度统计 -->
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?c6579e9a33cbc5260fc90231678556ec";
|
||||
hm.src = "https://hm.baidu.com/hm.js?<%- tongji %>";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"version": "1.1.6",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
@@ -15,14 +14,19 @@
|
||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"artplayer": "^4.5.12",
|
||||
"axios": "^1.2.0",
|
||||
"colorthief": "^2.4.0",
|
||||
"howler": "^2.2.3",
|
||||
"pinia": "^2.0.26",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
"plyr": "^3.7.3",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"sass": "^1.56.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
|
||||
638
pnpm-lock.yaml
generated
638
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
63
src/App.vue
63
src/App.vue
@@ -10,7 +10,15 @@
|
||||
:native-scrollbar="false"
|
||||
embedded
|
||||
>
|
||||
<main ref="mainContent" class="main">
|
||||
<main
|
||||
ref="mainContent"
|
||||
class="main"
|
||||
id="main"
|
||||
:class="{
|
||||
playlist: music.showPlayList,
|
||||
search: site.searchInputActive,
|
||||
}"
|
||||
>
|
||||
<n-back-top
|
||||
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
|
||||
style="transition: all 0.3s"
|
||||
@@ -30,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { musicStore, userStore, settingStore } from "@/store";
|
||||
import { musicStore, userStore, settingStore, siteStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getLoginState, refreshLogin } from "@/api/login";
|
||||
import { userDailySignin, userYunbeiSign } from "@/api/user";
|
||||
@@ -42,6 +50,7 @@ import packageJson from "@/../package.json";
|
||||
const music = musicStore();
|
||||
const user = userStore();
|
||||
const setting = settingStore();
|
||||
const site = siteStore();
|
||||
const router = useRouter();
|
||||
const mainContent = ref(null);
|
||||
|
||||
@@ -68,6 +77,20 @@ const spacePlayOrPause = (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更改页面标题
|
||||
const setSiteTitle = (val) => {
|
||||
const title = val
|
||||
? val === import.meta.env.VITE_SITE_TITLE
|
||||
? val
|
||||
: val + " - " + import.meta.env.VITE_SITE_TITLE
|
||||
: sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
|
||||
site.siteTitle = title;
|
||||
sessionStorage.setItem("siteTitle", title);
|
||||
if (!music.getPlayState) {
|
||||
window.document.title = title;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新登录
|
||||
const toRefreshLogin = () => {
|
||||
const today = Date.now();
|
||||
@@ -121,7 +144,7 @@ const signIn = () => {
|
||||
const cleanAll = () => {
|
||||
$message ? $message.success("重置成功") : alert("重置成功");
|
||||
localStorage.clear();
|
||||
window.location.href = "/";
|
||||
document.location.reload();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -129,6 +152,7 @@ onMounted(() => {
|
||||
window.$mainContent = mainContent.value;
|
||||
window.$cleanAll = cleanAll;
|
||||
window.$signIn = signIn;
|
||||
window.$setSiteTitle = setSiteTitle;
|
||||
|
||||
// 公告
|
||||
if (annShow) {
|
||||
@@ -140,7 +164,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// 版权声明
|
||||
const logoText = "SPlayer";
|
||||
const logoText = import.meta.env.VITE_SITE_TITLE;
|
||||
const copyrightNotice = `\n\n版本: ${packageJson.version}\n作者: ${packageJson.author}\n作者主页: ${packageJson.home}\nGitHub: ${packageJson.github}`;
|
||||
console.info(
|
||||
`%c${logoText} %c ${copyrightNotice}`,
|
||||
@@ -170,6 +194,7 @@ onMounted(() => {
|
||||
if (music.getPlayListMode === "cloud") {
|
||||
$message.info("登录已失效,请重新登录");
|
||||
music.setPlaylists([]);
|
||||
music.setPlayListMode("list");
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -208,6 +233,33 @@ onMounted(() => {
|
||||
.main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
div:nth-of-type(2) {
|
||||
transition: all 0.3s;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: all 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
&.playlist {
|
||||
div:nth-of-type(2) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
&.search {
|
||||
div:nth-of-type(2) {
|
||||
&::after {
|
||||
pointer-events: all;
|
||||
background-color: #00000040;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +272,7 @@ onMounted(() => {
|
||||
.scale-enter-from,
|
||||
.scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
// transform: scale(0.98);
|
||||
transform: translateX(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,6 +59,7 @@ export const getPlayListDetail = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/detail",
|
||||
withCredentials: false,
|
||||
params: {
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
|
||||
@@ -38,6 +38,28 @@ export const getMusicUrl = (id, level = "exhigh") => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 网易云解灰
|
||||
* @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
|
||||
@@ -53,6 +75,21 @@ export const getMusicLyric = (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用逗号分隔
|
||||
@@ -111,3 +148,22 @@ export const getSongDownload = (id, br = 999000) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 听歌打卡
|
||||
* @param {number} id - 音乐ID
|
||||
* @param {number} sourceid - 来源ID
|
||||
*/
|
||||
export const songScrobble = (id, sourceid = 0, time = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/scrobble",
|
||||
hiddenBar: true,
|
||||
params: {
|
||||
id,
|
||||
sourceid,
|
||||
time,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -116,7 +116,6 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log("销毁");
|
||||
window.removeEventListener("resize", getBannerHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,71 +1,84 @@
|
||||
<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?.redVipAnnualCount > 0"
|
||||
:src="commentData.user.vipRights.musicPackage.iconUrl"
|
||||
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"
|
||||
alt="associator"
|
||||
title="黑胶会员"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<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"
|
||||
/>
|
||||
<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="commentData.liked ? 'like liked' : 'like'"
|
||||
@click="toLikeComment"
|
||||
class="associator"
|
||||
v-if="commentData.user.vipRights?.redVipLevel > 0"
|
||||
>
|
||||
<n-icon>
|
||||
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
|
||||
</n-icon>
|
||||
{{ formatNumber(commentData.likedCount) }}
|
||||
<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 { getCommentTime, formatNumber } from "@/utils/timeTools";
|
||||
import { Local, Time, ThumbsUp } from "@icon-park/vue-next";
|
||||
import { userStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -77,7 +90,7 @@ const props = defineProps({
|
||||
// 评论 数据
|
||||
commentData: {
|
||||
type: Object,
|
||||
default: [],
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -105,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;
|
||||
@@ -174,7 +196,7 @@ const toLikeComment = () => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +213,7 @@ const toLikeComment = () => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,14 +238,14 @@ const toLikeComment = () => {
|
||||
transition: all 0.3s;
|
||||
opacity: 0.6;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
opacity: 1;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
&.liked {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -235,4 +257,10 @@ const toLikeComment = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,7 +31,7 @@ const router = useRouter();
|
||||
const props = defineProps({
|
||||
// 歌手数据
|
||||
artistsData: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
@@ -59,7 +59,7 @@ const jumpArtist = (id) => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.line {
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<n-avatar
|
||||
round
|
||||
class="shadow"
|
||||
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<n-icon size="40" :component="PeopleSearchOne" />
|
||||
</div>
|
||||
<n-text class="name text-hidden">{{ item.name }}</n-text>
|
||||
@@ -75,7 +81,7 @@ const router = useRouter();
|
||||
const props = defineProps({
|
||||
// 列表数据
|
||||
listData: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
// 折叠栅格
|
||||
@@ -205,12 +211,26 @@ onMounted(() => {
|
||||
box-shadow: 0 4px 16px 0 #00000020;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
.n-avatar {
|
||||
.coverImg {
|
||||
filter: brightness(1);
|
||||
transform: scale(1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
}
|
||||
.shadow {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(16px) opacity(0.6);
|
||||
transform: scale(0.92, 0.96);
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
aspect-ratio: 1/1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.n-icon {
|
||||
opacity: 0;
|
||||
@@ -218,6 +238,7 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px 0 #00000040;
|
||||
@@ -225,10 +246,13 @@ onMounted(() => {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.n-avatar {
|
||||
.coverImg {
|
||||
filter: brightness(0.8);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.shadow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
.n-avatar {
|
||||
@@ -242,7 +266,7 @@ onMounted(() => {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<n-avatar
|
||||
class="shadow"
|
||||
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<n-icon class="play" size="40">
|
||||
<PlayOne theme="filled" />
|
||||
</n-icon>
|
||||
@@ -86,13 +91,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { PlayOne, Headset } from "@icon-park/vue-next";
|
||||
import { NIcon } from "naive-ui";
|
||||
import {
|
||||
PlayOne,
|
||||
Headset,
|
||||
LinkTwo,
|
||||
Like,
|
||||
Unlike,
|
||||
Editor,
|
||||
DeleteFour,
|
||||
} 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 AllArtists from "./AllArtists.vue";
|
||||
import PlaylistUpdate from "@/components/DataModel/PlaylistUpdate.vue";
|
||||
import PlaylistUpdate from "@/components/DataModal/PlaylistUpdate.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
@@ -100,7 +114,7 @@ const user = userStore();
|
||||
const props = defineProps({
|
||||
// 列表数据
|
||||
listData: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
// 列表类型
|
||||
@@ -131,6 +145,19 @@ const props = defineProps({
|
||||
});
|
||||
const playlistUpdateRef = ref(null);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => icon,
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// 右键菜单数据
|
||||
const rightMenuX = ref(0);
|
||||
const rightMenuY = ref(0);
|
||||
@@ -150,9 +177,10 @@ const openRightMenu = (e, data) => {
|
||||
router.currentRoute.value.name === "user-playlists" ? true : false,
|
||||
props: {
|
||||
onClick: () => {
|
||||
playlistUpdateRef.value.openUpdateModel(data);
|
||||
playlistUpdateRef.value.openUpdateModal(data);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(Editor)),
|
||||
},
|
||||
{
|
||||
key: "del",
|
||||
@@ -164,6 +192,7 @@ const openRightMenu = (e, data) => {
|
||||
toDelPlayList(data);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(DeleteFour)),
|
||||
},
|
||||
{
|
||||
key: "likePlaylist",
|
||||
@@ -180,6 +209,7 @@ const openRightMenu = (e, data) => {
|
||||
toChangeLike(data.id);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
|
||||
},
|
||||
{
|
||||
key: "likeAlbum",
|
||||
@@ -195,6 +225,7 @@ const openRightMenu = (e, data) => {
|
||||
toChangeLike(data.id);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
|
||||
},
|
||||
{
|
||||
key: "copy",
|
||||
@@ -221,6 +252,7 @@ const openRightMenu = (e, data) => {
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(LinkTwo)),
|
||||
},
|
||||
];
|
||||
rightMenuShow.value = true;
|
||||
@@ -279,10 +311,10 @@ const isLikeOrDislike = (id) => {
|
||||
const playlists = user.getUserPlayLists.like;
|
||||
const albums = user.getUserAlbumLists.list;
|
||||
if (listType === "playlist" && playlists.length) {
|
||||
return !playlists.some((item) => item.id === id);
|
||||
return !playlists.some((item) => item.id === Number(id));
|
||||
}
|
||||
if (listType === "album" && albums.length) {
|
||||
return !albums.some((item) => item.id === id);
|
||||
return !albums.some((item) => item.id === Number(id));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -356,7 +388,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
@@ -365,7 +397,25 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
transition: filter 0.3s;
|
||||
z-index: 1;
|
||||
:deep(img) {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
.shadow {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(16px) opacity(0.6);
|
||||
transform: scale(0.92, 0.96);
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
aspect-ratio: 1/1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.play {
|
||||
opacity: 0;
|
||||
@@ -373,10 +423,12 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
padding: 0.5vw;
|
||||
background-color: #00000010;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 50%;
|
||||
transform: scale(0.8);
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
}
|
||||
.description {
|
||||
position: absolute;
|
||||
@@ -385,10 +437,13 @@ 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;
|
||||
border-bottom-right-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
.num {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -402,10 +457,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 15px 30px rgb(0 0 0 / 10%);
|
||||
.coverImg {
|
||||
filter: brightness(0.8);
|
||||
transform: scale(1.1);
|
||||
:deep(img) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
.play {
|
||||
transform: scale(1);
|
||||
@@ -414,6 +470,9 @@ onMounted(() => {
|
||||
.description {
|
||||
opacity: 0;
|
||||
}
|
||||
.shadow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
@@ -431,7 +490,7 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.by {
|
||||
@@ -441,7 +500,7 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.artists {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<Transition mode="out-in">
|
||||
<div class="datalists" v-if="listData[0]">
|
||||
<div class="datalists" id="datalists" v-if="listData[0]">
|
||||
<n-card
|
||||
hoverable
|
||||
v-for="item in listData"
|
||||
:key="item"
|
||||
:class="
|
||||
music.getPlaySongData
|
||||
? music.getPlaySongData.id == item.id
|
||||
? 'songs play'
|
||||
: 'songs'
|
||||
music.getPlaySongData && music.getPlaySongData.id == item.id
|
||||
? 'songs play'
|
||||
: 'songs'
|
||||
"
|
||||
:content-style="{
|
||||
@@ -17,8 +16,7 @@
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}"
|
||||
v-for="item in listData"
|
||||
:key="item"
|
||||
hoverable
|
||||
@dblclick="
|
||||
setting.listClickMode === 'dblclick' ? playSong(listData, item) : null
|
||||
"
|
||||
@@ -43,13 +41,13 @@
|
||||
@click.stop="jumpLink(item.id, 1)"
|
||||
/>
|
||||
<n-tag
|
||||
v-if="item.fee == 1"
|
||||
v-if="item.fee == 1 || item.fee == 4"
|
||||
class="vip"
|
||||
round
|
||||
:bordered="false"
|
||||
size="small"
|
||||
>
|
||||
VIP
|
||||
{{ item.fee == 1 ? "VIP" : "数字专辑" }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="item.pc"
|
||||
@@ -110,7 +108,7 @@
|
||||
<n-icon
|
||||
class="download"
|
||||
size="20"
|
||||
@click.stop="downloadSongRef.openDownloadModel(item)"
|
||||
@click.stop="downloadSongRef.openDownloadModal(item)"
|
||||
>
|
||||
<DownloadFour theme="filled" />
|
||||
</n-icon>
|
||||
@@ -204,7 +202,7 @@
|
||||
class="item"
|
||||
@click="
|
||||
() => {
|
||||
downloadSongRef.openDownloadModel(drawerData);
|
||||
downloadSongRef.openDownloadModal(drawerData);
|
||||
drawerShow = false;
|
||||
}
|
||||
"
|
||||
@@ -333,10 +331,11 @@ import { musicStore, settingStore, userStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { setCloudDel } from "@/api/user";
|
||||
import { NIcon } from "naive-ui";
|
||||
import { soundStop } from "@/utils/Player";
|
||||
import AllArtists from "./AllArtists.vue";
|
||||
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
|
||||
import CloudMatch from "@/components/DataModel/CloudMatch.vue";
|
||||
import DownloadSong from "@/components/DataModel/DownloadSong.vue";
|
||||
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
|
||||
import CloudMatch from "@/components/DataModal/CloudMatch.vue";
|
||||
import DownloadSong from "@/components/DataModal/DownloadSong.vue";
|
||||
import SmallSongData from "./SmallSongData.vue";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -350,7 +349,7 @@ const downloadSongRef = ref(null);
|
||||
const props = defineProps({
|
||||
// 列表数据
|
||||
listData: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
// 专辑隐藏
|
||||
@@ -387,13 +386,13 @@ const copySongData = (id, url = true) => {
|
||||
};
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
const renderIcon = (icon, filled = true) => {
|
||||
return () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ depth: 2, style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => h(icon, { theme: "filled" }),
|
||||
default: () => h(icon, { theme: filled ? "filled" : "outline" }),
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -420,9 +419,9 @@ const openRightMenu = (e, data) => {
|
||||
label: "下一首播放",
|
||||
icon: renderIcon(AddMusic),
|
||||
show:
|
||||
music.getPersonalFmMode || music.getPlaySongData.id == data.id
|
||||
? true
|
||||
: false,
|
||||
music.getPersonalFmMode || music.getPlaySongData?.id == data.id
|
||||
? false
|
||||
: true,
|
||||
props: {
|
||||
onClick: () => {
|
||||
music.addSongToNext(data);
|
||||
@@ -446,14 +445,14 @@ const openRightMenu = (e, data) => {
|
||||
icon: renderIcon(DownloadFour),
|
||||
props: {
|
||||
onClick: () => {
|
||||
downloadSongRef.value.openDownloadModel(data);
|
||||
downloadSongRef.value.openDownloadModal(data);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "comment",
|
||||
label: "前往评论区",
|
||||
icon: renderIcon(Comments),
|
||||
icon: renderIcon(Comments, false),
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push(`/comment?id=${data.id}`);
|
||||
@@ -463,7 +462,7 @@ const openRightMenu = (e, data) => {
|
||||
{
|
||||
key: "mv",
|
||||
label: "观看 MV",
|
||||
icon: renderIcon(Video),
|
||||
icon: renderIcon(Video, false),
|
||||
show: data.mv && data.mv != 0 ? true : false,
|
||||
props: {
|
||||
onClick: () => {
|
||||
@@ -505,7 +504,7 @@ const openRightMenu = (e, data) => {
|
||||
{
|
||||
key: "search",
|
||||
label: "同名搜索",
|
||||
icon: renderIcon(Search),
|
||||
icon: renderIcon(Search, false),
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -521,7 +520,7 @@ const openRightMenu = (e, data) => {
|
||||
{
|
||||
key: "copyId",
|
||||
label: "复制歌曲 ID",
|
||||
icon: renderIcon(FileMusic),
|
||||
icon: renderIcon(FileMusic, false),
|
||||
props: {
|
||||
onClick: () => {
|
||||
copySongData(data.id, false);
|
||||
@@ -583,7 +582,11 @@ const openDrawer = (data) => {
|
||||
// 播放并添加
|
||||
const playSong = (data, song) => {
|
||||
console.log(data, song);
|
||||
music.setPersonalFmMode(false);
|
||||
if (music.getPersonalFmMode) {
|
||||
soundStop($player);
|
||||
music.setPersonalFmMode(false);
|
||||
}
|
||||
music.setPlayState(true);
|
||||
if (router.currentRoute.value.name !== "history") music.setPlaylists(data);
|
||||
// 检查是否为云盘歌曲
|
||||
if (router.currentRoute.value.name === "user-cloud") {
|
||||
@@ -637,9 +640,10 @@ const jumpLink = (id, type) => {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border-color: $mainColor;
|
||||
box-shadow: 0 1px 2px -2px #f55e5526, 0 3px 6px 0 #f55e5530,
|
||||
0 5px 12px 4px #f55e5505;
|
||||
border-color: var(--main-color);
|
||||
box-shadow: 0 1px 2px -2px var(--main-boxshadow-color),
|
||||
0 3px 6px 0 var(--main-boxshadow-color),
|
||||
0 5px 12px 4px var(--main-boxshadow-hover-color);
|
||||
.action {
|
||||
.like,
|
||||
.download {
|
||||
@@ -652,18 +656,18 @@ const jumpLink = (id, type) => {
|
||||
// transform: scale(0.99);
|
||||
// }
|
||||
&.play {
|
||||
background-color: $mainSecondaryColor;
|
||||
border-color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
.artists {
|
||||
:deep(.artist) {
|
||||
.name,
|
||||
.line {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,7 +707,7 @@ const jumpLink = (id, type) => {
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.n-tag {
|
||||
@@ -712,8 +716,8 @@ const jumpLink = (id, type) => {
|
||||
height: 18px;
|
||||
}
|
||||
.vip {
|
||||
color: $mainColor;
|
||||
background-color: $mainSecondaryColor;
|
||||
color: var(--main-color);
|
||||
background-color: var(--main-second-color);
|
||||
}
|
||||
.mv {
|
||||
cursor: pointer;
|
||||
@@ -744,7 +748,7 @@ const jumpLink = (id, type) => {
|
||||
.n-text {
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,7 +774,7 @@ const jumpLink = (id, type) => {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
|
||||
@@ -1,178 +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"
|
||||
size="18"
|
||||
:component="DeleteFour"
|
||||
@click.stop="music.removeSong(index)"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-scrollbar>
|
||||
</n-card>
|
||||
</CollapseTransition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { musicStore } from "@/store";
|
||||
import { DeleteFour } from "@icon-park/vue-next";
|
||||
import AllArtists from "./AllArtists.vue";
|
||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.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;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
color: #999;
|
||||
padding: 6px;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
background-color: var(--n-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-scrollbar-rail {
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -105,6 +105,7 @@ onMounted(() => {
|
||||
.pic {
|
||||
margin-right: 12px;
|
||||
border-radius: 8px;
|
||||
min-width: 48px;
|
||||
}
|
||||
.name {
|
||||
line-height: 1.6;
|
||||
@@ -113,7 +114,7 @@ onMounted(() => {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.artists {
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
|
||||
<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";
|
||||
|
||||
@@ -74,7 +73,7 @@ const router = useRouter();
|
||||
const props = defineProps({
|
||||
// 列表数据
|
||||
listData: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
@@ -115,6 +114,7 @@ const props = defineProps({
|
||||
color: #fff;
|
||||
padding: 0.5vw;
|
||||
background-color: #00000010;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 50%;
|
||||
transform: scale(0.8);
|
||||
@@ -126,6 +126,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;
|
||||
@@ -175,7 +176,7 @@ const props = defineProps({
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.by {
|
||||
@@ -185,7 +186,7 @@ const props = defineProps({
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.artists {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<div class="copyright">
|
||||
<div class="desc">
|
||||
<n-text class="name">SPlayer</n-text>
|
||||
<n-text class="name">{{ siteTitle }}</n-text>
|
||||
<n-text class="version" :depth="3">
|
||||
v {{ packageJson.version }}
|
||||
</n-text>
|
||||
@@ -53,6 +53,7 @@ import { GithubOne } from "@icon-park/vue-next";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
// 关于本站数据
|
||||
const siteTitle = import.meta.env.VITE_SITE_TITLE;
|
||||
const showAboutModal = ref(false);
|
||||
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<template>
|
||||
<n-modal
|
||||
class="add-playlist s-modal"
|
||||
v-model:show="addToPlaylistModel"
|
||||
v-model:show="addToPlaylistModal"
|
||||
preset="card"
|
||||
title="添加到歌单"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeAddToPlaylist"
|
||||
>
|
||||
<template #header>
|
||||
添加到歌单
|
||||
<n-tag
|
||||
round
|
||||
class="tag"
|
||||
type="primary"
|
||||
:style="{
|
||||
marginLeft: '12px',
|
||||
fontSize: '13px',
|
||||
transform: 'translateY(-2px)',
|
||||
cursor: 'pointer',
|
||||
}"
|
||||
:bordered="false"
|
||||
@click="createPlaylistRef.openCreatePlaylist()"
|
||||
>
|
||||
新建歌单
|
||||
</n-tag>
|
||||
</template>
|
||||
<n-space vertical class="list" v-if="user.getUserPlayLists.own[0]">
|
||||
<div
|
||||
class="item"
|
||||
v-for="item in user.getUserPlayLists.own"
|
||||
v-for="item in user.getUserPlayLists.own.slice(1)"
|
||||
:key="item"
|
||||
@click="addToPlayList(item.id, addToPlaylistId)"
|
||||
>
|
||||
@@ -31,16 +48,20 @@
|
||||
</n-space>
|
||||
<n-text v-else>歌单列表加载中</n-text>
|
||||
</n-modal>
|
||||
<!-- 新建歌单 -->
|
||||
<CreatePlaylist ref="createPlaylistRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { userStore } from "@/store";
|
||||
import CreatePlaylist from "./CreatePlaylist.vue";
|
||||
|
||||
const user = userStore();
|
||||
const createPlaylistRef = ref(null);
|
||||
|
||||
// 收藏到歌单数据
|
||||
const addToPlaylistModel = ref(false);
|
||||
const addToPlaylistModal = ref(false);
|
||||
const addToPlaylistId = ref(null);
|
||||
|
||||
// 收藏到歌单
|
||||
@@ -67,14 +88,14 @@ const openAddToPlaylist = (id) => {
|
||||
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
|
||||
user.setUserPlayLists();
|
||||
}
|
||||
addToPlaylistModel.value = true;
|
||||
addToPlaylistModal.value = true;
|
||||
addToPlaylistId.value = id;
|
||||
console.log("开启", addToPlaylistModel.value, addToPlaylistId.value);
|
||||
console.log("开启", addToPlaylistModal.value, addToPlaylistId.value);
|
||||
};
|
||||
|
||||
// 关闭收藏到歌单
|
||||
const closeAddToPlaylist = () => {
|
||||
addToPlaylistModel.value = false;
|
||||
addToPlaylistModal.value = false;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="cloudMatchModel"
|
||||
v-model:show="cloudMatchModal"
|
||||
preset="card"
|
||||
title="歌曲信息纠正"
|
||||
:bordered="false"
|
||||
@@ -65,7 +65,7 @@ const user = userStore();
|
||||
// 歌曲信息纠正数据
|
||||
const cloudDataLoad = inject("cloudDataLoad", null);
|
||||
const smallSongDataRef = ref(null);
|
||||
const cloudMatchModel = ref(false);
|
||||
const cloudMatchModal = ref(false);
|
||||
const cloudMatchBeforeData = ref(null);
|
||||
const cloudMatchId = ref(null);
|
||||
const cloudMatchValue = ref({
|
||||
@@ -102,7 +102,7 @@ const setCloudMatchBtn = (data) => {
|
||||
const openCloudMatch = (data) => {
|
||||
cloudMatchValue.value.sid = data.id;
|
||||
cloudMatchBeforeData.value = data;
|
||||
cloudMatchModel.value = true;
|
||||
cloudMatchModal.value = true;
|
||||
};
|
||||
|
||||
// 关闭歌曲纠正
|
||||
@@ -110,7 +110,7 @@ const closeCloudMatch = () => {
|
||||
cloudMatchBeforeData.value = null;
|
||||
cloudMatchId.value = null;
|
||||
cloudMatchValue.value.asid = null;
|
||||
cloudMatchModel.value = false;
|
||||
cloudMatchModal.value = false;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
75
src/components/DataModal/CreatePlaylist.vue
Normal file
75
src/components/DataModal/CreatePlaylist.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="createPlaylistShow"
|
||||
preset="card"
|
||||
title="新建歌单"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeCreatePlaylist"
|
||||
>
|
||||
<n-input
|
||||
style="margin-bottom: 12px"
|
||||
v-model:value="createName"
|
||||
type="text"
|
||||
placeholder="请输入新歌单标题"
|
||||
/>
|
||||
<n-checkbox v-model:checked="createPrivacy"> 设置为隐私歌单 </n-checkbox>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="closeCreatePlaylist"> 取消 </n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="createPlaylistBtn(createName, createPrivacy)"
|
||||
>
|
||||
新建
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { userStore } from "@/store";
|
||||
import { createPlaylist } from "@/api/playlist";
|
||||
|
||||
const user = userStore();
|
||||
|
||||
// 新建歌单数据
|
||||
const createPlaylistShow = ref(false);
|
||||
const createPrivacy = ref(false);
|
||||
const createName = ref(null);
|
||||
|
||||
// 新建歌单
|
||||
const createPlaylistBtn = (name, privacy = false) => {
|
||||
if (createName.value) {
|
||||
createPlaylist(name, privacy ? "10" : null).then((res) => {
|
||||
if (res.code === 200) {
|
||||
closeCreatePlaylist();
|
||||
$message.success("歌单新建成功");
|
||||
user.setUserPlayLists();
|
||||
} else {
|
||||
$message.error("歌单新建失败,请重试");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$message.info("请输入歌单名称");
|
||||
}
|
||||
};
|
||||
|
||||
// 开启新建歌单
|
||||
const openCreatePlaylist = () => {
|
||||
createPlaylistShow.value = true;
|
||||
};
|
||||
|
||||
// 取消新建歌单
|
||||
const closeCreatePlaylist = () => {
|
||||
createName.value = null;
|
||||
createPrivacy.value = false;
|
||||
createPlaylistShow.value = false;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openCreatePlaylist,
|
||||
});
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<n-modal
|
||||
class="s-modal downloadModel"
|
||||
v-model:show="downloadModel"
|
||||
class="s-modal downloadModal"
|
||||
v-model:show="downloadModal"
|
||||
preset="card"
|
||||
title="歌曲下载"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeDownloadModel"
|
||||
:on-after-leave="closeDownloadModal"
|
||||
>
|
||||
<Transition mode="out-in">
|
||||
<div v-if="songData">
|
||||
@@ -42,7 +42,7 @@
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="closeDownloadModel"> 取消 </n-button>
|
||||
<n-button @click="closeDownloadModal"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!downloadChoose"
|
||||
:loading="downloadStatus"
|
||||
@@ -75,7 +75,7 @@ const router = useRouter();
|
||||
const songId = ref(null);
|
||||
const songData = ref(null);
|
||||
const downloadStatus = ref(false);
|
||||
const downloadModel = ref(false);
|
||||
const downloadModal = ref(false);
|
||||
const downloadChoose = ref(null);
|
||||
const downloadLevel = ref(null);
|
||||
|
||||
@@ -98,7 +98,7 @@ const toSongDownload = (id, br, name) => {
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
closeDownloadModel();
|
||||
closeDownloadModal();
|
||||
downloadStatus.value = false;
|
||||
$message.success(name + " 下载完成");
|
||||
});
|
||||
@@ -108,7 +108,7 @@ const toSongDownload = (id, br, name) => {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
closeDownloadModel();
|
||||
closeDownloadModal();
|
||||
console.error("下载出现错误:" + err);
|
||||
$message.error("下载出现错误,请重试");
|
||||
});
|
||||
@@ -133,7 +133,7 @@ const getMusicDetailData = (id) => {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
closeDownloadModel();
|
||||
closeDownloadModal();
|
||||
console.error("歌曲信息获取出现错误:" + err);
|
||||
$message.error("歌曲信息获取出现错误,请重试");
|
||||
});
|
||||
@@ -200,7 +200,7 @@ const getSongSize = (data, type) => {
|
||||
};
|
||||
|
||||
// 开启歌曲下载
|
||||
const openDownloadModel = (data) => {
|
||||
const openDownloadModal = (data) => {
|
||||
if (user.userLogin) {
|
||||
if (
|
||||
router.currentRoute.value.name === "user-cloud" ||
|
||||
@@ -209,7 +209,7 @@ const openDownloadModel = (data) => {
|
||||
data?.pc
|
||||
) {
|
||||
songId.value = data.id;
|
||||
downloadModel.value = true;
|
||||
downloadModal.value = true;
|
||||
getMusicDetailData(data.id);
|
||||
} else {
|
||||
$message.error("该歌曲需使用黑胶会员下载");
|
||||
@@ -220,22 +220,22 @@ const openDownloadModel = (data) => {
|
||||
};
|
||||
|
||||
// 关闭歌曲下载
|
||||
const closeDownloadModel = () => {
|
||||
const closeDownloadModal = () => {
|
||||
songId.value = null;
|
||||
songData.value = null;
|
||||
downloadStatus.value = false;
|
||||
downloadModel.value = false;
|
||||
downloadModal.value = false;
|
||||
downloadChoose.value = null;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openDownloadModel,
|
||||
openDownloadModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.downloadModel {
|
||||
.downloadModal {
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
189
src/components/DataModal/LyricSetting.vue
Normal file
189
src/components/DataModal/LyricSetting.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<!-- 歌词设置 -->
|
||||
<n-modal
|
||||
:bordered="false"
|
||||
:z-index="10000"
|
||||
class="s-modal lyric-set"
|
||||
v-model:show="LyricSettingModal"
|
||||
preset="card"
|
||||
title="歌词设置"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<div class="set">
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放页快捷设置
|
||||
<span class="tip">关闭后需在设置页开启</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showLyricSetting" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示逐字歌词
|
||||
<span class="tip">是否在歌曲具有逐字歌词时显示,实验性功能</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showYrc" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示歌词翻译
|
||||
<span class="tip">是否在具有翻译歌词时显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showTransl" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示歌词音译
|
||||
<span class="tip">是否在具有音译歌词时显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showRoma" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示前奏等待
|
||||
<span class="tip">部分歌曲前奏可能存在显示错误</span>
|
||||
</div>
|
||||
<n-switch v-model:value="countDownShow" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
智能暂停滚动
|
||||
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lrcMousePause" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌词模糊
|
||||
<span class="tip">未播放或已播放歌词模糊显示,实验性功能</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lyricsBlur" :round="false" />
|
||||
</n-card>
|
||||
<n-space justify="center">
|
||||
<n-button
|
||||
class="more"
|
||||
size="large"
|
||||
strong
|
||||
secondary
|
||||
round
|
||||
@click="
|
||||
() => {
|
||||
LyricSettingModal = false;
|
||||
music.setBigPlayerState(false);
|
||||
router.push('/setting/player');
|
||||
}
|
||||
"
|
||||
>
|
||||
更多设置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { settingStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicStore } from "@/store";
|
||||
|
||||
const setting = settingStore();
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
|
||||
// 歌词设置弹窗
|
||||
const LyricSettingModal = ref(false);
|
||||
|
||||
// 设置数据
|
||||
const {
|
||||
showTransl,
|
||||
lyricsBlur,
|
||||
lrcMousePause,
|
||||
showYrc,
|
||||
showRoma,
|
||||
countDownShow,
|
||||
showLyricSetting,
|
||||
} = storeToRefs(setting);
|
||||
|
||||
// 开启歌词设置弹窗
|
||||
const openLyricSetting = () => {
|
||||
LyricSettingModal.value = true;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openLyricSetting,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.n-card {
|
||||
&.lyric-set {
|
||||
background-color: #ffffff40;
|
||||
color: #fff;
|
||||
.n-card-header {
|
||||
.n-card-header__main,
|
||||
.n-card-header__close {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.set {
|
||||
width: 100%;
|
||||
:deep(.set-item) {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
background-color: #ffffff40;
|
||||
border-color: transparent;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.n-switch {
|
||||
--n-box-shadow-focus: none;
|
||||
&.n-switch--active {
|
||||
.n-switch__rail {
|
||||
background-color: #ffffff78;
|
||||
}
|
||||
}
|
||||
.n-switch__rail {
|
||||
background-color: #ffffff24;
|
||||
}
|
||||
}
|
||||
.set {
|
||||
width: 200px;
|
||||
@media (max-width: 768px) {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.more {
|
||||
margin: 12px 0;
|
||||
color: #fff;
|
||||
background-color: #ffffff40;
|
||||
&:hover {
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
272
src/components/DataModal/PlayListDrawer.vue
Normal file
272
src/components/DataModal/PlayListDrawer.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
class="playlist-drawer"
|
||||
v-model:show="playListShow"
|
||||
:z-index="1"
|
||||
: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 :native-scrollbar="false" closable>
|
||||
<template #header>
|
||||
<div class="text">
|
||||
<n-text class="name">播放列表</n-text>
|
||||
<n-text class="num" :depth="3" v-if="music.getPlaylists.length > 0">
|
||||
{{ music.getPlaylists.length }} 首
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<n-text
|
||||
v-if="index !== music.persistData.playSongIndex"
|
||||
:depth="3"
|
||||
class="num"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</n-text>
|
||||
<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 { soundStop } from "@/utils/Player";
|
||||
import AllArtists from "@/components/DataList/AllArtists.vue";
|
||||
|
||||
const music = musicStore();
|
||||
|
||||
// 播放列表显隐
|
||||
const playListShow = ref(false);
|
||||
|
||||
// 改变播放索引
|
||||
const changeIndex = (index) => {
|
||||
try {
|
||||
if (music.persistData.playSongIndex !== index) {
|
||||
soundStop($player);
|
||||
music.persistData.playSongIndex = index;
|
||||
music.isLoadingSong = true;
|
||||
music.setPlayState(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("切换失败:" + err);
|
||||
$message.error("切换失败,请刷新后重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 监听播放列表显隐
|
||||
const timeOut = ref(null);
|
||||
watch(
|
||||
() => music.showPlayList,
|
||||
(val) => {
|
||||
playListShow.value = val;
|
||||
nextTick().then(() => {
|
||||
if (val && music.getPlaylists[0]) {
|
||||
const el = document.getElementById(
|
||||
`playlist${music.persistData.playSongIndex}`
|
||||
);
|
||||
if (el) {
|
||||
timeOut.value = setTimeout(() => {
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.num {
|
||||
font-size: 14px;
|
||||
&::before {
|
||||
content: "-";
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
div,
|
||||
.n-icon {
|
||||
color: var(--main-color);
|
||||
}
|
||||
:deep(span) {
|
||||
color: var(--main-color);
|
||||
}
|
||||
.right {
|
||||
.remove {
|
||||
color: var(--main-color);
|
||||
&: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: var(--main-color);
|
||||
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: var(--main-color);
|
||||
background-color: var(--n-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="playlistUpdateModel"
|
||||
v-model:show="playlistUpdateModal"
|
||||
preset="card"
|
||||
title="歌单编辑"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeUpdateModel"
|
||||
:on-after-leave="closeUpdateModal"
|
||||
>
|
||||
<n-form
|
||||
ref="playlistUpdateRef"
|
||||
@@ -42,7 +42,7 @@
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="closeUpdateModel"> 取消 </n-button>
|
||||
<n-button @click="closeUpdateModal"> 取消 </n-button>
|
||||
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<script setup>
|
||||
import { playlistUpdate } from "@/api/playlist";
|
||||
import { formRules } from "@/utils/formRules.js";
|
||||
import { formRules } from "@/utils/formRules";
|
||||
import { musicStore, userStore } from "@/store";
|
||||
|
||||
const { textRule } = formRules();
|
||||
@@ -61,7 +61,7 @@ const user = userStore();
|
||||
// 更新歌单数据
|
||||
const playlistUpdateId = ref(null);
|
||||
const playlistUpdateRef = ref(null);
|
||||
const playlistUpdateModel = ref(false);
|
||||
const playlistUpdateModal = ref(false);
|
||||
const playlistUpdateRules = {
|
||||
name: textRule,
|
||||
};
|
||||
@@ -86,7 +86,7 @@ const toUpdatePlayList = (e) => {
|
||||
console.log(res);
|
||||
if (res.code === 200) {
|
||||
$message.success("编辑成功");
|
||||
closeUpdateModel();
|
||||
closeUpdateModal();
|
||||
user.setUserPlayLists();
|
||||
} else {
|
||||
$message.error("编辑失败,请重试");
|
||||
@@ -113,24 +113,24 @@ const openSelect = () => {
|
||||
};
|
||||
|
||||
// 开启编辑歌单
|
||||
const openUpdateModel = (data) => {
|
||||
const openUpdateModal = (data) => {
|
||||
playlistUpdateValue.value = {
|
||||
name: data.name,
|
||||
desc: data.desc,
|
||||
tags: data.tags,
|
||||
};
|
||||
playlistUpdateId.value = data.id;
|
||||
playlistUpdateModel.value = true;
|
||||
playlistUpdateModal.value = true;
|
||||
};
|
||||
|
||||
// 关闭更新歌单弹窗
|
||||
const closeUpdateModel = () => {
|
||||
playlistUpdateModel.value = false;
|
||||
const closeUpdateModal = () => {
|
||||
playlistUpdateModal.value = false;
|
||||
playlistUpdateId.value = null;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openUpdateModel,
|
||||
openUpdateModal,
|
||||
});
|
||||
</script>
|
||||
@@ -2,12 +2,14 @@
|
||||
<nav>
|
||||
<div class="left">
|
||||
<div class="logo" @click="router.push('/')">
|
||||
<img src="/images/logo/favicon.svg" alt="logo" />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<n-icon size="22" :component="Left" @click="router.go(-1)" />
|
||||
<n-icon size="22" :component="Right" @click="router.go(1)" />
|
||||
<img :src="logoUrl" alt="logo" />
|
||||
</div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-show="!site.searchInputActive" class="controls">
|
||||
<n-icon size="22" :component="Left" @click="router.go(-1)" />
|
||||
<n-icon size="22" :component="Right" @click="router.go(1)" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="center">
|
||||
<router-link class="link" to="/">首页</router-link>
|
||||
@@ -18,19 +20,22 @@
|
||||
>
|
||||
<router-link class="link" to="/discover">发现</router-link>
|
||||
</n-dropdown>
|
||||
<n-dropdown
|
||||
trigger="hover"
|
||||
:options="userOptions"
|
||||
@select="menuSelect"
|
||||
>
|
||||
<n-dropdown trigger="hover" :options="userOptions" @select="menuSelect">
|
||||
<router-link class="link" to="/user">我的</router-link>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
<div class="right">
|
||||
<SearchInp />
|
||||
<!-- 移动端菜单 -->
|
||||
<n-dropdown trigger="click" :options="mbMenuOptions" @select="menuSelect">
|
||||
<n-button class="mb-menu" circle>
|
||||
<template #icon>
|
||||
<n-icon :component="HamburgerButton" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<!-- 下拉菜单 -->
|
||||
<n-dropdown
|
||||
class="dropdown"
|
||||
placement="bottom-end"
|
||||
:show="showDropdown"
|
||||
:show-arrow="true"
|
||||
@@ -44,7 +49,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' }"
|
||||
@@ -70,16 +76,23 @@ import {
|
||||
History,
|
||||
SunOne,
|
||||
Moon,
|
||||
HamburgerButton,
|
||||
HomeTwo,
|
||||
FindOne,
|
||||
Me,
|
||||
} from "@icon-park/vue-next";
|
||||
import { userStore, settingStore } from "@/store";
|
||||
import { userStore, settingStore, siteStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import AboutSite from "@/components/DataModel/AboutSite.vue";
|
||||
import AboutSite from "@/components/DataModal/AboutSite.vue";
|
||||
import SearchInp from "@/components/SearchInp/index.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const user = userStore();
|
||||
const site = siteStore();
|
||||
const setting = settingStore();
|
||||
const aboutSiteRef = ref(null);
|
||||
const timeOut = ref(null);
|
||||
const logoUrl = import.meta.env.VITE_SITE_LOGO;
|
||||
|
||||
// 下拉菜单显隐
|
||||
const showDropdown = ref(false);
|
||||
@@ -92,6 +105,19 @@ const closeDropdown = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => icon,
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// 用户数据模块
|
||||
const userDataRender = () => {
|
||||
return h(
|
||||
@@ -109,7 +135,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",
|
||||
}),
|
||||
@@ -139,7 +166,7 @@ const userDataRender = () => {
|
||||
type: "line",
|
||||
percentage:
|
||||
user.getUserOtherData.level.progress * 100,
|
||||
color: "#f55e55",
|
||||
color: setting.themeData.primaryColor,
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
@@ -171,8 +198,9 @@ const discoverOptions = ref([
|
||||
key: "/discover/artists",
|
||||
},
|
||||
]);
|
||||
const userOptions = ref(
|
||||
user.userLogin
|
||||
const userOptions = ref([]);
|
||||
const changeUserOptions = (val) => {
|
||||
userOptions.value = val
|
||||
? [
|
||||
{
|
||||
label: "我的歌单",
|
||||
@@ -200,8 +228,8 @@ const userOptions = ref(
|
||||
label: "登录账号",
|
||||
key: "/login",
|
||||
},
|
||||
]
|
||||
);
|
||||
];
|
||||
};
|
||||
const dropdownOptions = ref([
|
||||
{
|
||||
key: "header",
|
||||
@@ -238,28 +266,12 @@ const dropdownOptions = ref([
|
||||
{
|
||||
label: "播放历史",
|
||||
key: "history",
|
||||
icon: () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => h(History),
|
||||
}
|
||||
);
|
||||
},
|
||||
icon: renderIcon(h(History)),
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
key: "setting",
|
||||
icon: () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => h(SettingTwo),
|
||||
}
|
||||
);
|
||||
},
|
||||
icon: renderIcon(h(SettingTwo)),
|
||||
},
|
||||
{
|
||||
label: () => {
|
||||
@@ -285,19 +297,30 @@ const dropdownOptions = ref([
|
||||
{
|
||||
label: "关于本站",
|
||||
key: "about",
|
||||
icon: () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => h(Info),
|
||||
}
|
||||
);
|
||||
},
|
||||
icon: renderIcon(h(Info)),
|
||||
},
|
||||
]);
|
||||
|
||||
// 下拉框事件
|
||||
// 移动端菜单
|
||||
const mbMenuOptions = ref([
|
||||
{
|
||||
label: "首页",
|
||||
key: "/",
|
||||
icon: renderIcon(h(HomeTwo)),
|
||||
},
|
||||
{
|
||||
label: "发现",
|
||||
key: "/discover",
|
||||
icon: renderIcon(h(FindOne)),
|
||||
},
|
||||
{
|
||||
label: "我的",
|
||||
key: "/user",
|
||||
icon: renderIcon(h(Me)),
|
||||
},
|
||||
]);
|
||||
|
||||
// 下拉框点击事件
|
||||
const menuSelect = (key) => {
|
||||
router.push(key);
|
||||
};
|
||||
@@ -331,6 +354,10 @@ const dropdownSelect = (key) => {
|
||||
onPositiveClick: () => {
|
||||
user.userLogOut();
|
||||
$message.success("已退出登录");
|
||||
// 刷新页面
|
||||
timeOut.value = setTimeout(() => {
|
||||
document.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -346,6 +373,22 @@ const dropdownSelect = (key) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听登录状态变化
|
||||
watch(
|
||||
() => user.userLogin,
|
||||
(val) => {
|
||||
changeUserOptions(val);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
changeUserOptions(user.userLogin);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(timeOut.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -358,6 +401,17 @@ nav {
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.fade-enter-active {
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.left {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
@@ -371,27 +425,35 @@ nav {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 12px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@media (max-width: 520px) {
|
||||
display: none;
|
||||
}
|
||||
.n-icon {
|
||||
margin: 0 4px;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--n-border-color);
|
||||
@media (min-width: 640px) {
|
||||
&:hover {
|
||||
background-color: var(--n-border-color);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
@@ -418,8 +480,8 @@ nav {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $mainColor;
|
||||
color: var(--n-color);
|
||||
background-color: var(--main-color);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
@@ -427,8 +489,8 @@ nav {
|
||||
}
|
||||
|
||||
.router-link-active {
|
||||
background-color: $mainColor;
|
||||
color: var(--n-color);
|
||||
background-color: var(--main-color);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
.right {
|
||||
@@ -438,6 +500,10 @@ nav {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@media (max-width: 520px) {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
}
|
||||
.avatar {
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
@@ -446,6 +512,13 @@ nav {
|
||||
box-shadow: 0 4px 12px -2px rgb(0 0 0 / 10%);
|
||||
cursor: pointer;
|
||||
}
|
||||
.mb-menu {
|
||||
margin-left: 12px;
|
||||
display: none;
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,6 +62,10 @@ const pageSizes = ref([
|
||||
label: "50条/页",
|
||||
value: 50,
|
||||
},
|
||||
{
|
||||
label: "100条/页",
|
||||
value: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
// 每页个数数据变化
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<script setup>
|
||||
import { getNewAlbum } from "@/api/home";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getLongTime } from "@/utils/timeTools.js";
|
||||
import { getLongTime } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -66,7 +66,7 @@ onMounted(() => {
|
||||
margin-left: 6px;
|
||||
}
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ onMounted(() => {
|
||||
margin-left: 6px;
|
||||
}
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #00000040;
|
||||
-webkit-backdrop-filter: blur(80px);
|
||||
backdrop-filter: blur(80px);
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -189,8 +190,10 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: #ffffff30;
|
||||
@media (min-width: 640px) {
|
||||
&:hover {
|
||||
background-color: #ffffff30;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<script setup>
|
||||
import { getPersonalized } from "@/api/home";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber } from "@/utils/timeTools.js";
|
||||
import { formatNumber } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -66,7 +66,7 @@ onMounted(() => {
|
||||
margin-left: 6px;
|
||||
}
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
<template>
|
||||
<Transition name="up">
|
||||
<div
|
||||
v-show="music.showBigPlayer"
|
||||
v-if="music.showBigPlayer"
|
||||
class="bplayer"
|
||||
:style="
|
||||
music.getPlaySongData
|
||||
:style="[
|
||||
music.getPlaySongData && setting.backgroundImageShow === 'blur'
|
||||
? 'background-image: url(' +
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
|
||||
'?param=50y50)'
|
||||
: ''
|
||||
"
|
||||
: '',
|
||||
`backgroundColor: ${site.songPicColor}`,
|
||||
]"
|
||||
>
|
||||
<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="setting.backgroundImageShow === 'blur' ? 'gray blur' : 'gray'"
|
||||
/>
|
||||
<div class="icon-menu">
|
||||
<div class="menu-left">
|
||||
<div v-if="setting.showLyricSetting" class="icon">
|
||||
<n-icon
|
||||
class="setting"
|
||||
size="30"
|
||||
:component="SettingsRound"
|
||||
@click="LyricSettingRef.openLyricSetting()"
|
||||
/>
|
||||
</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'
|
||||
"
|
||||
@@ -50,7 +69,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'">
|
||||
@@ -82,81 +102,22 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
setting.playerStyle === 'cover'
|
||||
? 'lrc-all cover'
|
||||
: 'lrc-all record'
|
||||
"
|
||||
v-if="music.getPlaySongLyric[0]"
|
||||
:style="
|
||||
setting.lyricsPosition === 'center'
|
||||
? { textAlign: 'center', paddingRight: '0' }
|
||||
: null
|
||||
"
|
||||
<RollingLyrics
|
||||
@mouseenter="
|
||||
lrcMouseStatus = setting.lrcMousePause ? true : false
|
||||
"
|
||||
@mouseleave="lrcAllLeave"
|
||||
>
|
||||
<!-- 提示文本 -->
|
||||
<div class="tip">
|
||||
<n-text>点击选中的歌词以调整播放进度</n-text>
|
||||
</div>
|
||||
<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="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.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>
|
||||
@lrcTextClick="lrcTextClick"
|
||||
/>
|
||||
<div
|
||||
:class="menuShow ? 'menu show' : 'menu'"
|
||||
v-show="setting.playerStyle === 'record'"
|
||||
>
|
||||
<n-icon
|
||||
v-if="music.getPlaySongTransl"
|
||||
:class="setting.getShowTransl ? 'open' : ''"
|
||||
:class="setting.showTransl ? 'open' : ''"
|
||||
:component="GTranslateFilled"
|
||||
@click="setting.setShowTransl(!setting.getShowTransl)"
|
||||
@click="setting.setShowTransl(!setting.showTransl)"
|
||||
/>
|
||||
<n-icon
|
||||
class="open"
|
||||
@@ -168,9 +129,10 @@
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvas">
|
||||
<canvas v-if="setting.musicFrequency" class="avBars" ref="avBars" />
|
||||
</div>
|
||||
<!-- 音乐频谱 -->
|
||||
<!-- <Spectrum v-if="setting.musicFrequency" /> -->
|
||||
<!-- 歌词设置 -->
|
||||
<LyricSetting ref="LyricSettingRef" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -182,38 +144,34 @@ import {
|
||||
MessageFilled,
|
||||
FullscreenRound,
|
||||
FullscreenExitRound,
|
||||
SettingsRound,
|
||||
} from "@vicons/material";
|
||||
import { musicStore, settingStore } from "@/store";
|
||||
import { musicStore, settingStore, siteStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import MusicFrequency from "@/utils/MusicFrequency.js";
|
||||
import { setSeek } from "@/utils/Player";
|
||||
import PlayerRecord from "./PlayerRecord.vue";
|
||||
import PlayerCover from "./PlayerCover.vue";
|
||||
import RollingLyrics from "./RollingLyrics.vue";
|
||||
// import Spectrum from "./Spectrum.vue";
|
||||
import LyricSetting from "@/components/DataModal/LyricSetting.vue";
|
||||
import screenfull from "screenfull";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
const site = siteStore();
|
||||
const setting = settingStore();
|
||||
|
||||
// 工具栏显隐
|
||||
const menuShow = ref(false);
|
||||
|
||||
// 音乐频谱
|
||||
const avBars = ref(null);
|
||||
const musicFrequency = ref(null);
|
||||
// 歌词设置弹窗
|
||||
const LyricSettingRef = ref(null);
|
||||
|
||||
// 歌词模糊数值
|
||||
const getFilter = (lrcIndex, index) => {
|
||||
if (lrcIndex >= index) {
|
||||
return lrcIndex - index;
|
||||
} else {
|
||||
return index - lrcIndex;
|
||||
}
|
||||
};
|
||||
|
||||
// 点击歌词跳转
|
||||
const jumpTime = (time) => {
|
||||
// 歌词文本点击事件
|
||||
const lrcTextClick = (time) => {
|
||||
if (typeof $player !== "undefined") setSeek($player, time);
|
||||
music.setPlayState(true);
|
||||
lrcMouseStatus.value = false;
|
||||
if ($player) $player.currentTime = time;
|
||||
};
|
||||
|
||||
// 鼠标移出歌词区域
|
||||
@@ -224,6 +182,7 @@ const lrcAllLeave = () => {
|
||||
};
|
||||
|
||||
// 全屏切换
|
||||
const timeOut = ref(null);
|
||||
const screenfullIcon = shallowRef(FullscreenRound);
|
||||
const screenfullChange = () => {
|
||||
if (screenfull.isEnabled) {
|
||||
@@ -232,7 +191,7 @@ const screenfullChange = () => {
|
||||
? FullscreenRound
|
||||
: FullscreenExitRound;
|
||||
// 延迟一段时间执行列表滚动
|
||||
setTimeout(() => {
|
||||
timeOut.value = setTimeout(() => {
|
||||
lrcMouseStatus.value = false;
|
||||
lyricsScroll(music.getPlaySongLyricIndex);
|
||||
}, 500);
|
||||
@@ -253,44 +212,58 @@ const toComment = () => {
|
||||
// 歌词滚动
|
||||
const lyricsScroll = (index) => {
|
||||
const type = setting.lyricsBlock;
|
||||
const el = document.getElementById(
|
||||
`lrc${type === "center" ? index : index - 2}`
|
||||
);
|
||||
const lrcType =
|
||||
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? "lrc" : "yrc";
|
||||
const el = document.getElementById(lrcType + index);
|
||||
if (el && !lrcMouseStatus.value) {
|
||||
el.scrollIntoView({
|
||||
const container = el.parentElement;
|
||||
const containerHeight = container.clientHeight;
|
||||
// 调整滚动的距离
|
||||
const scrollDistance =
|
||||
el.offsetTop -
|
||||
container.offsetTop -
|
||||
(type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 80);
|
||||
container.scrollTo({
|
||||
top: scrollDistance,
|
||||
behavior: "smooth",
|
||||
block: type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (setting.musicFrequency) {
|
||||
$player.crossOrigin = "anonymous";
|
||||
musicFrequency.value = new MusicFrequency(
|
||||
avBars.value,
|
||||
$player,
|
||||
null,
|
||||
50,
|
||||
null,
|
||||
null,
|
||||
5
|
||||
);
|
||||
musicFrequency.value.drawSpectrum();
|
||||
// 改变 PWA 应用标题栏颜色
|
||||
const changePwaColor = () => {
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (music.showBigPlayer) {
|
||||
themeColorMeta.setAttribute("content", site.songPicColor);
|
||||
} else {
|
||||
if (setting.getSiteTheme === "light") {
|
||||
themeColorMeta.setAttribute("content", "#ffffff");
|
||||
} else if (setting.getSiteTheme === "dark") {
|
||||
themeColorMeta.setAttribute("content", "#18181c");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick().then(() => {
|
||||
// 滚动歌词
|
||||
lyricsScroll(music.getPlaySongLyricIndex);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(timeOut.value);
|
||||
});
|
||||
|
||||
// 监听页面是否打开
|
||||
watch(
|
||||
() => music.showBigPlayer,
|
||||
(val) => {
|
||||
changePwaColor();
|
||||
if (val) {
|
||||
console.log("开启播放器", music.getPlaySongLyricIndex);
|
||||
nextTick(() => {
|
||||
nextTick().then(() => {
|
||||
music.showPlayList = false;
|
||||
lyricsScroll(music.getPlaySongLyricIndex);
|
||||
});
|
||||
}
|
||||
@@ -302,6 +275,12 @@ watch(
|
||||
() => music.getPlaySongLyricIndex,
|
||||
(val) => lyricsScroll(val)
|
||||
);
|
||||
|
||||
// 监听主题色改变
|
||||
watch(
|
||||
() => site.songPicColor,
|
||||
() => changePwaColor()
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -354,35 +333,61 @@ watch(
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #00000060;
|
||||
background-color: #00000030;
|
||||
-webkit-backdrop-filter: blur(80px);
|
||||
backdrop-filter: blur(80px);
|
||||
z-index: -1;
|
||||
&.blur {
|
||||
background-color: #00000060;
|
||||
}
|
||||
}
|
||||
.close,
|
||||
.screenfull {
|
||||
.icon-menu {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
opacity: 0.3;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 2;
|
||||
&:hover {
|
||||
background-color: #ffffff20;
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.screenfull {
|
||||
right: 80px;
|
||||
padding: 2px;
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
.menu-right {
|
||||
.icon {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.all {
|
||||
@@ -397,6 +402,12 @@ watch(
|
||||
.left {
|
||||
padding-right: 0;
|
||||
transform: translateX(25vh);
|
||||
@media (max-width: 1200px) {
|
||||
transform: translateX(22.2vh);
|
||||
}
|
||||
@media (min-width: 769px) and (max-width: 869px) {
|
||||
transform: translateX(20.1vh);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.left {
|
||||
@@ -438,6 +449,7 @@ watch(
|
||||
height: 40px;
|
||||
border-radius: 25px;
|
||||
background-color: #ffffff20;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -468,7 +480,7 @@ watch(
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
.data {
|
||||
padding: 0 20px;
|
||||
padding: 0 3vh;
|
||||
margin-bottom: 8px;
|
||||
.name {
|
||||
font-size: 3vh;
|
||||
@@ -495,103 +507,9 @@ watch(
|
||||
}
|
||||
}
|
||||
}
|
||||
.lrc-all {
|
||||
margin-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;
|
||||
}
|
||||
&:hover {
|
||||
.lrc-text {
|
||||
&.blur {
|
||||
filter: blur(0) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
.lrc {
|
||||
opacity: 0.4;
|
||||
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: left center;
|
||||
cursor: pointer;
|
||||
.lrc-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.35s ease-in-out;
|
||||
transform: scale(0.95);
|
||||
transform-origin: left center;
|
||||
.lyric {
|
||||
transition: all 0.3s;
|
||||
// font-size: 2.4vh;
|
||||
}
|
||||
.lyric-fy {
|
||||
margin-top: 2px;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.8;
|
||||
// font-size: 2vh;
|
||||
}
|
||||
}
|
||||
&.on {
|
||||
opacity: 1;
|
||||
.lrc-text {
|
||||
transform: scale(1.05);
|
||||
.lyric {
|
||||
font-weight: bold;
|
||||
text-shadow: 0px 0px 30px #ffffff40;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (min-width: 768px) {
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu {
|
||||
opacity: 0;
|
||||
padding: 0 20px;
|
||||
padding: 0 3vh;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
91
src/components/Player/CountDown.vue
Normal file
91
src/components/Player/CountDown.vue
Normal 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 > 1 &&
|
||||
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>
|
||||
@@ -11,6 +11,16 @@
|
||||
"
|
||||
alt="cover"
|
||||
/>
|
||||
<img
|
||||
class="shadow"
|
||||
:src="
|
||||
music.getPlaySongData
|
||||
? music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
|
||||
'?param=1024y1024'
|
||||
: '/images/pic/default.png'
|
||||
"
|
||||
alt="shadow"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="data">
|
||||
@@ -61,11 +71,14 @@
|
||||
</div>
|
||||
<div class="time">
|
||||
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
|
||||
<n-slider
|
||||
v-model:value="music.getPlaySongTime.barMoveDistance"
|
||||
class="progress"
|
||||
:step="0.01"
|
||||
@update:value="songTimeSliderUpdate"
|
||||
<vue-slider
|
||||
v-model="music.getPlaySongTime.barMoveDistance"
|
||||
@drag-start="music.setPlayState(false)"
|
||||
@drag-end="sliderDragEnd"
|
||||
@click.stop="
|
||||
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
|
||||
"
|
||||
:tooltip="'none'"
|
||||
/>
|
||||
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
|
||||
</div>
|
||||
@@ -98,6 +111,7 @@
|
||||
v-else
|
||||
class="dislike"
|
||||
size="20"
|
||||
:style="!user.userLogin ? 'opacity: 0.2;pointer-events: none;' : null"
|
||||
:component="ThumbDownRound"
|
||||
@click="music.setFmDislike(music.getPersonalFmData.id)"
|
||||
/>
|
||||
@@ -143,18 +157,25 @@ import {
|
||||
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
|
||||
import { musicStore, userStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { setSeek } from "@/utils/Player";
|
||||
import AllArtists from "@/components/DataList/AllArtists.vue";
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
const user = userStore();
|
||||
|
||||
const canvas = ref(null);
|
||||
|
||||
// 歌曲进度条更新
|
||||
const sliderDragEnd = () => {
|
||||
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
|
||||
music.setPlayState(true);
|
||||
};
|
||||
const songTimeSliderUpdate = (val) => {
|
||||
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
|
||||
$player.currentTime = (music.getPlaySongTime.duration / 100) * val;
|
||||
if (typeof $player !== "undefined" && music.getPlaySongTime?.duration) {
|
||||
const currentTime = (music.getPlaySongTime.duration / 100) * val;
|
||||
setSeek($player, currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面跳转
|
||||
@@ -170,11 +191,11 @@ const routerJump = (url, query) => {
|
||||
<style lang="scss" scoped>
|
||||
.cover {
|
||||
.pic {
|
||||
position: relative;
|
||||
width: 50vh;
|
||||
height: 50vh;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 40px 14px rgb(0 0 0 / 20%);
|
||||
z-index: 1;
|
||||
// overflow: hidden;
|
||||
@media (max-width: 1200px) {
|
||||
width: 44vh;
|
||||
height: 44vh;
|
||||
@@ -186,6 +207,19 @@ const routerJump = (url, query) => {
|
||||
.album {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.shadow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(16px) opacity(0.6);
|
||||
transform: scale(0.92, 0.96);
|
||||
z-index: -1;
|
||||
background-size: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
@@ -270,13 +304,26 @@ const routerJump = (url, query) => {
|
||||
span {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.progress {
|
||||
margin: 0 12px;
|
||||
--n-handle-size: 12px;
|
||||
--n-fill-color: #fff;
|
||||
--n-fill-color-hover: #fff;
|
||||
--n-rail-color: #ffffff20;
|
||||
--n-rail-color-hover: #ffffff30;
|
||||
.vue-slider {
|
||||
margin: 0 10px;
|
||||
width: 100% !important;
|
||||
transform: translateY(-1px);
|
||||
cursor: pointer;
|
||||
:deep(.vue-slider-rail) {
|
||||
background-color: #ffffff20;
|
||||
border-radius: 25px;
|
||||
.vue-slider-process {
|
||||
background-color: #fff;
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.vue-slider-dot-handle-focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
|
||||
@@ -190,6 +190,7 @@ const music = musicStore();
|
||||
height: 68%;
|
||||
border-radius: 50%;
|
||||
background-color: #00000050;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
transition: all 0.5s;
|
||||
.n-icon {
|
||||
|
||||
327
src/components/Player/RollingLyrics.vue
Normal file
327
src/components/Player/RollingLyrics.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<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
|
||||
v-if="setting.countDownShow"
|
||||
: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.hasLrcTran &&
|
||||
setting.showTransl &&
|
||||
item.tran
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
|
||||
class="lyric-fy"
|
||||
>
|
||||
{{ item.tran }}</span
|
||||
>
|
||||
<span
|
||||
v-show="
|
||||
music.getPlaySongLyric.hasLrcRoma && setting.showRoma && item.roma
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
|
||||
class="lyric-roma"
|
||||
>
|
||||
{{ item.roma }}</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': `${Math.max(v.duration - 0.15, 0.1)}s`,
|
||||
}"
|
||||
:class="
|
||||
music.getPlaySongLyricIndex === index &&
|
||||
music.getPlaySongTime.currentTime + 0.15 > v.time
|
||||
? 'text fill'
|
||||
: 'text'
|
||||
"
|
||||
>
|
||||
{{ v.content }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-show="
|
||||
music.getPlaySongLyric.hasYrcTran &&
|
||||
setting.showTransl &&
|
||||
item.tran
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
|
||||
class="lyric-fy"
|
||||
>
|
||||
{{ item.tran }}
|
||||
</span>
|
||||
<span
|
||||
v-show="
|
||||
music.getPlaySongLyric.hasYrcRoma && setting.showRoma && item.roma
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
|
||||
class="lyric-roma"
|
||||
>
|
||||
{{ item.roma }}
|
||||
</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,
|
||||
.yrc-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.3;
|
||||
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: 0 0 40px rgb(255 255 255 / 40%);
|
||||
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,
|
||||
.lyric-roma {
|
||||
margin-top: 4px;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
&.on {
|
||||
opacity: 1;
|
||||
.lrc-text {
|
||||
transform: scale(1.05);
|
||||
.lyric {
|
||||
text-shadow: 0 0 40px rgb(255 255 255 / 40%);
|
||||
}
|
||||
}
|
||||
.yrc-text {
|
||||
transform: scale(1.05);
|
||||
.lyric {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (min-width: 768px) {
|
||||
background-color: #ffffff20;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
.yrc {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
40
src/components/Player/Spectrum.vue
Normal file
40
src/components/Player/Spectrum.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="spectrum">
|
||||
<canvas class="avBars" ref="canvasRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { musicStore } from "@/store";
|
||||
|
||||
const music = musicStore();
|
||||
const canvasRef = ref(null);
|
||||
|
||||
const drawSpectrum = (data) => {
|
||||
canvasRef.value.width =
|
||||
document.body.clientWidth >= 1600 ? 1600 : document.body.clientWidth;
|
||||
canvasRef.value.height = 80;
|
||||
const ctx = canvasRef.value.getContext("2d");
|
||||
const barWidth = 2;
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
||||
for (let i = 0; i < 360; i++) {
|
||||
const barHeight = (data[i] / 255) * canvasRef.value.height;
|
||||
const x = i * (barWidth * 2);
|
||||
const y = canvasRef.value.height - barHeight;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(x, y, barWidth, barHeight);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => music.spectrumsData.data,
|
||||
(val) => drawSpectrum(val)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.spectrum {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,209 +1,239 @@
|
||||
<template>
|
||||
<n-card
|
||||
:class="
|
||||
music.getPlaylists[0] && music.showPlayBar ? 'player show' : 'player'
|
||||
"
|
||||
content-style="padding: 0"
|
||||
>
|
||||
<div class="slider">
|
||||
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
|
||||
<n-slider
|
||||
v-model:value="music.getPlaySongTime.barMoveDistance"
|
||||
class="progress"
|
||||
:step="0.01"
|
||||
:tooltip="false"
|
||||
@update:value="songTimeSliderUpdate"
|
||||
@click.stop
|
||||
/>
|
||||
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
|
||||
</div>
|
||||
<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="name">
|
||||
<div
|
||||
class="song text-hidden"
|
||||
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
|
||||
>
|
||||
{{
|
||||
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲"
|
||||
}}
|
||||
<Transition name="show">
|
||||
<n-card
|
||||
v-show="music.getPlaylists[0] && music.showPlayBar"
|
||||
class="player"
|
||||
content-style="padding: 0"
|
||||
>
|
||||
<div class="slider">
|
||||
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
|
||||
<vue-slider
|
||||
v-model="music.getPlaySongTime.barMoveDistance"
|
||||
@drag-start="music.setPlayState(false)"
|
||||
@drag-end="sliderDragEnd"
|
||||
@click.stop="
|
||||
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
|
||||
"
|
||||
:tooltip="'active'"
|
||||
:use-keyboard="false"
|
||||
>
|
||||
<template v-slot:tooltip>
|
||||
<div class="slider-tooltip">
|
||||
{{
|
||||
getSongPlayingTime(
|
||||
(music.getPlaySongTime.duration / 100) *
|
||||
music.getPlaySongTime.barMoveDistance
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</vue-slider>
|
||||
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
|
||||
</div>
|
||||
<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
|
||||
? 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>
|
||||
</div>
|
||||
<audio
|
||||
ref="player"
|
||||
:autoplay="music.getPlayState"
|
||||
@timeupdate="songUpdate"
|
||||
@play="songPlay"
|
||||
@pause="songPause"
|
||||
@canplay="songCanplay"
|
||||
@error="songError"
|
||||
@ended="music.setPlaySongIndex('next')"
|
||||
:src="music.getPlaySongLink"
|
||||
></audio>
|
||||
</n-card>
|
||||
</n-card>
|
||||
</Transition>
|
||||
<!-- 播放列表 -->
|
||||
<PlayListDrawer ref="PlayListDrawerRef" />
|
||||
<!-- 添加到歌单 -->
|
||||
<AddPlaylist ref="addPlayListRef" />
|
||||
<!-- 播放器 -->
|
||||
<BigPlayer />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkMusicCanUse, getMusicUrl, getMusicLyric } from "@/api/song";
|
||||
import {
|
||||
checkMusicCanUse,
|
||||
getMusicUrl,
|
||||
getMusicNumUrl,
|
||||
getMusicNewLyric,
|
||||
} from "@/api/song";
|
||||
import { NIcon } from "naive-ui";
|
||||
import {
|
||||
KeyboardArrowUpFilled,
|
||||
MusicNoteFilled,
|
||||
PlayCircleFilled,
|
||||
PauseCircleFilled,
|
||||
SkipNextRound,
|
||||
@@ -220,185 +250,114 @@ import {
|
||||
} from "@vicons/material";
|
||||
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicStore, settingStore } from "@/store";
|
||||
import { musicStore, settingStore, siteStore } from "@/store";
|
||||
import {
|
||||
createSound,
|
||||
setVolume,
|
||||
setSeek,
|
||||
fadePlayOrPause,
|
||||
} from "@/utils/Player";
|
||||
import { getSongPlayingTime } from "@/utils/timeTools";
|
||||
import { useRouter } from "vue-router";
|
||||
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import VueSlider from "vue-slider-component";
|
||||
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
|
||||
import PlayListDrawer from "@/components/DataModal/PlayListDrawer.vue";
|
||||
import AllArtists from "@/components/DataList/AllArtists.vue";
|
||||
import PlayList from "@/components/DataList/PlayList.vue";
|
||||
import ColorThief from "colorthief";
|
||||
import BigPlayer from "./BigPlayer.vue";
|
||||
import debounce from "@/utils/debounce";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
|
||||
const router = useRouter();
|
||||
const setting = settingStore();
|
||||
const music = musicStore();
|
||||
const site = siteStore();
|
||||
const { persistData } = storeToRefs(music);
|
||||
const addPlayListRef = ref(null);
|
||||
const PlayListDrawerRef = ref(null);
|
||||
|
||||
// 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 专享,仅可试听");
|
||||
}
|
||||
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
|
||||
});
|
||||
// 获取歌词
|
||||
getMusicLyric(id).then((res) => {
|
||||
music.setPlaySongLyric(res);
|
||||
});
|
||||
} else {
|
||||
console.log("无法播放");
|
||||
if (music.getPlayState && $player) {
|
||||
$message.error("当前歌曲无法播放,已跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 歌曲进度更新事件
|
||||
const songUpdate = (e) => {
|
||||
const currentTime = e.target.currentTime;
|
||||
const duration = e.target.duration;
|
||||
music.setPlaySongTime({ currentTime, duration });
|
||||
};
|
||||
|
||||
// 歌曲缓冲完毕
|
||||
const songCanplay = () => {
|
||||
console.log("缓冲完毕", music.getPlayState);
|
||||
if (music.getPlayState && $player) {
|
||||
music.setPlayState(true);
|
||||
songInOrOut("play");
|
||||
}
|
||||
};
|
||||
|
||||
// 歌曲开始播放
|
||||
const songPlay = () => {
|
||||
if (!music.getPlaySongData) {
|
||||
$message.error("音乐数据获取失败");
|
||||
return false;
|
||||
}
|
||||
music.setPlayState(true);
|
||||
// 兼容 mediaSession
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: music.getPlaySongData.name,
|
||||
artist: music.getPlaySongData.artist[0].name,
|
||||
album: music.getPlaySongData.album.name,
|
||||
artwork: [
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=96y96",
|
||||
sizes: "96x96",
|
||||
},
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=128y128",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=192y192",
|
||||
sizes: "192x192",
|
||||
},
|
||||
],
|
||||
// 免费或无版权
|
||||
else {
|
||||
checkMusicCanUse(id).then((res) => {
|
||||
if (res.success) {
|
||||
console.log("当前歌曲可用");
|
||||
if (!pc && (fee === 1 || fee === 4))
|
||||
$message.info("当前歌曲为 VIP 专享,仅可试听");
|
||||
// 获取音乐地址
|
||||
getMusicUrl(id, level).then((res) => {
|
||||
player.value = createSound(
|
||||
res.data[0].url.replace(/^http:/, "https:")
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (useUnmServerHas && setting.useUnmServer) {
|
||||
getMusicNumUrlData(id);
|
||||
} else {
|
||||
$message.warning("当前歌曲播放失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 获取歌词
|
||||
getMusicNewLyric(id).then((res) => {
|
||||
music.setPlaySongLyric(res);
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () => {
|
||||
} catch (err) {
|
||||
if (music.getPlaylists[0] && music.getPlayState) {
|
||||
console.log("当前歌曲所有音源匹配失败:" + err);
|
||||
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 网易云解灰
|
||||
const getMusicNumUrlData = (id) => {
|
||||
getMusicNumUrl(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
|
||||
player.value = createSound(res.data.url.replace(/^http:/, ""));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("解灰失败:" + err);
|
||||
$message.warning("当前歌曲解灰失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () => {
|
||||
music.setPlaySongIndex("prev");
|
||||
});
|
||||
}
|
||||
$message.info(
|
||||
music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name,
|
||||
{
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(MusicNoteFilled),
|
||||
}),
|
||||
}
|
||||
);
|
||||
// 写入播放历史
|
||||
music.setPlayHistory(music.getPlaySongData);
|
||||
// 更改页面标题
|
||||
window.document.title = music.getPlaySongData.name + " - SPlayer";
|
||||
};
|
||||
|
||||
// 音乐渐入渐出
|
||||
const isFading = ref(false);
|
||||
const songInOrOut = (type) => {
|
||||
if (isFading.value) {
|
||||
return;
|
||||
}
|
||||
isFading.value = true;
|
||||
|
||||
if (type === "play") {
|
||||
let volume = 0;
|
||||
$player.play();
|
||||
const interval = setInterval(() => {
|
||||
// 如果音量已经到达当前音量,则停止渐入
|
||||
if (volume >= persistData.value.playVolume) {
|
||||
clearInterval(interval);
|
||||
isFading.value = false;
|
||||
return;
|
||||
}
|
||||
// 增加音量
|
||||
volume += 0.1;
|
||||
if (volume > persistData.value.playVolume) {
|
||||
volume = persistData.value.playVolume;
|
||||
}
|
||||
$player.volume = volume;
|
||||
}, 30);
|
||||
} else if (type === "pause") {
|
||||
let volume = persistData.value.playVolume;
|
||||
const interval = setInterval(() => {
|
||||
// 如果音量已经到达最小值,则停止渐出
|
||||
if (volume <= 0) {
|
||||
clearInterval(interval);
|
||||
$player.pause();
|
||||
isFading.value = false;
|
||||
return;
|
||||
}
|
||||
// 减小音量
|
||||
volume -= 0.1;
|
||||
if (volume < 0) {
|
||||
volume = 0;
|
||||
}
|
||||
$player.volume = volume;
|
||||
}, 30);
|
||||
}
|
||||
};
|
||||
|
||||
// 歌曲暂停
|
||||
const songPause = () => {
|
||||
console.log("音乐暂停");
|
||||
if (!$player.ended) music.setPlayState(false);
|
||||
// 更改页面标题
|
||||
window.document.title = "SPlayer";
|
||||
};
|
||||
|
||||
// 歌曲进度条更新
|
||||
const songTimeSliderUpdate = (val) => {
|
||||
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
|
||||
$player.currentTime = (music.getPlaySongTime.duration / 100) * val;
|
||||
const sliderDragEnd = () => {
|
||||
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
|
||||
music.setPlayState(true);
|
||||
};
|
||||
|
||||
// 歌曲播放失败事件
|
||||
const songError = () => {
|
||||
console.error("歌曲播放失败");
|
||||
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData.id);
|
||||
if (music.getPlayState) songInOrOut("play");
|
||||
const songTimeSliderUpdate = (val) => {
|
||||
if (player.value && music.getPlaySongTime?.duration) {
|
||||
const currentTime = (music.getPlaySongTime.duration / 100) * val;
|
||||
setSeek(player.value, currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
// 静音事件
|
||||
@@ -411,27 +370,55 @@ const volumeMute = () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 获取音乐数据
|
||||
if (music.getPlaylists[0] && music.getPlaySongData)
|
||||
getPlaySongData(music.getPlaySongData.id);
|
||||
// 挂载播放器
|
||||
window.$player = player.value;
|
||||
// 恢复上次播放进度
|
||||
if (music.getPlaySongTime && music.getPlaySongTime.currentTime) {
|
||||
$player.currentTime = music.getPlaySongTime.currentTime;
|
||||
// 歌曲更换事件
|
||||
const songChange = debounce(500, (val) => {
|
||||
if (val === undefined) {
|
||||
window.document.title =
|
||||
sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
|
||||
}
|
||||
// 加载数据
|
||||
getPlaySongData(val);
|
||||
getPicColor(val?.album.picUrl);
|
||||
});
|
||||
|
||||
// 获取封面图主色
|
||||
const getPicColor = (url) => {
|
||||
if (!url) return false;
|
||||
const imgUrl = url.replace(/^http:/, "https:") + "?param=50y50";
|
||||
const img = new Image();
|
||||
fetch(imgUrl)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
img.src = URL.createObjectURL(blob);
|
||||
img.addEventListener("load", async () => {
|
||||
const colorThief = new ColorThief();
|
||||
const color = await colorThief.getColor(img);
|
||||
console.log(`当前封面主色:rgb(${color.join(",")})`);
|
||||
site.songPicColor = `rgb(${color.join(",")})`;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("图像处理出错:" + err);
|
||||
site.songPicColor = "rgb(128,128,128)";
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载方法
|
||||
window.$getPlaySongData = getPlaySongData;
|
||||
// 获取音乐数据
|
||||
if (music.getPlaylists[0] && music.getPlaySongData) {
|
||||
getPlaySongData(music.getPlaySongData);
|
||||
getPicColor(music.getPlaySongData.album.picUrl);
|
||||
}
|
||||
// 设置音量
|
||||
if ($player) $player.volume = persistData.value.playVolume;
|
||||
});
|
||||
|
||||
// 监听当前音乐数据变化
|
||||
watch(
|
||||
() => music.getPlaySongData,
|
||||
(val) => {
|
||||
debounce(() => {
|
||||
getPlaySongData(val.id);
|
||||
}, 500);
|
||||
music.setPlaySongTime({ currentTime: 0, duration: 0 });
|
||||
songChange(val);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -439,7 +426,7 @@ watch(
|
||||
watch(
|
||||
() => persistData.value.playVolume,
|
||||
(val) => {
|
||||
if ($player) $player.volume = val;
|
||||
if (player.value) setVolume(player.value, val);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -447,40 +434,35 @@ watch(
|
||||
watch(
|
||||
() => music.getPlayState,
|
||||
(val) => {
|
||||
nextTick(() => {
|
||||
// $player ? (val ? $player.play() : $player.pause()) : null;
|
||||
if ($player) {
|
||||
// val ? $player.play() : $player.pause();
|
||||
val ? songInOrOut("play") : songInOrOut("pause");
|
||||
} else {
|
||||
$message.error("播放器初始化失败,请重试");
|
||||
nextTick().then(() => {
|
||||
if (player.value && !music.isLoadingSong) {
|
||||
fadePlayOrPause(
|
||||
player.value,
|
||||
val ? "play" : "pause",
|
||||
persistData.value.playVolume
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 监听歌曲进度更新
|
||||
// watch(
|
||||
// () => music.getPlaySongTime,
|
||||
// (val) => {
|
||||
// if (val.barMoveDistance) {
|
||||
// songTimeVal.value = val.barMoveDistance;
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-enter-active,
|
||||
.show-leave-active {
|
||||
transform: translateY(0);
|
||||
transition: all 0.3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
}
|
||||
.show-enter-from,
|
||||
.show-leave-to {
|
||||
transform: translateY(80px);
|
||||
}
|
||||
.player {
|
||||
height: 70px;
|
||||
position: fixed;
|
||||
bottom: -90px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transition: all 0.3s;
|
||||
z-index: 2;
|
||||
&.show {
|
||||
bottom: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
@@ -488,19 +470,15 @@ watch(
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@media (max-width: 640px) {
|
||||
top: -6px;
|
||||
top: -8px;
|
||||
> {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
--n-handle-size: 12px;
|
||||
--n-rail-height: 3px;
|
||||
}
|
||||
> {
|
||||
span {
|
||||
font-size: 12px;
|
||||
@@ -512,6 +490,33 @@ watch(
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
.vue-slider {
|
||||
width: 100% !important;
|
||||
height: 3px !important;
|
||||
cursor: pointer;
|
||||
.slider-tooltip {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
background-color: var(--n-color);
|
||||
outline: 1px solid var(--n-border-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
:deep(.vue-slider-rail) {
|
||||
background-color: var(--n-border-color);
|
||||
border-radius: 25px;
|
||||
.vue-slider-process {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
.vue-slider-dot-handle-focus {
|
||||
box-shadow: 0px 0px 1px 2px var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.all {
|
||||
@@ -521,23 +526,6 @@ watch(
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.data {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -588,7 +576,7 @@ watch(
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.artisrOrLrc {
|
||||
@@ -614,7 +602,7 @@ watch(
|
||||
.next,
|
||||
.prev,
|
||||
.dislike {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
@@ -622,7 +610,7 @@ watch(
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: var(--n-color-embedded);
|
||||
background-color: $mainColor;
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
@@ -634,7 +622,7 @@ watch(
|
||||
.play-state {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
margin: 0 12px;
|
||||
cursor: pointer;
|
||||
transform: scale(1);
|
||||
@@ -654,7 +642,7 @@ watch(
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
@media (max-width: 640px) {
|
||||
.volume,
|
||||
.like,
|
||||
@@ -674,9 +662,11 @@ watch(
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainColor;
|
||||
color: var(--n-color-embedded);
|
||||
@media (min-width: 640px) {
|
||||
&:hover {
|
||||
background-color: var(--main-color);
|
||||
color: var(--n-color-embedded);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
@@ -709,6 +699,12 @@ watch(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&.open {
|
||||
.n-icon {
|
||||
background-color: var(--main-color);
|
||||
color: var(--n-color-embedded);
|
||||
}
|
||||
}
|
||||
}
|
||||
.volume {
|
||||
display: flex;
|
||||
@@ -725,6 +721,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>
|
||||
|
||||
@@ -43,51 +43,43 @@ import {
|
||||
useNotification,
|
||||
} from "naive-ui";
|
||||
import { settingStore } from "@/store";
|
||||
import themeColorData from "./themeColor.json";
|
||||
|
||||
const setting = settingStore();
|
||||
const osThemeRef = useOsTheme();
|
||||
const themeOverrides = ref(null);
|
||||
|
||||
// 明暗切换
|
||||
const theme = ref(null);
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
const changeTheme = () => {
|
||||
if (setting.getSiteTheme == "light") {
|
||||
theme.value = null;
|
||||
themeColorMeta.setAttribute("content", "#ffffff");
|
||||
} else if (setting.getSiteTheme == "dark") {
|
||||
theme.value = darkTheme;
|
||||
themeColorMeta.setAttribute("content", "#18181c");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
changeTheme();
|
||||
});
|
||||
|
||||
// 监听明暗变化
|
||||
watch(
|
||||
() => setting.getSiteTheme,
|
||||
() => {
|
||||
changeTheme();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听系统明暗变化
|
||||
watch(
|
||||
() => osThemeRef.value,
|
||||
(value) => {
|
||||
if (setting.themeAuto) {
|
||||
value == "dark"
|
||||
? setting.setSiteTheme("dark")
|
||||
: setting.setSiteTheme("light");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 配置主题色
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: "#f55e55",
|
||||
primaryColorHover: "#F57B74",
|
||||
primaryColorSuppl: "#F57B74",
|
||||
primaryColorPressed: "#F64B41",
|
||||
},
|
||||
const changeThemeColor = (val) => {
|
||||
const color = themeColorData[val];
|
||||
console.log("当前主题色:" + val, color);
|
||||
themeOverrides.value = {
|
||||
common: color,
|
||||
};
|
||||
setting.themeData = color;
|
||||
setCssVariable("--main-color", color.primaryColor);
|
||||
setCssVariable("--main-second-color", color.primaryColor + "1f");
|
||||
setCssVariable("--main-boxshadow-color", color.primaryColor + "26");
|
||||
setCssVariable("--main-boxshadow-hover-color", color.primaryColor + "05");
|
||||
};
|
||||
|
||||
// 修改全局颜色
|
||||
const setCssVariable = (name, value) => {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
// document.body.style.setProperty(name, value);
|
||||
};
|
||||
|
||||
// 挂载 naive 组件的方法
|
||||
@@ -102,12 +94,37 @@ const NaiveProviderContent = defineComponent({
|
||||
setup() {
|
||||
setupNaiveTools();
|
||||
},
|
||||
render() {
|
||||
return h("div", {
|
||||
class: {
|
||||
tools: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
render() {},
|
||||
});
|
||||
|
||||
// 监听明暗变化
|
||||
watch(
|
||||
() => setting.getSiteTheme,
|
||||
() => {
|
||||
changeTheme();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听系统明暗变化
|
||||
watch(
|
||||
() => osThemeRef.value,
|
||||
(val) => {
|
||||
if (setting.themeAuto) {
|
||||
val == "dark"
|
||||
? setting.setSiteTheme("dark")
|
||||
: setting.setSiteTheme("light");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听主题色变化
|
||||
watch(
|
||||
() => setting.themeType,
|
||||
(val) => changeThemeColor(val)
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
changeTheme();
|
||||
changeThemeColor(setting.themeType);
|
||||
});
|
||||
</script>
|
||||
|
||||
82
src/components/Provider/themeColor.json
Normal file
82
src/components/Provider/themeColor.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"red": {
|
||||
"name": "欢快派对",
|
||||
"label": "red",
|
||||
"primaryColor": "#f55e55",
|
||||
"primaryColorHover": "#F57B74",
|
||||
"primaryColorSuppl": "#F57B74",
|
||||
"primaryColorPressed": "#F64B41"
|
||||
},
|
||||
"orange": {
|
||||
"name": "柑橘桔梦",
|
||||
"label": "orange",
|
||||
"primaryColor": "#ff8c00",
|
||||
"primaryColorHover": "#ffa033",
|
||||
"primaryColorSuppl": "#ffa033",
|
||||
"primaryColorPressed": "#ff6b00"
|
||||
},
|
||||
"blue": {
|
||||
"name": "深海蓝梦",
|
||||
"label": "blue",
|
||||
"primaryColor": "#3b5998",
|
||||
"primaryColorHover": "#5475a3",
|
||||
"primaryColorSuppl": "#8b9dc3",
|
||||
"primaryColorPressed": "#2d4373"
|
||||
},
|
||||
"pink": {
|
||||
"name": "粉色梦幻",
|
||||
"label": "pink",
|
||||
"primaryColor": "#e91e63",
|
||||
"primaryColorHover": "#f06292",
|
||||
"primaryColorSuppl": "#f06292",
|
||||
"primaryColorPressed": "#c2185b"
|
||||
},
|
||||
"brown": {
|
||||
"name": "深棕林荫",
|
||||
"label": "brown",
|
||||
"primaryColor": "#795548",
|
||||
"primaryColorHover": "#8d6e63",
|
||||
"primaryColorSuppl": "#8d6e63",
|
||||
"primaryColorPressed": "#5d4037"
|
||||
},
|
||||
"indigo": {
|
||||
"name": "星空靛蓝",
|
||||
"label": "indigo",
|
||||
"primaryColor": "#3f51b5",
|
||||
"primaryColorHover": "#5c6bc0",
|
||||
"primaryColorSuppl": "#5c6bc0",
|
||||
"primaryColorPressed": "#3949ab"
|
||||
},
|
||||
"green": {
|
||||
"name": "生命绿洲",
|
||||
"label": "green",
|
||||
"primaryColor": "#2ecc71",
|
||||
"primaryColorHover": "#3ddc88",
|
||||
"primaryColorSuppl": "#3ddc88",
|
||||
"primaryColorPressed": "#27ae60"
|
||||
},
|
||||
"purple": {
|
||||
"name": "皇室紫梦",
|
||||
"label": "purple",
|
||||
"primaryColor": "#9c27b0",
|
||||
"primaryColorHover": "#ba68c8",
|
||||
"primaryColorSuppl": "#ba68c8",
|
||||
"primaryColorPressed": "#7b1fa2"
|
||||
},
|
||||
"yellow": {
|
||||
"name": "金色阳光",
|
||||
"label": "yellow",
|
||||
"primaryColor": "#FBC02D",
|
||||
"primaryColorHover": "#FFD54F",
|
||||
"primaryColorSuppl": "#FFD54F",
|
||||
"primaryColorPressed": "#FFC107"
|
||||
},
|
||||
"teal": {
|
||||
"name": "海洋碧绿",
|
||||
"label": "teal",
|
||||
"primaryColor": "#009688",
|
||||
"primaryColorHover": "#26a69a",
|
||||
"primaryColorSuppl": "#26a69a",
|
||||
"primaryColorPressed": "#00796b"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="searchInp">
|
||||
<n-input
|
||||
:class="inputActive ? 'input focus' : 'input'"
|
||||
:class="site.searchInputActive ? 'input focus' : 'input'"
|
||||
:input-props="{ autoComplete: false }"
|
||||
ref="searchInpRef"
|
||||
round
|
||||
clearable
|
||||
placeholder="搜索音乐/视频"
|
||||
@@ -14,7 +15,7 @@
|
||||
<template #prefix>
|
||||
<n-icon
|
||||
size="16"
|
||||
:color="inputActive ? '#f55e55' : ''"
|
||||
:class="site.searchInputActive ? 'active' : ''"
|
||||
:component="Search"
|
||||
/>
|
||||
</template>
|
||||
@@ -23,7 +24,7 @@
|
||||
<n-card
|
||||
class="list"
|
||||
v-show="
|
||||
inputActive &&
|
||||
site.searchInputActive &&
|
||||
!inputValue &&
|
||||
(music.getSearchHistory[0] || searchData.hot[0])
|
||||
"
|
||||
@@ -93,7 +94,7 @@
|
||||
<CollapseTransition easing="ease-in-out">
|
||||
<n-card
|
||||
class="list"
|
||||
v-show="inputActive && inputValue && searchData.suggest"
|
||||
v-show="site.searchInputActive && inputValue && searchData.suggest"
|
||||
content-style="padding: 0"
|
||||
>
|
||||
<n-scrollbar>
|
||||
@@ -195,20 +196,21 @@ import {
|
||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||
import debounce from "@/utils/debounce";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicStore, settingStore } from "@/store";
|
||||
import { musicStore, settingStore, siteStore } from "@/store";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
const setting = settingStore();
|
||||
const site = siteStore();
|
||||
|
||||
// 输入框内容
|
||||
const inputValue = ref(null);
|
||||
|
||||
// 输入框激活状态
|
||||
const inputActive = ref(false);
|
||||
const searchInpRef = ref(null);
|
||||
|
||||
// 输入框激活事件
|
||||
const inputFocus = () => {
|
||||
inputActive.value = true;
|
||||
searchInpRef.value?.focus();
|
||||
site.searchInputActive = true;
|
||||
music.showPlayList = false;
|
||||
getSearchHotData();
|
||||
};
|
||||
@@ -274,7 +276,8 @@ const toSearch = (val, type) => {
|
||||
const inputkeydown = (e) => {
|
||||
if (e.key === "Enter" && inputValue.value != null) {
|
||||
console.log("执行搜索" + inputValue.value.trim());
|
||||
inputActive.value = false;
|
||||
searchInpRef.value?.blur();
|
||||
site.searchInputActive = false;
|
||||
// 写入搜索历史
|
||||
music.setSearchHistory(inputValue.value.trim());
|
||||
router.push({
|
||||
@@ -306,13 +309,15 @@ onMounted(() => {
|
||||
getSearchHotData();
|
||||
// 搜索框失焦
|
||||
document.addEventListener("click", () => {
|
||||
inputActive.value = false;
|
||||
searchInpRef.value?.blur();
|
||||
site.searchInputActive = false;
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", () => {
|
||||
inputActive.value = false;
|
||||
searchInpRef.value?.blur();
|
||||
site.searchInputActive = false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +333,17 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听播放列表显隐
|
||||
watch(
|
||||
() => music.showPlayList,
|
||||
(val) => {
|
||||
if (val) {
|
||||
searchInpRef.value?.blur();
|
||||
site.searchInputActive = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -339,10 +355,30 @@ watch(
|
||||
.input {
|
||||
width: 200px;
|
||||
transition: all 0.3s;
|
||||
@media (max-width: 450px) {
|
||||
width: 40px;
|
||||
}
|
||||
&.focus {
|
||||
width: 280px;
|
||||
:deep(input) {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
width: 60vw;
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
width: 54vw;
|
||||
}
|
||||
@media (max-width: 320px) {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
:deep(.n-input__prefix) {
|
||||
.n-icon {
|
||||
transition: color 0.3s;
|
||||
&.active {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,125 +389,142 @@ watch(
|
||||
border-radius: 8px;
|
||||
width: 280px;
|
||||
z-index: 3;
|
||||
|
||||
@media (max-width: 450px) {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 58px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
z-index: 2006;
|
||||
&::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;
|
||||
.n-scrollbar-rail {
|
||||
width: 0;
|
||||
@media (max-width: 450px) {
|
||||
max-height: calc(100vh - 60px);
|
||||
min-height: calc(100vh - 60px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.n-scrollbar-content {
|
||||
padding: 12px;
|
||||
.list-title {
|
||||
color: $mainColor;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
.n-text {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
color: $mainColor;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
color: $mainColor;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
.del {
|
||||
.n-scrollbar-rail {
|
||||
width: 4px;
|
||||
}
|
||||
.n-scrollbar-container {
|
||||
padding-top: 8px;
|
||||
.n-scrollbar-content {
|
||||
padding: 12px;
|
||||
.list-title {
|
||||
color: var(--main-color);
|
||||
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;
|
||||
.n-text {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--main-color);
|
||||
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;
|
||||
}
|
||||
.history-list {
|
||||
margin-bottom: 18px;
|
||||
.n-space {
|
||||
margin: 12px 0;
|
||||
.n-tag {
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
.title {
|
||||
.del {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.suggest-tip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 6px;
|
||||
.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: var(--main-color);
|
||||
}
|
||||
}
|
||||
.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: var(--main-color);
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.suggest-all {
|
||||
.loading {
|
||||
.suggest-tip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@@ -480,32 +533,43 @@ watch(
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.suggest-item {
|
||||
margin-bottom: 12px;
|
||||
&:nth-last-of-type(1) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.type {
|
||||
color: #ff5656;
|
||||
.suggest-all {
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
.n-icon {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.name {
|
||||
margin-left: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.names {
|
||||
display: block;
|
||||
padding: 14px 18px 14px 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: var(--n-border-color);
|
||||
.suggest-item {
|
||||
margin-bottom: 12px;
|
||||
&:nth-last-of-type(1) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.type {
|
||||
color: var(--main-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
.n-icon {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.name {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.names {
|
||||
display: block;
|
||||
padding: 14px 18px 14px 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: var(--n-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main.js
15
src/main.js
@@ -18,10 +18,19 @@ app.use(router);
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
// PWA
|
||||
// 检测到更新提醒
|
||||
let pwaMessage = null;
|
||||
navigator.serviceWorker.addEventListener("onupdatefound", () => {
|
||||
console.info("发现站点更新,正在下载新版本");
|
||||
pwaMessage = $message.loading("发现站点更新,正在下载新版本", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
// 更新完成提醒
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
// 弹出更新提醒
|
||||
console.log("站点已更新,刷新后生效");
|
||||
console.info("站点已更新,刷新后生效");
|
||||
if (pwaMessage) pwaMessage?.destroy();
|
||||
$message.info("站点已更新,刷新后生效", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
|
||||
@@ -125,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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 登录页
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { defineStore } from "pinia";
|
||||
import useSettingDataStore from "./settingData";
|
||||
import useMusicDataStore from "./musicData";
|
||||
import useUserDataStore from "./userData";
|
||||
import useSiteDataStore from "./siteData";
|
||||
|
||||
export const settingStore = () => useSettingDataStore();
|
||||
export const musicStore = () => useMusicDataStore();
|
||||
export const userStore = () => useUserDataStore();
|
||||
export const siteStore = () => useSiteDataStore();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { getSongTime, getSongPlayingTime } from "@/utils/timeTools.js";
|
||||
import { nextTick } from "vue";
|
||||
import { getSongTime, getSongPlayingTime } from "@/utils/timeTools";
|
||||
import { getPersonalFm, setFmTrash } from "@/api/home";
|
||||
import { getLikelist, setLikeSong } from "@/api/user";
|
||||
import { getPlayListCatlist } from "@/api/playlist";
|
||||
import { userStore } from "@/store";
|
||||
import { userStore, settingStore } from "@/store";
|
||||
import { NIcon } from "naive-ui";
|
||||
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
|
||||
import lyricFormat from "@/utils/lyricFormat";
|
||||
import { soundStop, fadePlayOrPause } from "@/utils/Player";
|
||||
import parseLyric from "@/utils/parseLyric";
|
||||
|
||||
const useMusicDataStore = defineStore("musicData", {
|
||||
state: () => {
|
||||
@@ -20,19 +22,32 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
// 播放状态
|
||||
playState: false,
|
||||
// 当前歌曲播放链接
|
||||
playSongLink: null,
|
||||
// 当前歌曲歌词
|
||||
playSongLyric: [],
|
||||
// playSongLink: null,
|
||||
// 当前歌曲歌词数据
|
||||
playSongLyric: {
|
||||
lrc: [],
|
||||
yrc: [],
|
||||
hasTran: false,
|
||||
hasTran: false,
|
||||
hasYrc: false,
|
||||
},
|
||||
// 当前歌曲歌词播放索引
|
||||
playSongLyricIndex: 0,
|
||||
// 当前歌曲是否拥有翻译
|
||||
playSongTransl: false,
|
||||
// 每日推荐
|
||||
dailySongsData: [],
|
||||
// 歌单分类
|
||||
catList: {},
|
||||
// 精品歌单分类
|
||||
highqualityCatList: [],
|
||||
// 音乐频谱数据
|
||||
spectrumsData: {
|
||||
data: [],
|
||||
audio: null,
|
||||
analyser: null,
|
||||
audioCtx: null,
|
||||
},
|
||||
// 是否正在加载数据
|
||||
isLoadingSong: false,
|
||||
// 持久化数据
|
||||
persistData: {
|
||||
// 搜索历史
|
||||
@@ -80,10 +95,6 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
getPersonalFmData(state) {
|
||||
return state.persistData.personalFmData;
|
||||
},
|
||||
// 获取是否拥有翻译
|
||||
getPlaySongTransl(state) {
|
||||
return state.playSongTransl;
|
||||
},
|
||||
// 获取每日推荐
|
||||
getDailySongs(state) {
|
||||
return state.dailySongsData;
|
||||
@@ -116,10 +127,6 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
getPlayState(state) {
|
||||
return state.playState;
|
||||
},
|
||||
// 获取播放链接
|
||||
getPlaySongLink(state) {
|
||||
return state.playSongLink;
|
||||
},
|
||||
// 获取喜欢音乐列表
|
||||
getLikeList(state) {
|
||||
return state.persistData.likeList;
|
||||
@@ -142,8 +149,8 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
setPersonalFmMode(value) {
|
||||
this.persistData.personalFmMode = value;
|
||||
if (value) {
|
||||
this.playSongLink = null;
|
||||
if (this.persistData.personalFmData.id) {
|
||||
soundStop($player);
|
||||
if (this.persistData.personalFmData?.id) {
|
||||
this.persistData.playlists = [];
|
||||
this.persistData.playlists.push(this.persistData.personalFmData);
|
||||
this.persistData.playSongIndex = 0;
|
||||
@@ -175,7 +182,7 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
} else {
|
||||
this.persistData.personalFmData = fmData;
|
||||
if (this.persistData.personalFmMode) {
|
||||
this.playSongLink = null;
|
||||
soundStop($player);
|
||||
this.persistData.playlists = [];
|
||||
this.persistData.playlists.push(fmData);
|
||||
this.persistData.playSongIndex = 0;
|
||||
@@ -232,22 +239,22 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
setLikeSong(id, like).then((res) => {
|
||||
if (res.code == 200) {
|
||||
list.push(id);
|
||||
$message.info("成功喜欢歌曲");
|
||||
$message.info("已添加到我喜欢的音乐");
|
||||
} else {
|
||||
$message.error("喜欢歌曲时发生错误");
|
||||
$message.error("喜欢音乐时发生错误");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$message.info("我喜欢的列表中已存在该歌曲");
|
||||
$message.info("我喜欢的音乐中已存在该歌曲");
|
||||
}
|
||||
} else {
|
||||
if (exists) {
|
||||
setLikeSong(id, like).then((res) => {
|
||||
if (res.code == 200) {
|
||||
list.splice(list.indexOf(id), 1);
|
||||
$message.info("成功取消喜欢歌曲");
|
||||
$message.info("已从我喜欢的音乐中移除");
|
||||
} else {
|
||||
$message.error("取消喜欢歌曲时发生错误");
|
||||
$message.error("取消喜欢音乐时发生错误");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -270,18 +277,13 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
setPlayBarState(value) {
|
||||
this.showPlayBar = value;
|
||||
},
|
||||
// 更改歌曲播放链接
|
||||
setPlaySongLink(value) {
|
||||
this.playSongLink = value;
|
||||
},
|
||||
// 更改播放列表模式
|
||||
setPlayListMode(value) {
|
||||
console.log(value);
|
||||
this.persistData.playListMode = value;
|
||||
},
|
||||
// 添加歌单至播放列表
|
||||
setPlaylists(value) {
|
||||
this.persistData.playlists = value;
|
||||
this.persistData.playlists = value.slice();
|
||||
console.log(`已添加${value.length}首歌曲至播放列表`);
|
||||
},
|
||||
// 更改每日推荐数据
|
||||
@@ -308,25 +310,14 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
// 歌词处理
|
||||
setPlaySongLyric(value) {
|
||||
if (value.lrc) {
|
||||
this.playSongLyric = lyricFormat(value.lrc.lyric);
|
||||
if (value.tlyric && value.tlyric.lyric) {
|
||||
console.log("歌词有翻译");
|
||||
this.playSongTransl = true;
|
||||
let playSongLyric = this.playSongLyric;
|
||||
let playSongLyricFy = lyricFormat(value.tlyric.lyric);
|
||||
playSongLyric.forEach((v) => {
|
||||
playSongLyricFy.forEach((x) => {
|
||||
if (v.time === x.time) {
|
||||
v.lyricFy = x.lyric;
|
||||
}
|
||||
});
|
||||
});
|
||||
this.playSongLyric = playSongLyric;
|
||||
} else {
|
||||
this.playSongTransl = false;
|
||||
try {
|
||||
this.playSongLyric = parseLyric(value);
|
||||
} catch (err) {
|
||||
$message.error("歌词处理出错");
|
||||
console.error("歌词处理出错:" + err);
|
||||
}
|
||||
} else {
|
||||
console.log("无歌词");
|
||||
console.log("该歌曲暂无歌词");
|
||||
this.playSongLyric = [];
|
||||
}
|
||||
},
|
||||
@@ -335,9 +326,13 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
this.persistData.playSongTime.currentTime = value.currentTime;
|
||||
this.persistData.playSongTime.duration = value.duration;
|
||||
// 计算进度条应该移动的距离
|
||||
this.persistData.playSongTime.barMoveDistance = Number(
|
||||
(value.currentTime / (value.duration / 100)).toFixed(2)
|
||||
);
|
||||
if (value.duration === 0) {
|
||||
this.persistData.playSongTime.barMoveDistance = 0;
|
||||
} else {
|
||||
this.persistData.playSongTime.barMoveDistance = Number(
|
||||
(value.currentTime / (value.duration / 100)).toFixed(2)
|
||||
);
|
||||
}
|
||||
if (this.persistData.playSongTime.barMoveDistance) {
|
||||
// 歌曲播放进度转换
|
||||
this.persistData.playSongTime.songTimePlayed = getSongPlayingTime(
|
||||
@@ -348,16 +343,11 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
);
|
||||
}
|
||||
// 计算当前歌词播放索引
|
||||
const index = this.playSongLyric.findIndex(
|
||||
(v) => v.time >= value.currentTime
|
||||
);
|
||||
if (index === -1) {
|
||||
// 如果没有找到合适的歌词,则返回最后一句歌词
|
||||
this.playSongLyricIndex =
|
||||
this.playSongLyric.length - 1;
|
||||
} else {
|
||||
this.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() {
|
||||
@@ -380,7 +370,9 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
},
|
||||
// 上下曲调整
|
||||
setPlaySongIndex(type) {
|
||||
this.playState = false;
|
||||
// this.playState = false;
|
||||
soundStop($player);
|
||||
this.isLoadingSong = true;
|
||||
if (this.persistData.personalFmMode) {
|
||||
this.setPersonalFmData();
|
||||
} else {
|
||||
@@ -393,8 +385,9 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
this.persistData.playSongIndex = Math.floor(
|
||||
Math.random() * listLength
|
||||
);
|
||||
} else if (listMode === "single" && $player) {
|
||||
$player.currentTime = 0;
|
||||
} else if (listMode === "single" && typeof $player !== "undefined") {
|
||||
soundStop($player);
|
||||
fadePlayOrPause($player, "play", this.persistData.playVolume);
|
||||
} else {
|
||||
$message.error("播放出错,请刷新后重试");
|
||||
}
|
||||
@@ -403,11 +396,15 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
this.persistData.playSongIndex = listLength - 1;
|
||||
} else if (this.persistData.playSongIndex >= listLength) {
|
||||
this.persistData.playSongIndex = 0;
|
||||
soundStop($player);
|
||||
fadePlayOrPause($player, "play", this.persistData.playVolume);
|
||||
}
|
||||
if (listMode !== "single") {
|
||||
this.playSongLink = null;
|
||||
if (listMode !== "single" && listLength > 1) {
|
||||
soundStop($player);
|
||||
}
|
||||
this.playState = true;
|
||||
nextTick().then(() => {
|
||||
this.setPlayState(true);
|
||||
});
|
||||
}
|
||||
},
|
||||
// 添加歌曲至播放列表
|
||||
@@ -419,10 +416,11 @@ 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.playSongLink = null;
|
||||
soundStop($player);
|
||||
this.isLoadingSong = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("出现错误:" + error);
|
||||
@@ -433,7 +431,7 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
this.persistData.playlists.push(value);
|
||||
this.persistData.playSongIndex = this.persistData.playlists.length - 1;
|
||||
}
|
||||
play ? (this.playState = true) : null;
|
||||
play ? this.setPlayState(true) : null;
|
||||
},
|
||||
// 在当前播放歌曲后添加
|
||||
addSongToNext(value) {
|
||||
@@ -465,9 +463,17 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
const name = this.persistData.playlists[index].name;
|
||||
if (index < this.persistData.playSongIndex) {
|
||||
this.persistData.playSongIndex--;
|
||||
} else if (index === this.persistData.playSongIndex) {
|
||||
// 如果删除的是当前播放歌曲,则重置播放器
|
||||
soundStop($player);
|
||||
}
|
||||
$message.success(name + " 已从播放列表中移除");
|
||||
this.persistData.playlists.splice(index, 1);
|
||||
// 检查当前播放歌曲的索引是否超出了列表范围
|
||||
if (this.persistData.playSongIndex >= this.persistData.playlists.length) {
|
||||
this.persistData.playSongIndex = 0;
|
||||
soundStop($player);
|
||||
}
|
||||
},
|
||||
// 获取歌单分类
|
||||
setCatList(highquality = false) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const useSettingDataStore = defineStore("settingData", {
|
||||
// 全局主题
|
||||
theme: "light",
|
||||
themeAuto: true,
|
||||
themeType: "red",
|
||||
themeData: {},
|
||||
// 搜索历史
|
||||
searchHistory: true,
|
||||
// 轮播图显示
|
||||
@@ -20,22 +22,38 @@ const useSettingDataStore = defineStore("settingData", {
|
||||
playerStyle: "cover",
|
||||
// 底栏歌词显示
|
||||
bottomLyricShow: true,
|
||||
// 是否显示逐字歌词
|
||||
showYrc: true,
|
||||
// 是否显示歌词翻译
|
||||
showTransl: true,
|
||||
// 是否显示歌词音译
|
||||
showRoma: true,
|
||||
// 歌曲音质
|
||||
songLevel: "exhigh",
|
||||
// 歌词位置
|
||||
lyricsPosition: "left",
|
||||
// 歌词滚动位置
|
||||
lyricsBlock: "center",
|
||||
lyricsBlock: "start",
|
||||
// 歌词大小
|
||||
lyricsFontSize: 2.8,
|
||||
lyricsFontSize: 3.6,
|
||||
// 歌词模糊
|
||||
lyricsBlur: false,
|
||||
// 音乐频谱
|
||||
musicFrequency: false,
|
||||
// 鼠标移入歌词区域暂停滚动
|
||||
lrcMousePause: true,
|
||||
// 是否使用网易云解灰
|
||||
useUnmServer: true,
|
||||
// 播放背景是否显示图片
|
||||
backgroundImageShow: "blur",
|
||||
// 是否显示前奏等待
|
||||
countDownShow: true,
|
||||
// 是否显示歌词设置
|
||||
showLyricSetting: true,
|
||||
// 歌曲渐入渐出
|
||||
songVolumeFade: true,
|
||||
// 列表默认数量
|
||||
listNumber: 30,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
@@ -43,10 +61,6 @@ const useSettingDataStore = defineStore("settingData", {
|
||||
getSiteTheme(state) {
|
||||
return state.theme;
|
||||
},
|
||||
// 获取是否开启翻译
|
||||
getShowTransl(state) {
|
||||
return state.showTransl;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 切换明暗模式
|
||||
|
||||
25
src/store/siteData.js
Normal file
25
src/store/siteData.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const useSiteDataStore = defineStore("siteData", {
|
||||
state: () => {
|
||||
return {
|
||||
// 站点标题
|
||||
siteTitle: import.meta.env.VITE_SITE_TITLE,
|
||||
// 封面主题色
|
||||
songPicColor: "rgb(128,128,128)",
|
||||
// 搜索框激活状态
|
||||
searchInputActive: false,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {},
|
||||
// 开启数据持久化
|
||||
persist: [
|
||||
{
|
||||
storage: localStorage,
|
||||
paths: [""],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default useSiteDataStore;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getUserArtistlist,
|
||||
getUserAlbum,
|
||||
} from "@/api/user";
|
||||
import { formatNumber, getLongTime } from "@/utils/timeTools.js";
|
||||
import { formatNumber, getLongTime } from "@/utils/timeTools";
|
||||
|
||||
const useUserDataStore = defineStore("userData", {
|
||||
state: () => {
|
||||
@@ -101,10 +101,11 @@ const useUserDataStore = defineStore("userData", {
|
||||
this.userData = {};
|
||||
this.userOtherData = {};
|
||||
localStorage.removeItem("cookie");
|
||||
// 调用退出登录接口
|
||||
userLogOut();
|
||||
},
|
||||
// 更改用户歌单
|
||||
async setUserPlayLists() {
|
||||
async setUserPlayLists(callback) {
|
||||
if (this.userLogin) {
|
||||
try {
|
||||
if (!Object.keys(this.userOtherData).length) {
|
||||
@@ -144,6 +145,9 @@ const useUserDataStore = defineStore("userData", {
|
||||
});
|
||||
}
|
||||
});
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
this.userPlayLists.isLoading = false;
|
||||
} else {
|
||||
this.userPlayLists.isLoading = false;
|
||||
@@ -194,7 +198,7 @@ const useUserDataStore = defineStore("userData", {
|
||||
}
|
||||
},
|
||||
// 更改用户收藏专辑
|
||||
async setUserAlbumLists() {
|
||||
async setUserAlbumLists(callback) {
|
||||
if (this.userLogin) {
|
||||
try {
|
||||
let offset = 0;
|
||||
@@ -216,6 +220,9 @@ const useUserDataStore = defineStore("userData", {
|
||||
offset += 30;
|
||||
console.log(totalCount, offset, this.userAlbum.list);
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
this.userAlbum.isLoading = false;
|
||||
this.userAlbum.has = true;
|
||||
} catch (err) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: "HarmonyOS_Regular", sans-serif !important;
|
||||
}
|
||||
|
||||
@@ -47,16 +47,27 @@ body,
|
||||
}
|
||||
.n-card-header {
|
||||
.n-card-header__main {
|
||||
// font-weight: bold;
|
||||
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 {
|
||||
@@ -90,6 +101,18 @@ body,
|
||||
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;
|
||||
@@ -98,3 +121,26 @@ body,
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 响应式布局
|
||||
@mixin changeWidth($padding: 10vw) {
|
||||
.n-layout-header {
|
||||
padding: 0 $padding;
|
||||
}
|
||||
|
||||
.n-layout-content {
|
||||
.main {
|
||||
padding: 24px $padding 54px $padding;
|
||||
.player {
|
||||
padding: 0 $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include changeWidth;
|
||||
|
||||
/* 小于1200px时 */
|
||||
@media (max-width: 1200px) {
|
||||
@include changeWidth($padding: 5vw);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// 主题色
|
||||
$mainColor: #f55e55;
|
||||
$mainSecondaryColor: #f55e551f;
|
||||
|
||||
// 响应式布局
|
||||
@mixin changeWidth($padding:10vw) {
|
||||
.n-layout-header {
|
||||
padding: 0 $padding;
|
||||
}
|
||||
|
||||
.n-layout-content {
|
||||
.main {
|
||||
padding: 24px $padding 54px $padding;
|
||||
|
||||
.player {
|
||||
padding: 0 $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include changeWidth;
|
||||
|
||||
/* 小于1200px时 */
|
||||
@media (max-width: 1200px) {
|
||||
@include changeWidth($padding:5vw);
|
||||
}
|
||||
@@ -77,7 +77,15 @@ class MusicFrequency {
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.context.destination);
|
||||
}
|
||||
|
||||
// 断开音频元素和分析器之间的连接,释放音频上下文
|
||||
disconnect() {
|
||||
// 断开连接
|
||||
this.source.disconnect();
|
||||
this.analyser.disconnect();
|
||||
// 关闭音频上下文
|
||||
this.context.close();
|
||||
}
|
||||
// 绘制频谱
|
||||
drawSpectrum() {
|
||||
// 获取频域数据
|
||||
this.analyser.getByteFrequencyData(this.output);
|
||||
|
||||
294
src/utils/Player.js
Normal file
294
src/utils/Player.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import { Howl, Howler } from "howler";
|
||||
import { songScrobble } from "@/api/song";
|
||||
import { musicStore } from "@/store";
|
||||
import { NIcon } from "naive-ui";
|
||||
import { MusicNoteFilled } from "@vicons/material";
|
||||
|
||||
// 歌曲信息更新定时器
|
||||
let timeupdateInterval = null;
|
||||
// 重试次数
|
||||
let testNumber = 0;
|
||||
|
||||
/**
|
||||
* 创建音频对象
|
||||
* @param {string} src - 音频文件地址
|
||||
* @param {number} volume - 音量(默认为0.7)
|
||||
* @param {number} seek - 初始播放进度(默认为0)
|
||||
* @return {Howl} - 音频对象
|
||||
*/
|
||||
export const createSound = (src, autoPlay = true) => {
|
||||
try {
|
||||
Howler.unload();
|
||||
const music = musicStore();
|
||||
const sound = new Howl({
|
||||
src: [src],
|
||||
format: ["mp3", "flac"],
|
||||
html5: true,
|
||||
preload: true,
|
||||
volume: music.persistData.playVolume,
|
||||
});
|
||||
if (autoPlay && music.getPlayState) {
|
||||
fadePlayOrPause(sound, "play", music.persistData.playVolume);
|
||||
}
|
||||
// 首次加载事件
|
||||
sound?.once("load", () => {
|
||||
const songId = music.getPlaySongData?.id;
|
||||
const sourceId = music.getPlaySongData?.sourceId
|
||||
? music.getPlaySongData.sourceId
|
||||
: 0;
|
||||
const isLogin = JSON.parse(localStorage.getItem("userData")).userLogin;
|
||||
console.log("首次缓冲完成:" + songId + " / 来源:" + sourceId);
|
||||
sound?.seek(music.persistData.playSongTime.currentTime);
|
||||
// 取消加载状态
|
||||
music.isLoadingSong = false;
|
||||
// 听歌打卡
|
||||
if (isLogin) {
|
||||
songScrobble(songId, sourceId).catch((err) => {
|
||||
console.error("歌曲打卡失败:" + err);
|
||||
});
|
||||
}
|
||||
});
|
||||
// 播放事件
|
||||
sound?.on("play", () => {
|
||||
if (!Object.keys(music.getPlaySongData).length) {
|
||||
$message.error("音乐数据获取失败");
|
||||
return false;
|
||||
}
|
||||
testNumber = 0;
|
||||
music.setPlayState(true);
|
||||
const songName = music.getPlaySongData.name;
|
||||
const songArtist = music.getPlaySongData.artist[0].name;
|
||||
$message.info(songName + " - " + songArtist, {
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(MusicNoteFilled),
|
||||
}),
|
||||
});
|
||||
console.log("开始播放:" + songName + " - " + songArtist);
|
||||
// 获取播放器信息
|
||||
timeupdateInterval = setInterval(() => checkAudioTime(sound, music), 250);
|
||||
setMediaSession(music);
|
||||
// 写入播放历史
|
||||
music.setPlayHistory(music.getPlaySongData);
|
||||
// 播放时页面标题
|
||||
window.document.title =
|
||||
music.getPlaySongData.name +
|
||||
" - " +
|
||||
music.getPlaySongData.artist[0].name +
|
||||
" - " +
|
||||
import.meta.env.VITE_SITE_TITLE;
|
||||
});
|
||||
// 暂停事件
|
||||
sound?.on("pause", () => {
|
||||
clearInterval(timeupdateInterval);
|
||||
console.log("音乐暂停");
|
||||
music.setPlayState(false);
|
||||
// 更改页面标题
|
||||
$setSiteTitle();
|
||||
});
|
||||
// 结束事件
|
||||
sound?.on("end", () => {
|
||||
console.log("歌曲播放结束");
|
||||
music.setPlaySongIndex("next");
|
||||
});
|
||||
// 错误事件
|
||||
sound?.on("loaderror", () => {
|
||||
if (testNumber > 2) {
|
||||
$message.error("歌曲播放失败");
|
||||
console.error("歌曲播放失败");
|
||||
music.setPlayState(false);
|
||||
}
|
||||
if (testNumber < 4) {
|
||||
if (music.getPlaylists[0]) $getPlaySongData(music.getPlaySongData);
|
||||
testNumber++;
|
||||
} else {
|
||||
$message.error("歌曲重试次数过多,请刷新后重试", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
sound?.on("playerror", () => {
|
||||
$message.error("歌曲播放出错");
|
||||
console.error("歌曲播放出错");
|
||||
music.setPlayState(false);
|
||||
});
|
||||
// 生成频谱
|
||||
// createSpectrums(sound, music);
|
||||
// 返回音频对象
|
||||
return (window.$player = sound);
|
||||
} catch (err) {
|
||||
$message.error("音乐播放器初始化失败");
|
||||
console.error("音乐播放器初始化失败:" + err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置音量
|
||||
* @param {number} volume - 设置的音量值,0-1之间的浮点数
|
||||
*/
|
||||
export const setVolume = (sound, volume) => {
|
||||
sound?.volume(volume);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置进度
|
||||
* @param {number} seek - 设置的进度值,0-1之间的浮点数
|
||||
*/
|
||||
export const setSeek = (sound, seek) => {
|
||||
// const music = musicStore();
|
||||
// music.persistData.playSongTime.currentTime = seek;
|
||||
sound?.seek(seek);
|
||||
};
|
||||
|
||||
/**
|
||||
* 音频渐入渐出
|
||||
* @param {Howl} sound - 音频对象
|
||||
* @param {String} type - 渐入还是渐出
|
||||
* @param {number} volume - 渐出音量的大小,0-1之间的浮点数
|
||||
* @param {number} duration - 渐出音量的时长,单位为毫秒
|
||||
*/
|
||||
export const fadePlayOrPause = (sound, type, volume, duration = 300) => {
|
||||
const isFade =
|
||||
JSON.parse(localStorage.getItem("settingData")).songVolumeFade ?? true;
|
||||
if (isFade) {
|
||||
if (type === "play") {
|
||||
if (sound?.playing()) return;
|
||||
sound?.play();
|
||||
sound?.once("play", () => {
|
||||
sound?.fade(0, volume, duration);
|
||||
});
|
||||
} else if (type === "pause") {
|
||||
sound?.fade(volume, 0, duration);
|
||||
sound?.once("fade", () => {
|
||||
sound?.pause();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
type === "play" ? sound?.play() : sound?.pause();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止播放器
|
||||
* @param {Howl} sound - 音频对象
|
||||
*/
|
||||
export const soundStop = (sound) => {
|
||||
sound?.stop();
|
||||
setSeek(sound, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取播放进度
|
||||
* @param {Howl} sound - 音频对象
|
||||
* @param {music} music - pinia
|
||||
*/
|
||||
const checkAudioTime = (sound, music) => {
|
||||
if (sound.playing()) {
|
||||
const currentTime = sound.seek();
|
||||
const duration = sound._duration;
|
||||
music.setPlaySongTime({ currentTime, duration });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 MediaSession
|
||||
* @param {music} music - pinia
|
||||
*/
|
||||
const setMediaSession = (music) => {
|
||||
if (
|
||||
"mediaSession" in navigator &&
|
||||
Object.keys(music.getPlaySongData).length
|
||||
) {
|
||||
const artists = music.getPlaySongData.artist.map((a) => a.name);
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: music.getPlaySongData.name,
|
||||
artist: artists.join(" & "),
|
||||
album: music.getPlaySongData.album.name,
|
||||
artwork: [
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=96y96",
|
||||
sizes: "96x96",
|
||||
},
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=128y128",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src:
|
||||
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
|
||||
"?param=512x512",
|
||||
sizes: "512x512",
|
||||
},
|
||||
],
|
||||
length: music.getPlaySongTime?.duration,
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () => {
|
||||
music.setPlaySongIndex("next");
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () => {
|
||||
music.setPlaySongIndex("prev");
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("play", () => {
|
||||
music.setPlayState(true);
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("pause", () => {
|
||||
music.setPlayState(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成频谱数据 - 快速傅里叶变换(FFT)
|
||||
* @param {Howl} sound - 音频对象
|
||||
* @param {music} music - pinia
|
||||
*/
|
||||
const createSpectrums = (sound, music) => {
|
||||
try {
|
||||
if (!music.spectrumsData.audioCtx) {
|
||||
// 断开之前的连接
|
||||
music.spectrumsData.audio?.disconnect();
|
||||
music.spectrumsData.analyser?.disconnect();
|
||||
music.spectrumsData.audioCtx?.close();
|
||||
// 创建新的连接
|
||||
music.spectrumsData.audioCtx = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
const audioDom = sound._sounds[0]._node;
|
||||
audioDom.crossOrigin = "anonymous";
|
||||
const source =
|
||||
music.spectrumsData.audioCtx.createMediaElementSource(audioDom);
|
||||
const analyser = music.spectrumsData.audioCtx.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
source.connect(analyser);
|
||||
analyser.connect(music.spectrumsData.audioCtx.destination);
|
||||
// 更新频谱数据
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
updateSpectrums(analyser, dataArray, music);
|
||||
// 保存当前链接
|
||||
music.spectrumsData.audio = source;
|
||||
music.spectrumsData.analyser = analyser;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("音乐频谱生成失败:" + err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新音乐频谱数据
|
||||
*
|
||||
* @param {Object} analyser - 音频分析器
|
||||
* @param {Uint8Array} dataArray - 频谱数据数组
|
||||
* @param {Object} music - pinia
|
||||
*/
|
||||
const updateSpectrums = (analyser, dataArray, music) => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
music.spectrumsData.data = [...dataArray];
|
||||
// 递归调用,持续更新频谱数据
|
||||
requestAnimationFrame(() => {
|
||||
updateSpectrums(analyser, dataArray, music);
|
||||
});
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 格式化歌词字符串为时间轴和歌词文本的数组
|
||||
* @param {string} lrc 歌词字符串
|
||||
* @returns {Array<{ time: number, lyric: string }>} 时间轴和歌词文本的数组
|
||||
*/
|
||||
const lyricFormat = (lrc) => {
|
||||
// 匹配时间轴和歌词文本的正则表达式
|
||||
const regex = /^\[([^\]]+)\]\s*(.+?)\s*$/;
|
||||
// 将歌词字符串按行分割为数组
|
||||
const lines = lrc.split("\n");
|
||||
// 对每一行进行转换
|
||||
const result = lines
|
||||
// 筛选出包含时间轴和歌词文本的行
|
||||
.filter((line) => regex.test(line))
|
||||
// 转换时间轴和歌词文本为对象
|
||||
.map((line) => {
|
||||
const [, time, text] = line.match(regex);
|
||||
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("该歌曲为纯音乐");
|
||||
return [];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
export default lyricFormat;
|
||||
191
src/utils/parseLyric.js
Normal file
191
src/utils/parseLyric.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 将歌词接口数据解析出对应数据
|
||||
* @param {string} data 接口数据
|
||||
* @returns {Array} 对应数据
|
||||
*/
|
||||
const parseLyric = (data) => {
|
||||
// 判断是否具有内容
|
||||
const checkLyric = (lyric) => (lyric ? (lyric.lyric ? true : false) : false);
|
||||
// 初始化数据
|
||||
const { lrc, tlyric, romalrc, yrc, ytlrc, yromalrc } = data;
|
||||
const lrcData = {
|
||||
lrc: lrc?.lyric || null,
|
||||
tlyric: tlyric?.lyric || null,
|
||||
romalrc: romalrc?.lyric || null,
|
||||
yrc: yrc?.lyric || null,
|
||||
ytlrc: ytlrc?.lyric || null,
|
||||
yromalrc: yromalrc?.lyric || null,
|
||||
};
|
||||
// 初始化输出结果
|
||||
const result = {
|
||||
// 是否具有普通翻译
|
||||
hasLrcTran: checkLyric(tlyric),
|
||||
// 是否具有普通音译
|
||||
hasLrcRoma: checkLyric(romalrc),
|
||||
// 是否具有逐字歌词
|
||||
hasYrc: checkLyric(yrc),
|
||||
// 是否具有逐字翻译
|
||||
hasYrcTran: checkLyric(ytlrc),
|
||||
// 是否具有逐字音译
|
||||
hasYrcRoma: checkLyric(yromalrc),
|
||||
// 普通歌词数组
|
||||
lrc: [],
|
||||
// 逐字歌词数据
|
||||
yrc: [],
|
||||
};
|
||||
// 普通歌词
|
||||
if (lrcData.lrc) {
|
||||
result.lrc = parseLrc(lrcData.lrc);
|
||||
//判断是否有其他翻译
|
||||
result.lrc = lrcData.tlyric
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.tlyric), "tran")
|
||||
: result.lrc;
|
||||
result.lrc = lrcData.romalrc
|
||||
? parseOtherLrc(result.lrc, parseLrc(lrcData.romalrc), "roma")
|
||||
: result.lrc;
|
||||
}
|
||||
// 逐字歌词
|
||||
if (lrcData.yrc) {
|
||||
result.yrc = parseYrc(lrcData.yrc);
|
||||
//判断是否有其他翻译
|
||||
result.yrc = lrcData.ytlrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.ytlrc), "tran")
|
||||
: result.yrc;
|
||||
result.yrc = lrcData.yromalrc
|
||||
? parseOtherLrc(result.yrc, parseLrc(lrcData.yromalrc), "roma")
|
||||
: result.yrc;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译文本对齐
|
||||
* @param {string} lrc 歌词字符串
|
||||
* @param {string} tranLrc 翻译歌词字符串
|
||||
* @returns {Array} 包含翻译的歌词对象数组
|
||||
*/
|
||||
const parseOtherLrc = (lrc, tranLrc, name) => {
|
||||
const lyric = lrc;
|
||||
const tranLyric = tranLrc;
|
||||
if (lyric[0] && tranLyric[0]) {
|
||||
lyric.forEach((v) => {
|
||||
tranLyric.forEach((x) => {
|
||||
if (
|
||||
Number(v.time) === Number(x.time) ||
|
||||
Math.abs(Number(v.time) - Number(x.time)) < 0.6
|
||||
) {
|
||||
v[name] = x.content;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return lyric;
|
||||
};
|
||||
|
||||
/**
|
||||
* 普通歌词解析
|
||||
* @param {string} lyrics 歌词字符串
|
||||
* @returns {Array} 歌词对象数组
|
||||
*/
|
||||
const parseLrc = (lyrics) => {
|
||||
if (!lyrics) return [];
|
||||
try {
|
||||
// 匹配时间轴和歌词文本的正则表达式
|
||||
const regex = /^\[([^\]]+)\]\s*(.+?)\s*$/;
|
||||
// 将歌词字符串按行分割为数组
|
||||
const lines = lyrics.split("\n");
|
||||
// 对每一行进行转换
|
||||
const parsedLyrics = lines
|
||||
// 筛选出包含时间轴和歌词文本的行
|
||||
.filter((line) => regex.test(line))
|
||||
// 转换时间轴和歌词文本为对象
|
||||
.map((line) => {
|
||||
const [, time, text] = line.match(regex);
|
||||
const parts = time.split(":");
|
||||
const seconds =
|
||||
Number(parts[0]) * 60 +
|
||||
Number(parts[1]) +
|
||||
(parts.length > 2 ? Number(parts[2]) / 1000 : 0);
|
||||
return { time: Number(seconds.toFixed(2)), content: text.trim() };
|
||||
})
|
||||
.filter((c) => c.content.trim() !== "");
|
||||
// 检查是否为纯音乐,是则返回空数组
|
||||
if (parsedLyrics.length && /纯音乐,请欣赏/.test(parsedLyrics[0].content)) {
|
||||
console.log("该歌曲为纯音乐");
|
||||
return [];
|
||||
}
|
||||
return parsedLyrics;
|
||||
} catch (err) {
|
||||
console.error("普通歌词处理出错:" + err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 逐字歌词解析
|
||||
* @param {string} lyrics 逐字歌词字符串
|
||||
* @returns {Array} 歌词对象数组
|
||||
*/
|
||||
const parseYrc = (lyrics) => {
|
||||
if (!lyrics) return [];
|
||||
try {
|
||||
// 遍历每一行逐字歌词
|
||||
const parsedLyrics = lyrics
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
// 匹配每一行中的时间戳信息
|
||||
const timeReg = /\[(\d+),(\d+)\]/;
|
||||
const timeMatch = line.match(timeReg);
|
||||
if (!timeMatch) {
|
||||
return null;
|
||||
}
|
||||
// 解构出起始时间和结束时间
|
||||
const [_, startTime, endTime] = timeMatch;
|
||||
if (isNaN(startTime) || isNaN(endTime)) {
|
||||
return null;
|
||||
}
|
||||
// 去除当前行中的时间戳信息,得到歌词内容
|
||||
const content = line.replace(timeReg, "");
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
// 对歌词内容中的时间戳和歌词内容分离
|
||||
const contentArray = content
|
||||
.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g)
|
||||
.filter((c) => c.trim())
|
||||
.map((c) => {
|
||||
// 匹配当前片段中的时间戳信息
|
||||
const timeReg = /\((\d+),(\d+),(\d+)\)/;
|
||||
const timeMatch = c.match(timeReg);
|
||||
if (!timeMatch) {
|
||||
return null;
|
||||
}
|
||||
// 解构出时间戳,持续时间和歌词内容
|
||||
const [_, time, duration] = timeMatch;
|
||||
const content = c.replace(timeReg, "");
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
time: Number(time) / 1000,
|
||||
duration: Number(duration) / 1000,
|
||||
content,
|
||||
};
|
||||
})
|
||||
.filter((c) => c);
|
||||
// 返回当前行解析出的时间信息和歌词内容信息
|
||||
return {
|
||||
time: Number(startTime) / 1000,
|
||||
endTime: Number(endTime) / 1000,
|
||||
content: contentArray,
|
||||
};
|
||||
})
|
||||
.filter((line) => line);
|
||||
return parsedLyrics;
|
||||
} catch (err) {
|
||||
console.error("逐字歌词处理出错:" + err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default parseLyric;
|
||||
@@ -41,7 +41,7 @@ axios.interceptors.response.use(
|
||||
const data = error.response.data;
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
console.error("您未登录");
|
||||
console.error("无权限访问");
|
||||
break;
|
||||
case 301:
|
||||
console.error("请求发生重定向");
|
||||
|
||||
@@ -7,55 +7,92 @@
|
||||
:src="
|
||||
albumDetail.picUrl
|
||||
? albumDetail.picUrl.replace(/^http:/, 'https:') +
|
||||
'?param=500y500'
|
||||
'?param=1024y1024'
|
||||
: null
|
||||
"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<img src="/images/pic/album.png" class="album" alt="album" />
|
||||
</div>
|
||||
<div class="intr">
|
||||
<span class="name">歌单简介</span>
|
||||
<span class="desc text-hidden">
|
||||
{{ albumDetail.description }}
|
||||
</span>
|
||||
<n-button
|
||||
block
|
||||
strong
|
||||
secondary
|
||||
v-if="albumDetail?.description.length > 70"
|
||||
@click="albumDescShow = true"
|
||||
>
|
||||
全部简介
|
||||
</n-button>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="albumDescShow"
|
||||
preset="card"
|
||||
title="歌单简介"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</div>
|
||||
<div class="tag" v-if="albumDetail.tags">
|
||||
<n-tag
|
||||
class="tags"
|
||||
round
|
||||
:bordered="false"
|
||||
v-for="item in albumDetail.tags"
|
||||
:key="item"
|
||||
>
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
<div class="meta">
|
||||
<div class="title">
|
||||
<n-text class="name">{{ albumDetail.name }}</n-text>
|
||||
<n-text
|
||||
class="creator"
|
||||
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
|
||||
>
|
||||
{{ albumDetail.artist.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div class="intr">
|
||||
<span class="name">专辑简介</span>
|
||||
<span class="desc text-hidden">
|
||||
{{ albumDetail.description }}
|
||||
</span>
|
||||
<n-button
|
||||
class="all-desc"
|
||||
block
|
||||
strong
|
||||
secondary
|
||||
v-if="albumDetail?.description.length > 70"
|
||||
@click="albumDescShow = true"
|
||||
>
|
||||
全部简介
|
||||
</n-button>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="albumDescShow"
|
||||
preset="card"
|
||||
title="歌单简介"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</div>
|
||||
<n-space class="tag" v-if="albumDetail.tags">
|
||||
<n-tag
|
||||
class="tags"
|
||||
round
|
||||
:bordered="false"
|
||||
v-for="item in albumDetail.tags"
|
||||
:key="item"
|
||||
>
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-space class="control">
|
||||
<n-button strong secondary round type="primary" @click="playAllSong">
|
||||
<template #icon>
|
||||
<n-icon :component="MusicList" />
|
||||
</template>
|
||||
播放
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
:show-arrow="true"
|
||||
:options="dropdownOptions"
|
||||
>
|
||||
<n-button strong secondary circle>
|
||||
<template #icon>
|
||||
<n-icon :component="More" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="meta">
|
||||
<span class="name">{{ albumDetail.name }}</span>
|
||||
<span class="creator">{{ albumDetail.artist.name }}</span>
|
||||
<n-text class="name">{{ albumDetail.name }}</n-text>
|
||||
<n-text
|
||||
class="creator"
|
||||
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
|
||||
>
|
||||
{{ albumDetail.artist.name }}
|
||||
</n-text>
|
||||
<div class="time">
|
||||
<div class="createTime">
|
||||
<span class="num">发行时间:</span>
|
||||
@@ -91,24 +128,95 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getAlbum } from "@/api/album";
|
||||
import { NIcon, NAvatar, NText } from "naive-ui";
|
||||
import { getAlbum, likeAlbum } from "@/api/album";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
|
||||
import { getSongTime, getLongTime } from "@/utils/timeTools";
|
||||
import { MusicList, LinkTwo, More, Like, Unlike } from "@icon-park/vue-next";
|
||||
import { userStore, musicStore, settingStore } from "@/store";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
const router = useRouter();
|
||||
|
||||
// 歌单数据
|
||||
const router = useRouter();
|
||||
const user = userStore();
|
||||
const music = musicStore();
|
||||
const setting = settingStore();
|
||||
|
||||
// 专辑数据
|
||||
const albumId = ref(router.currentRoute.value.query.id);
|
||||
const albumDetail = ref(null);
|
||||
const albumData = ref([]);
|
||||
const albumDescShow = ref(false);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => icon,
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// 判断收藏还是取消
|
||||
const isLikeOrDislike = (id) => {
|
||||
const playlists = user.getUserAlbumLists.list;
|
||||
if (playlists.length) {
|
||||
return !playlists.some((item) => item.id === Number(id));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 专辑下拉菜单数据
|
||||
const dropdownOptions = ref([]);
|
||||
|
||||
// 更改专辑下拉菜单数据
|
||||
const setDropdownOptions = () => {
|
||||
dropdownOptions.value = [
|
||||
{
|
||||
key: "copy",
|
||||
label: "复制专辑链接",
|
||||
props: {
|
||||
onClick: () => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
navigator.clipboard.writeText(
|
||||
`https://music.163.com/#/playlist?id=${albumId.value}`
|
||||
);
|
||||
$message.success("专辑链接复制成功");
|
||||
} catch (err) {
|
||||
$message.error("复制失败:", err);
|
||||
}
|
||||
} else {
|
||||
$message.error("您的浏览器暂不支持该操作");
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(LinkTwo)),
|
||||
},
|
||||
{
|
||||
key: "like",
|
||||
label: isLikeOrDislike(albumId.value) ? "收藏专辑" : "取消收藏专辑",
|
||||
show: user.userLogin,
|
||||
props: {
|
||||
onClick: () => {
|
||||
toChangeLike(albumId.value);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(isLikeOrDislike(albumId.value) ? Like : Unlike)),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 获取歌单信息
|
||||
const getAlbumData = (id) => {
|
||||
getAlbum(id).then((res) => {
|
||||
console.log(res);
|
||||
// 专辑信息
|
||||
albumDetail.value = res.album;
|
||||
$setSiteTitle(res.album.name + " - 专辑");
|
||||
// 专辑歌曲
|
||||
if (res.songs) {
|
||||
albumData.value = [];
|
||||
@@ -122,6 +230,7 @@ const getAlbumData = (id) => {
|
||||
alia: v.alia,
|
||||
time: getSongTime(v.dt),
|
||||
fee: v.fee,
|
||||
sourceId: id,
|
||||
pc: v.pc ? v.pc : null,
|
||||
mv: v.mv ? v.mv : null,
|
||||
});
|
||||
@@ -132,9 +241,75 @@ const getAlbumData = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 播放专辑所有歌曲
|
||||
const playAllSong = () => {
|
||||
try {
|
||||
// 获取元素
|
||||
const songDom = document.getElementById("datalists").firstElementChild;
|
||||
const allSongDom = document.querySelectorAll("#datalists > *");
|
||||
// 是否有元素存在 play
|
||||
let isHasPlay = false;
|
||||
// 遍历
|
||||
allSongDom.forEach((child) => {
|
||||
if (child.classList.contains("play")) {
|
||||
isHasPlay = true;
|
||||
}
|
||||
});
|
||||
if (!isHasPlay) {
|
||||
// 双击操作
|
||||
const event = new MouseEvent("dblclick", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
// 双击或单击
|
||||
if (setting.listClickMode === "dblclick") {
|
||||
songDom.dispatchEvent(event);
|
||||
} else if (setting.listClickMode === "click") {
|
||||
songDom.click();
|
||||
}
|
||||
} else {
|
||||
music.setPlayState(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("播放全部歌曲失败:" + err);
|
||||
$message.error("播放全部歌曲失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏/取消收藏
|
||||
const toChangeLike = async (id) => {
|
||||
const type = isLikeOrDislike(id) ? 1 : 2;
|
||||
try {
|
||||
const res = await likeAlbum(type, id);
|
||||
if (res.code === 200) {
|
||||
$message.success(`专辑${type == 1 ? "收藏成功" : "取消收藏成功"}`);
|
||||
user.setUserAlbumLists(() => {
|
||||
setDropdownOptions();
|
||||
});
|
||||
} else {
|
||||
$message.error(`专辑${type == 1 ? "收藏失败" : "取消收藏失败"}`);
|
||||
}
|
||||
} catch (err) {
|
||||
$message.error(`专辑${type == 1 ? "收藏失败" : "取消收藏失败"}`);
|
||||
console.error(`专辑${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (albumId.value) {
|
||||
getAlbumData(albumId.value);
|
||||
if (
|
||||
user.userLogin &&
|
||||
!user.getUserAlbumLists.has &&
|
||||
!user.getUserAlbumLists.isLoading
|
||||
) {
|
||||
user.setUserAlbumLists(() => {
|
||||
setDropdownOptions();
|
||||
});
|
||||
} else {
|
||||
setDropdownOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,49 +341,84 @@ watch(
|
||||
align-items: flex-start;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
@media (max-width: 990px) {
|
||||
margin-right: 0;
|
||||
width: 30vw;
|
||||
}
|
||||
.cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
.n-avatar {
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.album {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 4%;
|
||||
right: -20%;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 24px;
|
||||
width: 80%;
|
||||
padding-left: 4px;
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 4;
|
||||
line-height: 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
margin-top: 20px;
|
||||
.meta {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
.tags {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
.title {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
margin-top: 0;
|
||||
.name {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.creator {
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 24px;
|
||||
width: 80%;
|
||||
padding-left: 4px;
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 4;
|
||||
line-height: 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
margin-top: 20px;
|
||||
.tags {
|
||||
line-height: 0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
.control {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,13 +437,19 @@ watch(
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.time {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1100px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@@ -259,21 +475,111 @@ watch(
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
.left {
|
||||
margin-bottom: 12px;
|
||||
position: static;
|
||||
width: 60vw;
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 30vw;
|
||||
max-width: none;
|
||||
.intr {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.cover {
|
||||
height: 100%;
|
||||
min-width: 30vw;
|
||||
width: 30vw;
|
||||
margin-right: 60px;
|
||||
}
|
||||
.meta {
|
||||
.title {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
.name {
|
||||
font-size: 25px;
|
||||
}
|
||||
.creator {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
.name,
|
||||
.all-desc {
|
||||
display: none;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 3;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 80px;
|
||||
.meta {
|
||||
.name {
|
||||
font-size: 26px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.left {
|
||||
.cover {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.meta {
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
.name {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.intr,
|
||||
.tag {
|
||||
display: none !important;
|
||||
}
|
||||
.control {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.left {
|
||||
.meta {
|
||||
.title {
|
||||
.name {
|
||||
font-size: 20px;
|
||||
}
|
||||
.creator {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 370px) {
|
||||
.left {
|
||||
.meta {
|
||||
.title {
|
||||
.name {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.title {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script setup>
|
||||
import { getArtistAblums } from "@/api/artist";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getLongTime } from "@/utils/timeTools.js";
|
||||
import { getLongTime } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getArtistAllSongs } from "@/api/artist";
|
||||
import { getArtistDetail, getArtistAllSongs } from "@/api/artist";
|
||||
import { getMusicDetail } from "@/api/song";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongTime } from "@/utils/timeTools.js";
|
||||
import { getSongTime } from "@/utils/timeTools";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
@@ -51,41 +51,56 @@ const pageNumber = ref(
|
||||
: 1
|
||||
);
|
||||
|
||||
// 获取歌手名称
|
||||
const getArtistDetailData = (id) => {
|
||||
getArtistDetail(id).then((res) => {
|
||||
artistName.value = res.data.artist.name;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取歌手信息
|
||||
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,
|
||||
if (!id) return false;
|
||||
getArtistAllSongs(id, limit, offset, order)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
// 获取歌手名称
|
||||
getArtistDetailData(id);
|
||||
// 全部歌曲数据
|
||||
if (res.songs[0]) {
|
||||
// 数据总数
|
||||
totalCount.value = res.total;
|
||||
// 列表数据
|
||||
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" });
|
||||
});
|
||||
} else {
|
||||
$message.error("歌手全部歌曲为空");
|
||||
}
|
||||
// 请求后回顶并结束加载条
|
||||
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
|
||||
})
|
||||
.catch((err) => {
|
||||
router.go(-1);
|
||||
console.error("歌手全部歌曲获取失败:" + err);
|
||||
$message.error("歌手全部歌曲获取失败");
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由参数变化
|
||||
|
||||
@@ -146,6 +146,7 @@ const getArtistDetailData = (id) => {
|
||||
musicSize: res.data.artist.musicSize,
|
||||
mvSize: res.data.artist.mvSize,
|
||||
};
|
||||
$setSiteTitle(res.data.artist.name + " - 歌手");
|
||||
// 请求后回顶
|
||||
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
|
||||
})
|
||||
@@ -307,13 +308,13 @@ watch(
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
.n-icon {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
transform: translateY(-1px);
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<script setup>
|
||||
import { getArtistSongs } from "@/api/artist";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongTime } from "@/utils/timeTools.js";
|
||||
import { getSongTime } from "@/utils/timeTools";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -73,8 +73,8 @@ watch(
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script setup>
|
||||
import { getArtistVideos } from "@/api/artist";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools";
|
||||
import VideoLists from "@/components/DataList/VideoLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ const pageNumberChange = (val) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("全部评论");
|
||||
// 获取评论数据
|
||||
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ const getDailySongsData = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("每日推荐");
|
||||
if (music.getDailySongs.length === 0) getDailySongsData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -200,6 +200,7 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("发现 - 歌手");
|
||||
// 获取歌手数据
|
||||
getArtistListData(
|
||||
artistType[artistTypeNamesChoose.value],
|
||||
@@ -239,8 +240,8 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
@@ -261,8 +262,8 @@ onMounted(() => {
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
class="cat"
|
||||
icon-placement="right"
|
||||
round
|
||||
@click="catModelShow = true"
|
||||
@click="catModalShow = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon class="up" :component="ChevronRightRound" />
|
||||
@@ -17,7 +17,7 @@
|
||||
</n-button>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="catModelShow"
|
||||
v-model:show="catModalShow"
|
||||
preset="card"
|
||||
title="歌单分类"
|
||||
:bordered="false"
|
||||
@@ -32,6 +32,7 @@
|
||||
marginLeft: '12px',
|
||||
fontSize: '12px',
|
||||
transform: 'translateY(-2px)',
|
||||
cursor: 'pointer',
|
||||
}"
|
||||
:bordered="false"
|
||||
@click="changeTagName('全部歌单')"
|
||||
@@ -39,7 +40,7 @@
|
||||
全部歌单
|
||||
</n-tag>
|
||||
</template>
|
||||
<n-scrollbar style="max-height: 80vh">
|
||||
<n-scrollbar>
|
||||
<div class="all-cat" v-if="music.catList?.sub[0]">
|
||||
<n-list>
|
||||
<n-list-item
|
||||
@@ -126,7 +127,7 @@ import { ChevronRightRound, LocalFireDepartmentRound } from "@vicons/material";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicStore } from "@/store";
|
||||
import { getHighqualityPlaylist, getTopPlaylist } from "@/api/playlist";
|
||||
import { formatNumber } from "@/utils/timeTools.js";
|
||||
import { formatNumber } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
@@ -134,7 +135,7 @@ const router = useRouter();
|
||||
const music = musicStore();
|
||||
|
||||
// 分类数据
|
||||
const catModelShow = ref(false);
|
||||
const catModalShow = ref(false);
|
||||
const catName = ref(
|
||||
router.currentRoute.value.query.cat
|
||||
? router.currentRoute.value.query.cat
|
||||
@@ -254,7 +255,7 @@ const changeTagName = (name) => {
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
catModelShow.value = false;
|
||||
catModalShow.value = false;
|
||||
};
|
||||
|
||||
// 排序方式变化
|
||||
@@ -299,6 +300,7 @@ watch(
|
||||
: false;
|
||||
if (val.name == "dsc-playlists") {
|
||||
if (hqPLayListOpen.value) {
|
||||
playlistsData.value = [];
|
||||
getHqPlaylistData(catName.value);
|
||||
} else {
|
||||
pageNumber.value = val.query.page ? Number(val.query.page) : 1;
|
||||
@@ -313,6 +315,7 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("发现 - 歌单");
|
||||
// 获取歌单分类
|
||||
if (!music.catList.sub || !music.highqualityCatList[0])
|
||||
music.setCatList(true);
|
||||
@@ -347,8 +350,8 @@ onMounted(() => {
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
@@ -372,14 +375,14 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.icon {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
transform: translateY(-1px);
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<script setup>
|
||||
import { getToplist } from "@/api/album";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber } from "@/utils/timeTools.js";
|
||||
import { formatNumber } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,10 @@ import DataLists from "@/components/DataList/DataLists.vue";
|
||||
|
||||
const music = musicStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("播放历史");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -28,6 +28,10 @@ import PaDailySongs from "@/components/Personalized/PaDailySongs.vue";
|
||||
import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
|
||||
|
||||
const setting = settingStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle(import.meta.env.VITE_SITE_TITLE);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
:size="180"
|
||||
level="H"
|
||||
background="#00000000"
|
||||
foreground="#f55e55"
|
||||
:foreground="setting.themeData.primaryColor"
|
||||
/>
|
||||
</n-card>
|
||||
<span class="tip">{{ qrText }}</span>
|
||||
@@ -94,7 +94,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { userStore } from "@/store";
|
||||
import { userStore, musicStore, settingStore } from "@/store";
|
||||
import {
|
||||
getLoginState,
|
||||
getQrKey,
|
||||
@@ -105,11 +105,13 @@ import {
|
||||
} from "@/api/login";
|
||||
import { useRouter } from "vue-router";
|
||||
import { PhoneAndroidRound, PasswordRound } from "@vicons/material";
|
||||
import { formRules } from "@/utils/formRules.js";
|
||||
import { formRules } from "@/utils/formRules";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const user = userStore();
|
||||
const music = musicStore();
|
||||
const setting = settingStore();
|
||||
const { numberRule, mobileRule } = formRules();
|
||||
|
||||
// 二维码数据
|
||||
@@ -138,6 +140,7 @@ const loginStateMessage = ref(null);
|
||||
|
||||
// 储存登录信息
|
||||
const saveLoginData = (data) => {
|
||||
data.cookie = data.cookie.replaceAll(" HTTPOnly", "");
|
||||
user.setCookie(data.cookie);
|
||||
// 验证用户登录信息
|
||||
getLoginState().then((res) => {
|
||||
@@ -149,7 +152,7 @@ const saveLoginData = (data) => {
|
||||
// 自动签到
|
||||
if ($signIn) $signIn();
|
||||
clearInterval(qrCheckInterval.value);
|
||||
router.go(-1);
|
||||
router.push("/user");
|
||||
} else {
|
||||
user.userLogOut();
|
||||
$message.error("登录出错,请重试");
|
||||
@@ -165,7 +168,7 @@ const getQrKeyData = () => {
|
||||
if (res.data.profile && window.localStorage.getItem("cookie")) {
|
||||
$message.info("已登录,请勿重复登录");
|
||||
user.userLogin = true;
|
||||
router.go(-1);
|
||||
router.push("/user");
|
||||
} else {
|
||||
user.userLogOut();
|
||||
clearInterval(qrCheckInterval.value);
|
||||
@@ -266,7 +269,7 @@ const phoneLogin = (e) => {
|
||||
phoneFormData._value.captcha
|
||||
).then((res) => {
|
||||
console.log(res);
|
||||
// 网易接口抽风,等好了再写
|
||||
// 暂时不支持,等支持了再写
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -288,11 +291,16 @@ const tabChange = (val) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("登录");
|
||||
// 隐藏控制条
|
||||
music.setPlayBarState(false);
|
||||
// 获取二维码登录 key
|
||||
getQrKeyData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 恢复控制条
|
||||
music.setPlayBarState(true);
|
||||
// 清除定时器
|
||||
clearInterval(qrCheckInterval.value);
|
||||
clearInterval(captchaTimeOut.value);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<script setup>
|
||||
import { getAlbumNew } from "@/api/album";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getLongTime } from "@/utils/timeTools.js";
|
||||
import { getLongTime } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
@@ -103,7 +103,7 @@ const getAlbumNewData = (area, limit = 30, offset = 0) => {
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(val) => {
|
||||
albumAreaChoose.value = val.query.area;
|
||||
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(
|
||||
@@ -149,6 +149,7 @@ const changeArea = (area) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("全部新碟");
|
||||
getAlbumNewData(
|
||||
albumAreaChoose.value,
|
||||
pagelimit.value,
|
||||
@@ -177,8 +178,8 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
|
||||
@@ -7,78 +7,85 @@
|
||||
:src="
|
||||
playListDetail.coverImgUrl
|
||||
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
|
||||
'?param=500y500'
|
||||
'?param=1024y1024'
|
||||
: null
|
||||
"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<n-avatar
|
||||
class="shadow"
|
||||
:src="
|
||||
playListDetail.coverImgUrl
|
||||
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
|
||||
'?param=1024y1024'
|
||||
: null
|
||||
"
|
||||
fallback-src="/images/pic/default.png"
|
||||
/>
|
||||
<img src="/images/pic/album.png" class="album" alt="album" />
|
||||
</div>
|
||||
<div class="intr">
|
||||
<span class="name">歌单简介</span>
|
||||
<span class="desc text-hidden">
|
||||
{{
|
||||
playListDetail.description
|
||||
? playListDetail.description
|
||||
: "太懒了吧,连简介都不写"
|
||||
}}
|
||||
</span>
|
||||
<n-button
|
||||
block
|
||||
strong
|
||||
secondary
|
||||
v-if="playListDetail?.description.length > 70"
|
||||
@click="playListDescShow = true"
|
||||
>
|
||||
全部简介
|
||||
</n-button>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="playListDescShow"
|
||||
preset="card"
|
||||
title="歌单简介"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<n-text
|
||||
v-html="playListDetail.description.replace(/\n/g, '<br>')"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</div>
|
||||
<n-space class="tag" v-if="playListDetail.tags">
|
||||
<n-tag
|
||||
class="tags"
|
||||
size="large"
|
||||
round
|
||||
:bordered="false"
|
||||
v-for="item in playListDetail.tags"
|
||||
:key="item"
|
||||
@click="router.push(`/discover/playlists?cat=${item}&page=1`)"
|
||||
>
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<!-- <div class="control" v-if="true">
|
||||
<n-space>
|
||||
<n-button strong secondary round>
|
||||
<template #icon>
|
||||
<n-icon :component="EditNoteRound" />
|
||||
</template>
|
||||
编辑
|
||||
</n-button>
|
||||
<n-button strong secondary round type="primary">
|
||||
<template #icon>
|
||||
<n-icon :component="DeleteRound" />
|
||||
</template>
|
||||
<div class="meta">
|
||||
<div class="title">
|
||||
<n-text class="name text-hidden">{{ playListDetail.name }}</n-text>
|
||||
<n-text class="creator">{{ playListDetail.creator.nickname }}</n-text>
|
||||
</div>
|
||||
<div class="intr">
|
||||
<span class="name">歌单简介</span>
|
||||
<span class="desc text-hidden">
|
||||
{{
|
||||
playListDetail.description
|
||||
? playListDetail.description
|
||||
: "太懒了吧,连简介都不写"
|
||||
}}
|
||||
</span>
|
||||
<n-button
|
||||
class="all-desc"
|
||||
block
|
||||
strong
|
||||
secondary
|
||||
v-if="playListDetail?.description?.length > 70"
|
||||
@click="playListDescShow = true"
|
||||
>
|
||||
全部简介
|
||||
</n-button>
|
||||
</div>
|
||||
<n-space class="tag" v-if="playListDetail.tags">
|
||||
<n-tag
|
||||
class="tags"
|
||||
round
|
||||
:bordered="false"
|
||||
v-for="item in playListDetail.tags"
|
||||
:key="item"
|
||||
@click="router.push(`/discover/playlists?cat=${item}&page=1`)"
|
||||
>
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div> -->
|
||||
<n-space class="control">
|
||||
<n-button strong secondary round type="primary" @click="playAllSong">
|
||||
<template #icon>
|
||||
<n-icon :component="MusicList" />
|
||||
</template>
|
||||
播放
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
:show-arrow="true"
|
||||
:options="dropdownOptions"
|
||||
>
|
||||
<n-button strong secondary circle>
|
||||
<template #icon>
|
||||
<n-icon :component="More" />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="meta">
|
||||
<span class="name">{{ playListDetail.name }}</span>
|
||||
<span class="creator">{{ playListDetail.creator.nickname }}</span>
|
||||
<n-text class="name">{{ playListDetail.name }}</n-text>
|
||||
<n-text class="creator">{{ playListDetail.creator.nickname }}</n-text>
|
||||
<div class="time">
|
||||
<div class="createTime">
|
||||
<span class="num">创建时间:</span>
|
||||
@@ -99,13 +106,27 @@
|
||||
@pageSizeChange="pageSizeChange"
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
<!-- 歌单简介 -->
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="playListDescShow"
|
||||
preset="card"
|
||||
title="歌单简介"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-scrollbar>
|
||||
<n-text v-html="playListDetail.description.replace(/\n/g, '<br>')" />
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title" v-else-if="!playListId">
|
||||
<span class="key">参数不完整</span>
|
||||
<div class="title" v-else-if="!playListId || !loadingState">
|
||||
<span class="key">{{
|
||||
loadingState ? "参数不完整" : "歌单信息加载失败"
|
||||
}}</span>
|
||||
<br />
|
||||
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
|
||||
返回上一级
|
||||
返回上一页
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="loading" v-else>
|
||||
@@ -122,16 +143,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getPlayListDetail, getAllPlayList } from "@/api/playlist";
|
||||
import { NIcon, NAvatar, NText } from "naive-ui";
|
||||
import {
|
||||
getPlayListDetail,
|
||||
getAllPlayList,
|
||||
delPlayList,
|
||||
likePlaylist,
|
||||
} from "@/api/playlist";
|
||||
import { useRouter } from "vue-router";
|
||||
import { userStore, musicStore } from "@/store";
|
||||
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
|
||||
import { EditNoteRound, DeleteRound } from "@vicons/material";
|
||||
import { userStore, musicStore, settingStore } from "@/store";
|
||||
import { getSongTime, getLongTime } from "@/utils/timeTools";
|
||||
import {
|
||||
MusicList,
|
||||
LinkTwo,
|
||||
More,
|
||||
DeleteFour,
|
||||
Like,
|
||||
Unlike,
|
||||
} from "@icon-park/vue-next";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const user = userStore();
|
||||
const music = musicStore();
|
||||
const setting = settingStore();
|
||||
|
||||
// 歌单数据
|
||||
const playListId = ref(router.currentRoute.value.query.id);
|
||||
@@ -139,6 +175,7 @@ const playListDetail = ref(null);
|
||||
const playListData = ref([]);
|
||||
const playListDescShow = ref(false);
|
||||
const pagelimit = ref(30);
|
||||
const loadingState = ref(true);
|
||||
const pageNumber = ref(
|
||||
router.currentRoute.value.query.page
|
||||
? Number(router.currentRoute.value.query.page)
|
||||
@@ -146,19 +183,105 @@ const pageNumber = ref(
|
||||
);
|
||||
const totalCount = ref(0);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => {
|
||||
return h(
|
||||
NIcon,
|
||||
{ style: { transform: "translateX(2px)" } },
|
||||
{
|
||||
default: () => icon,
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// 判断收藏还是取消
|
||||
const isLikeOrDislike = (id) => {
|
||||
const playlists = user.getUserPlayLists.like;
|
||||
if (playlists.length) {
|
||||
return !playlists.some((item) => item.id === Number(id));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 判断是否可删除
|
||||
const isCanDelete = (id) => {
|
||||
const playlists = user.getUserPlayLists.own;
|
||||
if (playlists.length) {
|
||||
return playlists.some((item) => item.id === Number(id));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 歌单下拉菜单数据
|
||||
const dropdownOptions = ref([]);
|
||||
|
||||
// 更改歌单下拉菜单数据
|
||||
const setDropdownOptions = () => {
|
||||
dropdownOptions.value = [
|
||||
{
|
||||
key: "copy",
|
||||
label: "复制歌单链接",
|
||||
props: {
|
||||
onClick: () => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
navigator.clipboard.writeText(
|
||||
`https://music.163.com/#/playlist?id=${playListId.value}`
|
||||
);
|
||||
$message.success("歌单链接复制成功");
|
||||
} catch (err) {
|
||||
$message.error("复制失败:", err);
|
||||
}
|
||||
} else {
|
||||
$message.error("您的浏览器暂不支持该操作");
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(LinkTwo)),
|
||||
},
|
||||
{
|
||||
key: "del",
|
||||
label: "删除歌单",
|
||||
show: user.userLogin && isCanDelete(playListId.value),
|
||||
props: {
|
||||
onClick: () => {
|
||||
toDelPlayList(playListDetail.value);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(DeleteFour)),
|
||||
},
|
||||
{
|
||||
key: "like",
|
||||
label: isLikeOrDislike(playListId.value) ? "收藏歌单" : "取消收藏歌单",
|
||||
show: user.userLogin && !isCanDelete(playListId.value),
|
||||
props: {
|
||||
onClick: () => {
|
||||
toChangeLike(playListId.value);
|
||||
},
|
||||
},
|
||||
icon: renderIcon(h(isLikeOrDislike(playListId.value) ? Like : Unlike)),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 获取歌单信息
|
||||
const getPlayListDetailData = (id) => {
|
||||
getPlayListDetail(id).then((res) => {
|
||||
console.log(res);
|
||||
if (res.playlist) {
|
||||
getPlayListDetail(id)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
// 歌单总数
|
||||
totalCount.value = res.playlist.trackCount;
|
||||
// 歌单信息
|
||||
playListDetail.value = res.playlist;
|
||||
} else {
|
||||
$message.error("获取歌单信息失败");
|
||||
}
|
||||
});
|
||||
$setSiteTitle(res.playlist.name + " - 歌单");
|
||||
})
|
||||
.catch((err) => {
|
||||
$setSiteTitle("歌单详情");
|
||||
loadingState.value = false;
|
||||
console.error("获取歌单信息失败:" + err);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取歌单所有歌曲
|
||||
@@ -177,6 +300,7 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
|
||||
alia: v.alia,
|
||||
time: getSongTime(v.dt),
|
||||
fee: v.fee,
|
||||
sourceId: id,
|
||||
pc: v.pc ? v.pc : null,
|
||||
mv: v.mv ? v.mv : null,
|
||||
});
|
||||
@@ -189,6 +313,85 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 播放歌单所有歌曲
|
||||
const playAllSong = () => {
|
||||
try {
|
||||
// 获取元素
|
||||
const songDom = document.getElementById("datalists").firstElementChild;
|
||||
const allSongDom = document.querySelectorAll("#datalists > *");
|
||||
// 是否有元素存在 play
|
||||
let isHasPlay = false;
|
||||
// 遍历
|
||||
allSongDom.forEach((child) => {
|
||||
if (child.classList.contains("play")) {
|
||||
isHasPlay = true;
|
||||
}
|
||||
});
|
||||
if (!isHasPlay) {
|
||||
// 双击操作
|
||||
const event = new MouseEvent("dblclick", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
// 双击或单击
|
||||
if (setting.listClickMode === "dblclick") {
|
||||
songDom.dispatchEvent(event);
|
||||
} else if (setting.listClickMode === "click") {
|
||||
songDom.click();
|
||||
}
|
||||
} else {
|
||||
music.setPlayState(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("播放全部歌曲失败:" + err);
|
||||
$message.error("播放全部歌曲失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 删除歌单
|
||||
const toDelPlayList = (data) => {
|
||||
if (data.id === user.getUserPlayLists?.own[0].id) {
|
||||
$message.warning("默认歌单无法删除");
|
||||
return false;
|
||||
}
|
||||
$dialog.warning({
|
||||
class: "s-dialog",
|
||||
title: "删除歌单",
|
||||
content: "确认删除歌单 " + data.name + "?删除后将不可恢复!",
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
delPlayList(data.id).then((res) => {
|
||||
if (res.code === 200) {
|
||||
$message.success("删除成功");
|
||||
user.setUserPlayLists();
|
||||
router.push("/user/playlists");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 收藏/取消收藏
|
||||
const toChangeLike = async (id) => {
|
||||
const type = isLikeOrDislike(id) ? 1 : 2;
|
||||
try {
|
||||
const res = await likePlaylist(type, id);
|
||||
if (res.code === 200) {
|
||||
$message.success(`歌单${type == 1 ? "收藏成功" : "取消收藏成功"}`);
|
||||
user.setUserPlayLists(() => {
|
||||
setDropdownOptions();
|
||||
});
|
||||
} else {
|
||||
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
|
||||
}
|
||||
} catch (err) {
|
||||
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
|
||||
console.error(`歌单${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (playListId.value) {
|
||||
getPlayListDetailData(playListId.value);
|
||||
@@ -197,6 +400,17 @@ onMounted(() => {
|
||||
pagelimit.value,
|
||||
(pageNumber.value - 1) * pagelimit.value
|
||||
);
|
||||
if (
|
||||
user.userLogin &&
|
||||
!user.getUserPlayLists.has &&
|
||||
!user.getUserPlayLists.isLoading
|
||||
) {
|
||||
user.setUserPlayLists(() => {
|
||||
setDropdownOptions();
|
||||
});
|
||||
} else {
|
||||
setDropdownOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -264,53 +478,94 @@ watch(
|
||||
align-items: flex-start;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
@media (max-width: 990px) {
|
||||
margin-right: 0;
|
||||
width: 30vw;
|
||||
}
|
||||
.cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
.n-avatar {
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
}
|
||||
.album {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 4%;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(16px) opacity(0.6);
|
||||
transform: scale(0.92, 0.96);
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 24px;
|
||||
width: 80%;
|
||||
padding-left: 4px;
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 4;
|
||||
line-height: 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
margin-top: 20px;
|
||||
.tags {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: $mainSecondaryColor;
|
||||
color: $mainColor;
|
||||
.meta {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
.title {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
margin-top: 0;
|
||||
.name {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
.creator {
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 24px;
|
||||
width: 80%;
|
||||
padding-left: 4px;
|
||||
.name {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
@media (max-width: 990px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 4;
|
||||
line-height: 26px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
margin-top: 20px;
|
||||
.tags {
|
||||
line-height: 0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
.control {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
@@ -328,6 +583,12 @@ watch(
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.time {
|
||||
margin-top: 8px;
|
||||
@@ -360,22 +621,110 @@ watch(
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
.left {
|
||||
margin-bottom: 12px;
|
||||
position: static;
|
||||
width: 60vw;
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 40vw;
|
||||
max-width: none;
|
||||
.intr,
|
||||
.tag {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.cover {
|
||||
height: 100%;
|
||||
min-width: 40vw;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.meta {
|
||||
.title {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
.name {
|
||||
font-size: 25px;
|
||||
}
|
||||
.creator {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.intr {
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
.name,
|
||||
.all-desc {
|
||||
display: none;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 80px;
|
||||
.meta {
|
||||
.name {
|
||||
font-size: 26px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.left {
|
||||
.cover {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.meta {
|
||||
.title {
|
||||
.name {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.intr,
|
||||
.tag {
|
||||
display: none !important;
|
||||
}
|
||||
.control {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.left {
|
||||
.meta {
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
.name {
|
||||
font-size: 20px;
|
||||
}
|
||||
.creator {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 370px) {
|
||||
.left {
|
||||
.meta {
|
||||
.title {
|
||||
.name {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
margin-top: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.title {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script setup>
|
||||
import { getSearchData } from "@/api/search";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getLongTime } from "@/utils/timeTools.js";
|
||||
import { getLongTime } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -71,6 +72,10 @@ const tabChange = (value) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (searchKeywords.value) $setSiteTitle(searchKeywords.value + "的搜索结果");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script setup>
|
||||
import { getSearchData } from "@/api/search";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber } from "@/utils/timeTools.js";
|
||||
import { formatNumber } from "@/utils/timeTools";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<script setup>
|
||||
import { getSearchData } from "@/api/search";
|
||||
import { getMusicDetail } from "@/api/song";
|
||||
// import { getMusicDetail } from "@/api/song";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongTime } from "@/utils/timeTools.js";
|
||||
import { getSongTime } from "@/utils/timeTools";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
const router = useRouter();
|
||||
@@ -41,7 +41,7 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1) => {
|
||||
totalCount.value = res.result.songCount;
|
||||
// const ids = res.result.songs.map((obj) => obj.id);
|
||||
// getMusicDetail(ids.join(",")).then((res) => {});
|
||||
console.log(res);
|
||||
// console.log(res);
|
||||
searchData.value = [];
|
||||
res.result.songs.forEach((v, i) => {
|
||||
searchData.value.push({
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script setup>
|
||||
import { getSearchData } from "@/api/search";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools";
|
||||
import VideoLists from "@/components/DataList/VideoLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
<template>
|
||||
<div class="setting">
|
||||
<div class="title">全局设置</div>
|
||||
<n-h6 prefix="bar"> 基础设置 </n-h6>
|
||||
<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">
|
||||
列表点击方式
|
||||
<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="bannerShow" :round="false" />
|
||||
</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>
|
||||
<n-h6 prefix="bar"> 歌词设置 </n-h6>
|
||||
<n-card class="set-item">
|
||||
<div class="name">显示歌词翻译</div>
|
||||
<n-switch v-model:value="showTransl" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
智能暂停滚动
|
||||
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lrcMousePause" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">播放器样式</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="playerStyle"
|
||||
:options="playerStyleOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card
|
||||
class="set-item"
|
||||
:content-style="{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}"
|
||||
>
|
||||
<div class="name">歌词文本大小</div>
|
||||
<n-slider
|
||||
v-model:value="lyricsFontSize"
|
||||
:tooltip="false"
|
||||
:max="3.4"
|
||||
:min="2.2"
|
||||
:step="0.01"
|
||||
:marks="{
|
||||
2.2: '最小',
|
||||
2.8: '默认',
|
||||
3.4: '最大',
|
||||
}"
|
||||
/>
|
||||
<div :class="lyricsBlur ? 'more blur' : 'more'">
|
||||
<div
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
:class="n === 2 ? 'lrc on' : 'lrc'"
|
||||
:style="{
|
||||
margin: n === 2 ? '12px 0' : null,
|
||||
alignItems: lyricsPosition == 'center' ? 'center' : null,
|
||||
transformOrigin:
|
||||
lyricsPosition == 'center' ? 'center' : 'center left',
|
||||
}"
|
||||
>
|
||||
<span :style="{ fontSize: lyricsFontSize + 'vh' }"
|
||||
>这是一句歌词
|
||||
</span>
|
||||
<span :style="{ fontSize: lyricsFontSize - 0.4 + 'vh' }"
|
||||
>This is a lyric
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">默认歌词位置</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="lyricsPosition"
|
||||
:options="lyricsPositionOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌词滚动位置
|
||||
<span class="tip">歌词高亮时所处的位置</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="lyricsBlock"
|
||||
:options="lyricsBlockOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌词模糊
|
||||
<span class="tip">实验性功能,未播放或已播放歌词模糊显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lyricsBlur" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示音乐频谱
|
||||
<span class="tip">实验性功能,可能会导致一些意想不到的后果</span>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="musicFrequency"
|
||||
:round="false"
|
||||
@click="changeMusicFrequency"
|
||||
/>
|
||||
</n-card>
|
||||
<n-h6 prefix="bar"> 其他设置 </n-h6>
|
||||
<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>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { settingStore, userStore } from "@/store";
|
||||
const setting = settingStore();
|
||||
const user = userStore();
|
||||
const {
|
||||
theme,
|
||||
themeAuto,
|
||||
showTransl,
|
||||
lyricsPosition,
|
||||
playerStyle,
|
||||
musicFrequency,
|
||||
listClickMode,
|
||||
lyricsFontSize,
|
||||
bottomLyricShow,
|
||||
lyricsBlock,
|
||||
songLevel,
|
||||
bannerShow,
|
||||
lyricsBlur,
|
||||
autoSignIn,
|
||||
lrcMousePause,
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// 歌词位置
|
||||
const lyricsPositionOptions = [
|
||||
{
|
||||
label: "居左",
|
||||
value: "left",
|
||||
},
|
||||
{
|
||||
label: "居中",
|
||||
value: "center",
|
||||
},
|
||||
];
|
||||
|
||||
// 歌词滚动位置
|
||||
const lyricsBlockOptions = [
|
||||
{
|
||||
label: "靠近顶部",
|
||||
value: "start",
|
||||
},
|
||||
{
|
||||
label: "水平居中",
|
||||
value: "center",
|
||||
},
|
||||
];
|
||||
|
||||
// 播放器样式
|
||||
const playerStyleOptions = [
|
||||
{
|
||||
label: "封面模式",
|
||||
value: "cover",
|
||||
},
|
||||
{
|
||||
label: "唱片模式",
|
||||
value: "record",
|
||||
},
|
||||
];
|
||||
|
||||
// 音乐频谱提醒
|
||||
const changeMusicFrequency = () => {
|
||||
if (musicFrequency.value) {
|
||||
$dialog.warning({
|
||||
class: "s-dialog",
|
||||
title: "实验性功能",
|
||||
content: "确认开启音乐频谱?将于刷新后生效",
|
||||
positiveText: "开启",
|
||||
negativeText: "取消",
|
||||
onMaskClick: () => {
|
||||
musicFrequency.value = false;
|
||||
},
|
||||
onPositiveClick: () => {
|
||||
musicFrequency.value = true;
|
||||
location.reload();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
musicFrequency.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 系统重置
|
||||
const resetApp = () => {
|
||||
const cleanAll = () => {
|
||||
$message ? $message.success("重置成功") : alert("重置成功");
|
||||
localStorage.clear();
|
||||
window.location.href = "/";
|
||||
};
|
||||
$dialog.warning({
|
||||
title: "系统重置",
|
||||
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
|
||||
positiveText: "重置",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
$cleanAll ? $cleanAll() : cleanAll();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting {
|
||||
padding: 0 10vw;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@media (max-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
.title {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.n-h {
|
||||
padding-left: 16px;
|
||||
font-size: 20px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.set-item {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
:deep(.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
src/views/Setting/index.vue
Normal file
137
src/views/Setting/index.vue
Normal 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>
|
||||
286
src/views/Setting/main.vue
Normal file
286
src/views/Setting/main.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div class="set-main">
|
||||
<n-card
|
||||
class="set-item"
|
||||
:content-style="{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}"
|
||||
>
|
||||
<div class="top">
|
||||
<div class="name">
|
||||
主题色选择
|
||||
<span class="tip">更换全站主题色,即时生效</span>
|
||||
</div>
|
||||
<n-button
|
||||
v-if="themeType !== 'red'"
|
||||
strong
|
||||
secondary
|
||||
@click="changeThemeColor(null, true)"
|
||||
>
|
||||
恢复默认
|
||||
</n-button>
|
||||
</div>
|
||||
<n-grid
|
||||
class="color-selete"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
responsive="screen"
|
||||
cols="3 s:4 m:5 l:6"
|
||||
>
|
||||
<n-grid-item
|
||||
v-for="item in themeColorData"
|
||||
:key="item"
|
||||
:style="{ '--color': item.primaryColor }"
|
||||
:class="item.label === themeType ? 'item check' : 'item'"
|
||||
@click="changeThemeColor(item)"
|
||||
>
|
||||
<n-text v-html="item.name" />
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<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-switch v-model:value="songVolumeFade" :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>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
尝试替换无法播放的歌曲
|
||||
<span class="tip">
|
||||
{{
|
||||
useUnmServerShow
|
||||
? "是否使用 UNM 替换无法播放的歌曲链接"
|
||||
: "请配置 UNM-Server 后使用解灰功能"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="useUnmServer"
|
||||
:round="false"
|
||||
:disabled="!useUnmServerShow"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放页快捷设置
|
||||
<span class="tip">是否在播放页面显示快捷设置</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showLyricSetting" :round="false" />
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { settingStore, userStore } from "@/store";
|
||||
import themeColorData from "@/components/Provider/themeColor.json";
|
||||
|
||||
const setting = settingStore();
|
||||
const user = userStore();
|
||||
const {
|
||||
theme,
|
||||
themeAuto,
|
||||
listClickMode,
|
||||
bottomLyricShow,
|
||||
songLevel,
|
||||
bannerShow,
|
||||
autoSignIn,
|
||||
searchHistory,
|
||||
themeType,
|
||||
showLyricSetting,
|
||||
songVolumeFade,
|
||||
useUnmServer,
|
||||
} = storeToRefs(setting);
|
||||
|
||||
// UNM 开关显示
|
||||
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
|
||||
|
||||
// 深浅模式
|
||||
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,
|
||||
},
|
||||
{
|
||||
label: "鲸云臻音",
|
||||
value: "jyeffect",
|
||||
disabled: user.userData?.vipType ? false : true,
|
||||
},
|
||||
{
|
||||
label: "鲸云母带",
|
||||
value: "jymaster",
|
||||
disabled: user.userData?.vipType ? false : true,
|
||||
},
|
||||
];
|
||||
|
||||
// 更换主题色
|
||||
const changeThemeColor = (data, reset = false) => {
|
||||
if (reset) {
|
||||
$dialog.warning({
|
||||
class: "s-dialog",
|
||||
title: "恢复默认",
|
||||
content: "确认恢复全站主题色为默认?",
|
||||
positiveText: "确认",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
$message.success("主题色已重置");
|
||||
themeType.value = "red";
|
||||
},
|
||||
});
|
||||
} else {
|
||||
$message.success("主题色更换为" + data.name);
|
||||
themeType.value = data.label;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.set-item {
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
.color-selete {
|
||||
margin-top: 16px;
|
||||
.item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
@media (max-width: 800px) {
|
||||
height: 60px;
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 2px solid var(--color);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
&.check {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.n-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/views/Setting/other.vue
Normal file
34
src/views/Setting/other.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<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({
|
||||
class: "s-dialog",
|
||||
title: "程序重置",
|
||||
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
|
||||
positiveText: "确认重置",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
$cleanAll ? $cleanAll() : cleanAll();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
234
src/views/Setting/player.vue
Normal file
234
src/views/Setting/player.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="set-player">
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放器样式
|
||||
<span class="tip">播放器左侧功能区样式</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="playerStyle"
|
||||
:options="playerStyleOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放背景样式
|
||||
<span class="tip">{{
|
||||
backgroundImageShow === "blur"
|
||||
? "将专辑封面模糊显示"
|
||||
: "提取专辑主色作为背景颜色"
|
||||
}}</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="backgroundImageShow"
|
||||
:options="backgroundImageShowOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示歌词翻译
|
||||
<span class="tip">是否在具有翻译歌词时显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showTransl" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示歌词音译
|
||||
<span class="tip">是否在具有音译歌词时显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showRoma" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示前奏等待
|
||||
<span class="tip">部分歌曲前奏可能存在显示错误</span>
|
||||
</div>
|
||||
<n-switch v-model:value="countDownShow" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示逐字歌词
|
||||
<span class="tip">是否在歌曲具有逐字歌词时显示,实验性功能</span>
|
||||
</div>
|
||||
<n-switch v-model:value="showYrc" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
智能暂停滚动
|
||||
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lrcMousePause" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌词滚动位置
|
||||
<span class="tip">歌词高亮时所处的位置</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="lyricsBlock"
|
||||
:options="lyricsBlockOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card
|
||||
class="set-item"
|
||||
:content-style="{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}"
|
||||
>
|
||||
<div class="name">歌词文本大小</div>
|
||||
<n-slider
|
||||
v-model:value="lyricsFontSize"
|
||||
:tooltip="false"
|
||||
:max="4"
|
||||
:min="3"
|
||||
:step="0.01"
|
||||
:marks="{
|
||||
3: '最小',
|
||||
3.6: '默认',
|
||||
4: '最大',
|
||||
}"
|
||||
/>
|
||||
<div :class="lyricsBlur ? 'more blur' : 'more'">
|
||||
<div
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
:class="n === 2 ? 'lrc on' : 'lrc'"
|
||||
:style="{
|
||||
margin: n === 2 ? '12px 0' : null,
|
||||
alignItems: lyricsPosition == 'center' ? 'center' : null,
|
||||
transformOrigin:
|
||||
lyricsPosition == 'center' ? 'center' : 'center left',
|
||||
}"
|
||||
>
|
||||
<span :style="{ fontSize: lyricsFontSize + 'vh' }"
|
||||
>这是一句歌词
|
||||
</span>
|
||||
<span :style="{ fontSize: lyricsFontSize - 0.4 + 'vh' }"
|
||||
>This is a lyric
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">默认歌词位置</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="lyricsPosition"
|
||||
:options="lyricsPositionOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌词模糊
|
||||
<span class="tip">未播放或已播放歌词模糊显示,实验性功能</span>
|
||||
</div>
|
||||
<n-switch v-model:value="lyricsBlur" :round="false" />
|
||||
</n-card>
|
||||
<!-- <n-card class="set-item">
|
||||
<div class="name">
|
||||
显示音乐频谱
|
||||
<span class="tip">可能会导致一些意想不到的后果,实验性功能</span>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="musicFrequency"
|
||||
:round="false"
|
||||
@click="changeMusicFrequency"
|
||||
/>
|
||||
</n-card> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { settingStore } from "@/store";
|
||||
|
||||
const setting = settingStore();
|
||||
const {
|
||||
showTransl,
|
||||
lyricsPosition,
|
||||
playerStyle,
|
||||
musicFrequency,
|
||||
lyricsFontSize,
|
||||
lyricsBlock,
|
||||
lyricsBlur,
|
||||
lrcMousePause,
|
||||
showYrc,
|
||||
showRoma,
|
||||
backgroundImageShow,
|
||||
countDownShow,
|
||||
} = storeToRefs(setting);
|
||||
|
||||
// 歌词位置
|
||||
const lyricsPositionOptions = [
|
||||
{
|
||||
label: "居左",
|
||||
value: "left",
|
||||
},
|
||||
{
|
||||
label: "居中",
|
||||
value: "center",
|
||||
},
|
||||
];
|
||||
|
||||
// 歌词滚动位置
|
||||
const lyricsBlockOptions = [
|
||||
{
|
||||
label: "靠近顶部",
|
||||
value: "start",
|
||||
},
|
||||
{
|
||||
label: "水平居中",
|
||||
value: "center",
|
||||
},
|
||||
];
|
||||
|
||||
// 播放器样式
|
||||
const playerStyleOptions = [
|
||||
{
|
||||
label: "封面模式",
|
||||
value: "cover",
|
||||
},
|
||||
{
|
||||
label: "唱片模式",
|
||||
value: "record",
|
||||
},
|
||||
];
|
||||
|
||||
// 播放背景类型
|
||||
const backgroundImageShowOptions = [
|
||||
{
|
||||
label: "封面主色",
|
||||
value: "solid",
|
||||
},
|
||||
{
|
||||
label: "封面模糊",
|
||||
value: "blur",
|
||||
},
|
||||
];
|
||||
|
||||
// 音乐频谱提醒
|
||||
// const changeMusicFrequency = () => {
|
||||
// if (musicFrequency.value) {
|
||||
// $dialog.warning({
|
||||
// class: "s-dialog",
|
||||
// title: "实验性功能",
|
||||
// content: "确认开启音乐频谱?将在重启应用后生效",
|
||||
// positiveText: "开启",
|
||||
// negativeText: "取消",
|
||||
// onMaskClick: () => {
|
||||
// musicFrequency.value = false;
|
||||
// },
|
||||
// onPositiveClick: () => {
|
||||
// musicFrequency.value = true;
|
||||
// },
|
||||
// onNegativeClick: () => {
|
||||
// musicFrequency.value = false;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
@@ -102,17 +102,17 @@
|
||||
import { getSimiPlayList, getMusicDetail } from "@/api/song";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicStore } from "@/store";
|
||||
import { getLongTime } from "@/utils/timeTools.js";
|
||||
import { getLongTime } from "@/utils/timeTools";
|
||||
import {
|
||||
PlayArrowRound,
|
||||
MessageFilled,
|
||||
VideocamRound,
|
||||
PlaylistAddRound,
|
||||
} from "@vicons/material";
|
||||
import { formatNumber } from "@/utils/timeTools.js";
|
||||
import { formatNumber } from "@/utils/timeTools";
|
||||
import AllArtists from "@/components/DataList/AllArtists.vue";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
|
||||
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicStore();
|
||||
@@ -131,6 +131,9 @@ const getMusicDetailData = (id) => {
|
||||
console.log(res);
|
||||
if (res.songs[0]) {
|
||||
musicDetail.value = res.songs[0];
|
||||
$setSiteTitle(
|
||||
res.songs[0].name + " - " + res.songs[0].ar[0].name + " - 单曲"
|
||||
);
|
||||
// 获取相似数据
|
||||
getSimiData(id);
|
||||
} else {
|
||||
@@ -237,7 +240,7 @@ watch(
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("403");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -24,4 +28,4 @@ const router = useRouter();
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("404");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -24,4 +28,4 @@ const router = useRouter();
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("500");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -11,6 +11,7 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
const user = userStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 收藏的专辑");
|
||||
if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading)
|
||||
user.setUserAlbumLists();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import ArtistLists from "@/components/DataList/ArtistLists.vue";
|
||||
const user = userStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 收藏的歌手");
|
||||
if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading)
|
||||
user.setUserArtistLists();
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<template #trigger>
|
||||
<n-progress
|
||||
type="line"
|
||||
color="#f55e55"
|
||||
:color="setting.themeData.primaryColor"
|
||||
class="progress"
|
||||
:show-indicator="false"
|
||||
:percentage="100 / (cloudSpace[1] / cloudSpace[0])"
|
||||
@@ -83,12 +83,14 @@
|
||||
<script setup>
|
||||
import { getCloud, upCloudSong } from "@/api/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongTime } from "@/utils/timeTools.js";
|
||||
import { settingStore } from "@/store";
|
||||
import { getSongTime } from "@/utils/timeTools";
|
||||
import { BackupRound } from "@vicons/material";
|
||||
import DataLists from "@/components/DataList/DataLists.vue";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const setting = settingStore();
|
||||
|
||||
// 云盘数据
|
||||
const cloudSpace = ref([]);
|
||||
@@ -225,13 +227,14 @@ watch(
|
||||
() => router.currentRoute.value,
|
||||
(val) => {
|
||||
pageNumber.value = Number(val.query.page ? val.query.page : 1);
|
||||
if (val.name == "cloud") {
|
||||
if (val.name == "user-cloud") {
|
||||
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 音乐云盘");
|
||||
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
round
|
||||
:src="
|
||||
user.getUserData.avatarUrl
|
||||
? user.getUserData.avatarUrl
|
||||
? user.getUserData.avatarUrl.replace(/^http:/, 'https:')
|
||||
: '/images/ico/user-filling.svg'
|
||||
"
|
||||
fallback-src="/images/ico/user-filling.svg"
|
||||
/>
|
||||
<n-text class="key">{{ user.getUserData.nickname }}</n-text>
|
||||
<n-text class="tip">的音乐库</n-text>
|
||||
<div class="text">
|
||||
<n-text class="key">{{ user.getUserData.nickname }}</n-text>
|
||||
<n-text class="tip">的音乐库</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<n-tabs
|
||||
class="main-tab"
|
||||
@@ -76,16 +78,33 @@ watch(
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
min-width: 80px;
|
||||
margin-right: 16px;
|
||||
box-shadow: 0 6px 8px -2px rgb(0 0 0 / 16%);
|
||||
}
|
||||
.key {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.tip {
|
||||
transform: translateY(8px);
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.key {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.tip {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.key {
|
||||
font-size: 30px;
|
||||
margin-right: 0;
|
||||
}
|
||||
.tip {
|
||||
font-size: 18px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
|
||||
@@ -11,6 +11,7 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
const user = userStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 收藏的歌单");
|
||||
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
|
||||
user.setUserPlayLists();
|
||||
});
|
||||
|
||||
@@ -7,85 +7,31 @@
|
||||
strong
|
||||
secondary
|
||||
round
|
||||
@click="createModelShow = true"
|
||||
@click="createPlaylistRef.openCreatePlaylist()"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon :component="AddCircleRound" />
|
||||
</template>
|
||||
新建歌单
|
||||
</n-button>
|
||||
<n-modal
|
||||
class="s-modal"
|
||||
v-model:show="createModelShow"
|
||||
preset="card"
|
||||
title="新建歌单"
|
||||
:bordered="false"
|
||||
:on-after-leave="createClose"
|
||||
>
|
||||
<n-input
|
||||
style="margin-bottom: 12px"
|
||||
v-model:value="createName"
|
||||
type="text"
|
||||
placeholder="请输入新歌单标题"
|
||||
/>
|
||||
<n-checkbox v-model:checked="createPrivacy">
|
||||
设置为隐私歌单
|
||||
</n-checkbox>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="createClose"> 取消 </n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="createPlaylistBtn(createName, createPrivacy)"
|
||||
>
|
||||
新建
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
<!-- 新建歌单 -->
|
||||
<CreatePlaylist ref="createPlaylistRef" />
|
||||
</div>
|
||||
<CoverLists :listData="user.getUserPlayLists.own" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createPlaylist } from "@/api/playlist";
|
||||
import { AddCircleRound } from "@vicons/material";
|
||||
import { userStore } from "@/store";
|
||||
import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
import CreatePlaylist from "@/components/DataModal/CreatePlaylist.vue";
|
||||
|
||||
const user = userStore();
|
||||
|
||||
// 新建歌单数据
|
||||
const createModelShow = ref(false);
|
||||
const createPrivacy = ref(false);
|
||||
const createName = ref(null);
|
||||
|
||||
// 新建歌单
|
||||
const createPlaylistBtn = (name, privacy = false) => {
|
||||
if (createName.value) {
|
||||
createPlaylist(name, privacy ? "10" : null).then((res) => {
|
||||
if (res.code === 200) {
|
||||
createClose();
|
||||
$message.success("歌单新建成功");
|
||||
user.setUserPlayLists();
|
||||
} else {
|
||||
$message.error("歌单新建失败,请重试");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$message.info("请输入歌单名称");
|
||||
}
|
||||
};
|
||||
|
||||
// 取消新建歌单
|
||||
const createClose = () => {
|
||||
createName.value = null;
|
||||
createPrivacy.value = false;
|
||||
createModelShow.value = false;
|
||||
};
|
||||
const createPlaylistRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 我的歌单");
|
||||
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
|
||||
user.setUserPlayLists();
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ import { useRouter } from "vue-router";
|
||||
import { musicStore } from "@/store";
|
||||
import { getVideoDetail, getVideoUrl, getSimiVideo } from "@/api/video";
|
||||
import { getComment } from "@/api/comment";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
|
||||
import { formatNumber, getSongTime } from "@/utils/timeTools";
|
||||
import {
|
||||
OndemandVideoFilled,
|
||||
ShareFilled,
|
||||
@@ -142,6 +142,7 @@ const commentsCount = ref(0);
|
||||
const getVideoData = (id) => {
|
||||
getVideoDetail(id).then((res) => {
|
||||
videoData.value = res.data;
|
||||
$setSiteTitle(res.data.name + " - " + res.data.artists[0].name + " - 视频");
|
||||
const requests = res.data.brs.map((v) => {
|
||||
return getVideoUrl(id, v.br);
|
||||
});
|
||||
@@ -212,8 +213,6 @@ const pageNumberChange = (val) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 隐藏控制条
|
||||
music.setPlayBarState(false);
|
||||
// 初始化播放器
|
||||
player.value = new Plyr(videoRef.value, playerOptions);
|
||||
// 获取视频数据
|
||||
@@ -223,6 +222,8 @@ onMounted(() => {
|
||||
// 播放器事件
|
||||
player.value.on("playing", () => {
|
||||
console.log("视频开始播放");
|
||||
// 隐藏控制条及暂停音乐
|
||||
music.setPlayBarState(false);
|
||||
music.setPlayState(false);
|
||||
});
|
||||
});
|
||||
@@ -247,7 +248,7 @@ watch(
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.video {
|
||||
--plyr-color-main: #f55e55;
|
||||
--plyr-color-main: var(--main-color);
|
||||
--plyr-control-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
@@ -296,11 +297,10 @@ watch(
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: $mainColor;
|
||||
color: var(--main-color);
|
||||
}
|
||||
.n-icon {
|
||||
margin-right: 6px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 578px) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default ({ mode }) =>
|
||||
@@ -27,6 +28,20 @@ export default ({ mode }) =>
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
template: "index.html",
|
||||
inject: {
|
||||
data: {
|
||||
logo: loadEnv(mode, process.cwd()).VITE_SITE_LOGO,
|
||||
title: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
|
||||
author: loadEnv(mode, process.cwd()).VITE_SITE_ANTHOR,
|
||||
keywords: loadEnv(mode, process.cwd()).VITE_SITE_KEYWORDS,
|
||||
description: loadEnv(mode, process.cwd()).VITE_SITE_DES,
|
||||
tongji: loadEnv(mode, process.cwd()).VITE_SITE_BAIDUTONGJI,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// PWA
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
@@ -53,9 +68,9 @@ export default ({ mode }) =>
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
name: "SPlayer",
|
||||
short_name: "SPlayer",
|
||||
description: "一个简约的在线音乐播放器",
|
||||
name: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
|
||||
short_name: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
|
||||
description: loadEnv(mode, process.cwd()).VITE_SITE_DES,
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
@@ -83,13 +98,6 @@ export default ({ mode }) =>
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@import "src/style/index.scss";',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
|
||||
Reference in New Issue
Block a user