Compare commits

...

17 Commits

Author SHA1 Message Date
底层用户
2370d237a8 Merge pull request #34 from imsyy/dev
feat: 同步开发分支
2023-05-11 14:05:25 +08:00
imsyy
ffbe6229f9 style: 调整进度条样式 2023-05-11 14:04:30 +08:00
imsyy
bf1312889d style: 部分组件样式调整 2023-05-11 11:19:20 +08:00
imsyy
2bf3d7db5a style: 样式微调 2023-05-10 18:25:58 +08:00
imsyy
06ccb969e4 feat: 新增歌单及专辑页播放全部 & 移动端体验优化 2023-05-10 18:18:31 +08:00
imsyy
6867897e02 fix: 在切换模式时播放进度异常
- 我愿称之为屎山添屎 🤡
2023-05-10 10:26:11 +08:00
imsyy
c78f94ae86 fix: 切换歌曲时多次弹窗 2023-05-09 17:57:06 +08:00
imsyy
40ed0dada1 fix: 切换歌曲时异常播放上一首 2023-05-09 17:33:13 +08:00
imsyy
f4d2c5f337 fix: 修复播放异常问题(可能) 2023-05-09 16:52:30 +08:00
imsyy
aace9e97b0 fix: 再次修复引用错误 😂 2023-05-09 16:20:15 +08:00
imsyy
b3641801df fix: 引用错误 2023-05-09 16:13:54 +08:00
imsyy
1dd877832c feat: 更换播放器为 Howler
- 新增站点标题自定义
- 暂时去除音乐频谱功能
2023-05-09 16:05:32 +08:00
imsyy
660cd33387 fix: 修复部分样式 2023-05-05 16:19:54 +08:00
imsyy
8caebf65f9 feat: 歌单页新增播放全部 2023-05-05 12:06:52 +08:00
imsyy
3b432dbd8b feat: 歌词页面快捷设置 2023-05-04 17:26:29 +08:00
imsyy
ee934c89f4 fix: 修复引用错误 2023-05-04 15:00:52 +08:00
imsyy
921b0eed0a fix: 修复歌手全部歌曲显示异常 #30 2023-05-04 14:55:27 +08:00
60 changed files with 2560 additions and 1002 deletions

13
.env
View File

@@ -2,10 +2,21 @@
## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi
VITE_MUSIC_API = "https://api-music.imsyy.top/"
# 网易云解灰 API 地址
# 网易云解灰 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"

View File

@@ -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
@@ -16,7 +20,7 @@
## 🎉 功能
- 支持扫码登录
- 支持手机号登录(目前暂时无法使用)
- 支持手机号登录(上游接口暂时无法使用)
- 自动进行每日签到及云贝签到
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
- 由于酷我音源不支持 `https`,故网页端替换可能不全面
@@ -32,7 +36,7 @@
- 支持逐字歌词
- 歌词滚动以及歌词翻译
- MV 与视频播放
- 音乐频谱显示( 实验性功能,需在设置中开启
- 音乐频谱显示( 暂时去除,还待完善
- 音乐渐入渐出
- 支持 PWA
- 支持评论区及评论点赞
@@ -41,7 +45,10 @@
#### 待办
- [ ] 电台节目支持
- [ ] 发表评论
- [ ] `i18n` 支持
- [ ] 重构(写成屎山了) 🤣
## 😍 Screenshots

View File

@@ -3,24 +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、歌词显示、歌曲评论、网易云登录与云盘等功能" />
<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>

View File

@@ -1,6 +1,6 @@
{
"name": "splayer",
"version": "1.1.5",
"version": "1.1.6",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",
@@ -15,14 +15,18 @@
"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",

304
pnpm-lock.yaml generated
View File

@@ -16,6 +16,9 @@ dependencies:
colorthief:
specifier: ^2.4.0
version: 2.4.0
howler:
specifier: ^2.2.3
version: 2.2.3
pinia:
specifier: ^2.0.26
version: 2.0.27(vue@3.2.45)
@@ -34,12 +37,21 @@ dependencies:
screenfull:
specifier: ^6.0.2
version: 6.0.2
throttle-debounce:
specifier: ^5.0.0
version: 5.0.0
vite-plugin-html:
specifier: ^3.2.0
version: 3.2.0(vite@3.2.4)
vue:
specifier: ^3.2.45
version: 3.2.45
vue-router:
specifier: ^4.1.6
version: 4.1.6(vue@3.2.45)
vue-slider-component:
specifier: 4.1.0-beta.7
version: 4.1.0-beta.7
devDependencies:
'@jridgewell/sourcemap-codec':
@@ -1259,7 +1271,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64@0.15.16:
@@ -1268,7 +1279,6 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@icon-park/vue-next@1.4.2(vue@3.2.45):
@@ -1299,35 +1309,29 @@ packages:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.14
'@jridgewell/trace-mapping': 0.3.17
dev: true
/@jridgewell/resolve-uri@3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/source-map@0.3.2:
resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
dependencies:
'@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.17
dev: true
/@jridgewell/sourcemap-codec@1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/trace-mapping@0.3.17:
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@juggle/resize-observer@3.4.0:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
@@ -1343,12 +1347,10 @@ packages:
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
dev: true
/@nodelib/fs.stat@2.0.5:
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
dev: true
/@nodelib/fs.walk@1.2.8:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
@@ -1356,7 +1358,6 @@ packages:
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.13.0
dev: true
/@rollup/plugin-babel@5.3.1(@babel/core@7.20.12)(rollup@2.79.1):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
@@ -1441,6 +1442,14 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/pluginutils@4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
dependencies:
estree-walker: 2.0.2
picomatch: 2.3.1
dev: false
/@rollup/pluginutils@5.0.2(rollup@2.79.1):
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
@@ -1612,7 +1621,6 @@ packages:
resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1644,7 +1652,6 @@ packages:
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
@@ -1677,7 +1684,6 @@ packages:
/async@3.2.4:
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
dev: true
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -1749,7 +1755,6 @@ packages:
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
@@ -1761,18 +1766,20 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: true
/brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
@@ -1793,7 +1800,6 @@ packages:
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
/builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
@@ -1807,6 +1813,13 @@ packages:
get-intrinsic: 1.1.3
dev: true
/camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
dependencies:
pascal-case: 3.1.2
tslib: 2.5.0
dev: false
/caniuse-lite@1.0.30001445:
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
dev: true
@@ -1830,7 +1843,6 @@ packages:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
@@ -1846,6 +1858,13 @@ packages:
optionalDependencies:
fsevents: 2.3.2
/clean-css@5.3.2:
resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==}
engines: {node: '>= 10.0'}
dependencies:
source-map: 0.6.1
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@@ -1857,7 +1876,6 @@ packages:
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
@@ -1865,7 +1883,10 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
dev: false
/colorthief@2.4.0:
resolution: {integrity: sha512-0U48RGNRo5fVO+yusBwgp+d3augWSorXabnqXUu9SabEhCpCgZJEUjUTTI41OOBBYuMMxawa3177POT6qLfLeQ==}
@@ -1883,7 +1904,11 @@ packages:
/commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
/commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
dev: false
/common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
@@ -1892,7 +1917,15 @@ packages:
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/connect-history-api-fallback@1.6.0:
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
engines: {node: '>=0.8'}
dev: false
/consola@2.15.3:
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
dev: false
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
@@ -1926,6 +1959,21 @@ packages:
csstype: 3.0.11
dev: true
/css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 4.3.1
domutils: 2.8.0
nth-check: 2.1.1
dev: false
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: false
/csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
@@ -1997,6 +2045,50 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
dependencies:
domelementtype: 2.3.0
domhandler: 4.3.1
entities: 2.2.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@4.3.1:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies:
dom-serializer: 1.4.1
domelementtype: 2.3.0
domhandler: 4.3.1
dev: false
/dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
no-case: 3.0.4
tslib: 2.5.0
dev: false
/dotenv-expand@8.0.3:
resolution: {integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==}
engines: {node: '>=12'}
dev: false
/dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
dev: false
/ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
@@ -2010,12 +2102,15 @@ packages:
hasBin: true
dependencies:
jake: 10.8.5
dev: true
/electron-to-chromium@1.4.284:
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
dev: true
/entities@2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: false
/es-abstract@1.21.1:
resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==}
engines: {node: '>= 0.4'}
@@ -2079,7 +2174,6 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/esbuild-android-arm64@0.15.16:
@@ -2088,7 +2182,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/esbuild-darwin-64@0.15.16:
@@ -2097,7 +2190,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/esbuild-darwin-arm64@0.15.16:
@@ -2106,7 +2198,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/esbuild-freebsd-64@0.15.16:
@@ -2115,7 +2206,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/esbuild-freebsd-arm64@0.15.16:
@@ -2124,7 +2214,6 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-32@0.15.16:
@@ -2133,7 +2222,6 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-64@0.15.16:
@@ -2142,7 +2230,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-arm64@0.15.16:
@@ -2151,7 +2238,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-arm@0.15.16:
@@ -2160,7 +2246,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-mips64le@0.15.16:
@@ -2169,7 +2254,6 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-ppc64le@0.15.16:
@@ -2178,7 +2262,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-riscv64@0.15.16:
@@ -2187,7 +2270,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-s390x@0.15.16:
@@ -2196,7 +2278,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-netbsd-64@0.15.16:
@@ -2205,7 +2286,6 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/esbuild-openbsd-64@0.15.16:
@@ -2214,7 +2294,6 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/esbuild-sunos-64@0.15.16:
@@ -2223,7 +2302,6 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-32@0.15.16:
@@ -2232,7 +2310,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-64@0.15.16:
@@ -2241,7 +2318,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-arm64@0.15.16:
@@ -2250,7 +2326,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild@0.15.16:
@@ -2281,7 +2356,6 @@ packages:
esbuild-windows-32: 0.15.16
esbuild-windows-64: 0.15.16
esbuild-windows-arm64: 0.15.16
dev: true
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
@@ -2335,7 +2409,6 @@ packages:
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.5
dev: true
/fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -2344,13 +2417,11 @@ packages:
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
dependencies:
reusify: 1.0.4
dev: true
/filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
dependencies:
minimatch: 5.1.0
dev: true
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
@@ -2396,6 +2467,15 @@ packages:
mime-types: 2.1.35
dev: false
/fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
dependencies:
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: false
/fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@@ -2419,7 +2499,6 @@ packages:
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/function.prototype.name@1.1.5:
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
@@ -2519,7 +2598,6 @@ packages:
/graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
dev: true
/har-schema@2.0.0:
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
@@ -2547,7 +2625,6 @@ packages:
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
dev: true
/has-property-descriptors@1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
@@ -2577,13 +2654,35 @@ packages:
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: true
/he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: false
/highlight.js@11.7.0:
resolution: {integrity: sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==}
engines: {node: '>=12.0.0'}
dev: true
/howler@2.2.3:
resolution: {integrity: sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==}
dev: false
/html-minifier-terser@6.1.0:
resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
engines: {node: '>=12'}
hasBin: true
dependencies:
camel-case: 4.1.2
clean-css: 5.3.2
commander: 8.3.0
he: 1.2.0
param-case: 3.0.4
relateurl: 0.2.7
terser: 5.16.1
dev: false
/http-signature@1.2.0:
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
engines: {node: '>=0.8', npm: '>=1.3.7'}
@@ -2665,7 +2764,6 @@ packages:
resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
dependencies:
has: 1.0.3
dev: true
/is-date-object@1.0.5:
resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
@@ -2781,7 +2879,6 @@ packages:
chalk: 4.1.2
filelist: 1.0.4
minimatch: 3.1.2
dev: true
/jest-worker@26.6.2:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
@@ -2846,7 +2943,6 @@ packages:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
dev: true
/jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
@@ -2898,6 +2994,12 @@ packages:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
/lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
tslib: 2.5.0
dev: false
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
@@ -2930,7 +3032,6 @@ packages:
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
dev: true
/micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
@@ -2938,7 +3039,6 @@ packages:
dependencies:
braces: 3.0.2
picomatch: 2.3.1
dev: true
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
@@ -2956,14 +3056,12 @@ packages:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: true
/minimatch@5.1.0:
resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: true
/mlly@1.0.0:
resolution: {integrity: sha512-QL108Hwt+u9bXdWgOI0dhzZfACovn5Aen4Xvc8Jasd9ouRH4NjnrXEiyP3nVvJo91zPlYjVRckta0Nt2zfoR6g==}
@@ -3023,11 +3121,25 @@ packages:
is-buffer: 1.1.6
dev: false
/no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
lower-case: 2.0.2
tslib: 2.5.0
dev: false
/node-bitmap@0.0.1:
resolution: {integrity: sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==}
engines: {node: '>=v0.6.5'}
dev: false
/node-html-parser@5.4.2:
resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==}
dependencies:
css-select: 4.3.0
he: 1.2.0
dev: false
/node-releases@2.0.8:
resolution: {integrity: sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==}
dev: true
@@ -3036,6 +3148,12 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
/nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: false
/oauth-sign@0.9.0:
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
dev: false
@@ -3075,12 +3193,26 @@ packages:
kind-of: 6.0.3
dev: false
/param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies:
dot-case: 3.0.4
tslib: 2.5.0
dev: false
/parse-data-uri@0.2.0:
resolution: {integrity: sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==}
dependencies:
data-uri-to-buffer: 0.0.3
dev: false
/pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
dependencies:
no-case: 3.0.4
tslib: 2.5.0
dev: false
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -3088,7 +3220,10 @@ packages:
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
/pathe@0.2.0:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
dev: false
/pathe@1.0.0:
resolution: {integrity: sha512-nPdMG0Pd09HuSsr7QOKUXO2Jr9eqaDiZvDwdyIhNG5SHYujkQHYKDfGQkulBxvbDHz8oHLsTgKN86LSwYzSHAg==}
@@ -3198,7 +3333,6 @@ packages:
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
/randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -3269,6 +3403,11 @@ packages:
jsesc: 0.5.0
dev: true
/relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
dev: false
/request@2.88.2:
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
engines: {node: '>= 6'}
@@ -3308,12 +3447,10 @@ packages:
is-core-module: 2.11.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true
/reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: true
/rollup-plugin-terser@7.0.2(rollup@2.79.1):
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
@@ -3334,7 +3471,6 @@ packages:
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: true
/rollup@3.10.0:
resolution: {integrity: sha512-JmRYz44NjC1MjVF2VKxc0M1a97vn+cDxeqWmnwyAF4FvpjK8YFdHpaqvQB+3IxCvX05vJxKZkoMDU8TShhmJVA==}
@@ -3348,7 +3484,6 @@ packages:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
queue-microtask: 1.2.3
dev: true
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -3425,7 +3560,6 @@ packages:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
dev: true
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
@@ -3519,12 +3653,10 @@ packages:
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
dev: true
/supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
dev: true
/temp-dir@2.0.0:
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
@@ -3550,7 +3682,11 @@ packages:
acorn: 8.8.1
commander: 2.20.3
source-map-support: 0.5.21
dev: true
/throttle-debounce@5.0.0:
resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==}
engines: {node: '>=12.22'}
dev: false
/through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -3584,6 +3720,10 @@ packages:
resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==}
dev: true
/tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: false
/tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
dependencies:
@@ -3675,7 +3815,6 @@ packages:
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
/unplugin-auto-import@0.12.0(rollup@2.79.1):
resolution: {integrity: sha512-y1flixUHt0UioxeUwXe4N9GmIJskBz7uoC2qFGaUJO1feN9PYITHhRVqfXxYt7VAaZ24InNIeZIAIoQbRm3ZPw==}
@@ -3780,6 +3919,26 @@ packages:
extsprintf: 1.3.0
dev: false
/vite-plugin-html@3.2.0(vite@3.2.4):
resolution: {integrity: sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==}
peerDependencies:
vite: '>=2.0.0'
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
connect-history-api-fallback: 1.6.0
consola: 2.15.3
dotenv: 16.0.3
dotenv-expand: 8.0.3
ejs: 3.1.8
fast-glob: 3.2.12
fs-extra: 10.1.0
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 3.2.4(sass@1.56.1)
dev: false
/vite-plugin-pwa@0.14.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4):
resolution: {integrity: sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==}
peerDependencies:
@@ -3831,7 +3990,6 @@ packages:
sass: 1.56.1
optionalDependencies:
fsevents: 2.3.2
dev: true
/vooks@0.2.12(vue@3.2.45):
resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==}
@@ -3866,6 +4024,10 @@ packages:
vue: 3.2.45
dev: false
/vue-slider-component@4.1.0-beta.7:
resolution: {integrity: sha512-Qb7K920ZG7PoQswoF6Ias+i3W2rd3k4fpk04JUl82kEUcN86Yg6et7bVSKWt/7VpQe8a5IT3BqCKSCOZ7AJgCA==}
dev: false
/vue@3.2.45:
resolution: {integrity: sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==}
dependencies:

View File

@@ -14,7 +14,10 @@
ref="mainContent"
class="main"
id="main"
:class="[music.showPlayList ? 'playlist' : null]"
:class="{
playlist: music.showPlayList,
search: site.searchInputActive,
}"
>
<n-back-top
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
@@ -77,10 +80,10 @@ const spacePlayOrPause = (e) => {
// 更改页面标题
const setSiteTitle = (val) => {
const title = val
? val === "SPlayer"
? val === import.meta.env.VITE_SITE_TITLE
? val
: val + " - SPlayer"
: site.siteTitle;
: 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) {
@@ -161,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}`,
@@ -232,12 +235,31 @@ onMounted(() => {
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;
}
}
}
}
}

View File

@@ -116,7 +116,6 @@ onMounted(() => {
});
onBeforeUnmount(() => {
console.log("销毁");
window.removeEventListener("resize", getBannerHeight);
});
</script>

View File

@@ -78,7 +78,7 @@
</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";

View File

@@ -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>
@@ -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 {

View File

@@ -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,7 +91,16 @@
</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";
@@ -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;
@@ -378,6 +428,7 @@ onMounted(() => {
border-radius: 50%;
transform: scale(0.8);
transition: all 0.3s;
z-index: 1;
}
.description {
position: absolute;
@@ -390,7 +441,9 @@ onMounted(() => {
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;
@@ -404,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);
@@ -416,6 +470,9 @@ onMounted(() => {
.description {
opacity: 0;
}
.shadow {
opacity: 1;
}
}
&:active {
transform: scale(0.98);

View File

@@ -1,6 +1,6 @@
<template>
<Transition mode="out-in">
<div class="datalists" v-if="listData[0]">
<div class="datalists" id="datalists" v-if="listData[0]">
<n-card
v-for="item in listData"
:key="item"
@@ -108,7 +108,7 @@
<n-icon
class="download"
size="20"
@click.stop="downloadSongRef.openDownloadModel(item)"
@click.stop="downloadSongRef.openDownloadModal(item)"
>
<DownloadFour theme="filled" />
</n-icon>
@@ -202,7 +202,7 @@
class="item"
@click="
() => {
downloadSongRef.openDownloadModel(drawerData);
downloadSongRef.openDownloadModal(drawerData);
drawerShow = false;
}
"
@@ -331,6 +331,7 @@ 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/DataModal/AddPlaylist.vue";
import CloudMatch from "@/components/DataModal/CloudMatch.vue";
@@ -418,7 +419,7 @@ const openRightMenu = (e, data) => {
label: "下一首播放",
icon: renderIcon(AddMusic),
show:
music.getPersonalFmMode || music.getPlaySongData.id == data.id
music.getPersonalFmMode || music.getPlaySongData?.id == data.id
? false
: true,
props: {
@@ -444,7 +445,7 @@ const openRightMenu = (e, data) => {
icon: renderIcon(DownloadFour),
props: {
onClick: () => {
downloadSongRef.value.openDownloadModel(data);
downloadSongRef.value.openDownloadModal(data);
},
},
},
@@ -581,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,7 +642,8 @@ const jumpLink = (id, type) => {
&:hover {
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);
0 3px 6px 0 var(--main-boxshadow-color),
0 5px 12px 4px var(--main-boxshadow-hover-color);
.action {
.like,
.download {

View File

@@ -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&nbsp;{{ 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);

View File

@@ -1,7 +1,7 @@
<template>
<n-modal
class="add-playlist s-modal"
v-model:show="addToPlaylistModel"
v-model:show="addToPlaylistModal"
preset="card"
:bordered="false"
:on-after-leave="closeAddToPlaylist"
@@ -61,7 +61,7 @@ const user = userStore();
const createPlaylistRef = ref(null);
// 收藏到歌单数据
const addToPlaylistModel = ref(false);
const addToPlaylistModal = ref(false);
const addToPlaylistId = ref(null);
// 收藏到歌单
@@ -88,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;
};
// 暴露方法

View File

@@ -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;
};
// 暴露方法

View File

@@ -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;

View 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>

View File

@@ -2,7 +2,7 @@
<n-drawer
class="playlist-drawer"
v-model:show="playListShow"
:z-index="2000"
:z-index="1"
:width="400"
:trap-focus="false"
:block-scroll="false"
@@ -11,7 +11,15 @@
@after-leave="music.showPlayList = false"
@mask-click="music.showPlayList = false"
>
<n-drawer-content title="播放列表" :native-scrollbar="false" closable>
<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
@@ -31,9 +39,13 @@
@click="changeIndex(index)"
>
<div class="left">
<div v-if="index !== music.persistData.playSongIndex" class="num">
<n-text
v-if="index !== music.persistData.playSongIndex"
:depth="3"
class="num"
>
{{ index + 1 }}
</div>
</n-text>
<div v-else class="bar">
<div
v-for="item in 3"
@@ -70,6 +82,7 @@
<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();
@@ -79,8 +92,17 @@ const playListShow = ref(false);
// 改变播放索引
const changeIndex = (index) => {
music.persistData.playSongIndex = index;
music.setPlayState(true);
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("切换失败,请刷新后重试");
}
};
// 监听播放列表显隐
@@ -89,7 +111,7 @@ watch(
() => music.showPlayList,
(val) => {
playListShow.value = val;
nextTick(() => {
nextTick().then(() => {
if (val && music.getPlaylists[0]) {
const el = document.getElementById(
`playlist${music.persistData.playSongIndex}`
@@ -129,6 +151,17 @@ onBeforeUnmount(() => {
.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;

View File

@@ -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>

View File

@@ -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="site.searchInputActive ? 'controls hidden' : '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>
@@ -24,9 +26,16 @@
</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"
@@ -67,6 +76,10 @@ import {
History,
SunOne,
Moon,
HamburgerButton,
HomeTwo,
FindOne,
Me,
} from "@icon-park/vue-next";
import { userStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router";
@@ -79,6 +92,7 @@ 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);
@@ -91,6 +105,19 @@ const closeDropdown = (event) => {
}
};
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 用户数据模块
const userDataRender = () => {
return h(
@@ -239,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: () => {
@@ -286,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);
};
@@ -362,7 +384,6 @@ watch(
onMounted(() => {
changeUserOptions(user.userLogin);
console.log(router);
});
onBeforeUnmount(() => {
@@ -380,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;
@@ -393,30 +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) {
&.hidden{
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);
@@ -444,7 +481,7 @@ nav {
cursor: pointer;
&:hover {
background-color: var(--main-color);
color: var(--n-color);
color: rgba(255, 255, 255, 0.9);
}
&:active {
transform: scale(0.95);
@@ -453,7 +490,7 @@ nav {
.router-link-active {
background-color: var(--main-color);
color: var(--n-color);
color: rgba(255, 255, 255, 0.9);
}
}
.right {
@@ -463,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;
@@ -471,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>

View File

@@ -62,6 +62,10 @@ const pageSizes = ref([
label: "50条/页",
value: 50,
},
{
label: "100条/页",
value: 100,
},
]);
// 每页个数数据变化

View File

@@ -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();

View File

@@ -190,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);

View File

@@ -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();

View File

@@ -1,7 +1,7 @@
<template>
<Transition name="up">
<div
v-show="music.showBigPlayer"
v-if="music.showBigPlayer"
class="bplayer"
:style="[
music.getPlaySongData && setting.backgroundImageShow === 'blur'
@@ -17,17 +17,12 @@
/>
<div class="icon-menu">
<div class="menu-left">
<div class="icon">
<div v-if="setting.showLyricSetting" class="icon">
<n-icon
class="setting"
size="30"
:component="SettingsRound"
@click="
() => {
music.setBigPlayerState(false);
router.push('/setting/player');
}
"
@click="LyricSettingRef.openLyricSetting()"
/>
</div>
</div>
@@ -134,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>
@@ -152,10 +148,12 @@ import {
} from "@vicons/material";
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();
@@ -166,13 +164,13 @@ const setting = settingStore();
// 工具栏显隐
const menuShow = ref(false);
// 音乐频谱
const avBars = ref(null);
const musicFrequency = ref(null);
// 歌词设置弹窗
const LyricSettingRef = ref(null);
// 歌词文本点击事件
const lrcTextClick = (time) => {
if ($player) $player.currentTime = time;
if (typeof $player !== "undefined") setSeek($player, time);
music.setPlayState(true);
lrcMouseStatus.value = false;
};
@@ -238,29 +236,16 @@ const changePwaColor = () => {
if (music.showBigPlayer) {
themeColorMeta.setAttribute("content", site.songPicColor);
} else {
if (setting.getSiteTheme == "light") {
if (setting.getSiteTheme === "light") {
themeColorMeta.setAttribute("content", "#ffffff");
} else if (setting.getSiteTheme == "dark") {
} else if (setting.getSiteTheme === "dark") {
themeColorMeta.setAttribute("content", "#18181c");
}
}
};
onMounted(() => {
nextTick(() => {
if (setting.musicFrequency) {
$player.crossOrigin = "anonymous";
musicFrequency.value = new MusicFrequency(
avBars.value,
$player,
null,
50,
null,
null,
5
);
musicFrequency.value.drawSpectrum();
}
nextTick().then(() => {
// 滚动歌词
lyricsScroll(music.getPlaySongLyricIndex);
});
@@ -277,9 +262,9 @@ watch(
changePwaColor();
if (val) {
console.log("开启播放器", music.getPlaySongLyricIndex);
nextTick(() => {
lyricsScroll(music.getPlaySongLyricIndex);
nextTick().then(() => {
music.showPlayList = false;
lyricsScroll(music.getPlaySongLyricIndex);
});
}
}
@@ -417,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 {

View File

@@ -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,16 +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 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);
}
};
// 页面跳转
@@ -168,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;
@@ -184,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 {
@@ -268,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 {

View File

@@ -23,6 +23,7 @@
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
/>
</div>
<!-- 普通歌词 -->
<template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc">
<div
v-for="(item, index) in music.getPlaySongLyric.lrc"
@@ -61,9 +62,7 @@
>
<span
v-show="
music.getPlaySongLyric.hasLrcRoma &&
setting.showRoma &&
item.roma
music.getPlaySongLyric.hasLrcRoma && setting.showRoma && item.roma
"
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
class="lyric-roma"
@@ -73,6 +72,7 @@
</div>
</div>
</template>
<!-- 逐字歌词 -->
<template v-else>
<div
v-for="(item, index) in music.getPlaySongLyric.yrc"
@@ -124,9 +124,7 @@
</span>
<span
v-show="
music.getPlaySongLyric.hasYrcRoma &&
setting.showRoma &&
item.roma
music.getPlaySongLyric.hasYrcRoma && setting.showRoma && item.roma
"
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
class="lyric-roma"
@@ -252,7 +250,7 @@ const lrcTextClick = (time) => {
}
.lrc,
.yrc {
opacity: 0.2;
opacity: 0.3;
transition: all 0.3s;
margin-bottom: 0.8vh;
padding: 1.8vh 4vh 1.8vh 3vh;
@@ -274,7 +272,7 @@ const lrcTextClick = (time) => {
transition: all var(--dur);
color: #ffffff66;
&.fill {
text-shadow: 0px 0px 30px #ffffff40;
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%;
@@ -302,7 +300,7 @@ const lrcTextClick = (time) => {
.lrc-text {
transform: scale(1.05);
.lyric {
text-shadow: 0px 0px 30px #ffffff40;
text-shadow: 0 0 40px rgb(255 255 255 / 40%);
}
}
.yrc-text {

View 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>

View File

@@ -1,220 +1,221 @@
<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">
<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"
>
<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-for="item in music.getPlaySongLyric.yrc[
music.getPlaySongLyricIndex
].content"
:key="item"
:depth="3"
v-else-if="
setting.showYrc &&
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.hasYrc
"
class="lrc text-hidden"
>
{{ item.content }}
<n-text
v-for="item in music.getPlaySongLyric.yrc[
music.getPlaySongLyricIndex
].content"
:key="item"
:depth="3"
>
{{ item.content }}
</n-text>
</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
"
/>
<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-else
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</Transition>
</template>
<template v-else>
<AllArtists
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</template>
</template>
</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">
<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"
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 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">
<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"
size="30"
:component="PlaylistAddRound"
@click.stop="
addPlayListRef.openAddToPlaylist(music.getPlaySongData.id)
"
/>
</div>
<div class="pattern">
<n-icon
:component="
persistData.playSongMode === 'normal'
? PlayCycle
: persistData.playSongMode === 'random'
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
/>
</div>
<div :class="music.showPlayList ? 'playlist open' : 'playlist'">
<n-icon
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
/>
</div>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"
:tooltip="false"
:min="0"
:max="1"
:step="0.01"
@click.stop
/>
</div>
</div>
</div>
<!-- 全局播放器 -->
<audio
ref="player"
:autoplay="music.getPlayState"
@timeupdate="songUpdate"
@play="songPlay"
@pause="songPause"
@canplay="songCanplay"
@loadeddata="songReady"
@error="songError"
@ended="music.setPlaySongIndex('next')"
:src="music.getPlaySongLink"
></audio>
</n-card>
</n-card>
</Transition>
<!-- 播放列表 -->
<PlayListDrawer ref="PlayListDrawerRef" />
<!-- 添加到歌单 -->
@@ -229,12 +230,10 @@ import {
getMusicUrl,
getMusicNumUrl,
getMusicNewLyric,
songScrobble,
} from "@/api/song";
import { NIcon } from "naive-ui";
import {
KeyboardArrowUpFilled,
MusicNoteFilled,
PlayCircleFilled,
PauseCircleFilled,
SkipNextRound,
@@ -251,27 +250,32 @@ import {
} from "@vicons/material";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { storeToRefs } from "pinia";
import { musicStore, settingStore, userStore, siteStore } 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 { 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 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 user = userStore();
const site = siteStore();
const { persistData } = storeToRefs(music);
const addPlayListRef = ref(null);
const PlayListDrawerRef = ref(null);
// 重试次数
const testNumber = ref(0);
// UNM 是否存在
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
@@ -301,7 +305,9 @@ const getPlaySongData = (data, level = setting.songLevel) => {
$message.info("当前歌曲为 VIP 专享,仅可试听");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
player.value = createSound(
res.data[0].url.replace(/^http:/, "https:")
);
});
} else {
if (useUnmServerHas && setting.useUnmServer) {
@@ -318,7 +324,7 @@ const getPlaySongData = (data, level = setting.songLevel) => {
music.setPlaySongLyric(res);
});
} catch (err) {
if (music.getPlaylists[0] && music.getPlayState && $player) {
if (music.getPlaylists[0] && music.getPlayState) {
console.log("当前歌曲所有音源匹配失败:" + err);
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
music.setPlaySongIndex("next");
@@ -332,7 +338,7 @@ const getMusicNumUrlData = (id) => {
.then((res) => {
if (res.code === 200) {
console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
music.setPlaySongLink(res.data.url.replace(/^http:/, ""));
player.value = createSound(res.data.url.replace(/^http:/, ""));
}
})
.catch((err) => {
@@ -342,171 +348,16 @@ const 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 songReady = () => {
const songId = music.getPlaySongData?.id;
const sourceId = music.getPlaySongData?.sourceId
? music.getPlaySongData.sourceId
: 0;
console.log("首次缓冲完成:" + songId + " / 来源:" + sourceId);
// 听歌打卡
if (user.userLogin) {
songScrobble(songId, sourceId).catch((err) => {
console.error("歌曲打卡失败:" + err);
});
}
};
// 歌曲开始播放
const songPlay = () => {
testNumber.value = 0;
if (!Object.keys(music.getPlaySongData).length) {
$message.error("音乐数据获取失败");
return false;
}
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);
// mediaSession
if (
"mediaSession" in navigator &&
Object.keys(music.getPlaySongData).length
) {
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=512x512",
sizes: "512x512",
},
],
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
music.setPlaySongIndex("next");
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
music.setPlaySongIndex("prev");
});
}
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 播放时页面标题
window.document.title =
music.getPlaySongData.name +
" - " +
music.getPlaySongData.artist[0].name +
" - SPlayer";
};
// 音乐渐入渐出
const isFading = ref(false);
const songInOrOut = (type) => {
if (isFading.value) {
return;
}
isFading.value = true;
if (type === "play") {
let volume = 0;
$player.play();
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);
// 更改页面标题
$setSiteTitle();
};
// 歌曲进度条更新
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("歌曲播放失败");
$message.error("歌曲播放失败");
if (testNumber.value < 4) {
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData);
testNumber.value++;
} else {
$message.error("歌曲重试次数过多,请刷新后重试");
const songTimeSliderUpdate = (val) => {
if (player.value && music.getPlaySongTime?.duration) {
const currentTime = (music.getPlaySongTime.duration / 100) * val;
setSeek(player.value, currentTime);
}
if (music.getPlayState) songInOrOut("play");
};
// 静音事件
@@ -519,8 +370,20 @@ const volumeMute = () => {
}
};
// 歌曲更换事件
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)
@@ -541,29 +404,21 @@ const getPicColor = (url) => {
};
onMounted(() => {
// 挂载方法
window.$getPlaySongData = getPlaySongData;
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData) {
getPlaySongData(music.getPlaySongData);
getPicColor(music.getPlaySongData.album.picUrl);
}
// 挂载播放器
window.$player = player.value;
// 恢复上次播放进度
if (music.getPlaySongTime && music.getPlaySongTime.currentTime) {
$player.currentTime = music.getPlaySongTime.currentTime;
}
// 设置音量
if ($player) $player.volume = persistData.value.playVolume;
});
// 监听当前音乐数据变化
watch(
() => music.getPlaySongData,
(val) => {
debounce(() => {
getPlaySongData(val);
getPicColor(val?.album.picUrl);
}, 500);
music.setPlaySongTime({ currentTime: 0, duration: 0 });
songChange(val);
}
);
@@ -571,7 +426,7 @@ watch(
watch(
() => persistData.value.playVolume,
(val) => {
if ($player) $player.volume = val;
if (player.value) setVolume(player.value, val);
}
);
@@ -579,11 +434,13 @@ watch(
watch(
() => music.getPlayState,
(val) => {
nextTick(() => {
if ($player) {
val ? songInOrOut("play") : songInOrOut("pause");
} else {
$message.error("播放器初始化失败,请重试");
nextTick().then(() => {
if (player.value && !music.isLoadingSong) {
fadePlayOrPause(
player.value,
val ? "play" : "pause",
persistData.value.playVolume
);
}
});
}
@@ -591,16 +448,21 @@ watch(
</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: 2004;
&.show {
bottom: 0;
}
z-index: 2;
.slider {
position: absolute;
top: -12px;
@@ -608,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;
@@ -632,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 {
@@ -777,9 +662,11 @@ watch(
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--main-color);
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);

View File

@@ -366,6 +366,12 @@ watch(
@media (max-width: 450px) {
width: 60vw;
}
@media (max-width: 380px) {
width: 54vw;
}
@media (max-width: 320px) {
width: 50vw;
}
}
:deep(.n-input__prefix) {
.n-icon {

View File

@@ -1,11 +1,13 @@
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, settingStore } from "@/store";
import { NIcon } from "naive-ui";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { soundStop, fadePlayOrPause } from "@/utils/Player";
import parseLyric from "@/utils/parseLyric";
const useMusicDataStore = defineStore("musicData", {
@@ -20,7 +22,7 @@ const useMusicDataStore = defineStore("musicData", {
// 播放状态
playState: false,
// 当前歌曲播放链接
playSongLink: null,
// playSongLink: null,
// 当前歌曲歌词数据
playSongLyric: {
lrc: [],
@@ -37,6 +39,15 @@ const useMusicDataStore = defineStore("musicData", {
catList: {},
// 精品歌单分类
highqualityCatList: [],
// 音乐频谱数据
spectrumsData: {
data: [],
audio: null,
analyser: null,
audioCtx: null,
},
// 是否正在加载数据
isLoadingSong: false,
// 持久化数据
persistData: {
// 搜索历史
@@ -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;
@@ -270,10 +277,6 @@ const useMusicDataStore = defineStore("musicData", {
setPlayBarState(value) {
this.showPlayBar = value;
},
// 更改歌曲播放链接
setPlaySongLink(value) {
this.playSongLink = value;
},
// 更改播放列表模式
setPlayListMode(value) {
this.persistData.playListMode = value;
@@ -323,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(
@@ -363,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 {
@@ -376,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("播放出错,请刷新后重试");
}
@@ -386,12 +396,15 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongIndex = listLength - 1;
} else if (this.persistData.playSongIndex >= listLength) {
this.persistData.playSongIndex = 0;
$player.currentTime = 0;
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
}
if (listMode !== "single" && listLength > 1) {
this.playSongLink = null;
soundStop($player);
}
this.playState = true;
nextTick().then(() => {
this.setPlayState(true);
});
}
},
// 添加歌曲至播放列表
@@ -406,7 +419,8 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playlists[this.persistData.playSongIndex]?.id
) {
console.log("播放歌曲与上一次不一致");
this.playSongLink = null;
soundStop($player);
this.isLoadingSong = true;
}
} catch (error) {
console.error("出现错误:" + error);
@@ -417,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) {
@@ -450,15 +464,15 @@ const useMusicDataStore = defineStore("musicData", {
if (index < this.persistData.playSongIndex) {
this.persistData.playSongIndex--;
} else if (index === this.persistData.playSongIndex) {
// 如果删除的是当前播放歌曲,则将播放链接置为null
this.playSongLink = null;
// 如果删除的是当前播放歌曲,则重置播放器
soundStop($player);
}
$message.success(name + " 已从播放列表中移除");
this.persistData.playlists.splice(index, 1);
// 检查当前播放歌曲的索引是否超出了列表范围
if (this.persistData.playSongIndex >= this.persistData.playlists.length) {
this.persistData.playSongIndex = 0;
this.playSongLink = null;
soundStop($player);
}
},
// 获取歌单分类

View File

@@ -48,6 +48,12 @@ const useSettingDataStore = defineStore("settingData", {
backgroundImageShow: "blur",
// 是否显示前奏等待
countDownShow: true,
// 是否显示歌词设置
showLyricSetting: true,
// 歌曲渐入渐出
songVolumeFade: true,
// 列表默认数量
listNumber: 30,
};
},
getters: {

View File

@@ -4,7 +4,7 @@ const useSiteDataStore = defineStore("siteData", {
state: () => {
return {
// 站点标题
siteTitle: "SPlayer",
siteTitle: import.meta.env.VITE_SITE_TITLE,
// 封面主题色
songPicColor: "rgb(128,128,128)",
// 搜索框激活状态

View File

@@ -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: () => {
@@ -105,7 +105,7 @@ const useUserDataStore = defineStore("userData", {
userLogOut();
},
// 更改用户歌单
async setUserPlayLists() {
async setUserPlayLists(callback) {
if (this.userLogin) {
try {
if (!Object.keys(this.userOtherData).length) {
@@ -145,6 +145,9 @@ const useUserDataStore = defineStore("userData", {
});
}
});
if (typeof callback === "function") {
callback();
}
this.userPlayLists.isLoading = false;
} else {
this.userPlayLists.isLoading = false;
@@ -195,7 +198,7 @@ const useUserDataStore = defineStore("userData", {
}
},
// 更改用户收藏专辑
async setUserAlbumLists() {
async setUserAlbumLists(callback) {
if (this.userLogin) {
try {
let offset = 0;
@@ -217,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) {

View File

@@ -60,7 +60,7 @@ body,
}
}
.n-modal-container {
z-index: 2006 !important;
// z-index: 2006 !important;
.n-modal-body-wrapper {
.n-modal-mask {
-webkit-backdrop-filter: blur(16px);

View File

@@ -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
View 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);
});
};

View File

@@ -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("请求发生重定向");

View File

@@ -14,53 +14,85 @@
/>
<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
<n-text class="name">{{ albumDetail.name }}</n-text>
<n-text
class="creator"
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
>
{{ albumDetail.artist.name }}
</span>
</n-text>
<div class="time">
<div class="createTime">
<span class="num">发行时间</span>
@@ -96,18 +128,88 @@
</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) => {
@@ -139,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();
}
}
});
@@ -173,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;
}
}
}
@@ -246,7 +449,7 @@ watch(
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 768px) {
@media (max-width: 1100px) {
flex-direction: column;
align-items: flex-start;
}
@@ -272,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 {

View File

@@ -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();

View File

@@ -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("歌手全部歌曲获取失败");
});
};
// 监听路由参数变化

View File

@@ -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();

View File

@@ -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";

View File

@@ -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"
@@ -127,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";
@@ -135,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
@@ -255,7 +255,7 @@ const changeTagName = (name) => {
page: 1,
},
});
catModelShow.value = false;
catModalShow.value = false;
};
// 排序方式变化
@@ -300,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;

View File

@@ -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();

View File

@@ -30,7 +30,7 @@ import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
const setting = settingStore();
onMounted(() => {
$setSiteTitle("SPlayer");
$setSiteTitle(import.meta.env.VITE_SITE_TITLE);
});
</script>

View File

@@ -105,7 +105,7 @@ 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();
@@ -152,7 +152,7 @@ const saveLoginData = (data) => {
// 自动签到
if ($signIn) $signIn();
clearInterval(qrCheckInterval.value);
router.go(-1);
router.push("/user");
} else {
user.userLogOut();
$message.error("登录出错,请重试");
@@ -168,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);

View File

@@ -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";

View File

@@ -12,73 +12,80 @@
"
fallback-src="/images/pic/default.png"
/>
<img src="/images/pic/album.png" class="album" alt="album" />
<n-avatar
class="shadow"
:src="
playListDetail.coverImgUrl
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
/>
</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,6 +106,18 @@
@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 || !loadingState">
@@ -124,17 +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 user = userStore();
const music = musicStore();
const setting = settingStore();
// 歌单数据
const playListId = ref(router.currentRoute.value.query.id);
@@ -150,6 +183,89 @@ 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)
@@ -197,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);
@@ -205,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();
}
}
});
@@ -272,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: var(--main-second-color);
color: var(--main-color);
.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 {
@@ -374,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 {

View File

@@ -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();

View File

@@ -74,7 +74,7 @@ const tabChange = (value) => {
};
onMounted(() => {
$setSiteTitle(searchKeywords.value + "的搜索结果");
if (searchKeywords.value) $setSiteTitle(searchKeywords.value + "的搜索结果");
});
</script>

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -12,7 +12,12 @@
主题色选择
<span class="tip">更换全站主题色即时生效</span>
</div>
<n-button strong secondary @click="changeThemeColor(null, true)">
<n-button
v-if="themeType !== 'red'"
strong
secondary
@click="changeThemeColor(null, true)"
>
恢复默认
</n-button>
</div>
@@ -27,7 +32,7 @@
v-for="item in themeColorData"
:key="item"
:style="{ '--color': item.primaryColor }"
:class="item.label === setting.themeType ? 'item check' : 'item'"
:class="item.label === themeType ? 'item check' : 'item'"
@click="changeThemeColor(item)"
>
<n-text v-html="item.name" />
@@ -75,6 +80,13 @@
</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">
歌曲音质
@@ -86,6 +98,30 @@
: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>
@@ -106,8 +142,14 @@ const {
autoSignIn,
searchHistory,
themeType,
showLyricSetting,
songVolumeFade,
useUnmServer,
} = storeToRefs(setting);
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
// 深浅模式
const darkOptions = [
{
@@ -180,12 +222,12 @@ const changeThemeColor = (data, reset = false) => {
negativeText: "取消",
onPositiveClick: () => {
$message.success("主题色已重置");
setting.themeType = "red";
themeType.value = "red";
},
});
} else {
$message.success("主题色更换为" + data.name);
setting.themeType = data.label;
themeType.value = data.label;
}
};
</script>

View File

@@ -2,7 +2,7 @@
<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">
@@ -13,7 +13,7 @@
</template>
<script setup>
// 系统重置
// 程序重置
const resetApp = () => {
const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功");
@@ -22,9 +22,9 @@ const resetApp = () => {
};
$dialog.warning({
class: "s-dialog",
title: "系统重置",
title: "程序重置",
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
positiveText: "重置",
positiveText: "确认重置",
negativeText: "取消",
onPositiveClick: () => {
$cleanAll ? $cleanAll() : cleanAll();

View File

@@ -26,23 +26,6 @@
:options="backgroundImageShowOptions"
/>
</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">
显示歌词翻译
@@ -145,7 +128,7 @@
</div>
<n-switch v-model:value="lyricsBlur" :round="false" />
</n-card>
<n-card class="set-item">
<!-- <n-card class="set-item">
<div class="name">
显示音乐频谱
<span class="tip">可能会导致一些意想不到的后果实验性功能</span>
@@ -155,7 +138,7 @@
:round="false"
@click="changeMusicFrequency"
/>
</n-card>
</n-card> -->
</div>
</template>
@@ -175,14 +158,10 @@ const {
lrcMousePause,
showYrc,
showRoma,
useUnmServer,
backgroundImageShow,
countDownShow,
} = storeToRefs(setting);
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
// 歌词位置
const lyricsPositionOptions = [
{
@@ -232,24 +211,24 @@ const backgroundImageShowOptions = [
];
// 音乐频谱提醒
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;
},
});
}
};
// 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>

View File

@@ -102,14 +102,14 @@
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/DataModal/AddPlaylist.vue";

View File

@@ -84,7 +84,7 @@
import { getCloud, upCloudSong } from "@/api/user";
import { useRouter } from "vue-router";
import { settingStore } from "@/store";
import { getSongTime } from "@/utils/timeTools.js";
import { getSongTime } from "@/utils/timeTools";
import { BackupRound } from "@vicons/material";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";

View File

@@ -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,
@@ -213,8 +213,6 @@ const pageNumberChange = (val) => {
};
onMounted(() => {
// 隐藏控制条
music.setPlayBarState(false);
// 初始化播放器
player.value = new Plyr(videoRef.value, playerOptions);
// 获取视频数据
@@ -224,6 +222,8 @@ onMounted(() => {
// 播放器事件
player.value.on("playing", () => {
console.log("视频开始播放");
// 隐藏控制条及暂停音乐
music.setPlayBarState(false);
music.setPlayState(false);
});
});

View File

@@ -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",