Compare commits

...

52 Commits

Author SHA1 Message Date
底层用户
4bd76ce157 Merge pull request #44 from imsyy/dev
fix: 修复下载歌曲异常 #43
2023-05-29 18:04:40 +08:00
imsyy
ee7fe27801 fix: 去除每次进入提醒 2023-05-29 18:02:41 +08:00
imsyy
73798ba196 fix: 修复下载歌曲异常 #43 2023-05-29 17:48:21 +08:00
imsyy
c30263b019 feat: 新增部分提醒 2023-05-29 17:47:02 +08:00
imsyy
f96b07a43c fix: 简单优化下歌词 2023-05-25 15:28:55 +08:00
底层用户
b23c2677e2 Merge pull request #42 from imsyy/dev
feat: 完善 i18n 支持 & fix: ios 端图标显示 #38
2023-05-24 09:54:46 +08:00
imsyy
3c64da89f6 docs: 更新说明 2023-05-23 17:32:02 +08:00
imsyy
0207b92f2a feat: 完善 i18n 支持 & fix: ios 端图标显示 #38 2023-05-23 17:30:57 +08:00
imsyy
0ee896d352 feat: 新增 i18n 支持 2023-05-19 10:25:43 +08:00
底层用户
0e88d1405f Merge pull request #36 from imsyy/dev
feat: 首页新增私人雷达 & 封面支持懒加载
2023-05-15 11:24:28 +08:00
imsyy
0e0a3911c1 feat: 部分封面支持懒加载 2023-05-15 11:15:08 +08:00
imsyy
b5cde21981 feat: 首页新增私人雷达 & fix: 部分封面无法显示
- 新增封面图片懒加载
- 部分样式调整
2023-05-12 18:09:28 +08:00
底层用户
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
底层用户
7142991f7d Merge pull request #28 from imsyy/dev
feat: 支持音译歌词
2023-04-28 11:29:36 +08:00
imsyy
a60e557ba2 feat: 支持音译歌词 2023-04-28 11:24:39 +08:00
底层用户
bc7031ba0c Merge pull request #26 from imsyy/dev
feat: 支持更高音质 & feat: 新增主题色设置
2023-04-25 15:42:12 +08:00
imsyy
82aabc555a fix: PWA 更新提醒失败 2023-04-25 15:37:41 +08:00
imsyy
8149c2dd71 feat: 新增主题色设置 & perf: 优化 PWA 更新提醒 2023-04-25 15:25:35 +08:00
imsyy
02084c6be0 fix: 修复部分样式问题 2023-04-24 17:20:21 +08:00
imsyy
ade3ddbe82 feat: 增加对鲸云臻音、鲸云母带音质的支持 2023-04-24 11:55:33 +08:00
底层用户
72e5b11558 Merge pull request #25 from imsyy/dev
feat: 同步开发分支
2023-04-23 15:45:33 +08:00
imsyy
c8cb4c2c9e feat: 动态改变 PWA 应用标题栏颜色 2023-04-23 15:23:46 +08:00
imsyy
396a54f646 feat: 新增听歌打卡功能 2023-04-23 10:42:27 +08:00
imsyy
cf8fd6b7fc feat: 完善部分弹窗 2023-04-23 10:34:12 +08:00
imsyy
4709ab3910 feat: 支持听歌打卡 2023-04-21 14:31:41 +08:00
底层用户
46e6ac3408 Merge pull request #23 from imsyy/dev
fix: 修复播放列表索引问题 #22
2023-04-21 10:19:40 +08:00
imsyy
a9a03e1cc4 fix: 修复播放列表索引问题 #22 2023-04-21 10:18:26 +08:00
底层用户
69a2855f77 Merge pull request #21 from imsyy/dev
feat: 播放列表重构
2023-04-20 17:13:39 +08:00
底层用户
bc84e11adf Merge pull request #18 from imsyy/dev
feat: 设置页面重构
2023-04-19 18:13:04 +08:00
底层用户
6a102a1bff Merge pull request #17 from imsyy/dev
feat: 支持 UnblockNeteaseMusic
2023-04-18 17:35:42 +08:00
底层用户
ddd12364fe Merge pull request #15 from imsyy/dev
fix: 修复逐字歌词导致的一系列问题
2023-04-17 14:28:27 +08:00
底层用户
144955e7c8 feat: 支持逐字歌词显示
feat: 支持逐字歌词显示
2023-04-14 17:26:45 +08:00
底层用户
43fe04b4fc feat: 新增歌曲前奏等待提醒
feat: 新增歌曲前奏等待提醒
2023-04-14 14:58:55 +08:00
底层用户
535d0f7493 Merge pull request #12 from imsyy/dev
feat: 更换歌词解析方式
2023-04-13 16:51:32 +08:00
底层用户
cd05376e18 Merge pull request #11 from imsyy/dev
fix: 歌单无法展示
2023-04-12 16:29:23 +08:00
底层用户
418da81738 Merge pull request #10 from imsyy/dev
feat: 站点标题跟随页面内容
2023-04-12 14:57:14 +08:00
105 changed files with 8066 additions and 3065 deletions

13
.env
View File

@@ -2,10 +2,21 @@
## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi ## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi
VITE_MUSIC_API = "https://api-music.imsyy.top/" VITE_MUSIC_API = "https://api-music.imsyy.top/"
# 网易云解灰 API 地址 # 网易云解灰 API 地址(可选功能)
## 需部署 API详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C ## 需部署 API详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C
VITE_UNM_API = "https://api-unm.imsyy.top/" 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 备案号 # ICP 备案号
## 若不需要,请设为空即可 ## 若不需要,请设为空即可
VITE_ICP = "豫ICP备2022018134号-1" VITE_ICP = "豫ICP备2022018134号-1"

View File

@@ -13,6 +13,15 @@
"backdrop-filter" "backdrop-filter"
] ]
} }
],
"compat-api/html": [
"default",
{
"ignore": [
"meta[name=theme-color]"
] ]
} }
],
"apple-touch-icons": "off"
}
} }

View File

@@ -6,8 +6,12 @@
</div> </div>
<br /> <br />
> 本项目采用 Vue 3 全家桶及 SCSS 开发 ## 说明
> 目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 `SCSS` 开发
- 目前主要以 `Web` 端为主,可能暂时不会考虑使用 `Electron` 构建客户端
- 仅对移动端做了基础适配,**不保证功能全部可用**
- 欢迎各位大佬指点和 `Star` 哦 😍
## 👀 Demo ## 👀 Demo
@@ -16,7 +20,7 @@
## 🎉 功能 ## 🎉 功能
- 支持扫码登录 - 支持扫码登录
- 支持手机号登录(目前暂时无法使用) - 支持手机号登录(上游接口暂时无法使用)
- 自动进行每日签到及云贝签到 - 自动进行每日签到及云贝签到
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲 - 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
- 由于酷我音源不支持 `https`,故网页端替换可能不全面 - 由于酷我音源不支持 `https`,故网页端替换可能不全面
@@ -32,16 +36,20 @@
- 支持逐字歌词 - 支持逐字歌词
- 歌词滚动以及歌词翻译 - 歌词滚动以及歌词翻译
- MV 与视频播放 - MV 与视频播放
- 音乐频谱显示( 实验性功能,需在设置中开启 - 音乐频谱显示( 暂时去除,还待完善
- 音乐渐入渐出 - 音乐渐入渐出
- 支持 PWA - 支持 PWA
- 支持评论区及评论点赞 - 支持评论区及评论点赞
- 明暗模式自动 / 手动切换 - 明暗模式自动 / 手动切换
- 移动端基础适配 - 移动端基础适配
- `i18n` 支持
#### 待办 #### 待办
- [ ] 电台节目支持
- [ ] 歌词页面进一步完善
- [ ] 发表评论 - [ ] 发表评论
- [ ] 重构(写成屎山了) 🤣
## 😍 Screenshots ## 😍 Screenshots
@@ -49,30 +57,35 @@
<summary>主页面</summary> <summary>主页面</summary>
![主页面](/screenshots/SPlayer%20-%20%E4%B8%BB%E9%A1%B5%E9%9D%A2.png) ![主页面](/screenshots/SPlayer%20-%20%E4%B8%BB%E9%A1%B5%E9%9D%A2.png)
</details> </details>
<details> <details>
<summary>播放页面</summary> <summary>播放页面</summary>
![播放页面](/screenshots/SPlayer%20-%20%E6%92%AD%E6%94%BE%E9%A1%B5%E9%9D%A2.png) ![播放页面](/screenshots/SPlayer%20-%20%E6%92%AD%E6%94%BE%E9%A1%B5%E9%9D%A2.png)
</details> </details>
<details> <details>
<summary>发现页面</summary> <summary>发现页面</summary>
![发现页面](/screenshots/SPlayer%20-%20%E5%8F%91%E7%8E%B0%E9%A1%B5%E9%9D%A2.png) ![发现页面](/screenshots/SPlayer%20-%20%E5%8F%91%E7%8E%B0%E9%A1%B5%E9%9D%A2.png)
</details> </details>
<details> <details>
<summary>歌单页面</summary> <summary>歌单页面</summary>
![歌单页面](/screenshots/SPlayer%20-%20%E6%AD%8C%E5%8D%95%E9%A1%B5%E9%9D%A2.png) ![歌单页面](/screenshots/SPlayer%20-%20%E6%AD%8C%E5%8D%95%E9%A1%B5%E9%9D%A2.png)
</details> </details>
<details> <details>
<summary>评论页面</summary> <summary>评论页面</summary>
![评论页面](/screenshots/SPlayer%20-%20%E8%AF%84%E8%AE%BA%E9%A1%B5%E9%9D%A2.png) ![评论页面](/screenshots/SPlayer%20-%20%E8%AF%84%E8%AE%BA%E9%A1%B5%E9%9D%A2.png)
</details> </details>
## ⚙️ 部署 ## ⚙️ 部署
@@ -86,7 +99,7 @@
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址(必需) - 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址(必需)
```js ```js
VITE_MUSIC_API = "your api url" VITE_MUSIC_API = "your api url";
``` ```
### 网易云解灰 API可选 ### 网易云解灰 API可选
@@ -131,9 +144,12 @@ npm build
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) - [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic) - [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer) - [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
## 📜 开源许可 ## 📜 开源许可
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途** - **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
- 本项目基于 [MIT license](https://opensource.org/license/mit/) 许可进行开源 - 本项目基于 [MIT license](https://opensource.org/license/mit/) 许可进行开源

View File

@@ -1,30 +1,79 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head>
<head> <meta charset="UTF-8" />
<meta charset="UTF-8"> <link rel="icon" href="<%- logo %>" />
<link rel="icon" href="/images/logo/favicon.svg"> <link rel="apple-touch-icon" href="<%- logo %>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="bookmark" href="<%- logo %>" />
<link
rel="apple-touch-icon-precomposed"
sizes="200x200"
href="<%- logo %>"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> --> <!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
<title>SPlayer</title> <title><%- title %></title>
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" /> <meta name="apple-mobile-web-app-title" content="<%- title %>" />
<meta name="description" content="一个简约的在线音乐播放器具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" /> <meta name="author" content="<%- author %>" />
<meta name="keywords" content="<%- keywords %>" />
<meta name="description" content="<%- description %>" />
<meta name="theme-color" content="#ffffff" />
<!-- HarmonyOS Sans --> <!-- HarmonyOS Sans -->
<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" /> <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> <script>
var _hmt = _hmt || []; var _hmt = _hmt || [];
(function () { (function () {
var hm = document.createElement("script"); 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]; var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();
</script> </script>
</head> <% } %>
<style>
noscript {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 16px);
font-family: "HarmonyOS_Regular", sans-serif;
}
noscript .title {
font-size: 30px;
font-weight: bold;
margin-top: 50px;
}
noscript .tip {
opacity: 0.6;
margin: 0;
}
</style>
</head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<noscript>
<img src="<%- logo %>" alt="logo" />
<p class="title"><%- title %></p>
<p class="tip">请开启 JavaScript</p>
</noscript>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,6 +1,6 @@
{ {
"name": "splayer", "name": "splayer",
"version": "1.1.3", "version": "1.1.7",
"author": "imsyy", "author": "imsyy",
"home": "https://imsyy.top", "home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer", "github": "https://github.com/imsyy/SPlayer",
@@ -14,24 +14,31 @@
"@ivanv/vue-collapse-transition": "^1.0.2", "@ivanv/vue-collapse-transition": "^1.0.2",
"artplayer": "^4.5.12", "artplayer": "^4.5.12",
"axios": "^1.2.0", "axios": "^1.2.0",
"colorthief": "^2.4.0",
"howler": "^2.2.3",
"pinia": "^2.0.26", "pinia": "^2.0.26",
"pinia-plugin-persistedstate": "^3.0.1", "pinia-plugin-persistedstate": "^3.0.1",
"plyr": "^3.7.3", "plyr": "^3.7.3",
"qrcode.vue": "^3.3.3", "qrcode.vue": "^3.3.3",
"sass": "^1.56.1", "sass": "^1.56.1",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"swiper": "^9.3.2",
"throttle-debounce": "^5.0.0",
"vite-plugin-html": "^3.2.0",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-router": "^4.1.6" "vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vue-slider-component": "4.1.0-beta.7"
}, },
"devDependencies": { "devDependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-terser": "^0.4.0", "@rollup/plugin-terser": "^0.4.0",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^4.2.3",
"naive-ui": "^2.34.2", "naive-ui": "^2.34.4",
"unplugin-auto-import": "^0.12.0", "unplugin-auto-import": "^0.12.0",
"unplugin-vue-components": "^0.22.11", "unplugin-vue-components": "^0.22.11",
"vite": "^3.2.4", "vite": "^4.3.8",
"vite-plugin-pwa": "^0.14.1" "vite-plugin-pwa": "^0.15.0"
} }
} }

1182
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/images/pic/radar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -13,8 +13,11 @@
<main <main
ref="mainContent" ref="mainContent"
class="main" class="main"
id="main" id="mainContent"
:class="[music.showPlayList ? 'playlist' : null]" :class="{
playlist: music.showPlayList,
search: site.searchInputActive,
}"
> >
<n-back-top <n-back-top
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40" :bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
@@ -35,18 +38,21 @@
</template> </template>
<script setup> <script setup>
import { musicStore, userStore, settingStore } from "@/store"; import { musicStore, userStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getLoginState, refreshLogin } from "@/api/login"; import { getLoginState, refreshLogin } from "@/api/login";
import { userDailySignin, userYunbeiSign } from "@/api/user"; import { userDailySignin, userYunbeiSign } from "@/api/user";
import { useI18n } from "vue-i18n";
import Provider from "@/components/Provider/index.vue"; import Provider from "@/components/Provider/index.vue";
import Nav from "@/components/Nav/index.vue"; import Nav from "@/components/Nav/index.vue";
import Player from "@/components/Player/index.vue"; import Player from "@/components/Player/index.vue";
import packageJson from "@/../package.json"; import packageJson from "@/../package.json";
const { t } = useI18n();
const music = musicStore(); const music = musicStore();
const user = userStore(); const user = userStore();
const setting = settingStore(); const setting = settingStore();
const site = siteStore();
const router = useRouter(); const router = useRouter();
const mainContent = ref(null); const mainContent = ref(null);
@@ -76,11 +82,11 @@ const spacePlayOrPause = (e) => {
// 更改页面标题 // 更改页面标题
const setSiteTitle = (val) => { const setSiteTitle = (val) => {
const title = val const title = val
? val === "SPlayer" ? val === import.meta.env.VITE_SITE_TITLE
? val ? val
: val + " - SPlayer" : val + " - " + import.meta.env.VITE_SITE_TITLE
: user.siteTitle; : sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
user.setSiteTitle(title); site.siteTitle = title;
sessionStorage.setItem("siteTitle", title); sessionStorage.setItem("siteTitle", title);
if (!music.getPlayState) { if (!music.getPlayState) {
window.document.title = title; window.document.title = title;
@@ -118,38 +124,50 @@ const signIn = () => {
Promise.all(signInPromises) Promise.all(signInPromises)
.then((results) => { .then((results) => {
localStorage.setItem("lastSignInDate", today); localStorage.setItem("lastSignInDate", today);
console.log("签到成功!"); console.log(t("general.message.signInSuccess"), results[0], results[1]);
console.log("userDailySignin:", results[0]);
console.log("userYunbeiSign:", results[1]);
$notification["success"]({ $notification["success"]({
content: "签到成功", content: t("general.message.signInSuccess"),
meta: "每日签到及云贝签到成功", meta: t("general.message.signInSuccessDesc"),
duration: 3000, duration: 3000,
}); });
}) })
.catch((error) => { .catch((error) => {
console.error("签到失败:", error); console.error(t("general.message.signInFailed"), error);
$message.error("每日签到失败"); $message.error(t("general.message.signInFailed"));
}); });
} else {
console.log("今天已经签到过了!");
} }
}; };
// 系统重置 // 系统重置
const cleanAll = () => { const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功"); $message ? $message.success(t("other.cleanAll")) : alert(t("other.cleanAll"));
localStorage.clear(); localStorage.clear();
window.location.href = "/"; document.location.reload();
};
// 滚动至顶部
const scrollToTop = () => {
nextTick().then(() => {
if (mainContent.value) {
mainContent.value?.scrollIntoView({ behavior: "smooth" });
} else {
const mainContent = document.getElementById("mainContent");
mainContent?.scrollIntoView({ behavior: "smooth" });
}
});
}; };
onMounted(() => { onMounted(() => {
// 挂载至全局 // 挂载方法至全局
window.$mainContent = mainContent.value; window.$scrollToTop = scrollToTop;
window.$cleanAll = cleanAll; window.$cleanAll = cleanAll;
window.$signIn = signIn; window.$signIn = signIn;
window.$setSiteTitle = setSiteTitle; window.$setSiteTitle = setSiteTitle;
// 更改页面语言
const html = document.documentElement;
if (html) html.setAttribute("lang", setting.language);
// 公告 // 公告
if (annShow) { if (annShow) {
$notification["info"]({ $notification["info"]({
@@ -160,7 +178,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}`; const copyrightNotice = `\n\n版本: ${packageJson.version}\n作者: ${packageJson.author}\n作者主页: ${packageJson.home}\nGitHub: ${packageJson.github}`;
console.info( console.info(
`%c${logoText} %c ${copyrightNotice}`, `%c${logoText} %c ${copyrightNotice}`,
@@ -188,14 +206,15 @@ onMounted(() => {
} else { } else {
user.userLogOut(); user.userLogOut();
if (music.getPlayListMode === "cloud") { if (music.getPlayListMode === "cloud") {
$message.info("登录已失效,请重新登录"); $message.info(t("other.loginExpired"));
music.setPlaylists([]); music.setPlaylists([]);
music.setPlayListMode("list");
} }
} }
}) })
.catch((err) => { .catch((err) => {
$message.error("请求发生错误"); console.error(t("general.message.acquisitionFailed"), err);
console.error("请求发生错误" + err); $message.error(t("general.message.acquisitionFailed"));
router.push("/500"); router.push("/500");
return false; return false;
}); });
@@ -230,12 +249,31 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
div:nth-of-type(2) { div:nth-of-type(2) {
transition: all 0.3s; 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 { &.playlist {
div:nth-of-type(2) { div:nth-of-type(2) {
transform: scale(0.98); transform: scale(0.98);
} }
} }
&.search {
div:nth-of-type(2) {
&::after {
pointer-events: all;
background-color: #00000040;
}
}
}
} }
} }
@@ -248,6 +286,7 @@ onMounted(() => {
.scale-enter-from, .scale-enter-from,
.scale-leave-to { .scale-leave-to {
opacity: 0; opacity: 0;
transform: scale(0.98); // transform: scale(0.98);
transform: translateX(10px);
} }
</style> </style>

View File

@@ -100,6 +100,7 @@ export const getMusicDetail = (ids) => {
url: "/song/detail", url: "/song/detail",
params: { params: {
ids, ids,
timestamp: new Date().getTime(),
}, },
}); });
}; };
@@ -148,3 +149,22 @@ export const getSongDownload = (id, br = 999000) => {
}, },
}); });
}; };
/**
* 听歌打卡
* @param {number} id - 音乐ID
* @param {number} sourceid - 来源ID
*/
export const songScrobble = (id, sourceid = 0, time = 0) => {
return axios({
method: "GET",
url: "/scrobble",
hiddenBar: true,
params: {
id,
sourceid,
time,
timestamp: new Date().getTime(),
},
});
};

View File

@@ -36,7 +36,9 @@
<script setup> <script setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getBanner } from "@/api/home"; import { getBanner } from "@/api/home";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 轮播图高度 // 轮播图高度
@@ -78,7 +80,7 @@ const bannerJump = (type, id, url) => {
const time = setTimeout(() => { const time = setTimeout(() => {
window.open(url); window.open(url);
}, 2000); }, 2000);
$message.loading("即将跳转至站外链接", { $message.loading(t("general.message.jumpOut"), {
closable: true, closable: true,
duration: 2000, duration: 2000,
onClose: () => { onClose: () => {
@@ -116,7 +118,6 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
console.log("销毁");
window.removeEventListener("resize", getBannerHeight); window.removeEventListener("resize", getBannerHeight);
}); });
</script> </script>

View File

@@ -78,12 +78,14 @@
</template> </template>
<script setup> <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 { Local, Time, ThumbsUp } from "@icon-park/vue-next";
import { userStore } from "@/store"; import { userStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { likeComment } from "@/api/comment"; import { likeComment } from "@/api/comment";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const user = userStore(); const user = userStore();
const router = useRouter(); const router = useRouter();
const props = defineProps({ const props = defineProps({
@@ -104,15 +106,14 @@ const toLikeComment = () => {
type type
).then((res) => { ).then((res) => {
if (res.code === 200) { if (res.code === 200) {
$message.success(type ? "点赞成功" : "取消点赞成功");
props.commentData.liked = !props.commentData.liked; props.commentData.liked = !props.commentData.liked;
props.commentData.likedCount += type ? 1 : -1; props.commentData.likedCount += type ? 1 : -1;
} else { } else {
$message.error("操作失败,请重试"); $message.error(t("general.message.operationFailed"));
} }
}); });
} else { } else {
$message.error("请登录账号后使用"); $message.error(t("general.message.needLogin"));
} }
}; };
</script> </script>
@@ -196,7 +197,7 @@ const toLikeComment = () => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }
@@ -213,7 +214,7 @@ const toLikeComment = () => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }
@@ -238,14 +239,14 @@ const toLikeComment = () => {
transition: all 0.3s; transition: all 0.3s;
opacity: 0.6; opacity: 0.6;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
opacity: 1; opacity: 1;
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
&.liked { &.liked {
color: $mainColor; color: var(--main-color);
opacity: 1; opacity: 1;
} }
} }

View File

@@ -7,7 +7,7 @@
> >
<n-text <n-text
class="name" class="name"
depth="3" :depth="isDark ? 3 : 0"
v-html="item.name" v-html="item.name"
@click.stop="jumpArtist(item.id)" @click.stop="jumpArtist(item.id)"
/> />
@@ -31,9 +31,14 @@ const router = useRouter();
const props = defineProps({ const props = defineProps({
// 歌手数据 // 歌手数据
artistsData: { artistsData: {
type: Object, type: Array,
default: [], default: [],
}, },
// 是否变灰
isDark: {
type: Boolean,
default: true,
},
}); });
// 跳转歌手页面 // 跳转歌手页面
@@ -59,7 +64,7 @@ const jumpArtist = (id) => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
.line { .line {

View File

@@ -19,16 +19,34 @@
> >
<div class="cover"> <div class="cover">
<n-avatar <n-avatar
lazy
round round
class="coverImg" class="coverImg"
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'" :src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
>
<template #placeholder>
<div class="cover-loading">
<n-spin size="small" />
</div>
</template>
</n-avatar>
<n-avatar
lazy
round
class="shadow"
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
fallback-src="/images/pic/default.png"
/> />
<n-icon size="40" :component="PeopleSearchOne" /> <n-icon size="40" :component="PeopleSearchOne" />
</div> </div>
<n-text class="name text-hidden">{{ item.name }}</n-text> <n-text class="name text-hidden">{{ item.name }}</n-text>
<n-text class="size" :depth="3" v-if="item.size"> <n-text class="size" :depth="3" v-if="item.size">
{{ item.size }} {{
$t("general.name.songSize", {
size: item.size,
})
}}
</n-text> </n-text>
</n-gi> </n-gi>
</n-grid> </n-grid>
@@ -65,17 +83,21 @@
</template> </template>
<script setup> <script setup>
import { PeopleSearchOne } from "@icon-park/vue-next"; import { NIcon } from "naive-ui";
import { PeopleSearchOne, LinkTwo, Like, Unlike } from "@icon-park/vue-next";
import { likeArtist } from "@/api/artist"; import { likeArtist } from "@/api/artist";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { userStore } from "@/store"; import { userStore, settingStore } from "@/store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const user = userStore(); const user = userStore();
const setting = settingStore();
const router = useRouter(); const router = useRouter();
const props = defineProps({ const props = defineProps({
// 列表数据 // 列表数据
listData: { listData: {
type: Object, type: Array,
default: [], default: [],
}, },
// 折叠栅格 // 折叠栅格
@@ -101,6 +123,19 @@ const rightMenuY = ref(0);
const rightMenuShow = ref(false); const rightMenuShow = ref(false);
const rightMenuOptions = ref(null); const rightMenuOptions = ref(null);
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 打开右键菜单 // 打开右键菜单
const openRightMenu = (e, data) => { const openRightMenu = (e, data) => {
e.preventDefault(); e.preventDefault();
@@ -109,8 +144,11 @@ const openRightMenu = (e, data) => {
rightMenuOptions.value = [ rightMenuOptions.value = [
{ {
key: "like", key: "like",
label: isLikeOrDislike(data.id) ? "收藏歌手" : "取消收藏歌手", label: isLikeOrDislike(data.id)
? t("menu.collection", { name: t("general.name.artists") })
: t("menu.cancelCollection", { name: t("general.name.artists") }),
show: user.userLogin && user.getUserArtistLists.has ? true : false, show: user.userLogin && user.getUserArtistLists.has ? true : false,
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
props: { props: {
onClick: () => { onClick: () => {
toLikeArtist(data); toLikeArtist(data);
@@ -119,7 +157,11 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "copy", key: "copy",
label: "复制歌手链接", label: t("menu.copy", {
name: t("general.name.artists"),
other: t("general.name.link"),
}),
icon: renderIcon(h(LinkTwo)),
props: { props: {
onClick: () => { onClick: () => {
if (navigator.clipboard) { if (navigator.clipboard) {
@@ -127,12 +169,13 @@ const openRightMenu = (e, data) => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`https://music.163.com/#/artist?id=${data.id}` `https://music.163.com/#/artist?id=${data.id}`
); );
$message.success("歌手链接复制成功"); $message.success(t("general.message.copySuccess"));
} catch (err) { } catch (err) {
$message.error("复制失败:", err); console.error(t("general.message.copyFailure"), err);
$message.error(t("general.message.copyFailure"));
} }
} else { } else {
$message.error("您的浏览器暂不支持该操作"); $message.error(t("general.message.notSupported"));
} }
}, },
}, },
@@ -152,14 +195,25 @@ const onClickoutside = () => {
// 收藏/取消收藏歌手 // 收藏/取消收藏歌手
const toLikeArtist = (data) => { const toLikeArtist = (data) => {
const type = isLikeOrDislike(data.id) ? 1 : 2; const type = isLikeOrDislike(data.id) ? 1 : 2;
const isThereASpace = setting.language === "zh-CN" ? "" : " ";
likeArtist(type, data.id).then((res) => { likeArtist(type, data.id).then((res) => {
if (res.code === 200) { if (res.code === 200) {
$message.success( $message.success(
`${data.name}${type == 1 ? "收藏成功" : "取消收藏成功"}` `${data.name + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.success") })
: t("menu.cancelCollection", { name: t("general.dialog.success") })
}`
); );
user.setUserArtistLists(); user.setUserArtistLists();
} else { } else {
$message.error(`${data.name}${type == 1 ? "收藏失败" : "取消收藏失败"}`); $message.error(
`${data.name + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
} }
}); });
}; };
@@ -205,12 +259,44 @@ onMounted(() => {
box-shadow: 0 4px 16px 0 #00000020; box-shadow: 0 4px 16px 0 #00000020;
border-radius: 50%; border-radius: 50%;
transition: all 0.3s; transition: all 0.3s;
.n-avatar { .coverImg {
filter: brightness(1); filter: brightness(1);
transform: scale(1); transform: scale(1);
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: all 0.3s; transition: all 0.3s;
z-index: 1;
.cover-loading {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 0;
padding-bottom: 100%;
background-color: #0001;
.n-spin-body {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.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 { .n-icon {
opacity: 0; opacity: 0;
@@ -218,6 +304,7 @@ onMounted(() => {
position: absolute; position: absolute;
color: #fff; color: #fff;
transition: all 0.3s; transition: all 0.3s;
z-index: 1;
} }
&:hover { &:hover {
box-shadow: 0 4px 16px 0 #00000040; box-shadow: 0 4px 16px 0 #00000040;
@@ -225,10 +312,13 @@ onMounted(() => {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
.n-avatar { .coverImg {
filter: brightness(0.8); filter: brightness(0.8);
transform: scale(1.05); transform: scale(1.05);
} }
.shadow {
opacity: 1;
}
} }
&:active { &:active {
.n-avatar { .n-avatar {
@@ -242,7 +332,7 @@ onMounted(() => {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }

View File

@@ -18,9 +18,24 @@
@contextmenu="openRightMenu($event, item)" @contextmenu="openRightMenu($event, item)"
> >
<div class="cover"> <div class="cover">
<n-avatar <n-image
lazy
class="coverImg" class="coverImg"
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'" preview-disabled
:src="getCoverUrl(item.cover, 300)"
fallback-src="/images/pic/default.png"
>
<template #placeholder>
<div class="cover-loading">
<n-spin size="small" />
</div>
</template>
</n-image>
<n-image
lazy
class="shadow"
preview-disabled
:src="getCoverUrl(item.cover, 300)"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
/> />
<n-icon class="play" size="40"> <n-icon class="play" size="40">
@@ -86,21 +101,34 @@
</template> </template>
<script setup> <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 { useI18n } from "vue-i18n";
import { delPlayList, likePlaylist } from "@/api/playlist"; import { delPlayList, likePlaylist } from "@/api/playlist";
import { likeAlbum } from "@/api/album"; import { likeAlbum } from "@/api/album";
import { musicStore, userStore } from "@/store"; import { musicStore, userStore, settingStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue"; import AllArtists from "./AllArtists.vue";
import PlaylistUpdate from "@/components/DataModel/PlaylistUpdate.vue"; import PlaylistUpdate from "@/components/DataModal/PlaylistUpdate.vue";
import getCoverUrl from "@/utils/getCoverUrl";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const user = userStore(); const user = userStore();
const setting = settingStore();
const props = defineProps({ const props = defineProps({
// 列表数据 // 列表数据
listData: { listData: {
type: Object, type: Array,
default: [], default: [],
}, },
// 列表类型 // 列表类型
@@ -131,6 +159,19 @@ const props = defineProps({
}); });
const playlistUpdateRef = ref(null); const playlistUpdateRef = ref(null);
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 右键菜单数据 // 右键菜单数据
const rightMenuX = ref(0); const rightMenuX = ref(0);
const rightMenuY = ref(0); const rightMenuY = ref(0);
@@ -145,18 +186,19 @@ const openRightMenu = (e, data) => {
rightMenuOptions.value = [ rightMenuOptions.value = [
{ {
key: "update", key: "update",
label: "编辑歌单", label: t("menu.update"),
show: show:
router.currentRoute.value.name === "user-playlists" ? true : false, router.currentRoute.value.name === "user-playlists" ? true : false,
props: { props: {
onClick: () => { onClick: () => {
playlistUpdateRef.value.openUpdateModel(data); playlistUpdateRef.value.openUpdateModal(data);
}, },
}, },
icon: renderIcon(h(Editor)),
}, },
{ {
key: "del", key: "del",
label: "删除歌单", label: t("menu.del"),
show: show:
router.currentRoute.value.name === "user-playlists" ? true : false, router.currentRoute.value.name === "user-playlists" ? true : false,
props: { props: {
@@ -164,10 +206,13 @@ const openRightMenu = (e, data) => {
toDelPlayList(data); toDelPlayList(data);
}, },
}, },
icon: renderIcon(h(DeleteFour)),
}, },
{ {
key: "likePlaylist", key: "likePlaylist",
label: isLikeOrDislike(data.id) ? "收藏歌单" : "取消收藏歌单", label: isLikeOrDislike(data.id)
? t("menu.collection", { name: t("general.name.playlist") })
: t("menu.cancelCollection", { name: t("general.name.playlist") }),
show: show:
user.userLogin && user.userLogin &&
user.getUserPlayLists.has && user.getUserPlayLists.has &&
@@ -180,10 +225,13 @@ const openRightMenu = (e, data) => {
toChangeLike(data.id); toChangeLike(data.id);
}, },
}, },
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
}, },
{ {
key: "likeAlbum", key: "likeAlbum",
label: isLikeOrDislike(data.id) ? "收藏专辑" : "取消收藏专辑", label: isLikeOrDislike(data.id)
? t("menu.collection", { name: t("general.name.album") })
: t("menu.cancelCollection", { name: t("general.name.album") }),
show: show:
user.userLogin && user.userLogin &&
user.getUserAlbumLists.has && user.getUserAlbumLists.has &&
@@ -195,10 +243,17 @@ const openRightMenu = (e, data) => {
toChangeLike(data.id); toChangeLike(data.id);
}, },
}, },
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
}, },
{ {
key: "copy", key: "copy",
label: `复制${props.listType === "playlist" ? "歌单" : "专辑"}链接`, label: t("menu.copy", {
name:
props.listType === "playlist"
? t("general.name.playlist")
: t("general.name.album"),
other: t("general.name.link"),
}),
props: { props: {
onClick: () => { onClick: () => {
if (navigator.clipboard) { if (navigator.clipboard) {
@@ -208,19 +263,17 @@ const openRightMenu = (e, data) => {
props.listType === "playlist" ? "playlist" : "album" props.listType === "playlist" ? "playlist" : "album"
}?id=${data.id}` }?id=${data.id}`
); );
$message.success( $message.success(t("general.message.copySuccess"));
`${
props.listType === "playlist" ? "歌单" : "专辑"
}链接复制成功`
);
} catch (err) { } catch (err) {
$message.error("复制失败:", err); console.error(t("general.message.copyFailure"), err);
$message.error(t("general.message.copyFailure"));
} }
} else { } else {
$message.error("您的浏览器暂不支持该操作"); $message.error(t("general.message.notSupported"));
} }
}, },
}, },
icon: renderIcon(h(LinkTwo)),
}, },
]; ];
rightMenuShow.value = true; rightMenuShow.value = true;
@@ -256,16 +309,22 @@ const toLink = (id) => {
// 删除歌单 // 删除歌单
const toDelPlayList = (data) => { const toDelPlayList = (data) => {
if (data.id === user.getUserPlayLists?.own[0].id) {
$message.warning(t("menu.unableToDelete"));
return false;
}
$dialog.warning({ $dialog.warning({
class: "s-dialog", class: "s-dialog",
title: "删除歌单", title: t("general.dialog.delete"),
content: "确认删除歌单 " + data.name + "?删除后将不可恢复!", content: t("menu.delQuestion", {
positiveText: "删除", name: data.name,
negativeText: "取消", }),
positiveText: t("general.dialog.delete"),
negativeText: t("general.dialog.cancel"),
onPositiveClick: () => { onPositiveClick: () => {
delPlayList(data.id).then((res) => { delPlayList(data.id).then((res) => {
if (res.code === 200) { if (res.code === 200) {
$message.success("删除成功"); $message.success(t("general.message.deleteSuccess"));
user.setUserPlayLists(); user.setUserPlayLists();
} }
}); });
@@ -279,10 +338,10 @@ const isLikeOrDislike = (id) => {
const playlists = user.getUserPlayLists.like; const playlists = user.getUserPlayLists.like;
const albums = user.getUserAlbumLists.list; const albums = user.getUserAlbumLists.list;
if (listType === "playlist" && playlists.length) { 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) { if (listType === "album" && albums.length) {
return !albums.some((item) => item.id === id); return !albums.some((item) => item.id === Number(id));
} }
return true; return true;
}; };
@@ -292,21 +351,48 @@ const toChangeLike = async (id) => {
const listType = props.listType; const listType = props.listType;
const type = isLikeOrDislike(id) ? 1 : 2; const type = isLikeOrDislike(id) ? 1 : 2;
const likeFn = listType === "playlist" ? likePlaylist : likeAlbum; const likeFn = listType === "playlist" ? likePlaylist : likeAlbum;
const likeMsg = listType === "playlist" ? "歌单" : "专辑"; const likeMsg =
listType === "playlist"
? t("general.name.playlist")
: t("general.name.album");
const isThereASpace = setting.language === "zh-CN" ? "" : " ";
try { try {
const res = await likeFn(type, id); const res = await likeFn(type, id);
if (res.code === 200) { if (res.code === 200) {
$message.success(`${likeMsg}${type == 1 ? "收藏成功" : "取消收藏成功"}`); $message.success(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.success") })
: t("menu.cancelCollection", { name: t("general.dialog.success") })
}`
);
listType === "playlist" listType === "playlist"
? user.setUserPlayLists() ? user.setUserPlayLists()
: user.setUserAlbumLists(); : user.setUserAlbumLists();
} else { } else {
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`); $message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
} }
} catch (err) { } catch (err) {
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`); $message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
console.error( console.error(
`${likeMsg}${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err `${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`,
err
); );
} }
}; };
@@ -356,7 +442,7 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
overflow: hidden; // overflow: hidden;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@@ -365,7 +451,44 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: all 0.3s; overflow: hidden;
transition: filter 0.3s;
z-index: 1;
:deep(img) {
width: 100%;
transition: transform 0.3s;
}
.cover-loading {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 0;
padding-bottom: 100%;
background-color: #0001;
.n-spin-body {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.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 { .play {
opacity: 0; opacity: 0;
@@ -378,6 +501,7 @@ onMounted(() => {
border-radius: 50%; border-radius: 50%;
transform: scale(0.8); transform: scale(0.8);
transition: all 0.3s; transition: all 0.3s;
z-index: 1;
} }
.description { .description {
position: absolute; position: absolute;
@@ -390,7 +514,9 @@ onMounted(() => {
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
padding: 6px; padding: 6px;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-bottom-right-radius: 8px;
transition: all 0.3s; transition: all 0.3s;
z-index: 1;
.num { .num {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -404,11 +530,12 @@ onMounted(() => {
} }
} }
&:hover { &:hover {
box-shadow: 0 15px 30px rgb(0 0 0 / 10%);
.coverImg { .coverImg {
filter: brightness(0.8); filter: brightness(0.8);
:deep(img) {
transform: scale(1.1); transform: scale(1.1);
} }
}
.play { .play {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
@@ -416,6 +543,9 @@ onMounted(() => {
.description { .description {
opacity: 0; opacity: 0;
} }
.shadow {
opacity: 1;
}
} }
&:active { &:active {
transform: scale(0.98); transform: scale(0.98);
@@ -433,7 +563,7 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
} }
} }
.by { .by {
@@ -443,7 +573,7 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
} }
} }
.artists { .artists {
@@ -460,5 +590,18 @@ onMounted(() => {
margin-bottom: 12px; margin-bottom: 12px;
} }
} }
@media (max-width: 450px) {
:deep(.n-grid) {
gap: 18px 10px;
}
.item {
.title {
margin-top: 8px;
.name {
font-size: 13px;
}
}
}
}
} }
</style> </style>

View File

@@ -1,14 +1,13 @@
<template> <template>
<Transition mode="out-in"> <Transition mode="out-in">
<div class="datalists" v-if="listData[0]"> <div class="datalists" id="datalists" v-if="listData[0]">
<n-card <n-card
hoverable v-for="item in listData"
:key="item"
:class=" :class="
music.getPlaySongData music.getPlaySongData && music.getPlaySongData.id == item.id
? music.getPlaySongData.id == item.id
? 'songs play' ? 'songs play'
: 'songs' : 'songs'
: 'songs'
" "
:content-style="{ :content-style="{
padding: '16px', padding: '16px',
@@ -17,8 +16,7 @@
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}" }"
v-for="item in listData" hoverable
:key="item"
@dblclick=" @dblclick="
setting.listClickMode === 'dblclick' ? playSong(listData, item) : null setting.listClickMode === 'dblclick' ? playSong(listData, item) : null
" "
@@ -27,6 +25,7 @@
> >
<n-avatar <n-avatar
v-if="item.album?.picUrl" v-if="item.album?.picUrl"
lazy
class="pic" class="pic"
:src="item.album.picUrl.replace(/^http:/, 'https:') + '?param=60y60'" :src="item.album.picUrl.replace(/^http:/, 'https:') + '?param=60y60'"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
@@ -43,13 +42,13 @@
@click.stop="jumpLink(item.id, 1)" @click.stop="jumpLink(item.id, 1)"
/> />
<n-tag <n-tag
v-if="item.fee == 1" v-if="item.fee == 1 || item.fee == 4"
class="vip" class="vip"
round round
:bordered="false" :bordered="false"
size="small" size="small"
> >
VIP {{ item.fee == 1 ? "VIP" : "EP" }}
</n-tag> </n-tag>
<n-tag <n-tag
v-if="item.pc" v-if="item.pc"
@@ -59,7 +58,7 @@
size="small" size="small"
:bordered="false" :bordered="false"
> >
云盘 {{ $t("general.name.cloud") }}
</n-tag> </n-tag>
<n-tag <n-tag
v-if="item.mv" v-if="item.mv"
@@ -110,7 +109,7 @@
<n-icon <n-icon
class="download" class="download"
size="20" size="20"
@click.stop="downloadSongRef.openDownloadModel(item)" @click.stop="downloadSongRef.openDownloadModal(item)"
> >
<DownloadFour theme="filled" /> <DownloadFour theme="filled" />
</n-icon> </n-icon>
@@ -166,7 +165,7 @@
<n-icon size="20"> <n-icon size="20">
<PlayOne theme="filled" /> <PlayOne theme="filled" />
</n-icon> </n-icon>
<n-text>立即播放</n-text> <n-text>{{ $t("menu.play") }}</n-text>
</div> </div>
<div <div
v-if=" v-if="
@@ -184,7 +183,7 @@
<n-icon size="20"> <n-icon size="20">
<AddMusic theme="filled" /> <AddMusic theme="filled" />
</n-icon> </n-icon>
<n-text>下一首播放</n-text> <n-text>{{ $t("menu.nextPlay") }}</n-text>
</div> </div>
<div <div
class="item" class="item"
@@ -198,13 +197,13 @@
<n-icon size="20"> <n-icon size="20">
<ListAdd theme="filled" /> <ListAdd theme="filled" />
</n-icon> </n-icon>
<n-text>添加到歌单</n-text> <n-text>{{ $t("menu.add") }}</n-text>
</div> </div>
<div <div
class="item" class="item"
@click=" @click="
() => { () => {
downloadSongRef.openDownloadModel(drawerData); downloadSongRef.openDownloadModal(drawerData);
drawerShow = false; drawerShow = false;
} }
" "
@@ -212,7 +211,7 @@
<n-icon size="20"> <n-icon size="20">
<DownloadFour theme="filled" /> <DownloadFour theme="filled" />
</n-icon> </n-icon>
<n-text>歌曲下载</n-text> <n-text>{{ $t("menu.download") }}</n-text>
</div> </div>
<div <div
class="item" class="item"
@@ -221,7 +220,7 @@
<n-icon size="20"> <n-icon size="20">
<Comments theme="filled" /> <Comments theme="filled" />
</n-icon> </n-icon>
<n-text>前往评论区</n-text> <n-text>{{ $t("menu.comment") }}</n-text>
</div> </div>
<div <div
class="item" class="item"
@@ -231,7 +230,7 @@
<n-icon size="20"> <n-icon size="20">
<Video theme="filled" /> <Video theme="filled" />
</n-icon> </n-icon>
<n-text>观看 MV</n-text> <n-text>{{ $t("menu.mv") }}</n-text>
</div> </div>
<div <div
class="item" class="item"
@@ -245,14 +244,14 @@
<n-icon size="20"> <n-icon size="20">
<LinkTwo theme="filled" /> <LinkTwo theme="filled" />
</n-icon> </n-icon>
<n-text>复制歌曲链接</n-text> <n-text>{{ $t("menu.copy") }}</n-text>
</div> </div>
<div class="item"> <div class="item">
<n-icon size="20"> <n-icon size="20">
<Voice theme="filled" /> <Voice theme="filled" />
</n-icon> </n-icon>
<n-text> <n-text>
歌手: {{ $t("general.name.artists") }}:
<AllArtists <AllArtists
class="text-hidden" class="text-hidden"
:artistsData="drawerData.artist" :artistsData="drawerData.artist"
@@ -266,7 +265,9 @@
<n-icon size="20"> <n-icon size="20">
<RecordDisc theme="filled" /> <RecordDisc theme="filled" />
</n-icon> </n-icon>
<n-text>专辑:{{ drawerData.album.name }}</n-text> <n-text>
{{ $t("general.name.album") }}: {{ drawerData.album.name }}
</n-text>
</div> </div>
<div <div
v-if="router.currentRoute.value.name === 'user-cloud'" v-if="router.currentRoute.value.name === 'user-cloud'"
@@ -281,7 +282,7 @@
<n-icon size="20"> <n-icon size="20">
<FileMusic theme="filled" /> <FileMusic theme="filled" />
</n-icon> </n-icon>
<n-text>歌曲信息纠正</n-text> <n-text>{{ $t("menu.match") }}</n-text>
</div> </div>
<div <div
v-if="router.currentRoute.value.name === 'user-cloud'" v-if="router.currentRoute.value.name === 'user-cloud'"
@@ -296,7 +297,7 @@
<n-icon size="20"> <n-icon size="20">
<DeleteFour theme="filled" /> <DeleteFour theme="filled" />
</n-icon> </n-icon>
<n-text>从云盘中删除</n-text> <n-text>{{ $t("menu.delete") }}</n-text>
</div> </div>
</div> </div>
</n-drawer-content> </n-drawer-content>
@@ -333,12 +334,15 @@ import { musicStore, settingStore, userStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { setCloudDel } from "@/api/user"; import { setCloudDel } from "@/api/user";
import { NIcon } from "naive-ui"; import { NIcon } from "naive-ui";
import { soundStop } from "@/utils/Player";
import { useI18n } from "vue-i18n";
import AllArtists from "./AllArtists.vue"; import AllArtists from "./AllArtists.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue"; import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
import CloudMatch from "@/components/DataModel/CloudMatch.vue"; import CloudMatch from "@/components/DataModal/CloudMatch.vue";
import DownloadSong from "@/components/DataModel/DownloadSong.vue"; import DownloadSong from "@/components/DataModal/DownloadSong.vue";
import SmallSongData from "./SmallSongData.vue"; import SmallSongData from "./SmallSongData.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const setting = settingStore(); const setting = settingStore();
@@ -350,7 +354,7 @@ const downloadSongRef = ref(null);
const props = defineProps({ const props = defineProps({
// 列表数据 // 列表数据
listData: { listData: {
type: Object, type: Array,
default: [], default: [],
}, },
// 专辑隐藏 // 专辑隐藏
@@ -370,30 +374,14 @@ const rightMenuOptions = ref(null);
const drawerShow = ref(false); const drawerShow = ref(false);
const drawerData = ref(null); const drawerData = ref(null);
// 复制歌曲链接或ID
const copySongData = (id, url = true) => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
url ? `https://music.163.com/#/song?id=${id}` : id
);
$message.success(`歌曲${url ? "链接" : " ID "}复制成功`);
} catch (err) {
$message.error("复制失败:", err);
}
} else {
$message.error("您的浏览器暂不支持该操作");
}
};
// 图标渲染 // 图标渲染
const renderIcon = (icon) => { const renderIcon = (icon, filled = true) => {
return () => { return () => {
return h( return h(
NIcon, NIcon,
{ depth: 2, style: { transform: "translateX(2px)" } }, { depth: 2, style: { transform: "translateX(2px)" } },
{ {
default: () => h(icon, { theme: "filled" }), default: () => h(icon, { theme: filled ? "filled" : "outline" }),
} }
); );
}; };
@@ -407,7 +395,7 @@ const openRightMenu = (e, data) => {
rightMenuOptions.value = [ rightMenuOptions.value = [
{ {
key: "play", key: "play",
label: "立即播放", label: t("menu.play"),
icon: renderIcon(PlayOne), icon: renderIcon(PlayOne),
props: { props: {
onClick: () => { onClick: () => {
@@ -417,10 +405,10 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "nextPlay", key: "nextPlay",
label: "下一首播放", label: t("menu.nextPlay"),
icon: renderIcon(AddMusic), icon: renderIcon(AddMusic),
show: show:
music.getPersonalFmMode || music.getPlaySongData.id == data.id music.getPersonalFmMode || music.getPlaySongData?.id == data.id
? false ? false
: true, : true,
props: { props: {
@@ -431,7 +419,7 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "add", key: "add",
label: "添加到歌单", label: t("menu.add"),
icon: renderIcon(ListAdd), icon: renderIcon(ListAdd),
show: user.userLogin ? true : false, show: user.userLogin ? true : false,
props: { props: {
@@ -442,18 +430,18 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "download", key: "download",
label: "歌曲下载", label: t("menu.download"),
icon: renderIcon(DownloadFour), icon: renderIcon(DownloadFour),
props: { props: {
onClick: () => { onClick: () => {
downloadSongRef.value.openDownloadModel(data); downloadSongRef.value.openDownloadModal(data);
}, },
}, },
}, },
{ {
key: "comment", key: "comment",
label: "前往评论区", label: t("menu.comment"),
icon: renderIcon(Comments), icon: renderIcon(Comments, false),
props: { props: {
onClick: () => { onClick: () => {
router.push(`/comment?id=${data.id}`); router.push(`/comment?id=${data.id}`);
@@ -462,8 +450,8 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "mv", key: "mv",
label: "观看 MV", label: t("menu.mv"),
icon: renderIcon(Video), icon: renderIcon(Video, false),
show: data.mv && data.mv != 0 ? true : false, show: data.mv && data.mv != 0 ? true : false,
props: { props: {
onClick: () => { onClick: () => {
@@ -478,7 +466,7 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "delete", key: "delete",
label: "从云盘中删除", label: t("menu.delete"),
icon: renderIcon(DeleteFour), icon: renderIcon(DeleteFour),
show: router.currentRoute.value.name === "user-cloud" ? true : false, show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: { props: {
@@ -489,7 +477,7 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "match", key: "match",
label: "歌曲信息纠正", label: t("menu.match"),
icon: renderIcon(FileMusic), icon: renderIcon(FileMusic),
show: router.currentRoute.value.name === "user-cloud" ? true : false, show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: { props: {
@@ -504,8 +492,8 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "search", key: "search",
label: "同名搜索", label: t("menu.search"),
icon: renderIcon(Search), icon: renderIcon(Search, false),
props: { props: {
onClick: () => { onClick: () => {
router.push({ router.push({
@@ -520,8 +508,11 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "copyId", key: "copyId",
label: "复制歌曲 ID", label: t("menu.copy", {
icon: renderIcon(FileMusic), name: t("general.name.song"),
other: "ID",
}),
icon: renderIcon(FileMusic, false),
props: { props: {
onClick: () => { onClick: () => {
copySongData(data.id, false); copySongData(data.id, false);
@@ -530,7 +521,10 @@ const openRightMenu = (e, data) => {
}, },
{ {
key: "copy", key: "copy",
label: "复制歌曲链接", label: t("menu.copy", {
name: t("general.name.song"),
other: t("general.name.link"),
}),
icon: renderIcon(LinkTwo), icon: renderIcon(LinkTwo),
props: { props: {
onClick: () => { onClick: () => {
@@ -550,23 +544,42 @@ const onClickoutside = () => {
rightMenuShow.value = false; rightMenuShow.value = false;
}; };
// 复制歌曲链接或ID
const copySongData = (id, url = true) => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
url ? `https://music.163.com/#/song?id=${id}` : id
);
$message.success(t("general.message.copySuccess"));
} catch (err) {
console.error(t("general.message.copyFailure"), err);
$message.error(t("general.message.copyFailure"));
}
} else {
$message.error(t("general.message.notSupported"));
}
};
// 云盘歌曲删除 // 云盘歌曲删除
const delCloudSong = (data) => { const delCloudSong = (data) => {
$dialog.warning({ $dialog.warning({
class: "s-dialog", class: "s-dialog",
title: "歌曲删除", title: t("general.dialog.delete"),
content: "确认从云盘中删除歌曲 " + data.name + " ", content: t("menu.deleteQuestion", {
positiveText: "删除", name: data.name,
negativeText: "取消", }),
positiveText: t("general.dialog.delete"),
negativeText: t("general.dialog.cancel"),
onPositiveClick: () => { onPositiveClick: () => {
setCloudDel(data.id).then((res) => { setCloudDel(data.id).then((res) => {
if (res.code == 200) { if (res.code == 200) {
$message.success("云盘歌曲删除成功"); $message.success(t("general.message.deleteSuccess"));
props.listData.forEach((v, i) => { props.listData.forEach((v, i) => {
if (v.id == data.id) props.listData.splice(i, 1); if (v.id == data.id) props.listData.splice(i, 1);
}); });
} else { } else {
$message.error("云盘歌曲删除失败"); $message.error(t("general.message.deleteFailure"));
} }
}); });
}, },
@@ -583,7 +596,11 @@ const openDrawer = (data) => {
// 播放并添加 // 播放并添加
const playSong = (data, song) => { const playSong = (data, song) => {
console.log(data, song); console.log(data, song);
if (music.getPersonalFmMode) {
soundStop($player);
music.setPersonalFmMode(false); music.setPersonalFmMode(false);
}
music.setPlayState(true);
if (router.currentRoute.value.name !== "history") music.setPlaylists(data); if (router.currentRoute.value.name !== "history") music.setPlaylists(data);
// 检查是否为云盘歌曲 // 检查是否为云盘歌曲
if (router.currentRoute.value.name === "user-cloud") { if (router.currentRoute.value.name === "user-cloud") {
@@ -637,9 +654,10 @@ const jumpLink = (id, type) => {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
border-color: $mainColor; border-color: var(--main-color);
box-shadow: 0 1px 2px -2px #f55e5526, 0 3px 6px 0 #f55e5530, box-shadow: 0 1px 2px -2px var(--main-boxshadow-color),
0 5px 12px 4px #f55e5505; 0 3px 6px 0 var(--main-boxshadow-color),
0 5px 12px 4px var(--main-boxshadow-hover-color);
.action { .action {
.like, .like,
.download { .download {
@@ -652,18 +670,18 @@ const jumpLink = (id, type) => {
// transform: scale(0.99); // transform: scale(0.99);
// } // }
&.play { &.play {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
border-color: $mainColor; border-color: var(--main-color);
a, a,
span, span,
.n-icon { .n-icon {
color: $mainColor; color: var(--main-color);
} }
.artists { .artists {
:deep(.artist) { :deep(.artist) {
.name, .name,
.line { .line {
color: $mainColor; color: var(--main-color);
} }
} }
} }
@@ -703,7 +721,7 @@ const jumpLink = (id, type) => {
font-weight: bold; font-weight: bold;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
.n-tag { .n-tag {
@@ -712,8 +730,8 @@ const jumpLink = (id, type) => {
height: 18px; height: 18px;
} }
.vip { .vip {
color: $mainColor; color: var(--main-color);
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
} }
.mv { .mv {
cursor: pointer; cursor: pointer;
@@ -744,7 +762,7 @@ const jumpLink = (id, type) => {
.n-text { .n-text {
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }
@@ -770,7 +788,7 @@ const jumpLink = (id, type) => {
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
color: $mainColor; color: var(--main-color);
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);

View File

@@ -14,7 +14,7 @@
<n-text <n-text
class="text-hidden" class="text-hidden"
depth="2" depth="2"
v-html="songDetail ? songDetail.name : '未知歌曲'" v-html="songDetail ? songDetail.name : $t('general.name.unknownSong')"
@click.stop="router.push(`/song?id=${songDetail.id}`)" @click.stop="router.push(`/song?id=${songDetail.id}`)"
/> />
<AllArtists <AllArtists
@@ -29,8 +29,10 @@
<script setup> <script setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getMusicDetail } from "@/api/song"; import { getMusicDetail } from "@/api/song";
import { useI18n } from "vue-i18n";
import AllArtists from "./AllArtists.vue"; import AllArtists from "./AllArtists.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const props = defineProps({ const props = defineProps({
// 歌曲数据 // 歌曲数据
@@ -64,7 +66,7 @@ const getMusicDetailData = (id) => {
id: res.songs[0].id, id: res.songs[0].id,
}; };
} else { } else {
$message.error("歌曲信息获取失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
@@ -105,6 +107,7 @@ onMounted(() => {
.pic { .pic {
margin-right: 12px; margin-right: 12px;
border-radius: 8px; border-radius: 8px;
min-width: 48px;
} }
.name { .name {
line-height: 1.6; line-height: 1.6;
@@ -113,7 +116,7 @@ onMounted(() => {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
.artists { .artists {

View File

@@ -66,7 +66,6 @@
<script setup> <script setup>
import { PlayOne, Youtube } from "@icon-park/vue-next"; import { PlayOne, Youtube } from "@icon-park/vue-next";
import { PlayArrowRound, OndemandVideoFilled } from "@vicons/material";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue"; import AllArtists from "./AllArtists.vue";
@@ -74,7 +73,7 @@ const router = useRouter();
const props = defineProps({ const props = defineProps({
// 列表数据 // 列表数据
listData: { listData: {
type: Object, type: Array,
default: [], default: [],
}, },
}); });
@@ -177,7 +176,7 @@ const props = defineProps({
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
} }
} }
.by { .by {
@@ -187,7 +186,7 @@ const props = defineProps({
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
} }
} }
.artists { .artists {

View File

@@ -4,13 +4,13 @@
class="s-modal" class="s-modal"
v-model:show="showAboutModal" v-model:show="showAboutModal"
preset="card" preset="card"
title="关于本站" :title="$t('nav.avatar.about')"
:bordered="false" :bordered="false"
transform-origin="center" transform-origin="center"
> >
<div class="copyright"> <div class="copyright">
<div class="desc"> <div class="desc">
<n-text class="name">SPlayer</n-text> <n-text class="name">{{ siteTitle }}</n-text>
<n-text class="version" :depth="3"> <n-text class="version" :depth="3">
v&nbsp;{{ packageJson.version }} v&nbsp;{{ packageJson.version }}
</n-text> </n-text>
@@ -53,6 +53,7 @@ import { GithubOne } from "@icon-park/vue-next";
import packageJson from "@/../package.json"; import packageJson from "@/../package.json";
// //
const siteTitle = import.meta.env.VITE_SITE_TITLE;
const showAboutModal = ref(false); const showAboutModal = ref(false);
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null); const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);

View File

@@ -0,0 +1,158 @@
<template>
<n-modal
class="add-playlist s-modal"
v-model:show="addToPlaylistModal"
preset="card"
:bordered="false"
:on-after-leave="closeAddToPlaylist"
>
<template #header>
{{ $t("menu.add") }}
<n-tag
round
class="tag"
type="primary"
:style="{
marginLeft: '12px',
fontSize: '13px',
transform: 'translateY(-2px)',
cursor: 'pointer',
}"
:bordered="false"
@click="createPlaylistRef.openCreatePlaylist()"
>
{{ $t("menu.create") }}
</n-tag>
</template>
<Transition mode="out-in">
<div v-if="user.getUserPlayLists.own[0]">
<n-space vertical class="list">
<div
class="item"
v-for="item in user.getUserPlayLists.own.slice(1)"
:key="item"
@click="addToPlayList(item.id, addToPlaylistId)"
>
<n-avatar
class="pic"
:src="
item.cover
? item.cover.replace(/^http:/, 'https:') + '?param=60y60'
: '/images/pic/default.png'
"
fallback-src="/images/pic/default.png"
/>
<div class="desc">
<n-text class="name">{{ item.name }}</n-text>
<n-text class="num">{{
$t("general.name.songSize", {
size: item.trackCount,
})
}}</n-text>
</div>
</div>
</n-space>
</div>
<n-text v-else>{{ $t("general.message.isLoading") }}</n-text>
</Transition>
</n-modal>
<!-- 新建歌单 -->
<CreatePlaylist ref="createPlaylistRef" />
</template>
<script setup>
import { addSongToPlayList } from "@/api/playlist";
import { userStore } from "@/store";
import { useI18n } from "vue-i18n";
import CreatePlaylist from "./CreatePlaylist.vue";
const { t } = useI18n();
const user = userStore();
const createPlaylistRef = ref(null);
// 收藏到歌单数据
const addToPlaylistModal = ref(false);
const addToPlaylistId = ref(null);
// 收藏到歌单
const addToPlayList = (pid, tracks) => {
addSongToPlayList(pid, tracks).then((res) => {
console.log(res);
if (res.status === 200) {
$message.success(t("general.message.addSuccess"));
closeAddToPlaylist();
user.setUserPlayLists();
} else {
$message.error(t("general.message.addFailure"));
}
});
};
// 开启收藏到歌单
const openAddToPlaylist = (id) => {
if (!user.userLogin) {
$message.error(t("general.message.needLogin"));
return false;
}
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
user.setUserPlayLists();
}
addToPlaylistModal.value = true;
addToPlaylistId.value = id;
console.log("开启", addToPlaylistModal.value, addToPlaylistId.value);
};
// 关闭收藏到歌单
const closeAddToPlaylist = () => {
addToPlaylistModal.value = false;
};
// 暴露方法
defineExpose({
openAddToPlaylist,
});
</script>
<style lang="scss" scoped>
.add-playlist {
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.list {
.item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--n-border-color);
}
.pic {
width: 60px;
height: 60px;
}
.desc {
margin-left: 12px;
display: flex;
flex-direction: column;
.name {
// font-weight: bold;
font-size: 15px;
}
.num {
margin-top: 2px;
opacity: 0.8;
}
}
}
}
}
</style>

View File

@@ -1,22 +1,22 @@
<template> <template>
<n-modal <n-modal
class="s-modal" class="s-modal"
v-model:show="cloudMatchModel" v-model:show="cloudMatchModal"
preset="card" preset="card"
title="歌曲信息纠正" :title="$t('menu.match')"
:bordered="false" :bordered="false"
:on-after-leave="closeCloudMatch" :on-after-leave="closeCloudMatch"
> >
<n-form class="cloud-match" :label-width="80" :model="cloudMatchValue"> <n-form class="cloud-match" :label-width="80" :model="cloudMatchValue">
<n-form-item label="原歌曲信息"> <n-form-item :label="$t('other.sData')">
<n-card content-style="padding: 16px" :bordered="false" embedded> <n-card content-style="padding: 16px" :bordered="false" embedded>
<SmallSongData :songData="cloudMatchBeforeData" notJump /> <SmallSongData :songData="cloudMatchBeforeData" notJump />
</n-card> </n-card>
</n-form-item> </n-form-item>
<n-form-item label="匹配 ID" path="asid"> <n-form-item :label="$t('other.asId')" path="asid">
<n-input-number <n-input-number
v-model:value="cloudMatchValue.asid" v-model:value="cloudMatchValue.asid"
placeholder="请输入要匹配的歌曲 ID" :placeholder="$t('other.asIdDes')"
:show-button="false" :show-button="false"
/> />
<n-button <n-button
@@ -24,7 +24,7 @@
:disabled="!cloudMatchValue.asid" :disabled="!cloudMatchValue.asid"
@click="cloudMatchId = cloudMatchValue.asid.toString()" @click="cloudMatchId = cloudMatchValue.asid.toString()"
> >
检查 {{ $t("general.dialog.check") }}
</n-button> </n-button>
</n-form-item> </n-form-item>
</n-form> </n-form>
@@ -42,13 +42,15 @@
</n-card> </n-card>
<template #footer> <template #footer>
<n-space justify="end"> <n-space justify="end">
<n-button @click="closeCloudMatch"> 取消 </n-button> <n-button @click="closeCloudMatch">
{{ $t("general.dialog.cancel") }}
</n-button>
<n-button <n-button
type="primary" type="primary"
@click="setCloudMatchBtn(cloudMatchValue)" @click="setCloudMatchBtn(cloudMatchValue)"
:disabled="!cloudMatchValue.asid" :disabled="!cloudMatchValue.asid"
> >
纠正歌曲 {{ $t("general.dialog.match") }}
</n-button> </n-button>
</n-space> </n-space>
</template> </template>
@@ -58,14 +60,16 @@
<script setup> <script setup>
import { setCloudMatch } from "@/api/user"; import { setCloudMatch } from "@/api/user";
import { userStore } from "@/store"; import { userStore } from "@/store";
import { useI18n } from "vue-i18n";
import SmallSongData from "@/components/DataList/SmallSongData.vue"; import SmallSongData from "@/components/DataList/SmallSongData.vue";
const { t } = useI18n();
const user = userStore(); const user = userStore();
// //
const cloudDataLoad = inject("cloudDataLoad", null); const cloudDataLoad = inject("cloudDataLoad", null);
const smallSongDataRef = ref(null); const smallSongDataRef = ref(null);
const cloudMatchModel = ref(false); const cloudMatchModal = ref(false);
const cloudMatchBeforeData = ref(null); const cloudMatchBeforeData = ref(null);
const cloudMatchId = ref(null); const cloudMatchId = ref(null);
const cloudMatchValue = ref({ const cloudMatchValue = ref({
@@ -77,23 +81,23 @@ const cloudMatchValue = ref({
// //
const setCloudMatchBtn = (data) => { const setCloudMatchBtn = (data) => {
if (data.sid == data.asid) { if (data.sid == data.asid) {
$message.error("与原歌曲 ID 一致,无需纠正"); $message.error(t("other.noNeedMatch"));
} else { } else {
if (!smallSongDataRef.value) { if (!smallSongDataRef.value) {
$message.error("请先检查"); $message.error(t("other.plaseCheck"));
} else if (smallSongDataRef.value.checkSongData()) { } else if (smallSongDataRef.value.checkSongData()) {
setCloudMatch(data.uid, data.sid, data.asid).then((res) => { setCloudMatch(data.uid, data.sid, data.asid).then((res) => {
console.log(res); console.log(res);
if (res.data) { if (res.data) {
closeCloudMatch(); closeCloudMatch();
$message.success("歌曲纠正成功"); $message.success(t("other.matchSuccess"));
cloudDataLoad(); cloudDataLoad();
} else { } else {
$message.error("歌曲纠正失败,请重试"); $message.error(t("other.matchFailed"));
} }
}); });
} else { } else {
$message.error("非正常歌曲 ID无法匹配"); $message.error(t("other.matchError"));
} }
} }
}; };
@@ -102,7 +106,7 @@ const setCloudMatchBtn = (data) => {
const openCloudMatch = (data) => { const openCloudMatch = (data) => {
cloudMatchValue.value.sid = data.id; cloudMatchValue.value.sid = data.id;
cloudMatchBeforeData.value = data; cloudMatchBeforeData.value = data;
cloudMatchModel.value = true; cloudMatchModal.value = true;
}; };
// //
@@ -110,7 +114,7 @@ const closeCloudMatch = () => {
cloudMatchBeforeData.value = null; cloudMatchBeforeData.value = null;
cloudMatchId.value = null; cloudMatchId.value = null;
cloudMatchValue.value.asid = null; cloudMatchValue.value.asid = null;
cloudMatchModel.value = false; cloudMatchModal.value = false;
}; };
// //

View File

@@ -0,0 +1,78 @@
<template>
<n-modal
class="s-modal"
v-model:show="createPlaylistShow"
preset="card"
:title="$t('menu.create')"
:bordered="false"
:on-after-leave="closeCreatePlaylist"
>
<n-input
style="margin-bottom: 12px"
v-model:value="createName"
type="text"
:placeholder="$t('other.newPlaylistName')"
/>
<n-checkbox v-model:checked="createPrivacy">
{{ $t("other.setPrivacy") }}
</n-checkbox>
<template #footer>
<n-space justify="end">
<n-button @click="closeCreatePlaylist">
{{ $t("general.dialog.cancel") }}
</n-button>
<n-button
type="primary"
:disabled="!createName"
@click="createPlaylistBtn(createName, createPrivacy)"
>
{{ $t("general.dialog.create") }}
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { userStore } from "@/store";
import { createPlaylist } from "@/api/playlist";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const user = userStore();
// 新建歌单数据
const createPlaylistShow = ref(false);
const createPrivacy = ref(false);
const createName = ref(null);
// 新建歌单
const createPlaylistBtn = (name, privacy = false) => {
createPlaylist(name, privacy ? "10" : null).then((res) => {
if (res.code === 200) {
closeCreatePlaylist();
$message.success(t("general.message.createSuccess"));
user.setUserPlayLists();
} else {
$message.error(t("general.message.createFailed"));
}
});
};
// 开启新建歌单
const openCreatePlaylist = () => {
createPlaylistShow.value = true;
};
// 取消新建歌单
const closeCreatePlaylist = () => {
createName.value = null;
createPrivacy.value = false;
createPlaylistShow.value = false;
};
// 暴露方法
defineExpose({
openCreatePlaylist,
});
</script>

View File

@@ -1,17 +1,17 @@
<template> <template>
<n-modal <n-modal
class="s-modal downloadModel" class="s-modal downloadModal"
v-model:show="downloadModel" v-model:show="downloadModal"
preset="card" preset="card"
title="歌曲下载" :title="$t('menu.download')"
:bordered="false" :bordered="false"
:on-after-leave="closeDownloadModel" :on-after-leave="closeDownloadModal"
> >
<Transition mode="out-in"> <Transition mode="out-in">
<div v-if="songData"> <div v-if="songData">
<SmallSongData ref="smallSongDataRef" :songData="songData" notJump /> <SmallSongData ref="smallSongDataRef" :songData="songData" notJump />
<n-alert v-if="songData.pc" class="tip" type="info" :show-icon="false"> <n-alert v-if="songData.pc" class="tip" type="info" :show-icon="false">
当前为云盘歌曲下载的文件均为最高音质 {{ $t("other.cloudTip") }}
</n-alert> </n-alert>
<n-radio-group <n-radio-group
class="downloadGroup" class="downloadGroup"
@@ -31,18 +31,20 @@
{{ item.size }} {{ item.size }}
</n-text> </n-text>
<n-text v-else-if="!item.disabled" class="error" :depth="3"> <n-text v-else-if="!item.disabled" class="error" :depth="3">
无法获取 {{ $t("general.message.acquisitionFailed") }}
</n-text> </n-text>
</div> </div>
</n-radio> </n-radio>
</n-space> </n-space>
</n-radio-group> </n-radio-group>
</div> </div>
<n-text v-else>正在获取歌曲下载数据</n-text> <n-text v-else>{{ $t("general.message.isLoading") }}</n-text>
</Transition> </Transition>
<template #footer> <template #footer>
<n-space justify="end"> <n-space justify="end">
<n-button @click="closeDownloadModel"> 取消 </n-button> <n-button @click="closeDownloadModal">
{{ $t("general.dialog.cancel") }}
</n-button>
<n-button <n-button
:disabled="!downloadChoose" :disabled="!downloadChoose"
:loading="downloadStatus" :loading="downloadStatus"
@@ -55,7 +57,11 @@
) )
" "
> >
{{ downloadStatus ? "正在下载" : "下载" }} {{
downloadStatus
? $t("general.dialog.downloadingNow")
: $t("general.dialog.download")
}}
</n-button> </n-button>
</n-space> </n-space>
</template> </template>
@@ -66,8 +72,10 @@
import { userStore } from "@/store"; import { userStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getMusicDetail, getSongDownload } from "@/api/song"; import { getMusicDetail, getSongDownload } from "@/api/song";
import { useI18n } from "vue-i18n";
import SmallSongData from "@/components/DataList/SmallSongData.vue"; import SmallSongData from "@/components/DataList/SmallSongData.vue";
const { t } = useI18n();
const user = userStore(); const user = userStore();
const router = useRouter(); const router = useRouter();
@@ -75,7 +83,7 @@ const router = useRouter();
const songId = ref(null); const songId = ref(null);
const songData = ref(null); const songData = ref(null);
const downloadStatus = ref(false); const downloadStatus = ref(false);
const downloadModel = ref(false); const downloadModal = ref(false);
const downloadChoose = ref(null); const downloadChoose = ref(null);
const downloadLevel = ref(null); const downloadLevel = ref(null);
@@ -87,8 +95,8 @@ const toSongDownload = (id, br, name) => {
console.log(name, res); console.log(name, res);
if (res.data.url) { if (res.data.url) {
const type = res.data.type.toLowerCase(); const type = res.data.type.toLowerCase();
const songName = name ? name : "未知曲目"; const songName = name ? name : t("general.name.unknownSong");
fetch(res.data.url) fetch(res.data.url.replace(/^http:/, "https:"))
.then((response) => response.blob()) .then((response) => response.blob())
.then((blob) => { .then((blob) => {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
@@ -98,19 +106,19 @@ const toSongDownload = (id, br, name) => {
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
closeDownloadModel(); closeDownloadModal();
downloadStatus.value = false; downloadStatus.value = false;
$message.success(name + " 下载完成"); $message.success(t("general.message.downloadSuccess", { name }));
}); });
} else { } else {
downloadStatus.value = false; downloadStatus.value = false;
$message.error("下载失败,请尝试其他音质"); $message.error(t("general.message.downloadFailure"));
} }
}) })
.catch((err) => { .catch((err) => {
closeDownloadModel(); closeDownloadModal();
console.error("下载出现错误:" + err); console.error(t("general.message.downloadError"), err);
$message.error("下载出现错误,请重试"); $message.error(t("general.message.downloadError"));
}); });
}; };
@@ -129,13 +137,13 @@ const getMusicDetailData = (id) => {
// //
generateLists(res); generateLists(res);
} else { } else {
$message.error("歌曲信息获取失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}) })
.catch((err) => { .catch((err) => {
closeDownloadModel(); closeDownloadModal();
console.error("歌曲信息获取出现错误:" + err); console.error(t("general.message.acquisitionFailed"), err);
$message.error("歌曲信息获取出现错误,请重试"); $message.error(t("general.message.acquisitionFailed"));
}); });
}; };
@@ -145,25 +153,25 @@ const generateLists = (data) => {
downloadLevel.value = [ downloadLevel.value = [
{ {
value: "128000", value: "128000",
label: "标准音质", label: t("general.type.quality.l"),
disabled: br >= 128000 ? false : true, disabled: br >= 128000 ? false : true,
size: getSongSize(data, "l"), size: getSongSize(data, "l"),
}, },
{ {
value: "192000", value: "192000",
label: "较高音质", label: t("general.type.quality.m"),
disabled: br >= 192000 ? false : true, disabled: br >= 192000 ? false : true,
size: getSongSize(data, "m"), size: getSongSize(data, "m"),
}, },
{ {
value: "320000", value: "320000",
label: "极高音质", label: t("general.type.quality.h"),
disabled: br >= 320000 ? false : true, disabled: br >= 320000 ? false : true,
size: getSongSize(data, "h"), size: getSongSize(data, "h"),
}, },
{ {
value: "420000", value: "420000",
label: "无损音质", label: t("general.type.quality.sq"),
disabled: [128000, 192000, 320000].includes(parseInt(br)), disabled: [128000, 192000, 320000].includes(parseInt(br)),
size: getSongSize(data, "sq"), size: getSongSize(data, "sq"),
}, },
@@ -200,7 +208,7 @@ const getSongSize = (data, type) => {
}; };
// //
const openDownloadModel = (data) => { const openDownloadModal = (data) => {
if (user.userLogin) { if (user.userLogin) {
if ( if (
router.currentRoute.value.name === "user-cloud" || router.currentRoute.value.name === "user-cloud" ||
@@ -209,33 +217,33 @@ const openDownloadModel = (data) => {
data?.pc data?.pc
) { ) {
songId.value = data.id; songId.value = data.id;
downloadModel.value = true; downloadModal.value = true;
getMusicDetailData(data.id); getMusicDetailData(data.id);
} else { } else {
$message.error("该歌曲需使用黑胶会员下载"); $message.error(t("general.message.needVip"));
} }
} else { } else {
$message.error("请登录后使用该功能"); $message.error(t("general.message.needLogin"));
} }
}; };
// //
const closeDownloadModel = () => { const closeDownloadModal = () => {
songId.value = null; songId.value = null;
songData.value = null; songData.value = null;
downloadStatus.value = false; downloadStatus.value = false;
downloadModel.value = false; downloadModal.value = false;
downloadChoose.value = null; downloadChoose.value = null;
}; };
// //
defineExpose({ defineExpose({
openDownloadModel, openDownloadModal,
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.downloadModel { .downloadModal {
.v-enter-active, .v-enter-active,
.v-leave-active { .v-leave-active {
transition: opacity 0.3s ease; 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,16 +2,26 @@
<n-drawer <n-drawer
class="playlist-drawer" class="playlist-drawer"
v-model:show="playListShow" v-model:show="playListShow"
:z-index="2000" :z-index="1"
:width="400" :width="400"
:trap-focus="false" :trap-focus="false"
:block-scroll="false" :block-scroll="false"
placement="right" placement="right"
to="#main" to="#mainContent"
@after-leave="music.showPlayList = false" @after-leave="music.showPlayList = false"
@mask-click="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">{{ $t("general.name.playlists") }}</n-text>
<n-text class="num" :depth="3" v-if="music.getPlaylists.length > 0">
{{
$t("general.name.songSize", { size: music.getPlaylists.length })
}}
</n-text>
</div>
</template>
<Transition mode="out-in"> <Transition mode="out-in">
<div v-if="music.getPlaylists[0]"> <div v-if="music.getPlaylists[0]">
<n-card <n-card
@@ -31,9 +41,13 @@
@click="changeIndex(index)" @click="changeIndex(index)"
> >
<div class="left"> <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 }} {{ index + 1 }}
</div> </n-text>
<div v-else class="bar"> <div v-else class="bar">
<div <div
v-for="item in 3" v-for="item in 3"
@@ -61,7 +75,7 @@
</div> </div>
</n-card> </n-card>
</div> </div>
<n-text v-else>暂无歌曲请前往列表添加</n-text> <n-text v-else>{{ $t("other.playlistEmpty") }}</n-text>
</Transition> </Transition>
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
@@ -70,8 +84,11 @@
<script setup> <script setup>
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { DeleteFour } from "@icon-park/vue-next"; import { DeleteFour } from "@icon-park/vue-next";
import { soundStop } from "@/utils/Player";
import { useI18n } from "vue-i18n";
import AllArtists from "@/components/DataList/AllArtists.vue"; import AllArtists from "@/components/DataList/AllArtists.vue";
const { t } = useI18n();
const music = musicStore(); const music = musicStore();
// //
@@ -79,8 +96,17 @@ const playListShow = ref(false);
// //
const changeIndex = (index) => { const changeIndex = (index) => {
try {
if (music.persistData.playSongIndex !== index) {
if (typeof $player !== "undefined") soundStop($player);
music.persistData.playSongIndex = index; music.persistData.playSongIndex = index;
music.isLoadingSong = true;
music.setPlayState(true); music.setPlayState(true);
}
} catch (err) {
console.error(t("general.message.operationFailed"), err);
$message.error(t("general.message.operationFailed"));
}
}; };
// //
@@ -89,7 +115,7 @@ watch(
() => music.showPlayList, () => music.showPlayList,
(val) => { (val) => {
playListShow.value = val; playListShow.value = val;
nextTick(() => { nextTick().then(() => {
if (val && music.getPlaylists[0]) { if (val && music.getPlaylists[0]) {
const el = document.getElementById( const el = document.getElementById(
`playlist${music.persistData.playSongIndex}` `playlist${music.persistData.playSongIndex}`
@@ -100,7 +126,7 @@ watch(
behavior: "smooth", behavior: "smooth",
block: "center", block: "center",
}); });
}, 300); }, 500);
} }
} else { } else {
clearTimeout(timeOut.value); clearTimeout(timeOut.value);
@@ -129,6 +155,17 @@ onBeforeUnmount(() => {
.v-leave-to { .v-leave-to {
opacity: 0; opacity: 0;
} }
.text {
display: flex;
align-items: center;
.num {
font-size: 14px;
&::before {
content: "-";
margin: 0 6px;
}
}
}
.songs { .songs {
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
@@ -150,20 +187,20 @@ onBeforeUnmount(() => {
} }
} }
&.play { &.play {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
border-color: $mainColor; border-color: var(--main-color);
a, a,
span, span,
div, div,
.n-icon { .n-icon {
color: $mainColor; color: var(--main-color);
} }
:deep(span) { :deep(span) {
color: $mainColor; color: var(--main-color);
} }
.right { .right {
.remove { .remove {
color: $mainColor; color: var(--main-color);
&:hover { &:hover {
background-color: var(--n-action-color); background-color: var(--n-action-color);
} }
@@ -186,7 +223,7 @@ onBeforeUnmount(() => {
.line { .line {
width: 3px; width: 3px;
height: 16px; height: 16px;
background-color: $mainColor; background-color: var(--main-color);
border-radius: 4px; border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
animation: lineMove 1s ease-in-out infinite; animation: lineMove 1s ease-in-out infinite;
@@ -229,7 +266,7 @@ onBeforeUnmount(() => {
color: #999; color: #999;
padding: 6px; padding: 6px;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
background-color: var(--n-border-color); background-color: var(--n-border-color);
} }
} }

View File

@@ -1,11 +1,11 @@
<template> <template>
<n-modal <n-modal
class="s-modal" class="s-modal"
v-model:show="playlistUpdateModel" v-model:show="playlistUpdateModal"
preset="card" preset="card"
title="歌单编辑" :title="$t('menu.update')"
:bordered="false" :bordered="false"
:on-after-leave="closeUpdateModel" :on-after-leave="closeUpdateModal"
> >
<n-form <n-form
ref="playlistUpdateRef" ref="playlistUpdateRef"
@@ -13,28 +13,28 @@
:label-width="80" :label-width="80"
:model="playlistUpdateValue" :model="playlistUpdateValue"
> >
<n-form-item label="歌单名称" path="name"> <n-form-item :label="$t('other.plName')" path="name">
<n-input <n-input
v-model:value="playlistUpdateValue.name" v-model:value="playlistUpdateValue.name"
placeholder="请输入歌单名称" :placeholder="$t('other.plNameTip')"
/> />
</n-form-item> </n-form-item>
<n-form-item label="歌单描述" path="desc"> <n-form-item :label="$t('other.plDes')" path="desc">
<n-input <n-input
v-model:value="playlistUpdateValue.desc" v-model:value="playlistUpdateValue.desc"
placeholder="请输入歌单描述"
type="textarea" type="textarea"
:placeholder="$t('other.plDesTip')"
:autosize="{ :autosize="{
minRows: 3, minRows: 3,
maxRows: 5, maxRows: 5,
}" }"
/> />
</n-form-item> </n-form-item>
<n-form-item label="歌单标签" path="tags"> <n-form-item :label="$t('other.plTag')" path="tags">
<n-select <n-select
multiple multiple
v-model:value="playlistUpdateValue.tags" v-model:value="playlistUpdateValue.tags"
placeholder="请输入歌单标签" :placeholder="$t('other.plTagTip')"
:options="playlistTags" :options="playlistTags"
@click="openSelect" @click="openSelect"
/> />
@@ -42,8 +42,12 @@
</n-form> </n-form>
<template #footer> <template #footer>
<n-space justify="end"> <n-space justify="end">
<n-button @click="closeUpdateModel"> 取消 </n-button> <n-button @click="closeUpdateModal">
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button> {{ $t("general.dialog.cancel") }}
</n-button>
<n-button type="primary" @click="toUpdatePlayList">
{{ $t("general.dialog.editor") }}
</n-button>
</n-space> </n-space>
</template> </template>
</n-modal> </n-modal>
@@ -51,9 +55,11 @@
<script setup> <script setup>
import { playlistUpdate } from "@/api/playlist"; import { playlistUpdate } from "@/api/playlist";
import { formRules } from "@/utils/formRules.js"; import { formRules } from "@/utils/formRules";
import { musicStore, userStore } from "@/store"; import { musicStore, userStore } from "@/store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { textRule } = formRules(); const { textRule } = formRules();
const music = musicStore(); const music = musicStore();
const user = userStore(); const user = userStore();
@@ -61,7 +67,7 @@ const user = userStore();
// //
const playlistUpdateId = ref(null); const playlistUpdateId = ref(null);
const playlistUpdateRef = ref(null); const playlistUpdateRef = ref(null);
const playlistUpdateModel = ref(false); const playlistUpdateModal = ref(false);
const playlistUpdateRules = { const playlistUpdateRules = {
name: textRule, name: textRule,
}; };
@@ -85,16 +91,16 @@ const toUpdatePlayList = (e) => {
).then((res) => { ).then((res) => {
console.log(res); console.log(res);
if (res.code === 200) { if (res.code === 200) {
$message.success("编辑成功"); $message.success(t("general.message.editorSuccess"));
closeUpdateModel(); closeUpdateModal();
user.setUserPlayLists(); user.setUserPlayLists();
} else { } else {
$message.error("编辑失败,请重试"); $message.error(t("general.message.editorFailed"));
} }
}); });
} else { } else {
$loadingBar.error(); $loadingBar.error();
$message.error("请检查您的输入"); $message.error(t("general.message.needCheck"));
} }
}); });
}; };
@@ -113,24 +119,24 @@ const openSelect = () => {
}; };
// //
const openUpdateModel = (data) => { const openUpdateModal = (data) => {
playlistUpdateValue.value = { playlistUpdateValue.value = {
name: data.name, name: data.name,
desc: data.desc, desc: data.desc,
tags: data.tags, tags: data.tags,
}; };
playlistUpdateId.value = data.id; playlistUpdateId.value = data.id;
playlistUpdateModel.value = true; playlistUpdateModal.value = true;
}; };
// //
const closeUpdateModel = () => { const closeUpdateModal = () => {
playlistUpdateModel.value = false; playlistUpdateModal.value = false;
playlistUpdateId.value = null; playlistUpdateId.value = null;
}; };
// //
defineExpose({ defineExpose({
openUpdateModel, openUpdateModal,
}); });
</script> </script>

View File

@@ -1,119 +0,0 @@
<template>
<n-modal
class="add-playlist s-modal"
v-model:show="addToPlaylistModel"
preset="card"
title="添加到歌单"
:bordered="false"
:on-after-leave="closeAddToPlaylist"
>
<n-space vertical class="list" v-if="user.getUserPlayLists.own[0]">
<div
class="item"
v-for="item in user.getUserPlayLists.own"
:key="item"
@click="addToPlayList(item.id, addToPlaylistId)"
>
<n-avatar
class="pic"
:src="
item.cover
? item.cover.replace(/^http:/, 'https:') + '?param=60y60'
: '/images/pic/default.png'
"
fallback-src="/images/pic/default.png"
/>
<div class="desc">
<n-text class="name">{{ item.name }}</n-text>
<n-text class="num">{{ item.trackCount }}</n-text>
</div>
</div>
</n-space>
<n-text v-else>歌单列表加载中</n-text>
</n-modal>
</template>
<script setup>
import { addSongToPlayList } from "@/api/playlist";
import { userStore } from "@/store";
const user = userStore();
// 收藏到歌单数据
const addToPlaylistModel = ref(false);
const addToPlaylistId = ref(null);
// 收藏到歌单
const addToPlayList = (pid, tracks) => {
console.log("添加" + tracks + "到" + pid);
addSongToPlayList(pid, tracks).then((res) => {
console.log(res);
if (res.status === 200) {
$message.success("添加歌曲至歌单成功");
closeAddToPlaylist();
user.setUserPlayLists();
} else {
$message.error("添加失败,请重试");
}
});
};
// 开启收藏到歌单
const openAddToPlaylist = (id) => {
if (!user.userLogin) {
$message.error("请登录账号后使用");
return false;
}
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
user.setUserPlayLists();
}
addToPlaylistModel.value = true;
addToPlaylistId.value = id;
console.log("开启", addToPlaylistModel.value, addToPlaylistId.value);
};
// 关闭收藏到歌单
const closeAddToPlaylist = () => {
addToPlaylistModel.value = false;
};
// 暴露方法
defineExpose({
openAddToPlaylist,
});
</script>
<style lang="scss" scoped>
.add-playlist {
.list {
.item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--n-border-color);
}
.pic {
width: 60px;
height: 60px;
}
.desc {
margin-left: 12px;
display: flex;
flex-direction: column;
.name {
// font-weight: bold;
font-size: 15px;
}
.num {
margin-top: 2px;
opacity: 0.8;
}
}
}
}
}
</style>

View File

@@ -2,31 +2,42 @@
<nav> <nav>
<div class="left"> <div class="left">
<div class="logo" @click="router.push('/')"> <div class="logo" @click="router.push('/')">
<img src="/images/logo/favicon.svg" alt="logo" /> <img :src="logoUrl" alt="logo" />
</div> </div>
<div class="controls"> <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="Left" @click="router.go(-1)" />
<n-icon size="22" :component="Right" @click="router.go(1)" /> <n-icon size="22" :component="Right" @click="router.go(1)" />
</div> </div>
</Transition>
</div> </div>
<div class="center"> <div class="center">
<router-link class="link" to="/">首页</router-link> <router-link class="link" to="/">{{ $t("nav.home") }}</router-link>
<n-dropdown <n-dropdown
trigger="hover" trigger="hover"
:options="discoverOptions" :options="discoverOptions"
@select="menuSelect" @select="menuSelect"
> >
<router-link class="link" to="/discover">发现</router-link> <router-link class="link" to="/discover">
{{ $t("nav.discover") }}
</router-link>
</n-dropdown> </n-dropdown>
<n-dropdown trigger="hover" :options="userOptions" @select="menuSelect"> <n-dropdown trigger="hover" :options="userOptions" @select="menuSelect">
<router-link class="link" to="/user">我的</router-link> <router-link class="link" to="/user">{{ $t("nav.user") }}</router-link>
</n-dropdown> </n-dropdown>
</div> </div>
<div class="right"> <div class="right">
<SearchInp /> <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 <n-dropdown
class="dropdown"
placement="bottom-end" placement="bottom-end"
:show="showDropdown" :show="showDropdown"
:show-arrow="true" :show-arrow="true"
@@ -67,16 +78,25 @@ import {
History, History,
SunOne, SunOne,
Moon, Moon,
HamburgerButton,
HomeTwo,
FindOne,
Me,
} from "@icon-park/vue-next"; } from "@icon-park/vue-next";
import { userStore, settingStore } from "@/store"; import { userStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AboutSite from "@/components/DataModel/AboutSite.vue"; import { useI18n } from "vue-i18n";
import AboutSite from "@/components/DataModal/AboutSite.vue";
import SearchInp from "@/components/SearchInp/index.vue"; import SearchInp from "@/components/SearchInp/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const user = userStore(); const user = userStore();
const site = siteStore();
const setting = settingStore(); const setting = settingStore();
const aboutSiteRef = ref(null); const aboutSiteRef = ref(null);
const timeOut = ref(null);
const logoUrl = import.meta.env.VITE_SITE_LOGO;
// 下拉菜单显隐 // 下拉菜单显隐
const showDropdown = ref(false); const showDropdown = ref(false);
@@ -89,6 +109,19 @@ const closeDropdown = (event) => {
} }
}; };
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 用户数据模块 // 用户数据模块
const userDataRender = () => { const userDataRender = () => {
return h( return h(
@@ -118,7 +151,9 @@ const userDataRender = () => {
{ depth: 2 }, { depth: 2 },
{ {
default: () => default: () =>
user.userLogin ? user.getUserData.nickname : "未登录", user.userLogin
? user.getUserData.nickname
: t("nav.avatar.notLogin"),
} }
), ),
]), ]),
@@ -137,15 +172,15 @@ const userDataRender = () => {
type: "line", type: "line",
percentage: percentage:
user.getUserOtherData.level.progress * 100, user.getUserOtherData.level.progress * 100,
color: "#f55e55", color: setting.themeData.primaryColor,
}, },
{ {
default: () => default: () =>
"Lv." + user.getUserOtherData.level.level, "Lv." + user.getUserOtherData.level.level,
} }
) )
: "等级信息获取失败" : t("nav.avatar.loginError")
: "登录后享受完整功能", : t("nav.avatar.notLoginSubtitle"),
} }
), ),
]), ]),
@@ -155,52 +190,60 @@ const userDataRender = () => {
}; };
// 下拉框数据 // 下拉框数据
const discoverOptions = ref([ const discoverOptions = ref([]);
const userOptions = ref([]);
const dropdownOptions = ref([]);
// 写入下拉框数据
const changeDiscoverOptions = () => {
discoverOptions.value = [
{ {
label: "歌单", label: t("nav.discoverChildren.playlists"),
key: "/discover/playlists", key: "/discover/playlists",
}, },
{ {
label: "排行榜", label: t("nav.discoverChildren.toplists"),
key: "/discover/toplists", key: "/discover/toplists",
}, },
{ {
label: "歌手", label: t("nav.discoverChildren.artists"),
key: "/discover/artists", key: "/discover/artists",
}, },
]); ];
const userOptions = ref( };
user.userLogin const changeUserOptions = (val) => {
userOptions.value = val
? [ ? [
{ {
label: "我的歌单", label: t("nav.userChildren.playlist"),
key: "/user/playlists", key: "/user/playlists",
}, },
{ {
label: "收藏的歌单", label: t("nav.userChildren.like"),
key: "/user/like", key: "/user/like",
}, },
{ {
label: "收藏的专辑", label: t("nav.userChildren.album"),
key: "/user/album", key: "/user/album",
}, },
{ {
label: "收藏的歌手", label: t("nav.userChildren.artist"),
key: "/user/artists", key: "/user/artists",
}, },
{ {
label: "音乐云盘", label: t("nav.userChildren.cloud"),
key: "/user/cloud", key: "/user/cloud",
}, },
] ]
: [ : [
{ {
label: "登录账号", label: t("nav.userChildren.login"),
key: "/login", key: "/login",
}, },
] ];
); };
const dropdownOptions = ref([ const changeDropdownOptions = () => {
dropdownOptions.value = [
{ {
key: "header", key: "header",
type: "render", type: "render",
@@ -217,7 +260,9 @@ const dropdownOptions = ref([
{ style: { transform: "translateX(2px)" } }, { style: { transform: "translateX(2px)" } },
{ {
default: () => default: () =>
setting.getSiteTheme == "light" ? "深色模式" : "浅色模式", setting.getSiteTheme == "light"
? t("nav.avatar.dark")
: t("nav.avatar.light"),
} }
); );
}, },
@@ -234,30 +279,14 @@ const dropdownOptions = ref([
}, },
}, },
{ {
label: "播放历史", label: t("nav.avatar.history"),
key: "history", key: "history",
icon: () => { icon: renderIcon(h(History)),
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(History),
}
);
},
}, },
{ {
label: "全局设置", label: t("nav.avatar.setting"),
key: "setting", key: "setting",
icon: () => { icon: renderIcon(h(SettingTwo)),
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(SettingTwo),
}
);
},
}, },
{ {
label: () => { label: () => {
@@ -265,7 +294,8 @@ const dropdownOptions = ref([
NText, NText,
{ style: { transform: "translateX(2px)" } }, { style: { transform: "translateX(2px)" } },
{ {
default: () => (user.userLogin ? "退出登录" : "登录账号"), default: () =>
user.userLogin ? t("nav.avatar.logout") : t("nav.avatar.login"),
} }
); );
}, },
@@ -281,21 +311,36 @@ const dropdownOptions = ref([
}, },
}, },
{ {
label: "关于本站", label: t("nav.avatar.about"),
key: "about", key: "about",
icon: () => { icon: renderIcon(h(Info)),
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(Info),
}
);
}, },
}, ];
]); };
// 下拉框事件 // 移动端菜单
const mbMenuOptions = ref([]);
const changeMbMenuOptions = () => {
mbMenuOptions.value = [
{
label: t("nav.home"),
key: "/",
icon: renderIcon(h(HomeTwo)),
},
{
label: t("nav.discover"),
key: "/discover",
icon: renderIcon(h(FindOne)),
},
{
label: t("nav.user"),
key: "/user",
icon: renderIcon(h(Me)),
},
];
};
// 下拉框点击事件
const menuSelect = (key) => { const menuSelect = (key) => {
router.push(key); router.push(key);
}; };
@@ -307,6 +352,7 @@ const dropdownSelect = (key) => {
setting.getSiteTheme == "light" setting.getSiteTheme == "light"
? setting.setSiteTheme("dark") ? setting.setSiteTheme("dark")
: setting.setSiteTheme("light"); : setting.setSiteTheme("light");
setting.themeAuto = false;
break; break;
// 播放历史 // 播放历史
case "history": case "history":
@@ -322,13 +368,17 @@ const dropdownSelect = (key) => {
// 退出登录 // 退出登录
$dialog.warning({ $dialog.warning({
class: "s-dialog", class: "s-dialog",
title: "退出登录", title: t("nav.avatar.logout"),
content: "确认退出当前用户登录?", content: t("nav.avatar.tip"),
positiveText: "退出登录", positiveText: t("nav.avatar.logout"),
negativeText: "取消", negativeText: t("general.dialog.cancel"),
onPositiveClick: () => { onPositiveClick: () => {
user.userLogOut(); user.userLogOut();
$message.success("已退出登录"); $message.success(t("nav.avatar.success"));
// 刷新页面
timeOut.value = setTimeout(() => {
document.location.reload();
}, 1000);
}, },
}); });
} else { } else {
@@ -344,6 +394,36 @@ const dropdownSelect = (key) => {
break; break;
} }
}; };
// 监听登录状态变化
watch(
() => user.userLogin,
(val) => {
changeUserOptions(val);
}
);
// 监听语言变化
watch(
() => setting.language,
() => {
changeDiscoverOptions();
changeMbMenuOptions();
changeDropdownOptions();
changeUserOptions(user.userLogin);
}
);
onMounted(() => {
changeDiscoverOptions();
changeMbMenuOptions();
changeDropdownOptions();
changeUserOptions(user.userLogin);
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -356,6 +436,17 @@ nav {
align-items: center; align-items: center;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; 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 { .left {
flex: 1; flex: 1;
max-width: 300px; max-width: 300px;
@@ -369,28 +460,36 @@ nav {
width: 30px; width: 30px;
height: 30px; height: 30px;
margin-right: 12px; margin-right: 12px;
transition: all 0.3s;
cursor: pointer; cursor: pointer;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (min-width: 640px) {
&:hover {
transform: scale(1.15);
}
}
&:active {
transform: scale(1);
}
} }
.controls { .controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@media (max-width: 520px) {
display: none;
}
.n-icon { .n-icon {
margin: 0 4px; margin: 0 4px;
border-radius: 8px; border-radius: 8px;
padding: 4px; padding: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@media (min-width: 640px) {
&:hover { &:hover {
background-color: var(--n-border-color); background-color: var(--n-border-color);
} }
}
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
@@ -416,8 +515,8 @@ nav {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: $mainColor; background-color: var(--main-color);
color: var(--n-color); color: rgba(255, 255, 255, 0.9);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
@@ -425,8 +524,8 @@ nav {
} }
.router-link-active { .router-link-active {
background-color: $mainColor; background-color: var(--main-color);
color: var(--n-color); color: rgba(255, 255, 255, 0.9);
} }
} }
.right { .right {
@@ -436,6 +535,10 @@ nav {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
@media (max-width: 520px) {
position: absolute;
right: 12px;
}
.avatar { .avatar {
width: 30px; width: 30px;
min-width: 30px; min-width: 30px;
@@ -444,6 +547,13 @@ nav {
box-shadow: 0 4px 12px -2px rgb(0 0 0 / 10%); box-shadow: 0 4px 12px -2px rgb(0 0 0 / 10%);
cursor: pointer; cursor: pointer;
} }
.mb-menu {
margin-left: 12px;
display: none;
@media (max-width: 768px) {
display: flex;
}
}
} }
} }
</style> </style>

View File

@@ -10,12 +10,17 @@
:show-size-picker="showSizePicker" :show-size-picker="showSizePicker"
:show-quick-jumper="showQuickJumper" :show-quick-jumper="showQuickJumper"
> >
<template #prefix="{ itemCount }"> {{ itemCount }} </template> <template #prefix="{ itemCount }">
<template #goto> 前往 </template> {{ $t("general.name.itemCount", { size: itemCount }) }}
</template>
<template #goto> {{ $t("general.name.goto") }} </template>
</n-pagination> </n-pagination>
</template> </template>
<script setup> <script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
// 数据总量 // 数据总量
totalCount: { totalCount: {
@@ -47,21 +52,25 @@ const currentPageNumber = ref(1);
// 自定义每页数量 // 自定义每页数量
const pageSizes = ref([ const pageSizes = ref([
{ {
label: "10条/页", label: t("general.name.pageSizes", { num: 10 }),
value: 10, value: 10,
}, },
{ {
label: "20条/页", label: t("general.name.pageSizes", { num: 20 }),
value: 20, value: 20,
}, },
{ {
label: "30条/页", label: t("general.name.pageSizes", { num: 30 }),
value: 30, value: 30,
}, },
{ {
label: "50条/页", label: t("general.name.pageSizes", { num: 50 }),
value: 50, value: 50,
}, },
{
label: t("general.name.pageSizes", { num: 100 }),
value: 100,
},
]); ]);
// 每页个数数据变化 // 每页个数数据变化

View File

@@ -1,8 +1,10 @@
<template> <template>
<div class="paalbum"> <div class="paalbum">
<n-h3 class="title" prefix="bar"> <n-h3 class="title" prefix="bar">
新碟上架 {{ $t("home.title.newAlbum") }}
<span class="more" @click="router.push('/new-album?page=1')">更多</span> <span class="more" @click="router.push('/new-album?page=1')">
{{ $t("home.title.more") }}
</span>
</n-h3> </n-h3>
<CoverLists <CoverLists
listType="album" listType="album"
@@ -16,7 +18,7 @@
<script setup> <script setup>
import { getNewAlbum } from "@/api/home"; import { getNewAlbum } from "@/api/home";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js"; import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
const router = useRouter(); const router = useRouter();
@@ -66,7 +68,7 @@ onMounted(() => {
margin-left: 6px; margin-left: 6px;
} }
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }

View File

@@ -1,21 +1,21 @@
<template> <template>
<div class="paartists"> <div class="paartists">
<n-h3 class="title" prefix="bar"> <n-h3 class="title" prefix="bar">
歌手推荐 {{ $t("home.title.artists") }}
<n-tabs <n-tabs
class="tab" class="tab"
:default-value="-1" :default-value="-1"
size="small" size="small"
@update:value="tabChange" @update:value="tabChange"
> >
<n-tab :name="-1"> 全部 </n-tab> <n-tab :name="-1"> {{ $t("general.type.all") }} </n-tab>
<n-tab :name="7"> 华语 </n-tab> <n-tab :name="7"> {{ $t("general.type.china") }} </n-tab>
<n-tab :name="96"> 欧美 </n-tab> <n-tab :name="96"> {{ $t("general.type.western") }} </n-tab>
<n-tab :name="8"> 日本 </n-tab> <n-tab :name="8"> {{ $t("general.type.japan") }} </n-tab>
<n-tab :name="16"> 韩国 </n-tab> <n-tab :name="16"> {{ $t("general.type.korea") }} </n-tab>
</n-tabs> </n-tabs>
<span class="more" @click="router.push('/discover/artists?page=1')"> <span class="more" @click="router.push('/discover/artists?page=1')">
更多 {{ $t("home.title.more") }}
</span> </span>
</n-h3> </n-h3>
<ArtistLists :listData="artistsData" :gridCollapsed="true" /> <ArtistLists :listData="artistsData" :gridCollapsed="true" />
@@ -92,7 +92,7 @@ onMounted(() => {
margin-left: 6px; margin-left: 6px;
} }
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }

View File

@@ -1,19 +1,32 @@
<template> <template>
<div class="padailysongs" @click="router.push('/dailySongs')"> <div
<img class="padailysongs"
class="pic" :style="`background-image: url(${cardImage})`"
:src=" @click="router.push('/dailySongs')"
music.getDailySongs[0] >
? music.getDailySongs[ <div class="gray" />
Math.floor(Math.random() * music.getDailySongs.length)
].album.picUrl.replace(/^http:/, 'https:') + '?param=800y800'
: '/images/pic/pic.jpg'
"
alt="pic"
/>
<div class="text"> <div class="text">
<span class="title">每日推荐</span> <div class="date">
<span class="tip">根据你的音乐口味生成 · 每天 6:00 更新</span> <n-icon class="calendar" :component="CalendarTodayFilled" size="40" />
<n-text class="num" v-html="new Date().getDate()" />
</div>
<div class="desc">
<n-text class="title">{{ $t("home.modules.dailySongs.title") }}</n-text>
<n-text class="tip">{{ $t("home.modules.dailySongs.subtitle") }}</n-text>
</div>
</div>
<div class="control">
<n-avatar
class="cover"
:src="cardImage"
fallback-src="/images/pic/default.png"
/>
<n-icon
class="play"
:component="PlayCircleFilled"
size="50"
@click.stop="playThisSong"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -22,24 +35,71 @@
import { getDailySongs } from "@/api/home"; import { getDailySongs } from "@/api/home";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { musicStore, userStore } from "@/store"; import { musicStore, userStore } from "@/store";
import { PlayCircleFilled, CalendarTodayFilled } from "@vicons/material";
const music = musicStore(); const music = musicStore();
const user = userStore(); const user = userStore();
const router = useRouter(); const router = useRouter();
// 卡片背景
const cardImage = ref(null);
// 随机数
const randomNumber = Math.floor(Math.random() * music.getDailySongs.length);
// 生成卡片背景
const getCardImage = () => {
if (user.userLogin && music.getDailySongs[0]) {
cardImage.value =
music.getDailySongs[randomNumber]?.album.picUrl.replace(
/^http:/,
"https:"
) + "?param=100y100";
} else {
cardImage.value = "/images/pic/pic.jpg";
}
};
// 获取每日推荐数据 // 获取每日推荐数据
const getDailySongsData = () => { const getDailySongsData = () => {
getCardImage();
if (music.getDailySongs.length === 0 && user.userLogin) {
getDailySongs().then((res) => { getDailySongs().then((res) => {
if (res.data.dailySongs) { if (res.data.dailySongs) {
music.setDailySongs(res.data.dailySongs); music.setDailySongs(res.data.dailySongs);
getCardImage();
} else { } else {
$message.error("每日推荐获取失败"); $message.error("每日推荐获取失败");
} }
}); });
}
};
// 从当前歌曲开始播放
const playThisSong = () => {
if (user.userLogin) {
if (music.getDailySongs.length !== 0) {
// 正在播放的歌曲id
const songId = music.getPlaySongData?.id;
// 查找是否在日推中
const isHas = music.getDailySongs.findIndex((o) => o.id === songId);
console.log(isHas);
music.setPersonalFmMode(false);
music.setPlayState(true);
if (isHas === -1) {
music.setPlaylists(music.getDailySongs);
music.addSongToPlaylists(music.getDailySongs[randomNumber]);
}
} else {
$message.error("每日推荐获取失败,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}
}; };
onMounted(() => { onMounted(() => {
if (music.getDailySongs.length === 0 && user.userLogin) getDailySongsData(); getDailySongsData();
}); });
</script> </script>
@@ -47,66 +107,130 @@ onMounted(() => {
.padailysongs { .padailysongs {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: space-evenly; align-items: center;
padding: 20px; justify-content: space-between;
height: 200px; height: 110px;
border-radius: 8px; border-radius: 8px;
padding: 0 28px;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
z-index: 0; z-index: 0;
margin-bottom: 20px;
transition: all 0.3s; transition: all 0.3s;
background-repeat: no-repeat;
background-size: 120% 120%;
background-position: center;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
.pic { .control {
transform: translateY(calc(-50% + 13vh)) scale(1.2); .cover {
filter: brightness(50%); opacity: 0;
}
.play {
transform: rotate(0);
opacity: 1;
right: 5px;
} }
} }
&:active {
transform: scale(0.98);
} }
.pic { .gray {
transition: all 0.3s;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
transform: translateY(calc(-50% + 13vh));
filter: brightness(70%);
z-index: -1;
@media (min-width: 750px) and (max-width: 1056px) {
transform: none;
height: 100%; height: 100%;
} background-color: #00000040;
@media (max-width: 500px) { -webkit-backdrop-filter: blur(20px);
transform: none; backdrop-filter: blur(20px);
} z-index: -1;
} }
.text { .text {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
.date {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: #fff; color: #fff;
text-shadow: 0px 0px 8px #00000082; margin-right: 18px;
.title { .num {
font-size: 34px; margin-top: 8px;
position: absolute;
font-size: 14px;
font-weight: bold; font-weight: bold;
margin-bottom: 8px; color: #fff;
letter-spacing: 2px;
}
@media (min-width: 750px) and (max-width: 1056px) {
.tip {
display: none;
} }
} }
.desc {
display: flex;
flex-direction: column;
span {
color: #fff;
} }
@media (max-width: 500px) {
height: 160px;
.text {
.title { .title {
font-size: 30px; font-size: 20px;
margin-bottom: 4px; margin-bottom: 2px;
@media (max-width: 1020px) {
font-size: 18px;
}
}
.tip {
color: #e9e9e9;
font-size: 13px;
}
}
}
.control {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: 12px;
.cover {
position: relative;
background-color: transparent;
width: 70px;
height: 70px;
transition: all 0.3s;
z-index: 0;
overflow: inherit;
:deep(img) {
border-radius: 8px;
}
&::before,
&::after {
content: "";
border-radius: 8px;
width: 100%;
height: 100%;
position: absolute;
background-color: #fff;
opacity: 0.6;
transform: scale(0.85) translateX(11px);
z-index: -1;
}
&::after {
transform: scale(0.7) translateX(27px);
opacity: 0.4;
z-index: -2;
}
}
.play {
color: #fff;
opacity: 0;
right: -70px;
position: absolute;
transform: rotate(180deg);
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(1);
} }
} }
} }

View File

@@ -0,0 +1,169 @@
<template>
<!-- 喜欢的音乐 -->
<div
class="like-song"
:style="`background-image: url(${cardImage})`"
@click="toLikeSongs"
>
<div class="gray" />
<div class="left">
<n-icon class="icon" :component="CollectionRecords" size="30" />
<div class="title">
<n-text class="name">{{ $t("home.modules.likeSong.title") }}</n-text>
<n-text class="tip">{{ $t("home.modules.likeSong.subtitle") }}</n-text>
</div>
</div>
<div class="right">
<n-icon class="icon" :component="Right" size="20" />
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { userStore } from "@/store";
import { CollectionRecords, Right } from "@icon-park/vue-next";
const router = useRouter();
const user = userStore();
// 卡片背景
const cardImage = ref(null);
// 生成卡片背景
const getCardImage = (index) => {
if (user.userLogin && user.getUserPlayLists.own[0]) {
const num =
index ?? Math.floor(Math.random() * user.getUserPlayLists.own.length);
cardImage.value =
user.getUserPlayLists.own[num]?.cover.replace(/^http:/, "https:") +
"?param=100y100";
} else {
cardImage.value = "/images/pic/pic.jpg";
}
};
// 跳转喜欢的音乐
const toLikeSongs = () => {
if (user.userLogin) {
const id = user.getUserPlayLists.own[0]?.id;
if (id) {
router.push(`/playlist?id=${id}&page=1`);
} else {
console.error("发生错误");
}
} else {
$message.error("请登录账号后使用");
router.push("/login");
}
};
onMounted(() => {
getCardImage();
if (
user.userLogin &&
!user.getUserPlayLists.has &&
!user.getUserPlayLists.isLoading
) {
user.setUserPlayLists(() => {
getCardImage();
});
}
});
</script>
<style lang="scss" scoped>
.like-song {
position: relative;
color: #fff;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 8px;
padding: 0 18px;
box-sizing: border-box;
background-repeat: no-repeat;
background-size: 120% 120%;
background-position: center;
cursor: pointer;
z-index: 0;
overflow: hidden;
&:hover {
.left {
.title {
.name {
opacity: 0;
transform: translateY(-50px);
}
.tip {
opacity: 1;
transform: translateY(0);
}
}
}
.right {
.icon {
opacity: 1;
transform: translateX(0);
}
}
}
.gray {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #00000040;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
z-index: -1;
}
.left {
height: 100%;
width: 100%;
display: flex;
align-items: center;
.icon {
margin-right: 12px;
}
.title {
height: 100%;
width: 100%;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
.name {
color: #fff;
font-size: 18px;
transition: all 0.3s;
@media (max-width: 1020px) {
font-size: 16px;
}
}
.tip {
height: 100%;
display: flex;
align-items: center;
position: absolute;
color: #fff;
opacity: 0;
transform: translateY(50px);
transition: all 0.3s;
}
}
}
.right {
display: flex;
.icon {
opacity: 0;
transform: translateX(-8px);
transition: all 0.3s;
}
}
}
</style>

View File

@@ -1,12 +1,11 @@
<template> <template>
<div <div
class="papersonalfm" class="papersonalfm"
v-if="music.getPersonalFmData.id" v-if="music.getPersonalFmData?.id"
:style=" :style="`background-image: url(${music.getPersonalFmData.album.picUrl.replace(
'background-image: url(' + /^http:/,
music.getPersonalFmData.album.picUrl.replace(/^http:/, 'https:') + 'https:'
'?param=300y300)' )}?param=300y300)`"
"
> >
<div class="gray" /> <div class="gray" />
<img <img
@@ -58,9 +57,11 @@
<div class="radio"> <div class="radio">
<div class="icon"> <div class="icon">
<n-icon size="20" :component="RadioFilled" /> <n-icon size="20" :component="RadioFilled" />
<span>私人FM</span> <span>{{ $t("home.modules.papersonalfm.title") }}</span>
</div> </div>
<span class="tip" v-if="!user.userLogin">未登录模式</span> <span class="tip" v-if="!user.userLogin">
{{ $t("home.modules.papersonalfm.subtitle") }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -110,7 +111,7 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
height: 200px; height: 100%;
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
@@ -127,8 +128,8 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #00000040; background-color: #00000040;
-webkit-backdrop-filter: blur(80px); -webkit-backdrop-filter: blur(40px);
backdrop-filter: blur(80px); backdrop-filter: blur(40px);
z-index: -1; z-index: -1;
} }
.pic { .pic {
@@ -172,8 +173,7 @@ onMounted(() => {
align-items: center; align-items: center;
.state { .state {
margin-right: 2px; margin-right: 2px;
transform: scale(1); transition: transform 0.3s;
transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);
@@ -190,9 +190,11 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
transform: scale(1); transform: scale(1);
transition: all 0.3s; transition: all 0.3s;
@media (min-width: 640px) {
&:hover { &:hover {
background-color: #ffffff30; background-color: #ffffff30;
} }
}
&:active { &:active {
transform: scale(0.9); transform: scale(0.9);
} }
@@ -234,7 +236,7 @@ onMounted(() => {
} }
} }
} }
@media (max-width: 500px) { @media (max-width: 1020px) {
.pic { .pic {
height: 96px; height: 96px;
position: absolute; position: absolute;

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="paplaylists"> <div class="paplaylists">
<n-h3 class="title" prefix="bar"> <n-h3 class="title" prefix="bar">
推荐歌单 {{ $t("home.title.playlists") }}
<span class="more" @click="router.push('/discover/playlists?page=1')"> <span class="more" @click="router.push('/discover/playlists?page=1')">
更多 {{ $t("home.title.more") }}
</span> </span>
</n-h3> </n-h3>
<CoverLists <CoverLists
@@ -17,7 +17,7 @@
<script setup> <script setup>
import { getPersonalized } from "@/api/home"; import { getPersonalized } from "@/api/home";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js"; import { formatNumber } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
const router = useRouter(); const router = useRouter();
@@ -66,7 +66,7 @@ onMounted(() => {
margin-left: 6px; margin-left: 6px;
} }
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }

View File

@@ -0,0 +1,123 @@
<template>
<!-- 私人雷达 -->
<div class="radar" @click="router.push(`/playlist?id=${radarId}&page=1`)">
<div class="gray" />
<div class="left">
<n-icon class="icon" :component="RadarThree" size="30" />
<div class="title">
<n-text class="name">{{ $t("home.modules.radar.title") }}</n-text>
<n-text class="tip">{{ $t("home.modules.radar.subtitle") }}</n-text>
</div>
</div>
<div class="right">
<n-icon class="icon" :component="Right" size="20" />
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { RadarThree, Right } from "@icon-park/vue-next";
const router = useRouter();
// 私人雷达歌单
const radarId = ref(3136952023);
</script>
<style lang="scss" scoped>
.radar {
position: relative;
color: #fff;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 8px;
padding: 0 18px;
box-sizing: border-box;
background-image: url("/images/pic/radar.jpg");
background-repeat: no-repeat;
background-size: 120% 120%;
background-position: center;
cursor: pointer;
z-index: 0;
overflow: hidden;
&:hover {
.left {
.title {
.name {
opacity: 0;
transform: translateY(-50px);
}
.tip {
opacity: 1;
transform: translateY(0);
}
}
}
.right {
.icon {
opacity: 1;
transform: translateX(0);
}
}
}
.gray {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #00000010;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
z-index: -1;
}
.left {
height: 100%;
width: 100%;
display: flex;
align-items: center;
.icon {
margin-right: 12px;
}
.title {
height: 100%;
width: 100%;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
.name {
color: #fff;
font-size: 18px;
transition: all 0.3s;
@media (max-width: 1020px) {
font-size: 16px;
}
}
.tip {
height: 100%;
display: flex;
align-items: center;
position: absolute;
color: #fff;
opacity: 0;
transform: translateY(50px);
transition: all 0.3s;
}
}
}
.right {
display: flex;
.icon {
opacity: 0;
transform: translateX(-8px);
transition: all 0.3s;
}
}
}
</style>

View File

@@ -1,30 +1,28 @@
<template> <template>
<Transition name="up"> <Transition name="up">
<div <div
v-show="music.showBigPlayer" v-if="music.showBigPlayer"
class="bplayer" class="bplayer"
:style=" :style="[
music.getPlaySongData music.getPlaySongData && setting.backgroundImageShow === 'blur'
? 'background-image: url(' + ? 'background-image: url(' +
music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') + music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
'?param=50y50)' '?param=50y50)'
: '' : '',
" `backgroundColor: ${site.songPicColor}`,
]"
> >
<div class="gray" /> <div
:class="setting.backgroundImageShow === 'blur' ? 'gray blur' : 'gray'"
/>
<div class="icon-menu"> <div class="icon-menu">
<div class="menu-left"> <div class="menu-left">
<div class="icon"> <div v-if="setting.showLyricSetting" class="icon">
<n-icon <n-icon
class="setting" class="setting"
size="30" size="30"
:component="SettingsRound" :component="SettingsRound"
@click=" @click="LyricSettingRef.openLyricSetting()"
() => {
music.setBigPlayerState(false);
router.push('/setting/player');
}
"
/> />
</div> </div>
</div> </div>
@@ -45,7 +43,6 @@
</div> </div>
</div> </div>
</div> </div>
<div <div
:class=" :class="
music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4 music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4
@@ -56,7 +53,7 @@
<!-- 提示文本 --> <!-- 提示文本 -->
<Transition name="lrc"> <Transition name="lrc">
<div class="tip" v-show="lrcMouseStatus"> <div class="tip" v-show="lrcMouseStatus">
<n-text>点击选中的歌词以调整播放进度</n-text> <n-text>{{ $t("other.lrcClicks") }}</n-text>
</div> </div>
</Transition> </Transition>
<div class="left"> <div class="left">
@@ -81,7 +78,7 @@
<span>{{ <span>{{
music.getPlaySongData music.getPlaySongData
? music.getPlaySongData.name ? music.getPlaySongData.name
: "暂无歌曲" : $t("other.noSong")
}}</span> }}</span>
<span <span
v-if="music.getPlaySongData && music.getPlaySongData.alia" v-if="music.getPlaySongData && music.getPlaySongData.alia"
@@ -118,9 +115,9 @@
> >
<n-icon <n-icon
v-if="music.getPlaySongTransl" v-if="music.getPlaySongTransl"
:class="setting.getShowTransl ? 'open' : ''" :class="setting.showTransl ? 'open' : ''"
:component="GTranslateFilled" :component="GTranslateFilled"
@click="setting.setShowTransl(!setting.getShowTransl)" @click="setting.setShowTransl(!setting.showTransl)"
/> />
<n-icon <n-icon
class="open" class="open"
@@ -132,9 +129,10 @@
</Transition> </Transition>
</div> </div>
</div> </div>
<div class="canvas"> <!-- 音乐频谱 -->
<canvas v-if="setting.musicFrequency" class="avBars" ref="avBars" /> <!-- <Spectrum v-if="setting.musicFrequency" /> -->
</div> <!-- 歌词设置 -->
<LyricSetting ref="LyricSettingRef" />
</div> </div>
</Transition> </Transition>
</template> </template>
@@ -148,28 +146,31 @@ import {
FullscreenExitRound, FullscreenExitRound,
SettingsRound, SettingsRound,
} from "@vicons/material"; } from "@vicons/material";
import { musicStore, settingStore } from "@/store"; import { musicStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import MusicFrequency from "@/utils/MusicFrequency.js"; import { setSeek } from "@/utils/Player";
import PlayerRecord from "./PlayerRecord.vue"; import PlayerRecord from "./PlayerRecord.vue";
import PlayerCover from "./PlayerCover.vue"; import PlayerCover from "./PlayerCover.vue";
import RollingLyrics from "./RollingLyrics.vue"; import RollingLyrics from "./RollingLyrics.vue";
// import Spectrum from "./Spectrum.vue";
import LyricSetting from "@/components/DataModal/LyricSetting.vue";
import screenfull from "screenfull"; import screenfull from "screenfull";
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const site = siteStore();
const setting = settingStore(); const setting = settingStore();
// 工具栏显隐 // 工具栏显隐
const menuShow = ref(false); const menuShow = ref(false);
// 音乐频谱 // 歌词设置弹窗
const avBars = ref(null); const LyricSettingRef = ref(null);
const musicFrequency = ref(null);
// 歌词文本点击事件 // 歌词文本点击事件
const lrcTextClick = (time) => { const lrcTextClick = (time) => {
if ($player) $player.currentTime = time; if (typeof $player !== "undefined") setSeek($player, time);
music.setPlayState(true);
lrcMouseStatus.value = false; lrcMouseStatus.value = false;
}; };
@@ -221,7 +222,7 @@ const lyricsScroll = (index) => {
const scrollDistance = const scrollDistance =
el.offsetTop - el.offsetTop -
container.offsetTop - container.offsetTop -
(type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 100); (type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 80);
container.scrollTo({ container.scrollTo({
top: scrollDistance, top: scrollDistance,
behavior: "smooth", behavior: "smooth",
@@ -229,21 +230,22 @@ const lyricsScroll = (index) => {
} }
}; };
onMounted(() => { // 改变 PWA 应用标题栏颜色
nextTick(() => { const changePwaColor = () => {
if (setting.musicFrequency) { const themeColorMeta = document.querySelector('meta[name="theme-color"]');
$player.crossOrigin = "anonymous"; if (music.showBigPlayer) {
musicFrequency.value = new MusicFrequency( themeColorMeta.setAttribute("content", site.songPicColor);
avBars.value, } else {
$player, if (setting.getSiteTheme === "light") {
null, themeColorMeta.setAttribute("content", "#ffffff");
50, } else if (setting.getSiteTheme === "dark") {
null, themeColorMeta.setAttribute("content", "#18181c");
null,
5
);
musicFrequency.value.drawSpectrum();
} }
}
};
onMounted(() => {
nextTick().then(() => {
// 滚动歌词 // 滚动歌词
lyricsScroll(music.getPlaySongLyricIndex); lyricsScroll(music.getPlaySongLyricIndex);
}); });
@@ -257,11 +259,12 @@ onBeforeUnmount(() => {
watch( watch(
() => music.showBigPlayer, () => music.showBigPlayer,
(val) => { (val) => {
changePwaColor();
if (val) { if (val) {
console.log("开启播放器", music.getPlaySongLyricIndex); console.log("开启播放器", music.getPlaySongLyricIndex);
nextTick(() => { nextTick().then(() => {
lyricsScroll(music.getPlaySongLyricIndex);
music.showPlayList = false; music.showPlayList = false;
lyricsScroll(music.getPlaySongLyricIndex);
}); });
} }
} }
@@ -272,6 +275,12 @@ watch(
() => music.getPlaySongLyricIndex, () => music.getPlaySongLyricIndex,
(val) => lyricsScroll(val) (val) => lyricsScroll(val)
); );
// 监听主题色改变
watch(
() => site.songPicColor,
() => changePwaColor()
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -324,10 +333,13 @@ watch(
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #00000060; background-color: #00000030;
-webkit-backdrop-filter: blur(80px); -webkit-backdrop-filter: blur(80px);
backdrop-filter: blur(80px); backdrop-filter: blur(80px);
z-index: -1; z-index: -1;
&.blur {
background-color: #00000060;
}
} }
.icon-menu { .icon-menu {
padding: 20px; padding: 20px;
@@ -378,38 +390,6 @@ watch(
} }
} }
} }
/*
.close,
.screenfull,
.setting {
position: absolute;
top: 24px;
right: 24px;
opacity: 0.3;
color: #fff;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
z-index: 2;
&:hover {
background-color: #ffffff20;
transform: scale(1.05);
opacity: 1;
}
&:active {
transform: scale(1);
}
}
.screenfull {
right: 80px;
padding: 2px;
@media (max-width: 768px) {
display: none;
}
}
.setting {
left: 24px;
}*/
.all { .all {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -422,6 +402,12 @@ watch(
.left { .left {
padding-right: 0; padding-right: 0;
transform: translateX(25vh); 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) { @media (max-width: 768px) {
.left { .left {
@@ -494,7 +480,7 @@ watch(
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
.data { .data {
padding: 0 20px; padding: 0 3vh;
margin-bottom: 8px; margin-bottom: 8px;
.name { .name {
font-size: 3vh; font-size: 3vh;
@@ -523,7 +509,7 @@ watch(
} }
.menu { .menu {
opacity: 0; opacity: 0;
padding: 0 20px; padding: 0 3vh;
margin-top: 20px; margin-top: 20px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -5,7 +5,7 @@
:style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }" :style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }"
v-if=" v-if="
remainingPoint <= 2 && remainingPoint <= 2 &&
totalDuration > 3 && totalDuration > 1 &&
music.getPlaySongLyric.lrc[0] music.getPlaySongLyric.lrc[0]
" "
> >

View File

@@ -11,13 +11,25 @@
" "
alt="cover" 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>
<div class="control"> <div class="control">
<div class="data"> <div class="data">
<div class="desc"> <div class="desc">
<span class="name text-hidden"> <span class="name text-hidden">
{{ {{
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲" music.getPlaySongData
? music.getPlaySongData.name
: $t("other.noSong")
}} }}
</span> </span>
<div v-if="music.getPlaySongData" class="message"> <div v-if="music.getPlaySongData" class="message">
@@ -61,11 +73,14 @@
</div> </div>
<div class="time"> <div class="time">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span> <span>{{ music.getPlaySongTime.songTimePlayed }}</span>
<n-slider <vue-slider
v-model:value="music.getPlaySongTime.barMoveDistance" v-model="music.getPlaySongTime.barMoveDistance"
class="progress" @drag-start="music.setPlayState(false)"
:step="0.01" @drag-end="sliderDragEnd"
@update:value="songTimeSliderUpdate" @click.stop="
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
"
:tooltip="'none'"
/> />
<span>{{ music.getPlaySongTime.songTimeDuration }}</span> <span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div> </div>
@@ -98,6 +113,7 @@
v-else v-else
class="dislike" class="dislike"
size="20" size="20"
:style="!user.userLogin ? 'opacity: 0.2;pointer-events: none;' : null"
:component="ThumbDownRound" :component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)" @click="music.setFmDislike(music.getPersonalFmData.id)"
/> />
@@ -143,16 +159,25 @@ import {
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next"; import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { musicStore, userStore } from "@/store"; import { musicStore, userStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { setSeek } from "@/utils/Player";
import AllArtists from "@/components/DataList/AllArtists.vue"; import AllArtists from "@/components/DataList/AllArtists.vue";
import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/default.css";
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const user = userStore(); const user = userStore();
// 歌曲进度条更新 // 歌曲进度条更新
const sliderDragEnd = () => {
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
music.setPlayState(true);
};
const songTimeSliderUpdate = (val) => { const songTimeSliderUpdate = (val) => {
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration) if (typeof $player !== "undefined" && music.getPlaySongTime?.duration) {
$player.currentTime = (music.getPlaySongTime.duration / 100) * val; const currentTime = (music.getPlaySongTime.duration / 100) * val;
setSeek($player, currentTime);
}
}; };
// 页面跳转 // 页面跳转
@@ -168,11 +193,11 @@ const routerJump = (url, query) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.cover { .cover {
.pic { .pic {
position: relative;
width: 50vh; width: 50vh;
height: 50vh; height: 50vh;
border-radius: 8px; z-index: 1;
overflow: hidden; // overflow: hidden;
box-shadow: 0 0 40px 14px rgb(0 0 0 / 20%);
@media (max-width: 1200px) { @media (max-width: 1200px) {
width: 44vh; width: 44vh;
height: 44vh; height: 44vh;
@@ -184,6 +209,19 @@ const routerJump = (url, query) => {
.album { .album {
width: 100%; width: 100%;
height: 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 { .control {
@@ -268,13 +306,26 @@ const routerJump = (url, query) => {
span { span {
opacity: 0.8; opacity: 0.8;
} }
.progress { .vue-slider {
margin: 0 12px; margin: 0 10px;
--n-handle-size: 12px; width: 100% !important;
--n-fill-color: #fff; transform: translateY(-1px);
--n-fill-color-hover: #fff; cursor: pointer;
--n-rail-color: #ffffff20; :deep(.vue-slider-rail) {
--n-rail-color-hover: #ffffff30; 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 { .buttons {

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<!-- 歌词滚动 --> <!-- 滚动歌词 -->
<div <div
v-if="music.getPlaySongLyric.lrc[0]" v-if="music.getPlaySongLyric.lrc[0]"
:class="[ :class="[
@@ -18,26 +18,31 @@
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? 'lrc-1' : 'yrc-1' !music.getPlaySongLyric.hasYrc || !setting.showYrc ? 'lrc-1' : 'yrc-1'
" "
> >
<CountDown :style="{ fontSize: setting.lyricsFontSize + 'vh' }" /> <CountDown
v-if="setting.countDownShow"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
/>
</div> </div>
<!-- 普通歌词 -->
<template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc"> <template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc">
<div <div
class="lrc"
v-for="(item, index) in music.getPlaySongLyric.lrc" v-for="(item, index) in music.getPlaySongLyric.lrc"
:class="music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'" :class="{
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }" on: music.getPlaySongLyricIndex == index,
:key="item" blur: setting.lyricsBlur,
:id="'lrc' + index" }"
@click="lrcTextClick(item.time)"
>
<div
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
:style="{ :style="{
marginBottom: setting.lyricsFontSize - 1.6 + 'vh',
transformOrigin: transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null, setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)` ? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null, : 'none',
}" }"
:key="item"
:id="'lrc' + index"
@click="lrcTextClick(item.time)"
> >
<span <span
class="lyric" class="lyric"
@@ -46,72 +51,94 @@
{{ item.content }} {{ item.content }}
</span> </span>
<span <span
v-show=" v-if="
music.getPlaySongLyric.hasTran && music.getPlaySongLyric.hasLrcTran && setting.showTransl && item.tran
setting.getShowTransl &&
item.tran
" "
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }" :style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy" class="lyric-fy"
> >
{{ item.tran }}</span {{ item.tran }}</span
> >
</div> <span
v-if="
music.getPlaySongLyric.hasLrcRoma && setting.showRoma && item.roma
"
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
class="lyric-roma"
>
{{ item.roma }}</span
>
</div> </div>
</template> </template>
<!-- 逐字歌词 -->
<template v-else> <template v-else>
<div <div
class="yrc"
v-for="(item, index) in music.getPlaySongLyric.yrc" v-for="(item, index) in music.getPlaySongLyric.yrc"
:class="music.getPlaySongLyricIndex == index ? 'yrc on' : 'yrc'" :class="{
:key="item" on: music.getPlaySongLyricIndex === index,
:id="'yrc' + index" blur: setting.lyricsBlur,
@click="lrcTextClick(item.time)" }"
>
<div
:class="setting.lyricsBlur ? 'yrc-text blur' : 'yrc-text'"
:style="{ :style="{
marginBottom: setting.lyricsFontSize - 1.6 + 'vh',
transformOrigin: transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null, setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)` ? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null, : 'none',
}" }"
:key="item"
:id="'yrc' + index"
@click="lrcTextClick(item.time)"
> >
<div class="lyric" :style="{ fontSize: setting.lyricsFontSize + 'vh' }">
<div <div
class="lyric" class="text"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
<span
v-for="(v, i) in item.content" v-for="(v, i) in item.content"
:class="{
fill:
music.getPlaySongLyricIndex === index &&
music.getPlaySongTime.currentTime + 0.2 >= v.time,
transform: setting.showYrcTransform,
}"
:key="i" :key="i"
:style="{ :style="{
'--dur': v.duration - 0.15 + 's', '--dur': `${Math.max(v.duration - 0.2, 0.1)}s`,
}" }"
:class="
music.getPlaySongLyricIndex == index &&
music.getPlaySongTime.currentTime + 0.15 >= v.time
? 'text fill'
: 'text'
"
> >
{{ v.content }} <span class="word" v-html="v.content.replace(/ /g, '&nbsp;')" />
</span> <span
class="filler"
:class="{
long: i === item.content.length - 1 && v.duration > 1,
animation: setting.showYrcAnimation,
paused: !music.playState,
}"
v-html="v.content.replace(/ /g, '&nbsp;')"
/>
</div>
</div> </div>
<span <span
v-show=" v-if="
music.getPlaySongLyric.hasTran && music.getPlaySongLyric.hasYrcTran && setting.showTransl && item.tran
setting.getShowTransl &&
item.tran
" "
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }" :style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy" class="lyric-fy"
> >
{{ item.tran }}</span {{ item.tran }}
</span>
<span
v-if="
music.getPlaySongLyric.hasYrcRoma && setting.showRoma && item.roma
"
:style="{ fontSize: setting.lyricsFontSize - 1.5 + 'vh' }"
class="lyric-roma"
> >
</div> {{ item.roma }}
</span>
</div> </div>
</template> </template>
<div class="placeholder"></div> <div class="placeholder" />
</div> </div>
</template> </template>
@@ -142,11 +169,133 @@ const lrcTextClick = (time) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.lrc-all { .lrc-all {
margin-right: 20%; display: flex;
flex-direction: column;
// margin-right: 20%;
scrollbar-width: none; scrollbar-width: none;
// max-width: 460px;
max-width: 52vh; max-width: 52vh;
overflow: auto; overflow: auto;
padding: 0 10px;
.placeholder {
width: 100%;
&:nth-of-type(1) {
min-height: 50%;
display: flex;
align-items: flex-end;
padding: 0 0 0.8vh 3vh;
}
&:nth-last-of-type(1) {
min-height: 80%;
}
}
.lrc,
.yrc {
display: flex;
flex-direction: column;
position: relative;
padding: 1.8vh 4vh 1.8vh 3vh;
box-sizing: border-box;
border-radius: 8px;
opacity: 0.3;
transform: scale(0.9);
transform-origin: left bottom;
transition: transform 0.3s ease, opacity 0.3s ease;
cursor: pointer;
.lyric {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-weight: bold;
.text {
position: relative;
transition: transform 0.3s ease;
.filler {
transition: color 0.3s ease, opacity 0.3s ease;
position: absolute;
top: 0;
left: 0;
opacity: 0;
&.animation {
color: white;
background: linear-gradient(to right, white, white) no-repeat 0 0;
-webkit-text-fill-color: transparent;
background-clip: text;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
&.fill {
&.transform {
transform: translateY(-1.5px);
}
.word {
opacity: 0.3;
}
.filler {
opacity: 1 !important;
&.long {
animation: shine calc var(--dur) ease-in-out;
}
&.animation {
background-size: 100% 100%;
animation: progress var(--dur) linear forwards;
}
&.paused {
animation-play-state: paused;
-webkit-animation-play-state: paused;
}
}
}
}
}
.lyric-fy,
.lyric-roma {
margin-top: 4px;
opacity: 0.6;
}
&.on {
opacity: 1;
transform: scale(1);
.lyric {
.text {
.word,
.filler {
opacity: 0.3;
}
}
}
}
&::before {
@media (min-width: 768px) {
content: "";
display: block;
position: absolute;
left: 0px;
top: 0;
width: 100%;
height: 100%;
border-radius: 8px;
background-color: #ffffff20;
opacity: 0;
z-index: 0;
transform: scale(1.05);
transition: transform 0.3s ease, opacity 0.3s ease;
pointer-events: none;
}
}
&:hover {
opacity: 1;
&::before {
transform: scale(1);
opacity: 1;
}
}
&:active {
&::before {
transform: scale(0.95);
}
}
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
@@ -154,7 +303,7 @@ const lrcTextClick = (time) => {
height: 80vh; height: 80vh;
} }
&.record { &.record {
height: 60vh; height: 70vh;
} }
&.center { &.center {
mask: linear-gradient( mask: linear-gradient(
@@ -197,106 +346,39 @@ const lrcTextClick = (time) => {
); );
.placeholder { .placeholder {
&:nth-of-type(1) { &:nth-of-type(1) {
height: 16%; min-height: 16%;
}
}
}
&:hover {
.lrc-text {
&.blur {
filter: blur(0) !important;
} }
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
height: 70vh; height: 70vh;
margin-right: 0; margin-right: 0;
padding: 0;
} }
.placeholder { }
width: 100%; @keyframes progress {
&:nth-of-type(1) { 0% {
height: 50%; background-size: 0 100%;
display: flex;
align-items: flex-end;
padding: 0 0 0.8vh 3vh;
} }
&:nth-last-of-type(1) { 100% {
height: 80%;
}
}
.lrc,
.yrc {
opacity: 0.2;
transition: all 0.3s;
margin-bottom: 0.8vh;
padding: 1.8vh 4vh 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: left bottom;
cursor: pointer;
.lrc-text,
.yrc-text {
display: flex;
flex-direction: column;
transition: all 0.35s ease-in-out;
transform: scale(0.95);
transform-origin: left bottom;
.lyric {
font-weight: bold;
transition: all 0.3s;
.text {
transition: all var(--dur);
color: #ffffff66;
&.fill {
text-shadow: 0px 0px 30px #ffffff40;
background-image: linear-gradient(to right, #fff 0%, #fff 0%);
background-repeat: no-repeat;
background-size: 0% 100%;
background-clip: text;
-webkit-background-clip: text;
color: #ffffff66;
animation: toRight var(--dur) forwards linear;
}
@keyframes toRight {
to {
background-size: 100% 100%; background-size: 100% 100%;
} }
}
@keyframes shine {
0% {
text-shadow: 0 0 0.1em rgba(255, 255, 255, 0);
} }
25% {
text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.5);
} }
50% {
text-shadow: 0 0 0.5em rgba(255, 255, 255, 0.7);
} }
.lyric-fy { 75% {
margin-top: 4px; text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.5);
transition: all 0.3s;
opacity: 0.6;
} }
} 100% {
&.on { text-shadow: 0 0 0.1em rgba(255, 255, 255, 0);
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
text-shadow: 0px 0px 30px #ffffff40;
}
}
.yrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
}
}
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
opacity: 1;
}
}
&:active {
transform: scale(0.95);
}
}
.yrc {
opacity: 0.6;
} }
} }
</style> </style>

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,20 +1,33 @@
<template> <template>
<Transition name="show">
<n-card <n-card
:class=" v-show="music.getPlaylists[0] && music.showPlayBar"
music.getPlaylists[0] && music.showPlayBar ? 'player show' : 'player' class="player"
"
content-style="padding: 0" content-style="padding: 0"
> >
<div class="slider"> <div class="slider">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span> <span>{{ music.getPlaySongTime.songTimePlayed }}</span>
<n-slider <vue-slider
v-model:value="music.getPlaySongTime.barMoveDistance" v-model="music.getPlaySongTime.barMoveDistance"
class="progress" @drag-start="music.setPlayState(false)"
:step="0.01" @drag-end="sliderDragEnd"
:tooltip="false" @click.stop="
@update:value="songTimeSliderUpdate" songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
@click.stop "
/> :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> <span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div> </div>
<div class="all"> <div class="all">
@@ -39,14 +52,20 @@
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)" @click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
> >
{{ {{
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲" music.getPlaySongData
? music.getPlaySongData.name
: $t("other.noSong")
}} }}
</div> </div>
<!-- 显示歌手或歌词 -->
<div class="artisrOrLrc" v-if="music.getPlaySongData"> <div class="artisrOrLrc" v-if="music.getPlaySongData">
<Transition name="fade" mode="out-in">
<template v-if="setting.bottomLyricShow"> <template v-if="setting.bottomLyricShow">
<Transition mode="out-in"> <Transition name="fade" mode="out-in">
<AllArtists <AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric.lrc[0]" v-if="
!music.getPlayState || !music.getPlaySongLyric.lrc[0]
"
class="text-hidden" class="text-hidden"
:artistsData="music.getPlaySongData.artist" :artistsData="music.getPlaySongData.artist"
/> />
@@ -93,13 +112,13 @@
:artistsData="music.getPlaySongData.artist" :artistsData="music.getPlaySongData.artist"
/> />
</template> </template>
</Transition>
</div> </div>
</div> </div>
</div> </div>
<div class="control"> <div class="control">
<n-icon <n-icon
v-if="!music.getPersonalFmMode" v-if="!music.getPersonalFmMode"
title="上一曲"
class="prev" class="prev"
size="30" size="30"
:component="SkipPreviousRound" :component="SkipPreviousRound"
@@ -115,7 +134,6 @@
<div class="play-state"> <div class="play-state">
<n-icon <n-icon
size="46" size="46"
:title="music.getPlayState ? '暂停' : '播放'"
:component=" :component="
music.getPlayState ? PauseCircleFilled : PlayCircleFilled music.getPlayState ? PauseCircleFilled : PlayCircleFilled
" "
@@ -130,6 +148,8 @@
/> />
</div> </div>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'"> <div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<n-popover trigger="hover" :keep-alive-on-hover="false">
<template #trigger>
<div class="like" v-if="music.getPlaySongData"> <div class="like" v-if="music.getPlaySongData">
<n-icon <n-icon
class="like-icon" class="like-icon"
@@ -146,6 +166,15 @@
" "
/> />
</div> </div>
</template>
{{
music.getSongIsLike(music.getPlaySongData.id)
? $t("menu.cancelCollection")
: $t("menu.collection")
}}
</n-popover>
<n-popover trigger="hover" :keep-alive-on-hover="false">
<template #trigger>
<div class="add-playlist"> <div class="add-playlist">
<n-icon <n-icon
class="add-icon" class="add-icon"
@@ -156,6 +185,9 @@
" "
/> />
</div> </div>
</template>
{{ $t("menu.add") }}
</n-popover>
<div class="pattern"> <div class="pattern">
<n-icon <n-icon
:component=" :component="
@@ -168,6 +200,8 @@
@click="music.setPlaySongMode()" @click="music.setPlaySongMode()"
/> />
</div> </div>
<n-popover trigger="hover" :keep-alive-on-hover="false">
<template #trigger>
<div :class="music.showPlayList ? 'playlist open' : 'playlist'"> <div :class="music.showPlayList ? 'playlist open' : 'playlist'">
<n-icon <n-icon
size="30" size="30"
@@ -175,6 +209,9 @@
@click.stop="music.showPlayList = !music.showPlayList" @click.stop="music.showPlayList = !music.showPlayList"
/> />
</div> </div>
</template>
{{ $t("general.name.playlists") }}
</n-popover>
<div class="volume"> <div class="volume">
<n-icon <n-icon
size="28" size="28"
@@ -201,18 +238,8 @@
</div> </div>
</div> </div>
</div> </div>
<audio
ref="player"
:autoplay="music.getPlayState"
@timeupdate="songUpdate"
@play="songPlay"
@pause="songPause"
@canplay="songCanplay"
@error="songError"
@ended="music.setPlaySongIndex('next')"
:src="music.getPlaySongLink"
></audio>
</n-card> </n-card>
</Transition>
<!-- 播放列表 --> <!-- 播放列表 -->
<PlayListDrawer ref="PlayListDrawerRef" /> <PlayListDrawer ref="PlayListDrawerRef" />
<!-- 添加到歌单 --> <!-- 添加到歌单 -->
@@ -231,7 +258,6 @@ import {
import { NIcon } from "naive-ui"; import { NIcon } from "naive-ui";
import { import {
KeyboardArrowUpFilled, KeyboardArrowUpFilled,
MusicNoteFilled,
PlayCircleFilled, PlayCircleFilled,
PauseCircleFilled, PauseCircleFilled,
SkipNextRound, SkipNextRound,
@@ -248,24 +274,34 @@ import {
} from "@vicons/material"; } from "@vicons/material";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next"; import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { musicStore, settingStore } from "@/store"; import { musicStore, settingStore, siteStore } from "@/store";
import {
createSound,
setVolume,
setSeek,
fadePlayOrPause,
} from "@/utils/Player";
import { getSongPlayingTime } from "@/utils/timeTools";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue"; import { debounce } from "throttle-debounce";
import PlayListDrawer from "@/components/DataModel/PlayListDrawer.vue"; import { useI18n } from "vue-i18n";
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 AllArtists from "@/components/DataList/AllArtists.vue";
import ColorThief from "colorthief";
import BigPlayer from "./BigPlayer.vue"; import BigPlayer from "./BigPlayer.vue";
import debounce from "@/utils/debounce"; import "vue-slider-component/theme/default.css";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const setting = settingStore(); const setting = settingStore();
const music = musicStore(); const music = musicStore();
const site = siteStore();
const { persistData } = storeToRefs(music); const { persistData } = storeToRefs(music);
const addPlayListRef = ref(null); const addPlayListRef = ref(null);
const PlayListDrawerRef = ref(null); const PlayListDrawerRef = ref(null);
// 重试次数
const testNumber = ref(0);
// UNM 是否存在 // UNM 是否存在
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false; const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
@@ -292,16 +328,18 @@ const getPlaySongData = (data, level = setting.songLevel) => {
if (res.success) { if (res.success) {
console.log("当前歌曲可用"); console.log("当前歌曲可用");
if (!pc && (fee === 1 || fee === 4)) if (!pc && (fee === 1 || fee === 4))
$message.info("当前歌曲为 VIP 专享,仅可试听"); $message.info(t("general.message.vipTip"));
// 获取音乐地址 // 获取音乐地址
getMusicUrl(id, level).then((res) => { 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 { } else {
if (useUnmServerHas && setting.useUnmServer) { if (useUnmServerHas && setting.useUnmServer) {
getMusicNumUrlData(id); getMusicNumUrlData(id);
} else { } else {
$message.warning("当前歌曲播放失败,跳至下一首"); $message.warning(t("general.message.playError"));
music.setPlaySongIndex("next"); music.setPlaySongIndex("next");
} }
} }
@@ -312,9 +350,9 @@ const getPlaySongData = (data, level = setting.songLevel) => {
music.setPlaySongLyric(res); music.setPlaySongLyric(res);
}); });
} catch (err) { } catch (err) {
if (music.getPlaylists[0] && music.getPlayState) {
console.log("当前歌曲所有音源匹配失败:" + err); console.log("当前歌曲所有音源匹配失败:" + err);
if (music.getPlayState && $player) { $message.warning(t("general.message.playError"));
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
music.setPlaySongIndex("next"); music.setPlaySongIndex("next");
} }
} }
@@ -326,167 +364,26 @@ const getMusicNumUrlData = (id) => {
.then((res) => { .then((res) => {
if (res.code === 200) { if (res.code === 200) {
console.log("替换成功:" + res.data.url.replace(/^http:/, "")); console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
music.setPlaySongLink(res.data.url.replace(/^http:/, "")); player.value = createSound(res.data.url.replace(/^http:/, ""));
} }
}) })
.catch((err) => { .catch((err) => {
console.log("解灰失败:" + err); console.log("解灰失败:" + err);
$message.warning("当前歌曲解灰失败,跳至下一首"); $message.warning(t("general.message.playError"));
music.setPlaySongIndex("next"); music.setPlaySongIndex("next");
}); });
}; };
// 歌曲进度更新事件
const songUpdate = (e) => {
const currentTime = e.target.currentTime;
const duration = e.target.duration;
music.setPlaySongTime({ currentTime, duration });
};
// 歌曲缓冲完毕
const songCanplay = () => {
console.log("缓冲完毕", music.getPlayState);
if (music.getPlayState && $player) {
music.setPlayState(true);
songInOrOut("play");
}
};
// 歌曲开始播放
const songPlay = () => {
testNumber.value = 0;
if (!music.getPlaySongData) {
$message.error("音乐数据获取失败");
return false;
}
music.setPlayState(true);
// 兼容 mediaSession
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: music.getPlaySongData.name,
artist: music.getPlaySongData.artist[0].name,
album: music.getPlaySongData.album.name,
artwork: [
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=96y96",
sizes: "96x96",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=128y128",
sizes: "128x128",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=192y192",
sizes: "192x192",
},
],
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
music.setPlaySongIndex("next");
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
music.setPlaySongIndex("prev");
});
}
$message.info(
music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name,
{
icon: () =>
h(NIcon, null, {
default: () => h(MusicNoteFilled),
}),
}
);
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 更改页面标题
// $setSiteTitle(
// music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name
// );
window.document.title =
music.getPlaySongData.name +
" - " +
music.getPlaySongData.artist[0].name +
" - SPlayer";
};
// 音乐渐入渐出
const isFading = ref(false);
const songInOrOut = (type) => {
if (isFading.value) {
return;
}
isFading.value = true;
if (type === "play") {
let volume = 0;
$player.play();
const interval = setInterval(() => {
// 如果音量已经到达当前音量,则停止渐入
if (volume >= persistData.value.playVolume) {
clearInterval(interval);
isFading.value = false;
return;
}
// 增加音量
volume += 0.1;
if (volume > persistData.value.playVolume) {
volume = persistData.value.playVolume;
}
$player.volume = volume;
}, 30);
} else if (type === "pause") {
let volume = persistData.value.playVolume;
const interval = setInterval(() => {
// 如果音量已经到达最小值,则停止渐出
if (volume <= 0) {
clearInterval(interval);
$player.pause();
isFading.value = false;
return;
}
// 减小音量
volume -= 0.1;
if (volume < 0) {
volume = 0;
}
$player.volume = volume;
}, 30);
}
};
// 歌曲暂停
const songPause = () => {
console.log("音乐暂停");
if (!$player.ended) music.setPlayState(false);
// 更改页面标题
// window.document.title = "SPlayer";
$setSiteTitle();
};
// 歌曲进度条更新 // 歌曲进度条更新
const songTimeSliderUpdate = (val) => { const sliderDragEnd = () => {
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration) songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
$player.currentTime = (music.getPlaySongTime.duration / 100) * val; music.setPlayState(true);
}; };
const songTimeSliderUpdate = (val) => {
// 歌曲播放失败事件 if (player.value && music.getPlaySongTime?.duration) {
const songError = () => { const currentTime = (music.getPlaySongTime.duration / 100) * val;
console.error("歌曲播放失败"); setSeek(player.value, currentTime);
$message.error("歌曲播放失败");
if (testNumber.value < 4) {
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData);
testNumber.value++;
} else {
$message.error("歌曲重试次数过多,请刷新后重试");
} }
if (music.getPlayState) songInOrOut("play");
}; };
// 静音事件 // 静音事件
@@ -499,27 +396,55 @@ const volumeMute = () => {
} }
}; };
onMounted(() => { // 歌曲更换事件
// 获取音乐数据 const songChange = debounce(500, (val) => {
if (music.getPlaylists[0] && music.getPlaySongData) if (val === undefined) {
getPlaySongData(music.getPlaySongData); window.document.title =
// 挂载播放器 sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
window.$player = player.value; }
// 恢复上次播放进度 // 加载数据
if (music.getPlaySongTime && music.getPlaySongTime.currentTime) { getPlaySongData(val);
$player.currentTime = music.getPlaySongTime.currentTime; getPicColor(val?.album.picUrl);
});
// 获取封面图主色
const getPicColor = (url) => {
if (!url) return false;
const imgUrl = url.replace(/^http:/, "https:") + "?param=50y50";
const img = new Image();
fetch(imgUrl)
.then((res) => res.blob())
.then((blob) => {
img.src = URL.createObjectURL(blob);
img.addEventListener("load", async () => {
const colorThief = new ColorThief();
const color = await colorThief.getColor(img);
console.log(`当前封面主色rgb(${color.join(",")})`);
site.songPicColor = `rgb(${color.join(",")})`;
});
})
.catch((err) => {
console.error("图像处理出错:" + err);
site.songPicColor = "rgb(128,128,128)";
});
};
onMounted(() => {
// 挂载方法
window.$getPlaySongData = getPlaySongData;
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData) {
getPlaySongData(music.getPlaySongData);
getPicColor(music.getPlaySongData.album.picUrl);
} }
// 设置音量
if ($player) $player.volume = persistData.value.playVolume;
}); });
// 监听当前音乐数据变化 // 监听当前音乐数据变化
watch( watch(
() => music.getPlaySongData, () => music.getPlaySongData,
(val) => { (val) => {
debounce(() => { music.setPlaySongTime({ currentTime: 0, duration: 0 });
getPlaySongData(val); songChange(val);
}, 500);
} }
); );
@@ -527,7 +452,7 @@ watch(
watch( watch(
() => persistData.value.playVolume, () => persistData.value.playVolume,
(val) => { (val) => {
if ($player) $player.volume = val; if (player.value) setVolume(player.value, val);
} }
); );
@@ -535,40 +460,44 @@ watch(
watch( watch(
() => music.getPlayState, () => music.getPlayState,
(val) => { (val) => {
nextTick(() => { nextTick().then(() => {
// $player ? (val ? $player.play() : $player.pause()) : null; if (player.value && !music.isLoadingSong) {
if ($player) { fadePlayOrPause(
// val ? $player.play() : $player.pause(); player.value,
val ? songInOrOut("play") : songInOrOut("pause"); val ? "play" : "pause",
} else { persistData.value.playVolume
$message.error("播放器初始化失败,请重试"); );
} }
}); });
} }
); );
// 监听歌曲进度更新
// watch(
// () => music.getPlaySongTime,
// (val) => {
// if (val.barMoveDistance) {
// songTimeVal.value = val.barMoveDistance;
// }
// }
// );
</script> </script>
<style lang="scss" scoped> <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);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.player { .player {
height: 70px; height: 70px;
position: fixed; position: fixed;
bottom: -90px;
left: 0;
transition: all 0.3s;
z-index: 2004;
&.show {
bottom: 0; bottom: 0;
} left: 0;
z-index: 2;
.slider { .slider {
position: absolute; position: absolute;
top: -12px; top: -12px;
@@ -576,19 +505,15 @@ watch(
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
@media (max-width: 640px) { @media (max-width: 640px) {
top: -6px; top: -8px;
> { > {
span { span {
display: none; display: none;
} }
} }
} }
.progress {
--n-handle-size: 12px;
--n-rail-height: 3px;
}
> { > {
span { span {
font-size: 12px; font-size: 12px;
@@ -600,6 +525,33 @@ watch(
margin: 0 2px; 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 { .all {
@@ -659,21 +611,12 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
.artisrOrLrc { .artisrOrLrc {
font-size: 12px; font-size: 12px;
margin-top: 2px; margin-top: 2px;
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
} }
} }
} }
@@ -685,7 +628,7 @@ watch(
.next, .next,
.prev, .prev,
.dislike { .dislike {
color: $mainColor; color: var(--main-color);
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
border-radius: 50%; border-radius: 50%;
@@ -693,7 +636,7 @@ watch(
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: var(--n-color-embedded); color: var(--n-color-embedded);
background-color: $mainColor; background-color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.9); transform: scale(0.9);
@@ -705,7 +648,7 @@ watch(
.play-state { .play-state {
width: 46px; width: 46px;
height: 46px; height: 46px;
color: $mainColor; color: var(--main-color);
margin: 0 12px; margin: 0 12px;
cursor: pointer; cursor: pointer;
transform: scale(1); transform: scale(1);
@@ -725,7 +668,7 @@ watch(
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
color: $mainColor; color: var(--main-color);
@media (max-width: 640px) { @media (max-width: 640px) {
.volume, .volume,
.like, .like,
@@ -745,10 +688,12 @@ watch(
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@media (min-width: 640px) {
&:hover { &:hover {
background-color: $mainColor; background-color: var(--main-color);
color: var(--n-color-embedded); color: var(--n-color-embedded);
} }
}
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
@@ -782,7 +727,7 @@ watch(
justify-content: center; justify-content: center;
&.open { &.open {
.n-icon { .n-icon {
background-color: $mainColor; background-color: var(--main-color);
color: var(--n-color-embedded); color: var(--n-color-embedded);
} }
} }

View File

@@ -43,51 +43,52 @@ import {
useNotification, useNotification,
} from "naive-ui"; } from "naive-ui";
import { settingStore } from "@/store"; import { settingStore } from "@/store";
import themeColorData from "./themeColor.json";
const setting = settingStore(); const setting = settingStore();
const osThemeRef = useOsTheme(); const osThemeRef = useOsTheme();
const themeOverrides = ref(null);
// 明暗切换 // 明暗切换
const theme = ref(null); const theme = ref(null);
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
const changeTheme = () => { const changeTheme = () => {
if (setting.getSiteTheme == "light") { if (setting.getSiteTheme == "light") {
theme.value = null; theme.value = null;
themeColorMeta.setAttribute("content", "#ffffff");
} else if (setting.getSiteTheme == "dark") { } else if (setting.getSiteTheme == "dark") {
theme.value = darkTheme; theme.value = darkTheme;
themeColorMeta.setAttribute("content", "#18181c");
} }
}; };
onMounted(() => { // 根据系统决定明暗切换
changeTheme(); const osThemeChange = (val) => {
});
// 监听明暗变化
watch(
() => setting.getSiteTheme,
() => {
changeTheme();
}
);
// 监听系统明暗变化
watch(
() => osThemeRef.value,
(value) => {
if (setting.themeAuto) { if (setting.themeAuto) {
value == "dark" val == "dark"
? setting.setSiteTheme("dark") ? setting.setSiteTheme("dark")
: setting.setSiteTheme("light"); : setting.setSiteTheme("light");
} }
} };
);
// 配置主题色 // 配置主题色
const themeOverrides = { const changeThemeColor = (val) => {
common: { const color = themeColorData[val];
primaryColor: "#f55e55", console.log("当前主题色:" + val, color);
primaryColorHover: "#F57B74", themeOverrides.value = {
primaryColorSuppl: "#F57B74", common: color,
primaryColorPressed: "#F64B41", };
}, setting.themeData = color;
setCssVariable("--main-color", color.primaryColor);
setCssVariable("--main-second-color", color.primaryColor + "1f");
setCssVariable("--main-boxshadow-color", color.primaryColor + "26");
setCssVariable("--main-boxshadow-hover-color", color.primaryColor + "05");
};
// 修改全局颜色
const setCssVariable = (name, value) => {
document.documentElement.style.setProperty(name, value);
// document.body.style.setProperty(name, value);
}; };
// 挂载 naive 组件的方法 // 挂载 naive 组件的方法
@@ -102,12 +103,34 @@ const NaiveProviderContent = defineComponent({
setup() { setup() {
setupNaiveTools(); setupNaiveTools();
}, },
render() { render() {},
return h("div", { });
class: {
tools: true, // 监听明暗变化
}, watch(
}); () => setting.getSiteTheme,
}, () => {
changeTheme();
}
);
// 监听系统明暗变化
watch(
() => osThemeRef.value,
(val) => {
osThemeChange(val);
}
);
// 监听主题色变化
watch(
() => setting.themeType,
(val) => changeThemeColor(val)
);
onMounted(() => {
changeTheme();
changeThemeColor(setting.themeType);
osThemeChange(osThemeRef.value);
}); });
</script> </script>

View File

@@ -0,0 +1,82 @@
{
"red": {
"name": "欢快派对",
"label": "red",
"primaryColor": "#f55e55",
"primaryColorHover": "#F57B74",
"primaryColorSuppl": "#F57B74",
"primaryColorPressed": "#F64B41"
},
"orange": {
"name": "柑橘桔梦",
"label": "orange",
"primaryColor": "#ff8c00",
"primaryColorHover": "#ffa033",
"primaryColorSuppl": "#ffa033",
"primaryColorPressed": "#ff6b00"
},
"blue": {
"name": "深海蓝梦",
"label": "blue",
"primaryColor": "#3b5998",
"primaryColorHover": "#5475a3",
"primaryColorSuppl": "#8b9dc3",
"primaryColorPressed": "#2d4373"
},
"pink": {
"name": "粉色梦幻",
"label": "pink",
"primaryColor": "#e91e63",
"primaryColorHover": "#f06292",
"primaryColorSuppl": "#f06292",
"primaryColorPressed": "#c2185b"
},
"brown": {
"name": "深棕林荫",
"label": "brown",
"primaryColor": "#795548",
"primaryColorHover": "#8d6e63",
"primaryColorSuppl": "#8d6e63",
"primaryColorPressed": "#5d4037"
},
"indigo": {
"name": "星空靛蓝",
"label": "indigo",
"primaryColor": "#3f51b5",
"primaryColorHover": "#5c6bc0",
"primaryColorSuppl": "#5c6bc0",
"primaryColorPressed": "#3949ab"
},
"green": {
"name": "生命绿洲",
"label": "green",
"primaryColor": "#2ecc71",
"primaryColorHover": "#3ddc88",
"primaryColorSuppl": "#3ddc88",
"primaryColorPressed": "#27ae60"
},
"purple": {
"name": "皇室紫梦",
"label": "purple",
"primaryColor": "#9c27b0",
"primaryColorHover": "#ba68c8",
"primaryColorSuppl": "#ba68c8",
"primaryColorPressed": "#7b1fa2"
},
"yellow": {
"name": "金色阳光",
"label": "yellow",
"primaryColor": "#FBC02D",
"primaryColorHover": "#FFD54F",
"primaryColorSuppl": "#FFD54F",
"primaryColorPressed": "#FFC107"
},
"teal": {
"name": "海洋碧绿",
"label": "teal",
"primaryColor": "#009688",
"primaryColorHover": "#26a69a",
"primaryColorSuppl": "#26a69a",
"primaryColorPressed": "#00796b"
}
}

View File

@@ -1,11 +1,12 @@
<template> <template>
<div class="searchInp"> <div class="searchInp">
<n-input <n-input
:class="inputActive ? 'input focus' : 'input'" :class="site.searchInputActive ? 'input focus' : 'input'"
:input-props="{ autoComplete: false }" :input-props="{ autoComplete: false }"
:placeholder="$t('nav.search.placeholder')"
ref="searchInpRef"
round round
clearable clearable
placeholder="搜索音乐/视频"
v-model:value="inputValue" v-model:value="inputValue"
@focus="inputFocus" @focus="inputFocus"
@keydown="inputkeydown($event)" @keydown="inputkeydown($event)"
@@ -14,7 +15,7 @@
<template #prefix> <template #prefix>
<n-icon <n-icon
size="16" size="16"
:color="inputActive ? '#f55e55' : ''" :class="site.searchInputActive ? 'active' : ''"
:component="Search" :component="Search"
/> />
</template> </template>
@@ -23,7 +24,7 @@
<n-card <n-card
class="list" class="list"
v-show=" v-show="
inputActive && site.searchInputActive &&
!inputValue && !inputValue &&
(music.getSearchHistory[0] || searchData.hot[0]) (music.getSearchHistory[0] || searchData.hot[0])
" "
@@ -36,7 +37,7 @@
> >
<div class="list-title"> <div class="list-title">
<n-icon size="16" :component="History" /> <n-icon size="16" :component="History" />
<n-text>搜索历史</n-text> <n-text>{{ $t("nav.search.history") }}</n-text>
</div> </div>
<n-space> <n-space>
<n-tag <n-tag
@@ -52,7 +53,7 @@
<n-icon size="16" :depth="3"> <n-icon size="16" :depth="3">
<DeleteFour theme="filled" /> <DeleteFour theme="filled" />
</n-icon> </n-icon>
<n-text :depth="3">删除搜索历史</n-text> <n-text :depth="3">{{ $t("nav.search.delHistory") }}</n-text>
</div> </div>
</div> </div>
<div class="hot-list" v-if="searchData.hot[0]"> <div class="hot-list" v-if="searchData.hot[0]">
@@ -60,7 +61,7 @@
<n-icon size="16"> <n-icon size="16">
<Fire theme="filled" /> <Fire theme="filled" />
</n-icon> </n-icon>
<n-text>热搜榜</n-text> <n-text>{{ $t("nav.search.hotList") }}</n-text>
</div> </div>
<div <div
class="hot-item" class="hot-item"
@@ -93,7 +94,7 @@
<CollapseTransition easing="ease-in-out"> <CollapseTransition easing="ease-in-out">
<n-card <n-card
class="list" class="list"
v-show="inputActive && inputValue && searchData.suggest" v-show="site.searchInputActive && inputValue && searchData.suggest"
content-style="padding: 0" content-style="padding: 0"
> >
<n-scrollbar> <n-scrollbar>
@@ -102,19 +103,19 @@
v-if="Object.keys(searchData.suggest).length === 0" v-if="Object.keys(searchData.suggest).length === 0"
> >
<n-icon size="16" :component="Find" /> <n-icon size="16" :component="Find" />
<span>暂无搜索结果</span> <span>{{ $t("nav.search.noSuggestions") }}</span>
</div> </div>
<div class="suggest-all" v-else> <div class="suggest-all" v-else>
<div class="loading" v-show="!searchData.suggest.order"> <div class="loading" v-show="!searchData.suggest.order">
<n-icon size="16" :component="Find" /> <n-icon size="16" :component="Find" />
<span>努力搜索中</span> <span>{{ $t("nav.search.searchTip") }}</span>
</div> </div>
<div class="suggest-item" v-if="searchData.suggest.songs"> <div class="suggest-item" v-if="searchData.suggest.songs">
<div class="type"> <div class="type">
<n-icon size="18"> <n-icon size="18">
<MusicOne theme="filled" /> <MusicOne theme="filled" />
</n-icon> </n-icon>
<span class="name">单曲</span> <span class="name">{{ $t("nav.search.songs") }}</span>
</div> </div>
<span <span
class="names" class="names"
@@ -130,7 +131,7 @@
<n-icon size="18"> <n-icon size="18">
<Voice theme="filled" /> <Voice theme="filled" />
</n-icon> </n-icon>
<span class="name">歌手</span> <span class="name">{{ $t("nav.search.artists") }}</span>
</div> </div>
<span <span
class="names" class="names"
@@ -145,7 +146,7 @@
<n-icon size="18"> <n-icon size="18">
<RecordDisc theme="filled" /> <RecordDisc theme="filled" />
</n-icon> </n-icon>
<span class="name">专辑</span> <span class="name">{{ $t("nav.search.albums") }}</span>
</div> </div>
<span <span
class="names" class="names"
@@ -161,7 +162,7 @@
<n-icon size="18"> <n-icon size="18">
<Record theme="filled" /> <Record theme="filled" />
</n-icon> </n-icon>
<span class="name">歌单</span> <span class="name">{{ $t("nav.search.playlists") }}</span>
</div> </div>
<span <span
class="names" class="names"
@@ -192,23 +193,26 @@ import {
History, History,
DeleteFour, DeleteFour,
} from "@icon-park/vue-next"; } from "@icon-park/vue-next";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { musicStore, settingStore, siteStore } from "@/store";
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue"; import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import debounce from "@/utils/debounce"; import debounce from "@/utils/debounce";
import { useRouter } from "vue-router";
import { musicStore, settingStore } from "@/store"; const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const setting = settingStore(); const setting = settingStore();
const site = siteStore();
// 输入框内容 // 输入框内容
const inputValue = ref(null); const inputValue = ref(null);
const searchInpRef = ref(null);
// 输入框激活状态
const inputActive = ref(false);
// 输入框激活事件 // 输入框激活事件
const inputFocus = () => { const inputFocus = () => {
inputActive.value = true; searchInpRef.value?.focus();
site.searchInputActive = true;
music.showPlayList = false; music.showPlayList = false;
getSearchHotData(); getSearchHotData();
}; };
@@ -274,7 +278,8 @@ const toSearch = (val, type) => {
const inputkeydown = (e) => { const inputkeydown = (e) => {
if (e.key === "Enter" && inputValue.value != null) { if (e.key === "Enter" && inputValue.value != null) {
console.log("执行搜索" + inputValue.value.trim()); console.log("执行搜索" + inputValue.value.trim());
inputActive.value = false; searchInpRef.value?.blur();
site.searchInputActive = false;
// 写入搜索历史 // 写入搜索历史
music.setSearchHistory(inputValue.value.trim()); music.setSearchHistory(inputValue.value.trim());
router.push({ router.push({
@@ -290,13 +295,13 @@ const inputkeydown = (e) => {
const delHistory = () => { const delHistory = () => {
$dialog.warning({ $dialog.warning({
class: "s-dialog", class: "s-dialog",
title: "删除历史", title: t("general.dialog.delete"),
content: "确认删除全部的搜索历史记录?", content: t("nav.search.tip"),
positiveText: "删除", positiveText: t("general.dialog.delete"),
negativeText: "取消", negativeText: t("general.dialog.cancel"),
onPositiveClick: () => { onPositiveClick: () => {
music.setSearchHistory(null, true); music.setSearchHistory(null, true);
$message.success("删除成功"); $message.success(t("general.message.deleteSuccess"));
}, },
}); });
}; };
@@ -306,13 +311,15 @@ onMounted(() => {
getSearchHotData(); getSearchHotData();
// 搜索框失焦 // 搜索框失焦
document.addEventListener("click", () => { document.addEventListener("click", () => {
inputActive.value = false; searchInpRef.value?.blur();
site.searchInputActive = false;
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("click", () => { document.removeEventListener("click", () => {
inputActive.value = false; searchInpRef.value?.blur();
site.searchInputActive = false;
}); });
}); });
@@ -333,7 +340,10 @@ watch(
watch( watch(
() => music.showPlayList, () => music.showPlayList,
(val) => { (val) => {
if (val) inputActive.value = false; if (val) {
searchInpRef.value?.blur();
site.searchInputActive = false;
}
} }
); );
</script> </script>
@@ -353,11 +363,25 @@ watch(
&.focus { &.focus {
width: 280px; width: 280px;
:deep(input) { :deep(input) {
color: $mainColor; color: var(--main-color);
} }
@media (max-width: 450px) { @media (max-width: 450px) {
width: 60vw; width: 60vw;
} }
@media (max-width: 380px) {
width: 54vw;
}
@media (max-width: 320px) {
width: 50vw;
}
}
:deep(.n-input__prefix) {
.n-icon {
transition: color 0.3s;
&.active {
color: var(--main-color);
}
}
} }
} }
.list { .list {
@@ -369,44 +393,49 @@ watch(
z-index: 3; z-index: 3;
@media (max-width: 450px) { @media (max-width: 450px) {
padding-top: 12px;
position: fixed; position: fixed;
width: 100%; width: 100%;
top: 58px; top: 58px;
right: 0; right: 0;
left: 0; left: 0;
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
z-index: 2006;
&::after { &::after {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
content: "收起"; content: "×";
padding: 4px 12px; padding: 4px 12px;
font-size: 12px; font-size: 12px;
background-color: #efefef; background-color: var(--n-action-color);
border-radius: 0 0 0 14px; border-radius: 0 0 0 14px;
} }
} }
:deep(.n-scrollbar) { :deep(.n-scrollbar) {
max-height: 80vh; max-height: 80vh;
@media (max-width: 450px) { @media (max-width: 450px) {
max-height: calc(100vh - 130px); max-height: calc(100vh - 60px);
min-height: calc(100vh - 130px); min-height: calc(100vh - 60px);
box-sizing: border-box;
} }
.n-scrollbar-rail { .n-scrollbar-rail {
width: 4px; width: 4px;
} }
.n-scrollbar-container {
@media (max-width: 450px) {
padding-top: 8px;
}
.n-scrollbar-content { .n-scrollbar-content {
padding: 12px; padding: 12px;
.list-title { .list-title {
color: $mainColor; color: var(--main-color);
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 8px; margin-bottom: 8px;
.n-text { .n-text {
margin-left: 4px; margin-left: 4px;
font-size: 14px; font-size: 14px;
color: $mainColor; color: var(--main-color);
line-height: 0; line-height: 0;
} }
} }
@@ -419,8 +448,8 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
@@ -467,7 +496,7 @@ watch(
font-weight: bold; font-weight: bold;
margin-right: 8px; margin-right: 8px;
&.hot { &.hot {
color: #ff5656; color: var(--main-color);
} }
} }
.title { .title {
@@ -488,9 +517,9 @@ watch(
transform: scale(0.9); transform: scale(0.9);
margin-left: 6px; margin-left: 6px;
height: 18px; height: 18px;
color: $mainColor; color: var(--main-color);
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
border-color: $mainColor; border-color: var(--main-color);
} }
} }
.tip { .tip {
@@ -524,7 +553,7 @@ watch(
margin-bottom: 0; margin-bottom: 0;
} }
.type { .type {
color: #ff5656; color: var(--main-color);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -551,5 +580,6 @@ watch(
} }
} }
} }
}
} }
</style> </style>

24
src/locale/index.js Normal file
View File

@@ -0,0 +1,24 @@
import { createI18n } from "vue-i18n";
import { settingStore } from "@/store";
// 引入语言文件
import en from "./lang/en.js";
import zhCN from "./lang/zh-CN.js";
// 注册 i8n 实例
export const useI18n = (app) => {
const setting = settingStore();
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: setting.language,
fallbackLocale: "zh-CN",
messages: {
en,
"zh-CN": zhCN,
},
});
app.config.globalProperties.$i18n = i18n;
app.use(i18n);
return i18n;
};

367
src/locale/lang/en.js Normal file
View File

@@ -0,0 +1,367 @@
export default {
// Navigation
nav: {
home: "Home",
discover: "Discover",
discoverChildren: {
playlists: "Playlists",
toplists: "Toplists",
artists: "Artists",
},
user: "Library",
userChildren: {
playlist: "My Playlists",
like: "Liked Playlists",
album: "Liked Albums",
artist: "Liked Artists",
cloud: "Music Cloud",
login: "Login",
results: "Music Library",
},
avatar: {
dark: "Dark Mode",
light: "Light Mode",
login: "Login",
logout: "Logout",
notLogin: "Not logged in",
notLoginSubtitle: "Log in for full functionality",
loginError: "Level information retrieval failed",
history: "Playback History",
setting: "Global Setting",
about: "About",
tip: "Confirm that you are logged out of the current user login ?",
success: "Log out successfully",
},
search: {
placeholder: "Search music/videos",
history: "Search History",
delHistory: "Delete Search History",
hotList: "Hot Searches",
searchTip: "Searching...",
noSuggestions: "No search results",
songs: "Songs",
artists: "Artists",
albums: "Albums",
playlists: "Playlists",
tip: "Confirm to delete all search history ?",
results: "search results",
},
officialList: "Official List",
globalList: "Global List",
},
// Home
home: {
title: {
exclusive: "Exclusive Recommend",
playlists: "Recommended Playlists",
artists: "Artist Recommend",
newAlbum: "New Albums",
more: "More",
},
modules: {
dailySongs: {
title: "Daily Recommend",
subtitle: "Updated at 6:00 am based on your music preference.",
},
radar: {
title: "Personal Radar",
subtitle: "Created for you based on your listening history.",
},
likeSong: {
title: "Liked Songs",
subtitle: "Discover your unique music taste.",
},
papersonalfm: {
title: "Personal FM",
subtitle: "Offline mode",
},
},
},
// Login
login: {
login: "Login {name}",
qr: "QR",
phone: "Captcha",
email: "Email",
canNotUse: "This login method is temporarily unavailable",
loggedIn: "Already logged in, please don't log in again",
qrText1: "Please open APP and scan the code to login",
qrText2: "The current QR code is invalid, please scan it again",
qrText3: "Scan successfully, please confirm login in the client",
qrText4: "Login successfully",
qrText5: "Login error, please try again",
qrText6: "Login QR code generation failed",
},
// Menu
menu: {
play: "Play now",
nextPlay: "Play next",
add: "Add to playlist",
create: "Create new playlist",
download: "Download",
comment: "Comment",
mv: "Watch MV",
delete: "Remove from cloud drive",
deleteQuestion:
"Confirm to delete song {name} from Cloud Drive? This action cannot be undone!",
match: "Song Information Match",
search: "Search for same name",
copy: "Copy {name} {other}",
update: "Edit Playlist",
del: "Delete Playlist",
delQuestion:
"Are you sure you want to delete the playlist {name}? This action cannot be undone!",
unableToDelete: "Default playlist cannot be deleted",
collection: "Add {name} to Collection",
cancelCollection: "Remove {name} from Collection",
},
// General
general: {
type: {
hot: "Hot",
all: "All",
china: "China",
chinaMale: "China Male",
chinaFemale: "China Female",
chinaGroup: "China Group",
western: "Western",
westernMale: "Western Male",
westernFemale: "Western Female",
westernGroup: "Western Group",
japan: "Japan",
japanMale: "Japan Male",
japanFemale: "Japan Female",
japanGroup: "Japan Group",
korea: "Korea",
koreaMale: "Korea Male",
koreaFemale: "Korea Female",
koreaGroup: "Korea Group",
other: "Other",
quality: {
l: "Standard quality",
m: "Higher quality",
h: "Very high quality",
sq: "Lossless",
},
},
name: {
song: "Song",
hotSong: "Top Songs",
playlist: "Playlist",
playlists: "Playlists",
videos: "Videos",
toplists: "Toplists",
artists: "Artists",
album: "Album",
link: "Link",
cloud: "Cloud",
songSize: "{size} songs",
albumSize: "{size} albums",
mvSize: "{size} MVs",
unknownSong: "Unknown Songs",
unknownArtist: "Unknown Artist",
itemCount: "Total {size} items",
goto: "Goto",
pageSizes: "{num} items/page",
desc: "{name} Introduction",
allDesc: "All Introduction",
allSong: "All Songs",
allPLaylist: "All Playlists",
artistDesc: "Singer Introduction",
play: "Play",
add: "Add",
comment: "Comment",
noKeywords: "Incomplete parameters",
goBack: "Back to previous page",
reload: "Reload",
allComments: "All Comments",
hotComments: "Hot Comments",
toCurrentlySong: "Go to currently playing song",
loadMore: "Load More",
playlistType: "Playlist Category",
bestPlaylist: "Best Playlist",
upCloud: "Cloud Upload",
cloudUsed: "Used {used}%, Remaining {remaining} G",
simiVideo: "Similar Videos",
restore: "Restore",
},
dialog: {
check: "Check",
cancel: "Cancel",
success: "Successfully",
failed: "Failed",
delete: "Confirm deletion",
match: "Match",
create: "Create",
download: "Downloading",
downloadingNow: "Downloading now",
editor: "Editor",
resetUp: "Re-Upload",
},
message: {
copySuccess: "Copied successfully",
copyFailure: "Copying failed",
addSuccess: "Add songs to playlist successfully",
addFailure: "Failed to add, please try again",
createSuccess: "Playlist creation success",
createFailed: "Playlist creation failed, please try again",
deleteSuccess: "Delete successfully",
deleteFailure: "Delete failed",
downloadSuccess: "{name} Download completed",
downloadFailure: "Download failed, please try another sound quality",
downloadError: "There was an error downloading, please try again",
upCloudSuccess: "{name} Upload successful",
upCloudFailure: "There was an error uploading the song",
upCloudError: "There was an error uploading the song, please try again",
upCloudNotHas: "Upload song details failed to get, try song match",
editorSuccess: "Edit successful",
editorFailed: "Edit failed, Please try again",
operationFailed: "Operation failed, please try again",
acquisitionFailed: "Failed to get",
notSupported: "Your browser does not support this operation",
jumpOut: "About to jump to off-site links",
needLogin: "Please login to your account to use",
needVip: "This operation requires a member account",
needCheck: "Please check your input",
isLoading: "Data loading",
vipTip:
"The current song is exclusive for VIP and can be listened to only",
playError: "Current song failed to play, skip to next song",
signInSuccess: "Sign In Success",
signInSuccessDesc: "Daily sign-in and Yunbei sign-in success",
signInFailed: "Failed to sign in",
},
},
// State
state: {
prohibition: "Prohibit access",
prohibitionDesc: "There are always doors that are closed to you",
notFound: "Resources do not exist",
notFoundDesc: "Why did you come here?",
error: "Server error",
errorDesc: "The server is broken, try again later",
},
// Other
other: {
sData: "Original song information",
asId: "Matching ID",
asIdDes: "Please enter the song ID to be matched",
noNeedMatch: "Consistent with original song ID, no correction required",
plaseCheck: "Please click to check first",
matchSuccess: "Song match successfully",
matchFailed: "Song match failed, please try again",
matchError: "Unusual song ID, can't match",
newPlaylistName: "Please enter a new playlist title",
setPrivacy: "Set as private playlist",
cloudTip:
"Currently for the cloud disk songs, downloaded files are the highest sound quality",
playlistEmpty: "There is no song, please go to the playlist to add",
plName: "Playlist Name",
plNameTip: "Please enter the playlist name",
plDes: "Playlist Description",
plDesTip: "Please enter the playlist description",
plTag: "Playlist Tags",
plTagTip: "Please enter the playlist tags",
lrcClicks: "Click on the selected lyrics to adjust the playback progress",
noSong: "No Song",
noHistory: "No play history",
justShow: "Show only the last {num} songs",
noDesc: "Too lazy, do not even write the introduction",
containing: "Song list containing this song",
loginExpired: "Login is disabled, please login again",
cleanAll: "Reset successful",
},
// Setting
setting: {
dev: "WIP",
main: "Basic",
player: "Player",
themeChange: "Theme color changed to {name}",
themeType: "Theme Color Selection",
themeTypeTip: "Change the site theme color, taking effect immediately",
themeTypeDialog: "Confirm to restore the full site theme color as default?",
language: "Language",
changeLanguage: "Language has been switched to {name}",
theme: "Light/Dark Mode",
themeAuto: "Follow System Light/Dark Mode",
autoSignIn: "Daily Check-in",
autoSignInTip: "Automatically perform daily check-in",
bannerShow: "Show Banner Image",
listClickMode: "List Click Mode",
listClickModeTip:
"This setting is ineffective on mobile, both click modes will be in effect",
dblclick: "Double-click to play",
click: "Single-click to play",
searchHistory: "Display Search History",
bottomLyricShow: "Display Bottom Lyrics",
bottomLyricShowTip:
"Whether to display lyrics at the bottom of the screen while playing",
songVolumeFade: "Song Volume Fade",
songVolumeFadeTip:
"Gradually fade in/out volume when stopping/starting playback",
memoryLastPlaybackPosition: "Remember Playback Position",
memoryLastPlaybackPositionTip:
"Resume last playback progress after refreshing the page",
songLevel: "Song Quality",
songLevelTip:
"Lossless quality and above require a Black Vinyl Club membership",
standard: "Standard",
higher: "Higher",
exhigh: "Extreme",
lossless: "Lossless",
hires: "Hi-Res",
jyeffect: "Whale Cloud Hi-Fi",
jymaster: "Whale Cloud Master",
useUnmServerShow: "Use UNM to play blocked songs",
useUnmServerShowTip1: "Whether to use UNM to replace blocked song links",
useUnmServerShowTip2:
"Please configure UNM-Server before using unblocking feature",
showLyricSetting: "Play Page Shortcut Settings",
showLyricSettingTip: "Show shortcut settings on the play page",
resetApp: "Reset the program",
resetAppTip:
"Try this if the program displays abnormally or if there is a problem",
resetAppWarning:
"Confirming reset to default? Your login status and custom settings will be lost!",
playerStyle: "Player Style",
playerStyleTip: "Style of the player's left-hand function area",
cover: "Cover Mode",
record: "Record Mode",
backgroundImageShow: "Play Background Style",
solid: "Solid Cover",
blur: "Blurry Cover",
backgroundImageShowTip1: "Display album cover in blurred mode",
backgroundImageShowTip2: "Extract album's main color as background color",
showTransl: "Show Lyric Translation",
showTranslTip: "Whether to display lyric translation when available",
showRoma: "Show Lyric Transliteration",
showRomaTip: "Whether to display lyric transliteration when available",
countDownShow: "Show Countdown Before Playing",
countDownShowTip: "Some songs may have incorrect countdown display",
showYrc: "Show Word-by-Word Lyrics",
showYrcTip: "Whether to display word-by-word lyrics when available",
showYrcAnimation: "Word by word lyric step animation",
showYrcAnimationTip:
"Whether to display verbatim lyrics step - by - step animation, more cost performance",
showYrcTransform: "Word for word lyrics come up",
showYrcTransformTip:
"Whether to display verbatim lyrics text floating animation",
lrcMousePause: "Intelligent Scroll Pause",
lrcMousePauseTip:
"Whether to pause scrolling when the mouse is over the lyrics area",
lyricsBlock: "Lyric Scrolling Position",
lyricsBlockTip: "The position where the lyrics are highlighted",
blockStart: "Near the Top",
blockCenter: "Horizontally Centered",
lyricsFontSize: "Lyric Text Size",
lyrics1: "Smallest",
lyrics2: "Default",
lyrics3: "Largest",
lyricsPosition: "Default Lyric Position",
positionLeft: "Left",
positionCenter: "Centered",
lyricsBlur: "Lyric Blur",
lyricsBlurTip:
"Blur lyrics other than the currently playing ones, experimental feature",
},
};

350
src/locale/lang/zh-CN.js Normal file
View File

@@ -0,0 +1,350 @@
export default {
// 导航栏
nav: {
home: "首页",
discover: "发现",
discoverChildren: {
playlists: "歌单",
toplists: "排行榜",
artists: "歌手",
},
user: "音乐库",
userChildren: {
login: "登录账号",
playlist: "我的歌单",
like: "收藏的歌单",
album: "收藏的专辑",
artist: "收藏的歌手",
cloud: "音乐云盘",
results: "的音乐库",
},
avatar: {
dark: "深色模式",
light: "浅色模式",
login: "登录账号",
logout: "退出登录",
notLogin: "未登录",
notLoginSubtitle: "登录后享受完整功能",
loginError: "等级信息获取失败",
history: "播放历史",
setting: "全局设置",
about: "关于本站",
tip: "确认退出当前用户登录?",
success: "成功退出登录",
},
search: {
placeholder: "搜索音乐/视频",
history: "搜索历史",
delHistory: "删除搜索历史",
hotList: "热搜榜",
searchTip: "努力搜索中",
noSuggestions: "暂无搜索结果",
songs: "单曲",
artists: "歌手",
albums: "专辑",
playlists: "歌单",
tip: "确认删除全部的搜索历史记录?",
results: "的搜索结果",
},
officialList: "官方榜",
globalList: "全球榜",
},
// 首页
home: {
title: {
exclusive: "专属推荐",
playlists: "推荐歌单",
artists: "歌手推荐",
newAlbum: "新碟上架",
more: "更多",
},
modules: {
dailySongs: {
title: "每日推荐",
subtitle: "根据你的音乐口味 · 每日 6:00 更新",
},
radar: {
title: "私人雷达",
subtitle: "根据听歌记录为你打造",
},
likeSong: {
title: "喜欢的音乐",
subtitle: "发现你独特的音乐品味",
},
papersonalfm: {
title: "私人FM",
subtitle: "未登录模式",
},
},
},
// Login
login: {
login: "登录 {name}",
qr: "扫码登录",
phone: "验证码登录",
email: "邮箱登录",
canNotUse: "该登录方式暂时无法使用",
loggedIn: "已登录,请勿重复登录",
qrText1: "请打开云音乐 APP 扫码登录",
qrText2: "当前二维码已失效,请重新扫码",
qrText3: "扫描成功,请在客户端确认登录",
qrText4: "登录成功",
qrText5: "登录出错,请重试",
qrText6: "登录二维码生成失败",
},
// 菜单
menu: {
play: "立即播放",
nextPlay: "下一首播放",
add: "添加到歌单",
create: "新建歌单",
download: "歌曲下载",
comment: "前往评论区",
mv: "观看 MV",
delete: "从云盘中删除",
deleteQuestion: "确认从云盘中删除歌曲 {name} ?删除后将不可恢复!",
match: "歌曲信息匹配",
search: "同名搜索",
copy: "复制{name}{other}",
update: "编辑歌单",
del: "删除歌单",
delQuestion: "确认删除歌单 {name} ?删除后将不可恢复!",
unableToDelete: "默认歌单无法删除",
collection: "收藏{name}",
cancelCollection: "取消收藏{name}",
},
// 通用
general: {
type: {
hot: "热门",
all: "全部",
china: "华语",
chinaMale: "华语男",
chinaFemale: "华语女",
chinaGroup: "华语组合",
western: "欧美",
westernMale: "欧美男",
westernFemale: "欧美女",
westernGroup: "欧美组合",
japan: "日本",
japanMale: "日本男",
japanFemale: "日本女",
japanGroup: "日本组合",
korea: "韩国",
koreaMale: "韩国男",
koreaFemale: "韩国女",
koreaGroup: "韩国组合",
other: "其他",
quality: {
l: "标准音质",
m: "较高音质",
h: "极高音质",
sq: "无损音质",
},
},
name: {
song: "歌曲",
hotSong: "热门歌曲",
playlist: "歌单",
videos: "视频",
playlists: "播放列表",
toplists: "排行榜",
artists: "歌手",
album: "专辑",
link: "链接",
cloud: "云盘",
songSize: "{size} 首",
albumSize: "{size} 张专辑",
mvSize: "{size} 个 MV",
unknownSong: "未知歌曲",
unknownArtist: "未知歌手",
itemCount: "共{size}项",
goto: "前往",
pageSizes: "{num}条/页",
desc: "{name}简介",
allDesc: "全部简介",
allSong: "全部歌曲",
allPLaylist: "全部歌单",
artistDesc: "歌手介绍",
play: "播放",
add: "添加",
comment: "评论",
noKeywords: "参数不完整",
goBack: "返回上一级",
reload: "重新载入",
allComments: "全部评论",
hotComments: "热门评论",
toCurrentlySong: "前往当前播放歌曲",
loadMore: "加载更多",
playlistType: "歌单分类",
bestPlaylist: "精品歌单",
upCloud: "云盘上传",
cloudUsed: "已用 {used}%,剩余 {remaining} G",
simiVideo: "相似视频",
restore: "恢复默认",
},
dialog: {
check: "检查",
cancel: "取消",
success: "成功",
failed: "失败",
delete: "确认删除",
match: "匹配歌曲",
create: "新建",
download: "下载",
downloadingNow: "正在下载",
editor: "编辑",
resetUp: "重新上传",
},
message: {
copySuccess: "复制成功",
copyFailure: "复制失败",
addSuccess: "添加歌曲至歌单成功",
addFailure: "添加失败,请重试",
createSuccess: "歌单新建成功",
createFailed: "歌单新建失败,请重试",
deleteSuccess: "删除成功",
deleteFailure: "删除失败",
downloadSuccess: "{name}下载完成",
downloadFailure: "下载失败,请尝试其他音质",
downloadError: "下载出现错误,请重试",
upCloudSuccess: "{name} 上传成功",
upCloudFailure: "歌曲上传出现错误",
upCloudError: "歌曲上传出错,请重试",
upCloudNotHas: "上传歌曲详细信息获取失败,可尝试歌曲匹配",
editorSuccess: "编辑成功",
editorFailed: "编辑失败,请重试",
operationFailed: "操作失败,请重试",
acquisitionFailed: "获取失败",
notSupported: "您的浏览器暂不支持该操作",
jumpOut: "即将跳转至站外链接",
needLogin: "请登录账号后使用",
needVip: "该操作需要账号为黑胶会员",
needCheck: "请检查您的输入",
isLoading: "数据加载中",
vipTip: "当前歌曲为 VIP 专享,仅可试听",
playError: "当前歌曲播放失败,跳至下一首",
signInSuccess: "签到成功",
signInSuccessDesc: "每日签到及云贝签到成功",
signInFailed: "签到失败",
},
},
// State
state: {
prohibition: "禁止访问",
prohibitionDesc: "总有些门是对你关闭的",
notFound: "资源不存在",
notFoundDesc: "怎么跑到这来了?",
error: "服务器错误",
errorDesc: "服务器寄了,等会再试吧",
},
// Other
other: {
sData: "原歌曲信息",
asId: "匹配的 ID",
asIdDes: "请输入要匹配的歌曲 ID",
noNeedMatch: "与原歌曲 ID 一致,无需匹配",
plaseCheck: "请先点击检查",
matchSuccess: "歌曲匹配成功",
matchFailed: "歌曲匹配失败,请重试",
matchError: "非正常歌曲 ID无法匹配",
newPlaylistName: "请输入新歌单标题",
setPrivacy: "设置为隐私歌单",
cloudTip: "当前为云盘歌曲,下载的文件均为最高音质",
playlistEmpty: "暂无歌曲,请前往列表添加",
plName: "歌单名称",
plNameTip: "请输入歌单名称",
plDes: "歌单描述",
plDesTip: "请输入歌单描述",
plTag: "歌单标签",
plTagTip: "请输入歌单标签",
lrcClicks: "点击选中的歌词以调整播放进度",
noSong: "暂无歌曲",
noHistory: "暂无播放历史",
justShow: "仅显示最近 {num} 首",
noDesc: "太懒了吧,连简介都不写",
containing: "包含这首歌的歌单",
loginExpired: "登录已失效,请重新登录",
cleanAll: "重置成功",
},
setting: {
dev: "开发中功能",
main: "基础",
player: "播放器",
themeChange: "主题色更换为 {name}",
themeType: "主题色选择",
themeTypeTip: "更换全站主题色,即时生效",
themeTypeDialog: "确认恢复全站主题色为默认?",
language: "语言",
changeLanguage: "语言已切换为 {name}",
theme: "明暗模式",
themeAuto: "明暗模式跟随系统",
autoSignIn: "每日签到",
autoSignInTip: "是否自动进行每日签到",
bannerShow: "显示轮播图",
listClickMode: "列表点击方式",
listClickModeTip: "移动端该设置项无效,单击同时生效",
dblclick: "双击播放",
click: "单击播放",
searchHistory: "显示搜索历史",
bottomLyricShow: "显示底栏歌词",
bottomLyricShowTip: "是否在播放时显示歌词",
songVolumeFade: "歌曲渐入渐出",
songVolumeFadeTip: "是否在歌曲暂停 / 播放时渐入渐出",
memoryLastPlaybackPosition: "记忆播放位置",
memoryLastPlaybackPositionTip: "是否在刷新后恢复上次播放进度",
songLevel: "歌曲音质",
songLevelTip: "无损音质及以上需要您为黑胶会员",
standard: "标准",
higher: "较高",
exhigh: "极高",
lossless: "无损",
hires: "Hi-Res",
jyeffect: "鲸云臻音",
jymaster: "鲸云母带",
useUnmServerShow: "尝试替换无法播放的歌曲",
useUnmServerShowTip1: "是否使用 UNM 替换无法播放的歌曲链接",
useUnmServerShowTip2: "请配置 UNM-Server 后使用解灰功能",
showLyricSetting: "播放页快捷设置",
showLyricSettingTip: "是否在播放页面显示快捷设置",
resetApp: "程序重置",
resetAppTip: "若程序显示异常或出现问题时可尝试此操作",
resetAppWarning: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
playerStyle: "播放器样式",
playerStyleTip: "播放器左侧功能区样式",
cover: "封面模式",
record: "唱片模式",
backgroundImageShow: "播放背景样式",
solid: "封面主色",
blur: "封面模糊",
backgroundImageShowTip1: "将专辑封面模糊显示",
backgroundImageShowTip2: "提取专辑主色作为背景颜色",
showTransl: "显示歌词翻译",
showTranslTip: "是否在具有翻译歌词时显示",
showRoma: "显示歌词音译",
showRomaTip: "是否在具有音译歌词时显示",
countDownShow: "显示前奏等待",
countDownShowTip: "部分歌曲前奏可能存在显示错误",
showYrc: "显示逐字歌词",
showYrcTip: "是否在歌曲具有逐字歌词时显示",
showYrcAnimation: "逐字歌词步进动画",
showYrcAnimationTip: "是否显示逐字歌词步进动画,较耗费性能",
showYrcTransform: "逐字歌词上浮",
showYrcTransformTip: "是否显示逐字歌词文字上浮动画",
lrcMousePause: "智能暂停滚动",
lrcMousePauseTip: "鼠标移入歌词区域是否暂停滚动",
lyricsBlock: "歌词滚动位置",
lyricsBlockTip: "歌词高亮时所处的位置",
blockStart: "靠近顶部",
blockCenter: "水平居中",
lyricsFontSize: "歌词文本大小",
lyrics1: "最小",
lyrics2: "默认",
lyrics3: "最大",
lyricsPosition: "默认歌词位置",
positionLeft: "居左",
positionCenter: "居中",
lyricsBlur: "歌词模糊",
lyricsBlurTip: "除当前播放歌词外模糊显示,实验性功能",
},
};

View File

@@ -1,9 +1,10 @@
import { createApp } from "vue"; import { createApp } from "vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import { useI18n } from "@/locale";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import App from "./App.vue"; import App from "@/App.vue";
import router from "./router"; import router from "@/router";
// 全局样式 // 全局样式
import "@/style/global.scss"; import "@/style/global.scss";
@@ -12,18 +13,34 @@ const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(pinia);
app.use(router); app.use(router);
// 国际化
useI18n(app);
app.mount("#app"); app.mount("#app");
// PWA if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("controllerchange", () => { let pwaMessage = null;
// 弹出更新提醒
console.log("站点已更新,刷新后生效"); // 检测到更新提醒
navigator.serviceWorker.addEventListener("onupdatefound", () => {
console.info("发现站点更新,正在下载新版本");
pwaMessage = $message.loading("发现站点更新,正在下载新版本", {
closable: true,
duration: 0,
});
});
// 更新完成提醒
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.info("站点已更新,刷新后生效");
if (pwaMessage) pwaMessage?.destroy();
$message.info("站点已更新,刷新后生效", { $message.info("站点已更新,刷新后生效", {
closable: true, closable: true,
duration: 0, duration: 0,
}); });
}); });
}

View File

@@ -11,7 +11,7 @@ const router = createRouter({
// 路由守卫 // 路由守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const user = userStore(); const user = userStore();
$loadingBar.start(); if (typeof $loadingBar !== "undefined") $loadingBar.start();
// 判断是否需要登录 // 判断是否需要登录
if (to.meta.needLogin) { if (to.meta.needLogin) {
getLoginState() getLoginState()
@@ -45,7 +45,7 @@ router.beforeEach((to, from, next) => {
}); });
router.afterEach(() => { router.afterEach(() => {
$loadingBar.finish(); if (typeof $loadingBar !== "undefined") $loadingBar.finish();
}); });
export default router; export default router;

View File

@@ -1,7 +1,9 @@
import useSettingDataStore from "./settingData"; import useSettingDataStore from "./settingData";
import useMusicDataStore from "./musicData"; import useMusicDataStore from "./musicData";
import useUserDataStore from "./userData"; import useUserDataStore from "./userData";
import useSiteDataStore from "./siteData";
export const settingStore = () => useSettingDataStore(); export const settingStore = () => useSettingDataStore();
export const musicStore = () => useMusicDataStore(); export const musicStore = () => useMusicDataStore();
export const userStore = () => useUserDataStore(); export const userStore = () => useUserDataStore();
export const siteStore = () => useSiteDataStore();

View File

@@ -1,12 +1,15 @@
import { defineStore } from "pinia"; 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 { getPersonalFm, setFmTrash } from "@/api/home";
import { getLikelist, setLikeSong } from "@/api/user"; import { getLikelist, setLikeSong } from "@/api/user";
import { getPlayListCatlist } from "@/api/playlist"; import { getPlayListCatlist } from "@/api/playlist";
import { userStore, settingStore } from "@/store"; import { userStore, settingStore } from "@/store";
import { NIcon } from "naive-ui"; import { NIcon } from "naive-ui";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next"; import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { soundStop, fadePlayOrPause } from "@/utils/Player";
import parseLyric from "@/utils/parseLyric"; import parseLyric from "@/utils/parseLyric";
import getLanguageData from "@/utils/getLanguageData";
const useMusicDataStore = defineStore("musicData", { const useMusicDataStore = defineStore("musicData", {
state: () => { state: () => {
@@ -20,7 +23,7 @@ const useMusicDataStore = defineStore("musicData", {
// 播放状态 // 播放状态
playState: false, playState: false,
// 当前歌曲播放链接 // 当前歌曲播放链接
playSongLink: null, // playSongLink: null,
// 当前歌曲歌词数据 // 当前歌曲歌词数据
playSongLyric: { playSongLyric: {
lrc: [], lrc: [],
@@ -37,6 +40,15 @@ const useMusicDataStore = defineStore("musicData", {
catList: {}, catList: {},
// 精品歌单分类 // 精品歌单分类
highqualityCatList: [], highqualityCatList: [],
// 音乐频谱数据
spectrumsData: {
data: [],
audio: null,
analyser: null,
audioCtx: null,
},
// 是否正在加载数据
isLoadingSong: false,
// 持久化数据 // 持久化数据
persistData: { persistData: {
// 搜索历史 // 搜索历史
@@ -116,10 +128,6 @@ const useMusicDataStore = defineStore("musicData", {
getPlayState(state) { getPlayState(state) {
return state.playState; return state.playState;
}, },
// 获取播放链接
getPlaySongLink(state) {
return state.playSongLink;
},
// 获取喜欢音乐列表 // 获取喜欢音乐列表
getLikeList(state) { getLikeList(state) {
return state.persistData.likeList; return state.persistData.likeList;
@@ -142,8 +150,8 @@ const useMusicDataStore = defineStore("musicData", {
setPersonalFmMode(value) { setPersonalFmMode(value) {
this.persistData.personalFmMode = value; this.persistData.personalFmMode = value;
if (value) { if (value) {
this.playSongLink = null; if (typeof $player !== "undefined") soundStop($player);
if (this.persistData.personalFmData.id) { if (this.persistData.personalFmData?.id) {
this.persistData.playlists = []; this.persistData.playlists = [];
this.persistData.playlists.push(this.persistData.personalFmData); this.persistData.playlists.push(this.persistData.personalFmData);
this.persistData.playSongIndex = 0; this.persistData.playSongIndex = 0;
@@ -175,7 +183,7 @@ const useMusicDataStore = defineStore("musicData", {
} else { } else {
this.persistData.personalFmData = fmData; this.persistData.personalFmData = fmData;
if (this.persistData.personalFmMode) { if (this.persistData.personalFmMode) {
this.playSongLink = null; if (typeof $player !== "undefined") soundStop($player);
this.persistData.playlists = []; this.persistData.playlists = [];
this.persistData.playlists.push(fmData); this.persistData.playlists.push(fmData);
this.persistData.playSongIndex = 0; this.persistData.playSongIndex = 0;
@@ -183,29 +191,28 @@ const useMusicDataStore = defineStore("musicData", {
} }
} }
} else { } else {
$message.error("获取私人 FM 失败"); $message.error(getLanguageData("personalFmError"));
} }
}); });
} catch (err) { } catch (err) {
console.error("获取私人 FM 失败:" + err); console.error(getLanguageData("personalFmError"), err);
$message.error("获取私人 FM 失败"); $message.error(getLanguageData("personalFmError"));
} }
}, },
// 私人fm垃圾桶 // 私人fm垃圾桶
setFmDislike(id, tip = true) { setFmDislike(id) {
const user = userStore(); const user = userStore();
if (user.userLogin) { if (user.userLogin) {
setFmTrash(id).then((res) => { setFmTrash(id).then((res) => {
if (res.code == 200) { if (res.code == 200) {
if (tip) $message.success("已将该歌曲移除至垃圾桶");
this.persistData.personalFmMode = true; this.persistData.personalFmMode = true;
this.setPlaySongIndex("next"); this.setPlaySongIndex("next");
} else { } else {
$message.error("歌曲移除至垃圾桶失败"); $message.error(getLanguageData("fmTrashError"));
} }
}); });
} else { } else {
$message.error("请登录账号后使用"); $message.error(getLanguageData("needLogin"));
} }
}, },
// 更改喜欢列表 // 更改喜欢列表
@@ -232,30 +239,30 @@ const useMusicDataStore = defineStore("musicData", {
setLikeSong(id, like).then((res) => { setLikeSong(id, like).then((res) => {
if (res.code == 200) { if (res.code == 200) {
list.push(id); list.push(id);
$message.info("成功喜欢歌曲"); $message.info(getLanguageData("loveSong"));
} else { } else {
$message.error("喜欢歌曲时发生错误"); $message.error(getLanguageData("loveSongError"));
} }
}); });
} else { } else {
$message.info("我喜欢的列表中已存在该歌曲"); $message.info(getLanguageData("loveSongRepeat"));
} }
} else { } else {
if (exists) { if (exists) {
setLikeSong(id, like).then((res) => { setLikeSong(id, like).then((res) => {
if (res.code == 200) { if (res.code == 200) {
list.splice(list.indexOf(id), 1); list.splice(list.indexOf(id), 1);
$message.info("成功取消喜欢歌曲"); $message.info(getLanguageData("loveSongRemove"));
} else { } else {
$message.error("取消喜欢歌曲时发生错误"); $message.error(getLanguageData("loveSongRemoveError"));
} }
}); });
} else { } else {
$message.error("我喜欢的列表中未找到该歌曲"); $message.error(getLanguageData("loveSongNoFound"));
} }
} }
} else { } else {
$message.error("请登录账号后使用"); $message.error(getLanguageData("needLogin"));
} }
}, },
// 更改音乐播放状态 // 更改音乐播放状态
@@ -270,19 +277,13 @@ const useMusicDataStore = defineStore("musicData", {
setPlayBarState(value) { setPlayBarState(value) {
this.showPlayBar = value; this.showPlayBar = value;
}, },
// 更改歌曲播放链接
setPlaySongLink(value) {
this.playSongLink = value;
},
// 更改播放列表模式 // 更改播放列表模式
setPlayListMode(value) { setPlayListMode(value) {
console.log(value);
this.persistData.playListMode = value; this.persistData.playListMode = value;
}, },
// 添加歌单至播放列表 // 添加歌单至播放列表
setPlaylists(value) { setPlaylists(value) {
this.persistData.playlists = value; this.persistData.playlists = value.slice();
console.log(`已添加${value.length}首歌曲至播放列表`);
}, },
// 更改每日推荐数据 // 更改每日推荐数据
setDailySongs(value) { setDailySongs(value) {
@@ -301,8 +302,6 @@ const useMusicDataStore = defineStore("musicData", {
mv: v.mv ? v.mv : null, mv: v.mv ? v.mv : null,
}); });
}); });
} else {
$message.error("处理每日推荐发生错误");
} }
}, },
// 歌词处理 // 歌词处理
@@ -311,8 +310,8 @@ const useMusicDataStore = defineStore("musicData", {
try { try {
this.playSongLyric = parseLyric(value); this.playSongLyric = parseLyric(value);
} catch (err) { } catch (err) {
$message.error("歌词处理出错"); $message.error(getLanguageData("getLrcError"));
console.error("歌词处理出错:" + err); console.error(getLanguageData("getLrcError"), err);
} }
} else { } else {
console.log("该歌曲暂无歌词"); console.log("该歌曲暂无歌词");
@@ -324,9 +323,13 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongTime.currentTime = value.currentTime; this.persistData.playSongTime.currentTime = value.currentTime;
this.persistData.playSongTime.duration = value.duration; this.persistData.playSongTime.duration = value.duration;
// 计算进度条应该移动的距离 // 计算进度条应该移动的距离
if (value.duration === 0) {
this.persistData.playSongTime.barMoveDistance = 0;
} else {
this.persistData.playSongTime.barMoveDistance = Number( this.persistData.playSongTime.barMoveDistance = Number(
(value.currentTime / (value.duration / 100)).toFixed(2) (value.currentTime / (value.duration / 100)).toFixed(2)
); );
}
if (this.persistData.playSongTime.barMoveDistance) { if (this.persistData.playSongTime.barMoveDistance) {
// 歌曲播放进度转换 // 歌曲播放进度转换
this.persistData.playSongTime.songTimePlayed = getSongPlayingTime( this.persistData.playSongTime.songTimePlayed = getSongPlayingTime(
@@ -347,24 +350,26 @@ const useMusicDataStore = defineStore("musicData", {
setPlaySongMode() { setPlaySongMode() {
if (this.persistData.playSongMode === "normal") { if (this.persistData.playSongMode === "normal") {
this.persistData.playSongMode = "random"; this.persistData.playSongMode = "random";
$message.info("随机播放", { $message.info(getLanguageData("random"), {
icon: () => h(NIcon, null, { default: () => h(ShuffleOne) }), icon: () => h(NIcon, null, { default: () => h(ShuffleOne) }),
}); });
} else if (this.persistData.playSongMode === "random") { } else if (this.persistData.playSongMode === "random") {
this.persistData.playSongMode = "single"; this.persistData.playSongMode = "single";
$message.info("单曲循环", { $message.info(getLanguageData("single"), {
icon: () => h(NIcon, null, { default: () => h(PlayOnce) }), icon: () => h(NIcon, null, { default: () => h(PlayOnce) }),
}); });
} else { } else {
this.persistData.playSongMode = "normal"; this.persistData.playSongMode = "normal";
$message.info("列表循环", { $message.info(getLanguageData("normal"), {
icon: () => h(NIcon, null, { default: () => h(PlayCycle) }), icon: () => h(NIcon, null, { default: () => h(PlayCycle) }),
}); });
} }
}, },
// 上下曲调整 // 上下曲调整
setPlaySongIndex(type) { setPlaySongIndex(type) {
this.playState = false; if (typeof $player === "undefined") return false;
soundStop($player);
this.isLoadingSong = true;
if (this.persistData.personalFmMode) { if (this.persistData.personalFmMode) {
this.setPersonalFmData(); this.setPersonalFmData();
} else { } else {
@@ -377,21 +382,26 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongIndex = Math.floor( this.persistData.playSongIndex = Math.floor(
Math.random() * listLength Math.random() * listLength
); );
} else if (listMode === "single" && $player) { } else if (listMode === "single" && typeof $player !== "undefined") {
$player.currentTime = 0; soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
} else { } else {
$message.error("播放出错,请刷新后重试"); $message.error(getLanguageData("playError"));
} }
// 判断是否处于最后/第一首 // 判断是否处于最后/第一首
if (this.persistData.playSongIndex < 0) { if (this.persistData.playSongIndex < 0) {
this.persistData.playSongIndex = listLength - 1; this.persistData.playSongIndex = listLength - 1;
} else if (this.persistData.playSongIndex >= listLength) { } else if (this.persistData.playSongIndex >= listLength) {
this.persistData.playSongIndex = 0; this.persistData.playSongIndex = 0;
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
} }
if (listMode !== "single") { if (listMode !== "single" && listLength > 1) {
this.playSongLink = null; soundStop($player);
} }
this.playState = true; nextTick().then(() => {
this.setPlayState(true);
});
} }
}, },
// 添加歌曲至播放列表 // 添加歌曲至播放列表
@@ -405,11 +415,12 @@ const useMusicDataStore = defineStore("musicData", {
value.id !== value.id !==
this.persistData.playlists[this.persistData.playSongIndex]?.id this.persistData.playlists[this.persistData.playSongIndex]?.id
) { ) {
console.log("播放歌曲与上一次不一致"); console.log("Play a song that is not the same as the last one");
this.playSongLink = null; if (typeof $player !== "undefined") soundStop($player);
this.isLoadingSong = true;
} }
} catch (error) { } catch (error) {
console.error("出现错误:" + error); console.error("Error:" + error);
} }
if (index !== -1) { if (index !== -1) {
this.persistData.playSongIndex = index; this.persistData.playSongIndex = index;
@@ -417,10 +428,13 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playlists.push(value); this.persistData.playlists.push(value);
this.persistData.playSongIndex = this.persistData.playlists.length - 1; this.persistData.playSongIndex = this.persistData.playlists.length - 1;
} }
play ? (this.playState = true) : null; play ? this.setPlayState(true) : null;
}, },
// 在当前播放歌曲后添加 // 在当前播放歌曲后添加
addSongToNext(value) { addSongToNext(value) {
// 更改播放模式为列表循环
this.persistData.playSongMode = "normal";
// 查找是否存在于播放列表
const index = this.persistData.playlists.findIndex( const index = this.persistData.playlists.findIndex(
(o) => o.id === value.id (o) => o.id === value.id
); );
@@ -442,16 +456,25 @@ const useMusicDataStore = defineStore("musicData", {
value value
); );
} }
$message.success(value.name + " 已添加至下一曲播放"); $message.success(value.name + " " + getLanguageData("addSongToNext"));
}, },
// 播放列表移除歌曲 // 播放列表移除歌曲
removeSong(index) { removeSong(index) {
if (typeof $player === "undefined") return false;
const name = this.persistData.playlists[index].name; const name = this.persistData.playlists[index].name;
if (index < this.persistData.playSongIndex) { if (index < this.persistData.playSongIndex) {
this.persistData.playSongIndex--; this.persistData.playSongIndex--;
} else if (index === this.persistData.playSongIndex) {
// 如果删除的是当前播放歌曲,则重置播放器
soundStop($player);
} }
$message.success(name + " 已从播放列表中移除"); $message.success(name + " " + getLanguageData("removeSong"));
this.persistData.playlists.splice(index, 1); this.persistData.playlists.splice(index, 1);
// 检查当前播放歌曲的索引是否超出了列表范围
if (this.persistData.playSongIndex >= this.persistData.playlists.length) {
this.persistData.playSongIndex = 0;
soundStop($player);
}
}, },
// 获取歌单分类 // 获取歌单分类
setCatList(highquality = false) { setCatList(highquality = false) {
@@ -459,7 +482,7 @@ const useMusicDataStore = defineStore("musicData", {
if (res.code == 200) { if (res.code == 200) {
this.catList = res; this.catList = res;
} else { } else {
$message.error("歌单分类获取失败"); $message.error(getLanguageData("getDataError"));
} }
}); });
if (highquality) { if (highquality) {
@@ -467,7 +490,7 @@ const useMusicDataStore = defineStore("musicData", {
if (res.code == 200) { if (res.code == 200) {
this.highqualityCatList = res.tags; this.highqualityCatList = res.tags;
} else { } else {
$message.error("精品歌单分类获取失败"); $message.error(getLanguageData("getDataError"));
} }
}); });
} }

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { NIcon } from "naive-ui"; import { NIcon } from "naive-ui";
import { WbSunnyFilled, DarkModeFilled } from "@vicons/material"; import { WbSunnyFilled, DarkModeFilled } from "@vicons/material";
import getLanguageData from "@/utils/getLanguageData";
const useSettingDataStore = defineStore("settingData", { const useSettingDataStore = defineStore("settingData", {
state: () => { state: () => {
@@ -8,6 +9,8 @@ const useSettingDataStore = defineStore("settingData", {
// 全局主题 // 全局主题
theme: "light", theme: "light",
themeAuto: true, themeAuto: true,
themeType: "red",
themeData: {},
// 搜索历史 // 搜索历史
searchHistory: true, searchHistory: true,
// 轮播图显示 // 轮播图显示
@@ -22,8 +25,14 @@ const useSettingDataStore = defineStore("settingData", {
bottomLyricShow: true, bottomLyricShow: true,
// 是否显示逐字歌词 // 是否显示逐字歌词
showYrc: true, showYrc: true,
// 是否显示逐字歌词动画
showYrcAnimation: true,
// 是否显示逐字歌词上浮
showYrcTransform: false,
// 是否显示歌词翻译 // 是否显示歌词翻译
showTransl: true, showTransl: true,
// 是否显示歌词音译
showRoma: true,
// 歌曲音质 // 歌曲音质
songLevel: "exhigh", songLevel: "exhigh",
// 歌词位置 // 歌词位置
@@ -37,9 +46,23 @@ const useSettingDataStore = defineStore("settingData", {
// 音乐频谱 // 音乐频谱
musicFrequency: false, musicFrequency: false,
// 鼠标移入歌词区域暂停滚动 // 鼠标移入歌词区域暂停滚动
lrcMousePause: true, lrcMousePause: false,
// 是否使用网易云解灰 // 是否使用网易云解灰
useUnmServer: true, useUnmServer: true,
// 播放背景是否显示图片
backgroundImageShow: "blur",
// 是否显示前奏等待
countDownShow: true,
// 是否显示歌词设置
showLyricSetting: false,
// 歌曲渐入渐出
songVolumeFade: true,
// 列表默认数量
listNumber: 30,
// 记忆上次播放位置
memoryLastPlaybackPosition: true,
// 语言
language: "zh-CN",
}; };
}, },
getters: { getters: {
@@ -47,16 +70,14 @@ const useSettingDataStore = defineStore("settingData", {
getSiteTheme(state) { getSiteTheme(state) {
return state.theme; return state.theme;
}, },
// 获取是否开启翻译
getShowTransl(state) {
return state.showTransl;
},
}, },
actions: { actions: {
// 切换明暗模式 // 切换明暗模式
setSiteTheme(value) { setSiteTheme(value) {
const isLightMode = value === "light"; const isLightMode = value === "light";
const message = isLightMode ? "已切换至浅色模式" : "已切换至深色模式"; const message = isLightMode
? getLanguageData("lightMode")
: getLanguageData("darkMode");
const icon = isLightMode ? WbSunnyFilled : DarkModeFilled; const icon = isLightMode ? WbSunnyFilled : DarkModeFilled;
this.theme = value; this.theme = value;
$message.info(message, { $message.info(message, {

25
src/store/siteData.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineStore } from "pinia";
const useSiteDataStore = defineStore("siteData", {
state: () => {
return {
// 站点标题
siteTitle: import.meta.env.VITE_SITE_TITLE,
// 封面主题色
songPicColor: "rgb(128,128,128)",
// 搜索框激活状态
searchInputActive: false,
};
},
getters: {},
actions: {},
// 开启数据持久化
persist: [
{
storage: localStorage,
paths: [""],
},
],
});
export default useSiteDataStore;

View File

@@ -7,13 +7,12 @@ import {
getUserArtistlist, getUserArtistlist,
getUserAlbum, getUserAlbum,
} from "@/api/user"; } from "@/api/user";
import { formatNumber, getLongTime } from "@/utils/timeTools.js"; import { formatNumber, getLongTime } from "@/utils/timeTools";
import getLanguageData from "@/utils/getLanguageData";
const useUserDataStore = defineStore("userData", { const useUserDataStore = defineStore("userData", {
state: () => { state: () => {
return { return {
// 站点标题
siteTitle: "SPlayer",
// 用户登录状态 // 用户登录状态
userLogin: false, userLogin: false,
// 用户 cookie // 用户 cookie
@@ -70,10 +69,6 @@ const useUserDataStore = defineStore("userData", {
}, },
}, },
actions: { actions: {
// 更改站点标题
setSiteTitle(value) {
this.siteTitle = value;
},
// 更改 cookie // 更改 cookie
setCookie(value) { setCookie(value) {
window.localStorage.setItem("cookie", value); window.localStorage.setItem("cookie", value);
@@ -92,11 +87,11 @@ const useUserDataStore = defineStore("userData", {
console.log(res); console.log(res);
this.userOtherData.level = res[0].data; this.userOtherData.level = res[0].data;
this.userOtherData.subcount = res[1]; this.userOtherData.subcount = res[1];
this.setUserPlayLists(); // this.setUserPlayLists();
}) })
.catch((err) => { .catch((err) => {
console.error("获取用户详情失败:" + err); console.error(getLanguageData("getDataError"), err);
$message.error("获取用户详情失败,请刷新后重试"); $message.error(getLanguageData("getDataError"));
}); });
} }
}, },
@@ -107,20 +102,19 @@ const useUserDataStore = defineStore("userData", {
this.userData = {}; this.userData = {};
this.userOtherData = {}; this.userOtherData = {};
localStorage.removeItem("cookie"); localStorage.removeItem("cookie");
// 调用退出登录接口
userLogOut(); userLogOut();
}, },
// 更改用户歌单 // 更改用户歌单
async setUserPlayLists() { async setUserPlayLists(callback) {
if (this.userLogin) { if (this.userLogin) {
try { try {
if (!Object.keys(this.userOtherData).length) {
this.setUserOtherData();
} else {
this.userPlayLists.isLoading = true; this.userPlayLists.isLoading = true;
const { userId } = this.userData; const { userId } = this.userData;
const { subcount } = this.userOtherData; // const { subcount } = this.userOtherData;
const number = const { createdPlaylistCount, subPlaylistCount } =
subcount.createdPlaylistCount + subcount.subPlaylistCount; await getUserSubcount();
const number = createdPlaylistCount + subPlaylistCount ?? 30;
const res = await getUserPlaylist(userId, number); const res = await getUserPlaylist(userId, number);
if (res.playlist) { if (res.playlist) {
this.userPlayLists = { this.userPlayLists = {
@@ -150,19 +144,23 @@ const useUserDataStore = defineStore("userData", {
}); });
} }
}); });
if (callback && typeof callback === "function") {
callback();
}
this.userPlayLists.isLoading = false; this.userPlayLists.isLoading = false;
} else { } else {
this.userPlayLists.isLoading = false; this.userPlayLists.isLoading = false;
$message.error("用户歌单为空"); $message.info(getLanguageData("getDaraEmpty"));
}
} }
} catch (err) { } catch (err) {
this.userPlayLists.isLoading = false; this.userPlayLists.isLoading = false;
console.error("获取用户歌单时出现错误:" + err); if (this.userLogin) {
$message.error("获取用户歌单时出现错误,请刷新后重试"); console.error(getLanguageData("getDataError"), err);
$message.error(getLanguageData("getDataError"));
}
} }
} else { } else {
$message.error("请登录账号后使用"); $message.error(getLanguageData("needLogin"));
} }
}, },
// 更改用户收藏歌手 // 更改用户收藏歌手
@@ -182,25 +180,27 @@ const useUserDataStore = defineStore("userData", {
size: v.musicSize, size: v.musicSize,
}); });
}); });
if (typeof callback === "function") { if (callback && typeof callback === "function") {
callback(); callback();
} }
this.userArtistLists.isLoading = false; this.userArtistLists.isLoading = false;
} else { } else {
this.userArtistLists.isLoading = false; this.userArtistLists.isLoading = false;
$message.error("用户收藏歌手为空"); $message.info(getLanguageData("getDaraEmpty"));
} }
} catch (err) { } catch (err) {
this.userArtistLists.isLoading = false; this.userArtistLists.isLoading = false;
console.error("用户收藏歌手获取失败:" + err); if (this.userLogin) {
$message.error("用户收藏歌手获取失败,请刷新后重试"); console.error(getLanguageData("getDataError"), err);
$message.error(getLanguageData("getDataError"));
}
} }
} else { } else {
$message.error("请登录账号后使用"); $message.error(getLanguageData("needLogin"));
} }
}, },
// 更改用户收藏专辑 // 更改用户收藏专辑
async setUserAlbumLists() { async setUserAlbumLists(callback) {
if (this.userLogin) { if (this.userLogin) {
try { try {
let offset = 0; let offset = 0;
@@ -222,15 +222,20 @@ const useUserDataStore = defineStore("userData", {
offset += 30; offset += 30;
console.log(totalCount, offset, this.userAlbum.list); console.log(totalCount, offset, this.userAlbum.list);
} }
if (callback && typeof callback === "function") {
callback();
}
this.userAlbum.isLoading = false; this.userAlbum.isLoading = false;
this.userAlbum.has = true; this.userAlbum.has = true;
} catch (err) { } catch (err) {
this.userAlbum.isLoading = false; this.userAlbum.isLoading = false;
console.error("用户收藏专辑获取失败:" + err); if (this.userLogin) {
$message.error("用户收藏专辑获取失败,请刷新后重试"); console.error(getLanguageData("getDataError"), err);
$message.error(getLanguageData("getDataError"));
}
} }
} else { } else {
$message.error("请登录账号后使用"); $message.error(getLanguageData("needLogin"));
} }
}, },
}, },

View File

@@ -60,7 +60,7 @@ body,
} }
} }
.n-modal-container { .n-modal-container {
z-index: 2006 !important; // z-index: 2006 !important;
.n-modal-body-wrapper { .n-modal-body-wrapper {
.n-modal-mask { .n-modal-mask {
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
@@ -121,3 +121,26 @@ body,
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
} }
// 响应式布局
@mixin changeWidth($padding: 10vw) {
.n-layout-header {
padding: 0 $padding;
}
.n-layout-content {
.main {
padding: 24px $padding 54px $padding;
.player {
padding: 0 $padding;
}
}
}
}
@include changeWidth;
/* 小于1200px时 */
@media (max-width: 1200px) {
@include changeWidth($padding: 5vw);
}

View File

@@ -1,27 +0,0 @@
// 主题色
$mainColor: #f55e55;
$mainSecondaryColor: #f55e551f;
// 响应式布局
@mixin changeWidth($padding:10vw) {
.n-layout-header {
padding: 0 $padding;
}
.n-layout-content {
.main {
padding: 24px $padding 54px $padding;
.player {
padding: 0 $padding;
}
}
}
}
@include changeWidth;
/* 小于1200px时 */
@media (max-width: 1200px) {
@include changeWidth($padding:5vw);
}

View File

@@ -77,7 +77,15 @@ class MusicFrequency {
this.source.connect(this.analyser); this.source.connect(this.analyser);
this.analyser.connect(this.context.destination); this.analyser.connect(this.context.destination);
} }
// 断开音频元素和分析器之间的连接,释放音频上下文
disconnect() {
// 断开连接
this.source.disconnect();
this.analyser.disconnect();
// 关闭音频上下文
this.context.close();
}
// 绘制频谱
drawSpectrum() { drawSpectrum() {
// 获取频域数据 // 获取频域数据
this.analyser.getByteFrequencyData(this.output); this.analyser.getByteFrequencyData(this.output);

317
src/utils/Player.js Normal file
View File

@@ -0,0 +1,317 @@
import { Howl, Howler } from "howler";
import { songScrobble } from "@/api/song";
import { musicStore } from "@/store";
import { NIcon } from "naive-ui";
import { MusicNoteFilled } from "@vicons/material";
import getLanguageData from "./getLanguageData";
// 歌曲信息更新定时器
let timeupdateInterval = null;
// 听歌打卡延时器
let scrobbleTimeout = 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 user = JSON.parse(localStorage.getItem("userData"));
const settings = JSON.parse(localStorage.getItem("settingData"));
const isLogin = user.userLogin;
const isMemory = settings.memoryLastPlaybackPosition;
console.log("首次缓冲完成:" + songId + " / 来源:" + sourceId);
if (isMemory) {
sound?.seek(music.persistData.playSongTime.currentTime);
} else {
music.persistData.playSongTime = {
currentTime: 0,
duration: 0,
barMoveDistance: 0,
songTimePlayed: "00:00",
songTimeDuration: "00:00",
};
}
// 取消加载状态
music.isLoadingSong = false;
// 听歌打卡
if (isLogin) {
clearTimeout(scrobbleTimeout);
scrobbleTimeout = setTimeout(() => {
songScrobble(songId, sourceId)
.then((res) => {
console.log("歌曲打卡完成", res);
})
.catch((err) => {
console.error("歌曲打卡失败:" + err);
});
}, 3000);
}
});
// 播放事件
sound?.on("play", () => {
if (!Object.keys(music.getPlaySongData).length) {
$message.error(getLanguageData("songLoadError"));
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(getLanguageData("songPlayError"));
console.error(getLanguageData("songPlayError"));
music.setPlayState(false);
}
if (testNumber < 4) {
if (music.getPlaylists[0]) $getPlaySongData(music.getPlaySongData);
testNumber++;
} else {
$message.error(getLanguageData("songLoadTest"), {
closable: true,
duration: 0,
});
}
});
sound?.on("playerror", () => {
$message.error(getLanguageData("songPlayError"));
console.error(getLanguageData("songPlayError"));
music.setPlayState(false);
});
// 生成频谱
// createSpectrums(sound, music);
// 返回音频对象
return (window.$player = sound);
} catch (err) {
$message.error(getLanguageData("songLoadError"));
console.error(getLanguageData("songLoadError"), 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);
});
};

17
src/utils/getCoverUrl.js Normal file
View File

@@ -0,0 +1,17 @@
/**
* 获取图片的 url
* @param {string} url - 必选参数输入的原始图片url
* @param {number} size - 可选参数需要生成的图片尺寸默认为null
* @return {string} 返回根据 url 和 size 参数生成的图片url
*/
const getCoverUrl = (url, size = null) => {
if (!url) return "/images/pic/default.png";
const sizeUrl = size ? `?param=${size}y${size}` : "";
const imageUrl = url.replace(/^http:/, "https:");
if (imageUrl.endsWith(".jpg")) {
return imageUrl + sizeUrl;
}
return imageUrl;
};
export default getCoverUrl;

View File

@@ -0,0 +1,88 @@
import { settingStore } from "@/store";
/**
* 翻译文本数据
*/
const languageData = {
"zh-CN": {
million: "万",
billion: "亿",
year: "年",
month: "月",
day: "日",
just: "刚刚发布",
minutesAgo: "分钟前",
yesterday: "昨天",
lightMode: "已切换至浅色模式",
darkMode: "已切换至深色模式",
personalFmError: "获取私人 FM 失败",
fmTrashError: "歌曲移除至垃圾桶失败",
needLogin: "请登录账号后使用",
loveSong: "已添加到我喜欢的音乐",
loveSongError: "喜欢音乐时发生错误",
loveSongRepeat: "我喜欢的音乐中已存在该歌曲",
loveSongRemove: "已从我喜欢的音乐中移除",
loveSongRemoveError: "取消喜欢音乐时发生错误",
loveSongNoFound: "我喜欢的列表中未找到该歌曲",
getDataError: "数据获取失败,请刷新后重试",
getDaraEmpty: "数据为空",
getLrcError: "歌词处理出错",
random: "随机播放",
single: "单曲循环",
normal: "列表循环",
playError: "播放出错,请刷新后重试",
addSongToNext: "已添加至下一曲播放",
removeSong: "已从播放列表中移除",
songLoadError: "音乐数据获取失败",
songPlayError: "歌曲播放失败",
songLoadTest: "歌曲重试次数过多,请刷新后重试",
},
en: {
million: "M",
billion: "B",
year: "-",
month: "-",
day: "",
just: "Just released",
minutesAgo: "Minutes ago",
yesterday: "Yesterday",
lightMode: "Switched to light mode",
darkMode: "Switched to dark mode",
personalFmError: "Get Private FM Failed",
fmTrashError: "Song removal to trash failed",
needLogin: "Please login to your account to use",
loveSong: "Added to my favorite music",
loveSongError: "An error occurred while liking music",
loveSongRepeat: "The song already exists in my favorite music",
loveSongRemove: "Removed from my favorite music",
loveSongError: "An error occurred while unliking music",
loveSongNoFound: "The song was not found in my favorite list",
getDataError: "Data acquisition failed, please refresh and try again",
getDaraEmpty: "Data is empty",
getLrcError: "Lyrics processing error",
random: "Random play",
single: "Single loop",
normal: "list loop",
playError: "Playback error, please refresh and try again",
addSongToNext: "has been added to the next song to play",
removeSong: "has been removed from the playlist",
},
};
/**
* 返回翻译文本
* @param {String} type 文本类别
* @returns {Object} 对应语种文本
*/
const getLanguageData = (type) => {
try {
const setting = settingStore();
const language = setting.language;
return languageData[language][type];
} catch (err) {
console.log("Failed to get translated:" + err);
return null;
}
};
export default getLanguageData;

View File

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

View File

@@ -1,193 +1,192 @@
/** /**
* 将接口数据解析出对应数据 * 将歌词接口数据解析出对应数据
* @param {string} data 接口数据 * @param {string} data 接口数据
* @returns {Array} 对应数据 * @returns {Array} 对应数据
*/ */
const parseLyric = (data) => { const parseLyric = (data) => {
// 判断是否具有内容
const checkLyric = (lyric) => (lyric ? (lyric.lyric ? true : false) : false);
// 初始化数据 // 初始化数据
const { lrc, tlyric, romalrc, yrc, ytlrc } = data; const { lrc, tlyric, romalrc, yrc, ytlrc, yromalrc } = data;
const lyrics = lrc ? lrc.lyric : null; const lrcData = {
const otherLyrics = { lrc: lrc?.lyric || null,
tran: tlyric ? tlyric.lyric : null, tlyric: tlyric?.lyric || null,
roma: romalrc ? romalrc.lyric : null, romalrc: romalrc?.lyric || null,
yrc: yrc ? yrc.lyric : null, yrc: yrc?.lyric || null,
ytlrc: ytlrc ? ytlrc.lyric : null, ytlrc: ytlrc?.lyric || null,
yromalrc: yromalrc?.lyric || null,
}; };
// 初始化输出结果 // 初始化输出结果
let result = { const result = {
lrc: [], // 歌词数组 {time:时间,content:歌词} // 是否具有普通翻译
yrc: [], // 逐字歌词数据 hasLrcTran: checkLyric(tlyric),
// 是否具有 // 是否具有普通音
hasTran: tlyric ? (tlyric.lyric ? true : false) : false, hasLrcRoma: checkLyric(romalrc),
// 是否具有音译
hasRoma: romalrc ? (romalrc.lyric ? true : false) : false,
// 是否具有逐字歌词 // 是否具有逐字歌词
hasYrc: yrc ? (yrc.lyric ? true : false) : false, hasYrc: checkLyric(yrc),
// 是否具有逐字翻译
hasYrcTran: checkLyric(ytlrc),
// 是否具有逐字音译
hasYrcRoma: checkLyric(yromalrc),
// 普通歌词数组
lrc: [],
// 逐字歌词数据
yrc: [],
}; };
// 普通歌词数据 // 普通歌词
let lrcData = Lrcsplit(lyrics); if (lrcData.lrc) {
// 翻译歌词数据 result.lrc = parseLrc(lrcData.lrc);
let tranLrcData = null; //判断是否有其他翻译
// 循环遍历 otherLyrics 参数对象 result.lrc = lrcData.tlyric
for (let i in otherLyrics) { ? parseOtherLrc(result.lrc, parseLrc(lrcData.tlyric), "tran")
const element = otherLyrics[i]; : result.lrc;
if (element !== null) { result.lrc = lrcData.romalrc
// 若存在逐字歌词 ? parseOtherLrc(result.lrc, parseLrc(lrcData.romalrc), "roma")
if (i == "yrc" && otherLyrics[i] != null) { : result.lrc;
result[i] = parseYrc(otherLyrics[i]);
continue;
} }
// 若存在翻译 // 逐字歌词
if (i == "ytlrc" && element != null) { if (lrcData.yrc) {
tranLrcData = Lrcsplit(element); result.yrc = parseYrc(lrcData.yrc);
for (let num in tranLrcData) { //判断是否有其他翻译
// 翻译文本对齐 result.yrc = lrcData.ytlrc
let objNum = result["yrc"].findIndex( ? parseOtherLrc(result.yrc, parseLrc(lrcData.ytlrc), "tran")
(o) => o.time == tranLrcData[num].time : result.yrc;
); result.yrc = lrcData.yromalrc
if (objNum != -1) ? parseOtherLrc(result.yrc, parseLrc(lrcData.yromalrc), "roma")
result["yrc"][objNum]["tran"] = tranLrcData[num].content; : result.yrc;
} }
} console.log(result);
// 若存在其他翻译
tranLrcData = Lrcsplit(element);
if (tranLrcData[0]) {
console.log(`歌曲存在 ${i} 翻译`, tranLrcData);
for (let num in tranLrcData) {
// 翻译文本对齐
let objNum = lrcData.findIndex(
(o) => o.time == tranLrcData[num].time
);
if (objNum != -1) lrcData[objNum][i] = tranLrcData[num].content;
}
}
}
}
// 将歌词按时间排序
result.lrc = lrcData.sort((a, b) => {
return a.t - b.t;
});
return result; return result;
}; };
/** /**
* 将歌词字符串解析为歌词对象数组 * 翻译文本对齐
* @param {string} lrc 歌词字符串 * @param {string} lrc 歌词字符串
* @param {string} tranLrc 翻译歌词字符串
* @returns {Array} 包含翻译的歌词对象数组
*/
const parseOtherLrc = (lrc, tranLrc, name) => {
const lyric = lrc;
const tranLyric = tranLrc;
if (lyric[0] && tranLyric[0]) {
lyric.forEach((v) => {
tranLyric.forEach((x) => {
if (
Number(v.time) === Number(x.time) ||
Math.abs(Number(v.time) - Number(x.time)) < 0.6
) {
v[name] = x.content;
}
});
});
}
return lyric;
};
/**
* 普通歌词解析
* @param {string} lyrics 歌词字符串
* @returns {Array} 歌词对象数组 * @returns {Array} 歌词对象数组
*/ */
const Lrcsplit = (lrc) => { const parseLrc = (lyrics) => {
const lyrics = lrc.split("\n"); if (!lyrics) return [];
const lrcData = []; try {
lyrics.forEach((lyric) => { // 匹配时间轴和歌词文本的正则表达式
lyric = lyric.replace(/(^\s*)|(\s*$)/g, ""); const regex = /^\[([^\]]+)\]\s*(.+?)\s*$/;
const time = lyric.substring(lyric.indexOf("[") + 1, lyric.indexOf("]")); // 将歌词字符串按行分割为数组
const timeArr = time.split(":"); const lines = lyrics.split("\n");
if (isNaN(parseInt(timeArr[0]))) { // 对每一行进行转换
for (let i in lyrics) { const parsedLyrics = lines
if (i != "lrc" && i == timeArr[0].toLowerCase()) { // 筛选出包含时间轴和歌词文本的行
lyrics[i] = timeArr[1]; .filter((line) => regex.test(line))
// 转换时间轴和歌词文本为对象
.map((line) => {
const [, time, text] = line.match(regex);
const parts = time.split(":");
const seconds =
Number(parts[0]) * 60 +
Number(parts[1]) +
(parts.length > 2 ? Number(parts[2]) / 1000 : 0);
return { time: Number(seconds.toFixed(2)), content: text.trim() };
})
.filter((c) => c.content.trim() !== "");
// 检查是否为纯音乐,是则返回空数组
if (parsedLyrics.length && /纯音乐,请欣赏/.test(parsedLyrics[0].content)) {
console.log("该歌曲为纯音乐");
return [];
} }
return parsedLyrics;
} catch (err) {
console.error("普通歌词处理出错:" + err);
return [];
} }
} else {
const lyricArr = lyric.match(/\[(\d+:.+?)\]/g);
let start = 0;
for (let k in lyricArr) {
start += lyricArr[k].length;
}
const content = lyric.substring(start);
if (content == "") {
return false;
}
lyricArr.forEach((t) => {
let time = t.substring(1, t.length - 1);
let second = time.split(":");
if (
(parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3) == 0
) {
return;
}
lrcData.push({
time: (parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3),
content: content,
});
});
}
});
return lrcData;
}; };
/** /**
* 逐字歌词解析 * 逐字歌词解析
* @param {string} lyrics 歌词字符串 * @param {string} lyrics 逐字歌词字符串
* @returns {Array} 歌词对象数组 * @returns {Array} 歌词对象数组
*/ */
const parseYrc = (lyrics) => { const parseYrc = (lyrics) => {
// 若无内容,则返回空数组 if (!lyrics) return [];
if (lyrics == undefined) { try {
// 遍历每一行逐字歌词
const parsedLyrics = lyrics
.split("\n")
.map((line) => {
// 匹配每一行中的时间戳信息
const timeReg = /\[(\d+),(\d+)\]/;
const timeMatch = line.match(timeReg);
if (!timeMatch) {
return null;
}
// 解构出起始时间和结束时间
const [_, startTime, endTime] = timeMatch;
if (isNaN(startTime) || isNaN(endTime)) {
return null;
}
// 去除当前行中的时间戳信息,得到歌词内容
const content = line.replace(timeReg, "");
if (!content) {
return null;
}
// 对歌词内容中的时间戳和歌词内容分离
const contentArray = content
.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g)
.filter((c) => c.trim())
.map((c) => {
// 匹配当前片段中的时间戳信息
const timeReg = /\((\d+),(\d+),(\d+)\)/;
const timeMatch = c.match(timeReg);
if (!timeMatch) {
return null;
}
// 解构出时间戳,持续时间和歌词内容
const [_, time, duration] = timeMatch;
const content = c.replace(timeReg, "");
if (!content) {
return null;
}
return {
time: Number(time) / 1000 + 0.1,
duration: Number(duration) / 1000,
content,
};
})
.filter((c) => c);
// 返回当前行解析出的时间信息和歌词内容信息
return {
time: Number(startTime) / 1000,
endTime: Number(endTime) / 1000,
content: contentArray,
};
})
.filter((line) => line);
return parsedLyrics;
} catch (err) {
console.error("逐字歌词处理出错:" + err);
return []; return [];
} }
let lines = lyrics.split("\n");
let parsedLyrics = [];
// 解析每一句
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 创建一个空对象,用于存放当前句的信息
let parsedLine = {
time: undefined, // 开始时间
endTime: undefined, // 结束时间
content: undefined, // 歌词内容
};
// 分离出时间信息,并转换为秒
let timeInfo = line
.substring(line.indexOf("[") + 1, line.indexOf("]"))
.split(",");
// 将开始时间转换为秒
parsedLine.time = Number(timeInfo[0]) / 1000;
// 将结束时间转换为秒
parsedLine.endTime = Number(timeInfo[1]) / 1000;
// 若时间信息不合法,将跳过该句
if (isNaN(parsedLine.time) || isNaN(parsedLine.endTime)) {
continue;
}
// 寻找歌词内容
const lyricArr = line.match(/\[[1-9]\d*,[1-9]\d*]/g);
if (!lyricArr) {
continue;
}
// 去除时间信息,获取歌词内容
let contentArray = [];
// 分离成单个字或词,并解析时间信息
let splitContent = line.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g);
for (let j = 0; j < splitContent.length; j++) {
const splitc = splitContent[j];
if (splitc == "") {
continue;
}
// 创建一个对象,用于存放当前字或词的信息,并添加到当前句的歌词内容中
let contentObj = {
time: undefined, // 开始时间
duration: undefined, // 持续时间
content: "", // 字或词的文本内容
};
// 提取时间和文本信息,并转换为秒
let time = splitc.match(/\([1-9]\d*,[1-9]\d*,\d*\)/);
if (!time) {
continue;
}
let timeArray = time[0].slice(1, -1).split(",");
// 将开始时间转换为秒
contentObj.time = Number(timeArray[0]) / 1000;
// 将持续时间转换为秒
contentObj.duration = Number(timeArray[1]) / 1000;
// 获取字或词的文本内容
contentObj.content = splitc.slice(time[0].length);
contentArray.push(contentObj);
}
parsedLine.content = contentArray;
parsedLyrics.push(parsedLine);
}
return parsedLyrics;
}; };
export default parseLyric; export default parseLyric;

View File

@@ -19,7 +19,8 @@ axios.defaults.withCredentials = true;
// 请求拦截 // 请求拦截
axios.interceptors.request.use( axios.interceptors.request.use(
(request) => { (request) => {
if (!request.hiddenBar) $loadingBar.start(); if (!request.hiddenBar && typeof $loadingBar !== "undefined")
$loadingBar.start();
return request; return request;
}, },
(error) => { (error) => {
@@ -32,7 +33,7 @@ axios.interceptors.request.use(
// 响应拦截 // 响应拦截
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => { (response) => {
$loadingBar.finish(); if (typeof $loadingBar !== "undefined") $loadingBar.finish();
return response.data; return response.data;
}, },
(error) => { (error) => {
@@ -41,7 +42,7 @@ axios.interceptors.response.use(
const data = error.response.data; const data = error.response.data;
switch (error.response.status) { switch (error.response.status) {
case 401: case 401:
console.error("您未登录"); console.error("无权限访问");
break; break;
case 301: case 301:
console.error("请求发生重定向"); console.error("请求发生重定向");

View File

@@ -1,3 +1,5 @@
import getLanguageData from "./getLanguageData";
/** /**
* 歌曲时长时间戳转换 * 歌曲时长时间戳转换
* @param {number} mss 毫秒数 * @param {number} mss 毫秒数
@@ -47,26 +49,30 @@ export const getCommentTime = (t) => {
: userDate.getMinutes(); : userDate.getMinutes();
// 判断时间差 // 判断时间差
if (nowDate - t <= 60000) { if (nowDate - t <= 60000) {
return "刚刚发布"; return getLanguageData("just");
} else if (nowDate - t > 60000 && nowDate - t <= 3600000) { } else if (nowDate - t > 60000 && nowDate - t <= 3600000) {
const pastTimeUnix = nowDate - t; const pastTimeUnix = nowDate - t;
const pastTime = new Date(Number(pastTimeUnix)); const pastTime = new Date(Number(pastTimeUnix));
return `${pastTime.getMinutes()} 分钟前`; return `${pastTime.getMinutes("yesterday")} ${getLanguageData(
"minutesAgo"
)}`;
} else if (todayLast - t > 3600000 && todayLast - t <= 86400000) { } else if (todayLast - t > 3600000 && todayLast - t <= 86400000) {
return `${UH}:${Um}`; return `${UH}:${Um}`;
} else if (todayLast - t > 86400000 && todayLast - t <= 172800000) { } else if (todayLast - t > 86400000 && todayLast - t <= 172800000) {
return `昨天 ${UH}:${Um}`; return `${getLanguageData("yesterday")} ${UH}:${Um}`;
} else if (todayLast - t > 172800000 && todayLast - t <= 31557600000) { } else if (todayLast - t > 172800000 && todayLast - t <= 31557600000) {
return `${userDate.getMonth() + 1}${userDate.getDate()}`; return `${userDate.getMonth() + 1}${getLanguageData(
"month"
)}${userDate.getDate()}${getLanguageData("day")}`;
} else { } else {
return `${userDate.getFullYear()}${ return `${userDate.getFullYear()}${getLanguageData("year")}${
userDate.getMonth() + 1 userDate.getMonth() + 1
}${userDate.getDate()}`; }${getLanguageData("month")}${userDate.getDate()}${getLanguageData("day")}`;
} }
}; };
/** /**
* 过万数字转化 * 过万/亿数字转化
* @param {number} num 需要格式化的数字 * @param {number} num 需要格式化的数字
* @returns {string|number} 格式化后的字符串或原样返回的数字 * @returns {string|number} 格式化后的字符串或原样返回的数字
*/ */
@@ -74,8 +80,17 @@ export const formatNumber = (num) => {
const n = Number(num); const n = Number(num);
if (n === 0 || n < 10000) { if (n === 0 || n < 10000) {
return n; return n;
} else if (n < 100000000) {
const numString = (n / 10000).toFixed(1);
return numString.endsWith(".0")
? numString.slice(0, -2) + getLanguageData("million")
: numString + getLanguageData("million");
} else {
const numString = (n / 100000000).toFixed(1);
return numString.endsWith(".0")
? numString.slice(0, -2) + getLanguageData("billion")
: numString + getLanguageData("billion");
} }
return (n / 10000).toFixed(1) + "万";
}; };
/** /**

View File

@@ -1,46 +1,50 @@
<template> <template>
<div class="playlist" v-if="albumDetail"> <div class="album" v-if="albumDetail">
<div class="left"> <div class="left">
<div class="cover"> <div class="cover">
<n-avatar <n-image
show-toolbar-tooltip
class="coverImg" class="coverImg"
:src=" :src="getCoverUrl(albumDetail.picUrl, 1024)"
albumDetail.picUrl :previewed-img-props="{ style: { borderRadius: '8px' } }"
? albumDetail.picUrl.replace(/^http:/, 'https:') + :preview-src="getCoverUrl(albumDetail.picUrl)"
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
/> />
<img src="/images/pic/album.png" class="album" alt="album" /> <img src="/images/pic/album.png" class="album" alt="album" />
</div> </div>
<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"> <div class="intr">
<span class="name">歌单简介</span> <span class="name">{{
$t("general.name.desc", { name: $t("general.name.album") })
}}</span>
<span class="desc text-hidden"> <span class="desc text-hidden">
{{ albumDetail.description }} {{
albumDetail.description
? albumDetail.description
: $t("other.noDesc")
}}
</span> </span>
<n-button <n-button
class="all-desc"
block block
strong strong
secondary secondary
v-if="albumDetail?.description.length > 70" v-if="albumDetail?.description.length > 70"
@click="albumDescShow = true" @click="albumDescShow = true"
> >
全部简介 {{ $t("general.name.allDesc") }}
</n-button> </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>
<div class="tag" v-if="albumDetail.tags"> <n-space class="tag" v-if="albumDetail.tags">
<n-tag <n-tag
class="tags" class="tags"
round round
@@ -50,36 +54,70 @@
> >
{{ item }} {{ item }}
</n-tag> </n-tag>
</n-space>
<n-space class="control">
<n-button strong secondary round type="primary" @click="playAllSong">
<template #icon>
<n-icon :component="MusicList" />
</template>
{{ $t("general.name.play") }}
</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> </div>
<div class="right"> <div class="right">
<div class="meta"> <div class="meta">
<span class="name">{{ albumDetail.name }}</span> <n-text class="name">{{ albumDetail.name }}</n-text>
<span <n-text
class="creator" class="creator"
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)" @click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
> >
<n-icon :depth="3" :component="People" />
{{ albumDetail.artist.name }} {{ albumDetail.artist.name }}
</span> </n-text>
<div class="time"> <n-space class="time">
<div class="createTime"> <div class="num">
<span class="num">发行时间</span> <n-icon :depth="3" :component="Time" />
{{ getLongTime(albumDetail.publishTime) }} <n-text v-html="getLongTime(albumDetail.publishTime)" />
</div>
<div class="company" v-if="albumDetail.company">
<span class="num">发行公司</span>
{{ albumDetail.company }}
</div> </div>
<div class="num" v-if="albumDetail.company">
<n-icon :depth="3" :component="City" />
<n-text v-html="albumDetail.company" />
</div> </div>
</n-space>
</div> </div>
<DataLists :listData="albumData" hideAlbum /> <DataLists :listData="albumData" hideAlbum />
<!-- 专辑简介 -->
<n-modal
class="s-modal"
v-model:show="albumDescShow"
preset="card"
:title="$t('general.name.desc', { name: $t('general.name.album') })"
:bordered="false"
>
<n-scrollbar>
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div> </div>
</div> </div>
<div class="title" v-else-if="!albumId"> <div class="title" v-else-if="!albumId">
<span class="key">参数不完整</span> <span class="key">{{ $t("general.name.noKeywords") }}</span>
<br /> <br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px"> <n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<div class="loading" v-else> <div class="loading" v-else>
@@ -96,25 +134,113 @@
</template> </template>
<script setup> <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 { useRouter } from "vue-router";
import { getSongTime, getLongTime } from "@/utils/timeTools.js"; import { getSongTime, getLongTime } from "@/utils/timeTools";
import {
MusicList,
LinkTwo,
More,
Like,
Unlike,
People,
Time,
City,
} from "@icon-park/vue-next";
import { userStore, musicStore, settingStore } from "@/store";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
const router = useRouter(); import getCoverUrl from "@/utils/getCoverUrl";
// 歌单数据 const { t } = useI18n();
const router = useRouter();
const user = userStore();
const music = musicStore();
const setting = settingStore();
// 专辑数据
const albumId = ref(router.currentRoute.value.query.id); const albumId = ref(router.currentRoute.value.query.id);
const albumDetail = ref(null); const albumDetail = ref(null);
const albumData = ref([]); const albumData = ref([]);
const albumDescShow = ref(false); 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: t("menu.copy", {
name: t("general.name.album"),
other: t("general.name.link"),
}),
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${albumId.value}`
);
$message.success(t("general.message.copySuccess"));
} catch (err) {
console.error(t("general.message.copyFailure"), err);
$message.error(t("general.message.copyFailure"));
}
} else {
$message.error(t("general.message.notSupported"));
}
},
},
icon: renderIcon(h(LinkTwo)),
},
{
key: "like",
label: isLikeOrDislike(albumId.value)
? t("menu.collection", { name: t("general.name.album") })
: t("menu.cancelCollection", { name: t("general.name.album") }),
show: user.userLogin,
props: {
onClick: () => {
toChangeLike(albumId.value);
},
},
icon: renderIcon(h(isLikeOrDislike(albumId.value) ? Like : Unlike)),
},
];
};
// 获取歌单信息 // 获取歌单信息
const getAlbumData = (id) => { const getAlbumData = (id) => {
getAlbum(id).then((res) => { getAlbum(id).then((res) => {
console.log(res); console.log(res);
// 专辑信息 // 专辑信息
albumDetail.value = res.album; albumDetail.value = res.album;
$setSiteTitle(res.album.name + " - 专辑"); $setSiteTitle(res.album.name + " - " + t("general.name.album"));
// 专辑歌曲 // 专辑歌曲
if (res.songs) { if (res.songs) {
albumData.value = []; albumData.value = [];
@@ -128,19 +254,113 @@ const getAlbumData = (id) => {
alia: v.alia, alia: v.alia,
time: getSongTime(v.dt), time: getSongTime(v.dt),
fee: v.fee, fee: v.fee,
sourceId: id,
pc: v.pc ? v.pc : null, pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null, mv: v.mv ? v.mv : null,
}); });
}); });
} else { } else {
$message.error("获取专辑歌曲失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
// 播放专辑所有歌曲
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($message.error(t("general.message.operationFailed")), err);
$message.error($message.error(t("general.message.operationFailed")));
}
};
// 收藏/取消收藏
const toChangeLike = async (id) => {
const type = isLikeOrDislike(id) ? 1 : 2;
const likeMsg = t("general.name.album");
const isThereASpace = setting.language === "zh-CN" ? "" : " ";
try {
const res = await likeAlbum(type, id);
if (res.code === 200) {
$message.success(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.success") })
: t("menu.cancelCollection", { name: t("general.dialog.success") })
}`
);
user.setUserAlbumLists(() => {
setDropdownOptions();
});
} else {
$message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
}
} catch (err) {
$message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
console.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`,
err
);
}
};
onMounted(() => { onMounted(() => {
if (albumId.value) { if (albumId.value) {
getAlbumData(albumId.value); getAlbumData(albumId.value);
if (
user.userLogin &&
!user.getUserAlbumLists.has &&
!user.getUserAlbumLists.isLoading
) {
user.setUserAlbumLists(() => {
setDropdownOptions();
});
} else {
setDropdownOptions();
}
} }
}); });
@@ -157,7 +377,7 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.playlist, .album,
.loading { .loading {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -172,22 +392,59 @@ watch(
align-items: flex-start; align-items: flex-start;
position: sticky; position: sticky;
top: 24px; top: 24px;
@media (max-width: 990px) {
margin-right: 0;
width: 30vw;
}
.cover { .cover {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
.n-avatar {
border-radius: 8px;
width: 80%; width: 80%;
height: 80%; height: 80%;
border-radius: 8px;
position: relative;
transition: transform 0.3s;
&:active {
transform: scale(0.95);
}
.coverImg {
border-radius: 8px;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
:deep(img) {
width: 100%;
}
} }
.album { .album {
height: 100%; height: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
right: 4%; right: -20%;
}
}
.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;
}
.creator {
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
} }
} }
.intr { .intr {
@@ -208,13 +465,22 @@ watch(
} }
.tag { .tag {
margin-top: 20px; margin-top: 20px;
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
.tags { .tags {
margin-right: 8px; line-height: 0;
font-size: 13px; 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;
} }
} }
} }
@@ -230,6 +496,8 @@ watch(
font-weight: bold; font-weight: bold;
} }
.creator { .creator {
display: flex;
align-items: center;
margin-top: 6px; margin-top: 6px;
font-size: 16px; font-size: 16px;
opacity: 0.8; opacity: 0.8;
@@ -237,7 +505,10 @@ watch(
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
}
.n-icon {
margin-right: 6px;
} }
} }
.time { .time {
@@ -245,15 +516,18 @@ watch(
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@media (max-width: 768px) { @media (max-width: 1100px) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.num { .num {
color: #999; // color: #999;
display: flex;
flex-direction: row;
align-items: center;
.n-icon {
margin-right: 6px;
} }
div {
margin-right: 12px;
} }
} }
} }
@@ -271,20 +545,110 @@ watch(
@media (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
.left { .left {
margin-bottom: 12px; position: relative;
position: static; top: 0;
width: 60vw; width: 100%;
height: 30vw;
max-width: none; max-width: 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 { .intr {
margin-top: 0;
padding-left: 0;
.name,
.all-desc {
display: none; display: none;
} }
.desc {
-webkit-line-clamp: 3;
margin-bottom: 0;
}
}
.control {
position: absolute;
left: 0;
bottom: -60px;
}
}
} }
.right { .right {
margin-top: 80px;
.meta { .meta {
display: none;
}
}
}
@media (max-width: 540px) {
.left {
.cover {
margin-right: 44px;
}
.meta {
.title {
margin-bottom: 0;
.name { .name {
font-size: 26px; 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;
} }
} }
} }

View File

@@ -13,7 +13,7 @@
<script setup> <script setup>
import { getArtistAblums } from "@/api/artist"; import { getArtistAblums } from "@/api/artist";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js"; import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const router = useRouter(); const router = useRouter();
@@ -51,7 +51,7 @@ const getArtistAblumsData = (id, limit = 30, offset = 0) => {
$message.error("搜索内容为空"); $message.error("搜索内容为空");
} }
// 请求后回顶 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -1,11 +1,15 @@
<template> <template>
<div class="all-songs"> <div class="all-songs">
<div class="title" v-if="artistId"> <div class="title" v-if="artistId">
<span class="key">{{ artistName ? artistName : "未知歌手" }}</span> <template v-if="artistData[0]">
<span>全部歌曲</span> <span class="key">{{
artistName ? artistName : $t("general.name.unknownArtist")
}}</span>
<span>{{ $t("general.name.allSong") }} </span>
</template>
</div> </div>
<div class="title" v-else> <div class="title" v-else>
<span class="key">未提供所需信息</span> <span class="key">{{ $t("general.name.noKeywords") }}</span>
<br /> <br />
<n-button <n-button
strong strong
@@ -13,7 +17,7 @@
@click="router.go(-1)" @click="router.go(-1)"
style="margin-top: 20px" style="margin-top: 20px"
> >
返回上一级 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<div class="songs" v-if="artistId"> <div class="songs" v-if="artistId">
@@ -30,13 +34,15 @@
</template> </template>
<script setup> <script setup>
import { getArtistAllSongs } from "@/api/artist"; import { getArtistDetail, getArtistAllSongs } from "@/api/artist";
import { getMusicDetail } from "@/api/song"; import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js"; import { getSongTime } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 歌手信息 // 歌手信息
@@ -51,15 +57,25 @@ const pageNumber = ref(
: 1 : 1
); );
// 获取歌手名称
const getArtistDetailData = (id) => {
getArtistDetail(id).then((res) => {
artistName.value = res.data.artist.name;
});
};
// 获取歌手信息 // 获取歌手信息
const getArtistAllSongsData = (id, limit = 30, offset = 0, order = "hot") => { const getArtistAllSongsData = (id, limit = 30, offset = 0, order = "hot") => {
getArtistAllSongs(id, limit, offset, order).then((res) => { if (!id) return false;
getArtistAllSongs(id, limit, offset, order)
.then((res) => {
console.log(res); console.log(res);
// 获取歌手名称
getArtistDetailData(id);
// 全部歌曲数据
if (res.songs[0]) { if (res.songs[0]) {
// 数据总数 // 数据总数
totalCount.value = res.total; totalCount.value = res.total;
// 歌手名称
artistName.value = res.songs[0].ar[0].name;
// 列表数据 // 列表数据
const ids = res.songs.map((obj) => obj.id); const ids = res.songs.map((obj) => obj.id);
getMusicDetail(ids.join(",")).then((res) => { getMusicDetail(ids.join(",")).then((res) => {
@@ -81,10 +97,15 @@ const getArtistAllSongsData = (id, limit = 30, offset = 0, order = "hot") => {
}); });
}); });
} else { } else {
$message.error("歌手全部歌曲为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
})
.catch((err) => {
router.go(-1);
console.error(t("general.message.acquisitionFailed"), err);
$message.error(t("general.message.acquisitionFailed"));
}); });
}; };

View File

@@ -17,15 +17,15 @@
<div class="num"> <div class="num">
<n-text class="musicSize" @click="tabChange('songs')"> <n-text class="musicSize" @click="tabChange('songs')">
<n-icon :component="MusicNoteFilled" /> <n-icon :component="MusicNoteFilled" />
{{ artistData.musicSize }} 首歌 {{ $t("general.name.songSize", { size: artistData.musicSize }) }}
</n-text> </n-text>
<n-text class="albumSize" @click="tabChange('albums')"> <n-text class="albumSize" @click="tabChange('albums')">
<n-icon :component="AlbumFilled" /> <n-icon :component="AlbumFilled" />
{{ artistData.albumSize }} 张专辑 {{ $t("general.name.albumSize", { size: artistData.albumSize }) }}
</n-text> </n-text>
<n-text class="mvSize" @click="tabChange('videos')"> <n-text class="mvSize" @click="tabChange('videos')">
<n-icon :component="VideocamRound" /> <n-icon :component="VideocamRound" />
{{ artistData.mvSize }} MV {{ $t("general.name.mvSize", { size: artistData.mvSize }) }}
</n-text> </n-text>
</div> </div>
<n-text class="desc text-hidden" @click="artistDescShow = true"> <n-text class="desc text-hidden" @click="artistDescShow = true">
@@ -51,7 +51,13 @@
" "
/> />
</template> </template>
{{ artistLikeBtn ? "收藏歌手" : "取消收藏歌手" }} {{
artistLikeBtn
? $t("menu.collection", { name: $t("general.name.artists") })
: $t("menu.cancelCollection", {
name: $t("general.name.artists"),
})
}}
</n-button> </n-button>
</n-space> </n-space>
<!-- 歌手介绍 --> <!-- 歌手介绍 -->
@@ -59,7 +65,7 @@
class="s-modal" class="s-modal"
v-model:show="artistDescShow" v-model:show="artistDescShow"
preset="card" preset="card"
title="歌手介绍" :title="$t('general.name.artistDesc')"
:bordered="false" :bordered="false"
> >
<n-scrollbar> <n-scrollbar>
@@ -69,7 +75,7 @@
</div> </div>
</div> </div>
<div class="error" v-else-if="!artistId"> <div class="error" v-else-if="!artistId">
<n-text>参数不完整</n-text> <n-text>{{ $t("general.name.noKeywords") }}</n-text>
<br /> <br />
<n-button <n-button
strong strong
@@ -77,7 +83,7 @@
@click="router.go(-1)" @click="router.go(-1)"
style="margin-top: 20px" style="margin-top: 20px"
> >
返回上一级 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<n-tabs <n-tabs
@@ -87,8 +93,8 @@
v-model:value="tabValue" v-model:value="tabValue"
v-if="artistData" v-if="artistData"
> >
<n-tab name="songs"> 热门单曲 </n-tab> <n-tab name="songs"> {{ $t("general.name.hotSong") }} </n-tab>
<n-tab name="albums"> 专辑 </n-tab> <n-tab name="albums"> {{ $t("general.name.album") }} </n-tab>
<n-tab name="videos"> MV </n-tab> <n-tab name="videos"> MV </n-tab>
</n-tabs> </n-tabs>
<main class="content" v-if="artistData"> <main class="content" v-if="artistData">
@@ -117,7 +123,9 @@ import {
PersonAddAlt1Round, PersonAddAlt1Round,
PersonRemoveAlt1Round, PersonRemoveAlt1Round,
} from "@vicons/material"; } from "@vicons/material";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const user = userStore(); const user = userStore();
@@ -146,17 +154,15 @@ const getArtistDetailData = (id) => {
musicSize: res.data.artist.musicSize, musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize, mvSize: res.data.artist.mvSize,
}; };
$setSiteTitle(res.data.artist.name + " - 歌手"); $setSiteTitle(res.data.artist.name + " - " + t("general.name.artists"));
// 请求后回顶 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}) })
.catch((err) => { .catch((err) => {
router.go(-1); router.go(-1);
console.error("歌手信息获取失败:" + err); console.error(t("general.message.acquisitionFailed"), err);
$message.error("歌手信息获取失败"); $message.error(t("general.message.acquisitionFailed"));
}); });
} else {
$message.error("参数不完整");
} }
}; };
@@ -193,13 +199,23 @@ const toLikeArtist = (data) => {
likeArtist(type, data.id).then((res) => { likeArtist(type, data.id).then((res) => {
if (res.code === 200) { if (res.code === 200) {
$message.success( $message.success(
`${data.name}${type == 1 ? "收藏成功" : "取消收藏成功"}` `${data.name} ${
type == 1
? t("menu.collection", { name: t("general.dialog.success") })
: t("menu.cancelCollection", { name: t("general.dialog.success") })
}`
); );
user.setUserArtistLists(() => { user.setUserArtistLists(() => {
artistLikeBtn.value = isLikeOrDislike(artistId.value); artistLikeBtn.value = isLikeOrDislike(artistId.value);
}); });
} else { } else {
$message.error(`${data.name}${type == 1 ? "收藏失败" : "取消收藏失败"}`); $message.error(
`${data.name} ${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
} }
}); });
}; };
@@ -308,13 +324,13 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
.n-icon { .n-icon {
color: $mainColor; color: var(--main-color);
transform: translateY(-1px); transform: translateY(-1px);
font-size: 16px; font-size: 16px;
margin-right: 4px; margin-right: 4px;
} }
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }

View File

@@ -10,7 +10,7 @@
round round
@click="router.push(`/all-songs?id=${artistId}&page=1`)" @click="router.push(`/all-songs?id=${artistId}&page=1`)"
> >
全部歌曲 {{ $t("general.name.allSong") }}
</n-button> </n-button>
</n-space> </n-space>
</div> </div>
@@ -19,7 +19,7 @@
<script setup> <script setup>
import { getArtistSongs } from "@/api/artist"; import { getArtistSongs } from "@/api/artist";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js"; import { getSongTime } from "@/utils/timeTools";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
const router = useRouter(); const router = useRouter();
@@ -73,8 +73,8 @@ watch(
font-size: 16px; font-size: 16px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);

View File

@@ -13,7 +13,7 @@
<script setup> <script setup>
import { getArtistVideos } from "@/api/artist"; import { getArtistVideos } from "@/api/artist";
import { useRouter } from "vue-router"; 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 VideoLists from "@/components/DataList/VideoLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
@@ -59,8 +59,8 @@ const getArtistVideosData = (id, limit = 30, offset = 0) => {
} else { } else {
$message.error("搜索内容为空"); $message.error("搜索内容为空");
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -6,17 +6,18 @@
class="goback" class="goback"
@click="router.push(`/comment?id=${music.getPlaySongData.id}&page=1`)" @click="router.push(`/comment?id=${music.getPlaySongData.id}&page=1`)"
content-style="padding: 6px" content-style="padding: 6px"
>前往当前播放歌曲 >
{{ $t("general.name.toCurrentlySong") }}
</n-card> </n-card>
</Transition> </Transition>
<div class="title" v-if="songId"> <div class="title" v-if="songId">
<span class="key">全部评论</span> <span class="key">{{ $t("general.name.allComments") }}</span>
<n-card class="song"> <n-card class="song">
<SmallSongData :getDataByID="songId" /> <SmallSongData :getDataByID="songId" />
</n-card> </n-card>
</div> </div>
<div class="title" v-else> <div class="title" v-else>
<span class="key">缺少必要参数</span> <span class="key">{{ $t("general.name.noKeywords") }}</span>
<br /> <br />
<n-button <n-button
strong strong
@@ -24,12 +25,12 @@
@click="router.go(-1)" @click="router.go(-1)"
style="margin-top: 20px" style="margin-top: 20px"
> >
返回上一页 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<div class="commentData" v-if="songId && commentData.allComments[0]"> <div class="commentData" v-if="songId && commentData.allComments[0]">
<div class="hotComments" v-if="commentData.hotComments[0]"> <div class="hotComments" v-if="commentData.hotComments[0]">
<n-h6 prefix="bar"> 热门评论 </n-h6> <n-h6 prefix="bar"> {{ $t("general.name.hotComments") }} </n-h6>
<div class="loading" v-if="!commentData.hotComments[0]"> <div class="loading" v-if="!commentData.hotComments[0]">
<n-skeleton text :repeat="3" /> <n-skeleton text :repeat="3" />
<n-skeleton text style="width: 60%" /> <n-skeleton text style="width: 60%" />
@@ -44,8 +45,8 @@
</div> </div>
<div class="allComments" ref="allCommentsRef"> <div class="allComments" ref="allCommentsRef">
<n-h6 prefix="bar"> <n-h6 prefix="bar">
全部评论 {{ $t("general.name.allComments") }}
<span class="count">{{ commentsCount }} </span> <span class="count">{{ commentsCount }} +</span>
</n-h6> </n-h6>
<div class="loading" v-if="!commentData.allComments[0]"> <div class="loading" v-if="!commentData.allComments[0]">
<n-skeleton text :repeat="3" /> <n-skeleton text :repeat="3" />
@@ -73,9 +74,12 @@
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getComment } from "@/api/comment"; import { getComment } from "@/api/comment";
import { useI18n } from "vue-i18n";
import SmallSongData from "@/components/DataList/SmallSongData.vue"; import SmallSongData from "@/components/DataList/SmallSongData.vue";
import Comment from "@/components/Comment/index.vue"; import Comment from "@/components/Comment/index.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
@@ -114,11 +118,11 @@ const getCommentData = (id, offset = 0) => {
commentData.allComments = res.comments; commentData.allComments = res.comments;
commentsCount.value = res.total; commentsCount.value = res.total;
} else { } else {
$message.info("暂无评论"); $message.error(t("general.message.acquisitionFailed"));
router.go(-1); router.go(-1);
} }
// 滚动至顶部 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };
@@ -134,7 +138,7 @@ const pageNumberChange = (val) => {
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle("全部评论"); $setSiteTitle(t("general.name.allComments"));
// 获取评论数据 // 获取评论数据
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20); if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
}); });

View File

@@ -6,9 +6,11 @@
<n-text class="num" v-html="new Date().getDate()" /> <n-text class="num" v-html="new Date().getDate()" />
</div> </div>
<div class="right"> <div class="right">
<n-gradient-text class="big" type="danger"> 每日推荐 </n-gradient-text> <n-gradient-text class="big" type="danger">
{{ $t("home.modules.dailySongs.title") }}
</n-gradient-text>
<n-text class="tip" :depth="3"> <n-text class="tip" :depth="3">
根据你的音乐口味生成 · 每天 6:00 更新 {{ $t("home.modules.dailySongs.subtitle") }}
</n-text> </n-text>
</div> </div>
</div> </div>
@@ -20,24 +22,28 @@
import { getDailySongs } from "@/api/home"; import { getDailySongs } from "@/api/home";
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { CalendarTodayFilled } from "@vicons/material"; import { CalendarTodayFilled } from "@vicons/material";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
const { t } = useI18n();
const music = musicStore(); const music = musicStore();
// 获取每日推荐数据 // 获取每日推荐数据
const getDailySongsData = () => { const getDailySongsData = () => {
if (music.getDailySongs.length === 0) {
getDailySongs().then((res) => { getDailySongs().then((res) => {
if (res.data.dailySongs) { if (res.data.dailySongs) {
music.setDailySongs(res.data.dailySongs); music.setDailySongs(res.data.dailySongs);
} else { } else {
$message.error("每日推荐获取失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle("每日推荐"); $setSiteTitle(t("home.modules.dailySongs.title"));
if (music.getDailySongs.length === 0) getDailySongsData(); getDailySongsData();
}); });
</script> </script>

View File

@@ -41,7 +41,7 @@
:loading="loading" :loading="loading"
@click="loadingMore" @click="loadingMore"
> >
加载更多 {{ $t("general.name.loadMore") }}
</n-button> </n-button>
</n-space> </n-space>
</div> </div>
@@ -49,14 +49,16 @@
<script setup> <script setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { getArtistList } from "@/api/artist"; import { getArtistList } from "@/api/artist";
import ArtistLists from "@/components/DataList/ArtistLists.vue"; import ArtistLists from "@/components/DataList/ArtistLists.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 歌手标签数据 // 歌手标签数据
const artistInitials = [ const artistInitials = [
{ key: "-1", value: "热门" }, { key: "-1", value: t("general.type.hot") },
...Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 65)).map( ...Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 65)).map(
(v) => ({ (v) => ({
key: v, key: v,
@@ -73,24 +75,24 @@ const artistInitialChoose = ref(
// 歌手分类数据 // 歌手分类数据
const artistTypeNames = [ const artistTypeNames = [
"全部", t("general.type.all"),
"华语", t("general.type.china"),
"华语男", t("general.type.chinaMale"),
"华语女", t("general.type.chinaFemale"),
"华语组合", t("general.type.chinaGroup"),
"欧美", t("general.type.western"),
"欧美男", t("general.type.westernMale"),
"欧美女", t("general.type.westernFemale"),
"欧美组合", t("general.type.westernGroup"),
"日本", t("general.type.japan"),
"日本男", t("general.type.japanMale"),
"日本女", t("general.type.japanFemale"),
"日本组合", t("general.type.japanGroup"),
"韩国", t("general.type.korea"),
"韩国男", t("general.type.koreaMale"),
"韩国女", t("general.type.koreaFemale"),
"韩国组合", t("general.type.koreaGroup"),
"其他", t("general.type.other"),
]; ];
const artistType = [-1, -1, 1, 2, 3, -1, 1, 2, 3, -1, 1, 2, 3, -1, 1, 2, 3, -1]; const artistType = [-1, -1, 1, 2, 3, -1, 1, 2, 3, -1, 1, 2, 3, -1, 1, 2, 3, -1];
const artistArea = [ const artistArea = [
@@ -132,7 +134,7 @@ const getArtistListData = (
}); });
} else { } else {
hasMore.value = false; hasMore.value = false;
$message.error("歌手内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
@@ -167,7 +169,6 @@ const artistTypeChange = (index) => {
const loadingMore = () => { const loadingMore = () => {
loading.value = true; loading.value = true;
artistsOffset.value += 30; artistsOffset.value += 30;
if (artistsOffset.value >= 300) $message.info("太多了吧 😲");
getArtistListData( getArtistListData(
artistType[artistTypeNamesChoose.value], artistType[artistTypeNamesChoose.value],
artistArea[artistTypeNamesChoose.value], artistArea[artistTypeNamesChoose.value],
@@ -200,7 +201,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
$setSiteTitle("发现 - 歌手"); $setSiteTitle(t("nav.discover") + " - " + t("nav.discoverChildren.artists"));
// 获取歌手数据 // 获取歌手数据
getArtistListData( getArtistListData(
artistType[artistTypeNamesChoose.value], artistType[artistTypeNamesChoose.value],
@@ -240,8 +241,8 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.9); transform: scale(0.9);
@@ -262,8 +263,8 @@ onMounted(() => {
font-size: 16px; font-size: 16px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);

View File

@@ -1,15 +1,15 @@
<template> <template>
<div class="discover"> <div class="discover">
<n-text class="title">发现</n-text> <n-text class="title">{{ $t("nav.discover") }}</n-text>
<n-tabs <n-tabs
class="main-tab" class="main-tab"
type="segment" type="segment"
@update:value="tabChange" @update:value="tabChange"
v-model:value="tabValue" v-model:value="tabValue"
> >
<n-tab name="playlists"> 歌单 </n-tab> <n-tab name="playlists">{{ $t("nav.discoverChildren.playlists") }}</n-tab>
<n-tab name="toplists"> 排行榜 </n-tab> <n-tab name="toplists">{{ $t("nav.discoverChildren.toplists") }}</n-tab>
<n-tab name="artists"> 歌手 </n-tab> <n-tab name="artists">{{ $t("nav.discoverChildren.artists") }}</n-tab>
</n-tabs> </n-tabs>
<main class="content"> <main class="content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">

View File

@@ -8,7 +8,7 @@
class="cat" class="cat"
icon-placement="right" icon-placement="right"
round round
@click="catModelShow = true" @click="catModalShow = true"
> >
<template #icon> <template #icon>
<n-icon class="up" :component="ChevronRightRound" /> <n-icon class="up" :component="ChevronRightRound" />
@@ -17,13 +17,13 @@
</n-button> </n-button>
<n-modal <n-modal
class="s-modal" class="s-modal"
v-model:show="catModelShow" v-model:show="catModalShow"
preset="card" preset="card"
title="歌单分类" :title="$t('general.name.playlistType')"
:bordered="false" :bordered="false"
> >
<template #header> <template #header>
歌单分类 {{ $t("general.name.playlistType") }}
<n-tag <n-tag
round round
class="tag" class="tag"
@@ -32,14 +32,15 @@
marginLeft: '12px', marginLeft: '12px',
fontSize: '12px', fontSize: '12px',
transform: 'translateY(-2px)', transform: 'translateY(-2px)',
cursor: 'pointer',
}" }"
:bordered="false" :bordered="false"
@click="changeTagName('全部歌单')" @click="changeTagName('全部歌单')"
> >
全部歌单 {{ $t("general.name.allPLaylist") }}
</n-tag> </n-tag>
</template> </template>
<n-scrollbar style="max-height: 80vh"> <n-scrollbar>
<div class="all-cat" v-if="music.catList?.sub[0]"> <div class="all-cat" v-if="music.catList?.sub[0]">
<n-list> <n-list>
<n-list-item <n-list-item
@@ -75,7 +76,9 @@
</n-list-item> </n-list-item>
</n-list> </n-list>
</div> </div>
<div class="error" v-else>分类数据获取失败</div> <div class="error" v-else>
{{ $t("general.message.acquisitionFailed") }}
</div>
</n-scrollbar> </n-scrollbar>
</n-modal> </n-modal>
<!-- 精品歌单开关 --> <!-- 精品歌单开关 -->
@@ -83,7 +86,7 @@
v-if="getHaveHqPlaylists(music.highqualityCatList, catName)" v-if="getHaveHqPlaylists(music.highqualityCatList, catName)"
align="center" align="center"
> >
<n-text>精品歌单</n-text> <n-text>{{ $t("general.name.bestPlaylist") }}</n-text>
<n-switch <n-switch
v-model:value="hqPLayListOpen" v-model:value="hqPLayListOpen"
@update:value="hqPLayListChange" @update:value="hqPLayListChange"
@@ -115,7 +118,7 @@
} }
" "
> >
加载更多 {{ $t("general.name.loadMore") }}
</n-button> </n-button>
</n-space> </n-space>
</div> </div>
@@ -126,15 +129,17 @@ import { ChevronRightRound, LocalFireDepartmentRound } from "@vicons/material";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { getHighqualityPlaylist, getTopPlaylist } from "@/api/playlist"; import { getHighqualityPlaylist, getTopPlaylist } from "@/api/playlist";
import { formatNumber } from "@/utils/timeTools.js"; import { formatNumber } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
// 分类数据 // 分类数据
const catModelShow = ref(false); const catModalShow = ref(false);
const catName = ref( const catName = ref(
router.currentRoute.value.query.cat router.currentRoute.value.query.cat
? router.currentRoute.value.query.cat ? router.currentRoute.value.query.cat
@@ -205,10 +210,10 @@ const getPlaylistData = (cat = "全部歌单", limit = 30, offset = 0) => {
}); });
}); });
} else { } else {
$message.error("歌单列表为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };
@@ -239,7 +244,7 @@ const getHqPlaylistData = (cat = "全部歌单", limit = 30) => {
}); });
} else { } else {
hasMore.value = false; hasMore.value = false;
$message.error("精品歌单列表为空"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
@@ -254,7 +259,7 @@ const changeTagName = (name) => {
page: 1, page: 1,
}, },
}); });
catModelShow.value = false; catModalShow.value = false;
}; };
// 排序方式变化 // 排序方式变化
@@ -299,6 +304,7 @@ watch(
: false; : false;
if (val.name == "dsc-playlists") { if (val.name == "dsc-playlists") {
if (hqPLayListOpen.value) { if (hqPLayListOpen.value) {
playlistsData.value = [];
getHqPlaylistData(catName.value); getHqPlaylistData(catName.value);
} else { } else {
pageNumber.value = val.query.page ? Number(val.query.page) : 1; pageNumber.value = val.query.page ? Number(val.query.page) : 1;
@@ -313,7 +319,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
$setSiteTitle("发现 - 歌单"); $setSiteTitle(t("nav.discover") + " - " + t("general.name.playlist"));
// 获取歌单分类 // 获取歌单分类
if (!music.catList.sub || !music.highqualityCatList[0]) if (!music.catList.sub || !music.highqualityCatList[0])
music.setCatList(true); music.setCatList(true);
@@ -348,8 +354,8 @@ onMounted(() => {
font-size: 16px; font-size: 16px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
@@ -373,14 +379,14 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
.icon { .icon {
color: $mainColor; color: var(--main-color);
font-size: 16px; font-size: 16px;
margin-left: 4px; margin-left: 4px;
transform: translateY(-1px); transform: translateY(-1px);

View File

@@ -1,6 +1,8 @@
<template> <template>
<div class="toplists"> <div class="toplists">
<n-divider v-if="toplistData.officialList[0]">官方榜</n-divider> <n-divider v-if="toplistData.officialList[0]">
{{ $t("nav.officialList") }}
</n-divider>
<Transition mode="out-in"> <Transition mode="out-in">
<n-grid <n-grid
class="official" class="official"
@@ -49,7 +51,7 @@
</n-gi> </n-gi>
</n-grid> </n-grid>
</Transition> </Transition>
<n-divider>全球榜</n-divider> <n-divider>{{ $t("nav.globalList") }}</n-divider>
<CoverLists :listData="toplistData.globalList" listType="topList" /> <CoverLists :listData="toplistData.globalList" listType="topList" />
</div> </div>
</template> </template>
@@ -57,9 +59,11 @@
<script setup> <script setup>
import { getToplist } from "@/api/album"; import { getToplist } from "@/api/album";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js"; import { formatNumber } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 排行榜数据 // 排行榜数据
@@ -91,13 +95,13 @@ const getToplistData = () => {
}); });
console.log(toplistData); console.log(toplistData);
} else { } else {
$message.error("排行榜获取失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle("发现 - 排行榜"); $setSiteTitle(t("nav.discover") + " - " + t("nav.discoverChildren.toplists"));
getToplistData(); getToplistData();
}); });
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="history"> <div class="history">
<div class="title" v-if="music.getPlayHistory[0]"> <div class="title" v-if="music.getPlayHistory[0]">
<span class="key">播放历史</span> <span class="key">{{ $t("nav.avatar.history") }}</span>
</div> </div>
<div class="title" v-else> <div class="title" v-else>
<span class="key">暂无播放历史</span> <span class="key">{{ $t("other.noHistory") }}</span>
<br /> <br />
<n-button <n-button
strong strong
@@ -12,7 +12,7 @@
@click="router.go(-1)" @click="router.go(-1)"
style="margin-top: 20px" style="margin-top: 20px"
> >
返回上一页 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<DataLists <DataLists
@@ -20,7 +20,9 @@
:listData="music.getPlayHistory" :listData="music.getPlayHistory"
/> />
<n-divider v-if="music.getPlayHistory[0]" class="tip" dashed> <n-divider v-if="music.getPlayHistory[0]" class="tip" dashed>
<n-text :depth="3" style="font-size: 12px">仅显示最近 100 </n-text> <n-text :depth="3" style="font-size: 12px">
{{ $t("other.justShow", { num: 100 }) }}
</n-text>
</n-divider> </n-divider>
</div> </div>
</template> </template>
@@ -28,13 +30,15 @@
<script setup> <script setup>
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
const { t } = useI18n();
const music = musicStore(); const music = musicStore();
const router = useRouter(); const router = useRouter();
onMounted(() => { onMounted(() => {
$setSiteTitle("播放历史"); $setSiteTitle(t("nav.avatar.history"));
}); });
</script> </script>

View File

@@ -2,14 +2,24 @@
<div class="home"> <div class="home">
<Banner v-if="setting.bannerShow" /> <Banner v-if="setting.bannerShow" />
<!-- 个性化推荐 --> <!-- 个性化推荐 -->
<n-h3 class="title" prefix="bar"> 专属推荐 </n-h3> <n-h3 class="title" prefix="bar">{{ $t("home.title.exclusive") }}</n-h3>
<n-grid class="recommendation" cols="4" item-responsive x-gap="20"> <n-grid class="recommend" :x-gap="20" :cols="2">
<n-grid-item span="1 950:2"> <n-gi class="rec-left">
<!-- 每日推荐 -->
<PaDailySongs /> <PaDailySongs />
</n-grid-item> <!-- 其他推荐 -->
<n-grid-item span="3 950:2"> <n-grid class="rec-func" x-gap="12" :cols="2">
<n-gi>
<PaRadar />
</n-gi>
<n-gi>
<PaLikeSongs />
</n-gi>
</n-grid>
</n-gi>
<n-gi class="rec-right">
<PaPersonalFm /> <PaPersonalFm />
</n-grid-item> </n-gi>
</n-grid> </n-grid>
<!-- 公共推荐 --> <!-- 公共推荐 -->
<PaPlayLists /> <PaPlayLists />
@@ -26,11 +36,16 @@ import PaArtists from "@/components/Personalized/PaArtists.vue";
import PaAlbum from "@/components/Personalized/PaAlbum.vue"; import PaAlbum from "@/components/Personalized/PaAlbum.vue";
import PaDailySongs from "@/components/Personalized/PaDailySongs.vue"; import PaDailySongs from "@/components/Personalized/PaDailySongs.vue";
import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue"; import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
import PaRadar from "@/components/Personalized/PaRadar.vue";
import PaLikeSongs from "@/components/Personalized/PaLikeSongs.vue";
const setting = settingStore(); const setting = settingStore();
onMounted(() => { onMounted(() => {
$setSiteTitle("SPlayer"); if (typeof $setSiteTitle !== "undefined")
$setSiteTitle(import.meta.env.VITE_SITE_TITLE);
// 回顶
if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
</script> </script>
@@ -42,10 +57,25 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
padding-left: 16px; padding-left: 16px;
} }
:deep(.recommendation) { .recommend {
@media (max-width: 750px) { @media (max-width: 850px) {
grid-template-columns: repeat(1, minmax(0px, 1fr)) !important; grid-template-columns: repeat(1, minmax(0px, 1fr)) !important;
gap: 20px 0 !important; gap: 20px 0 !important;
.rec-left {
display: flex;
flex-direction: column-reverse;
.padailysongs {
margin-bottom: 0;
margin-top: 20px;
}
}
}
.rec-left,
.rec-right {
height: 200px;
}
.rec-func {
height: 70px;
} }
} }
} }

View File

@@ -2,7 +2,7 @@
<div class="login"> <div class="login">
<div class="title"> <div class="title">
<img src="/images/logo/favicon.png" alt="logo" /> <img src="/images/logo/favicon.png" alt="logo" />
<span>登录云音乐</span> <n-text>{{ $t("login.login", { name: siteTitle }) }}</n-text>
</div> </div>
<n-tabs <n-tabs
animated animated
@@ -17,7 +17,7 @@
}" }"
@update:value="tabChange" @update:value="tabChange"
> >
<n-tab-pane name="qr" tab="二维码登录"> <n-tab-pane name="qr" :tab="$t('login.qr')">
<n-card class="qr-img"> <n-card class="qr-img">
<n-skeleton <n-skeleton
v-if="!qrImg" v-if="!qrImg"
@@ -32,19 +32,19 @@
:size="180" :size="180"
level="H" level="H"
background="#00000000" background="#00000000"
foreground="#f55e55" :foreground="setting.themeData.primaryColor"
/> />
</n-card> </n-card>
<span class="tip">{{ qrText }}</span> <span class="tip">{{ qrText }}</span>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="phone" tab="手机号登录"> <n-tab-pane name="phone" :tab="$t('login.phone')">
<n-alert <n-alert
style="width: 100%; margin-top: -20px; margin-bottom: 12px" style="width: 100%; margin-top: -20px; margin-bottom: 12px"
type="warning" type="warning"
> >
该登录方式暂时无法使用 {{ $t("login.canNotUse") }}
</n-alert> </n-alert>
<n-form <!-- <n-form
class="phone" class="phone"
ref="phoneFormRef" ref="phoneFormRef"
:model="phoneFormData" :model="phoneFormData"
@@ -83,18 +83,25 @@
</n-form-item> </n-form-item>
<n-form-item> <n-form-item>
<n-button style="width: 100%" type="primary" @click="phoneLogin"> <n-button style="width: 100%" type="primary" @click="phoneLogin">
登录 {{$t("login.login")}}
</n-button> </n-button>
</n-form-item> </n-form-item>
</n-form> </n-form> -->
</n-tab-pane>
<n-tab-pane name="email" :tab="$t('login.email')">
<n-alert
style="width: 100%; margin-top: -20px; margin-bottom: 12px"
type="warning"
>
{{ $t("login.canNotUse") }}
</n-alert>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="email" tab="邮箱登录"> 还没搞 </n-tab-pane>
</n-tabs> </n-tabs>
</div> </div>
</template> </template>
<script setup> <script setup>
import { userStore, musicStore } from "@/store"; import { userStore, musicStore, settingStore } from "@/store";
import { import {
getLoginState, getLoginState,
getQrKey, getQrKey,
@@ -105,17 +112,21 @@ import {
} from "@/api/login"; } from "@/api/login";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { PhoneAndroidRound, PasswordRound } from "@vicons/material"; import { PhoneAndroidRound, PasswordRound } from "@vicons/material";
import { formRules } from "@/utils/formRules.js"; import { formRules } from "@/utils/formRules";
import { useI18n } from "vue-i18n";
import QrcodeVue from "qrcode.vue"; import QrcodeVue from "qrcode.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const user = userStore(); const user = userStore();
const music = musicStore(); const music = musicStore();
const setting = settingStore();
const siteTitle = import.meta.env.VITE_SITE_TITLE;
const { numberRule, mobileRule } = formRules(); const { numberRule, mobileRule } = formRules();
// 二维码数据 // 二维码数据
const qrImg = ref(null); const qrImg = ref(null);
const qrText = ref("请打开云音乐 APP 扫码登录"); const qrText = ref(t("login.qrText1"));
// 手机号登录数据 // 手机号登录数据
const phoneFormRef = ref(null); const phoneFormRef = ref(null);
@@ -139,21 +150,22 @@ const loginStateMessage = ref(null);
// 储存登录信息 // 储存登录信息
const saveLoginData = (data) => { const saveLoginData = (data) => {
data.cookie = data.cookie.replaceAll(" HTTPOnly", "");
user.setCookie(data.cookie); user.setCookie(data.cookie);
// 验证用户登录信息 // 验证用户登录信息
getLoginState().then((res) => { getLoginState().then((res) => {
if (res.data.profile) { if (res.data.profile) {
user.setUserData(res.data.profile); user.setUserData(res.data.profile);
user.userLogin = true; user.userLogin = true;
qrText.value = "登录成功"; qrText.value = t("login.qrText4");
$message.success("登录成功"); $message.success(t("login.qrText4"));
// 自动签到 // 自动签到
if ($signIn) $signIn(); if ($signIn) $signIn();
clearInterval(qrCheckInterval.value); clearInterval(qrCheckInterval.value);
router.go(-1); router.push("/user");
} else { } else {
user.userLogOut(); user.userLogOut();
$message.error("登录出错,请重试"); $message.error(t("login.qrText5"));
getQrKeyData(); getQrKeyData();
} }
}); });
@@ -164,9 +176,9 @@ const getQrKeyData = () => {
// 检测是否登录 // 检测是否登录
getLoginState().then((res) => { getLoginState().then((res) => {
if (res.data.profile && window.localStorage.getItem("cookie")) { if (res.data.profile && window.localStorage.getItem("cookie")) {
$message.info("已登录,请勿重复登录"); $message.info(t("login.loggedIn"));
user.userLogin = true; user.userLogin = true;
router.go(-1); router.push("/user");
} else { } else {
user.userLogOut(); user.userLogOut();
clearInterval(qrCheckInterval.value); clearInterval(qrCheckInterval.value);
@@ -176,7 +188,7 @@ const getQrKeyData = () => {
qrImg.value = `https://music.163.com/login?codekey=${res.data.unikey}`; qrImg.value = `https://music.163.com/login?codekey=${res.data.unikey}`;
checkQrState(res.data.unikey); checkQrState(res.data.unikey);
} else { } else {
$message.error("登录二维码生成失败"); $message.error(t("login.qrText6"));
} }
}); });
} }
@@ -192,19 +204,16 @@ const checkQrState = (key) => {
if (res.code == 800) { if (res.code == 800) {
getQrKeyData(); getQrKeyData();
loginStateMessage.value = null; loginStateMessage.value = null;
qrText.value = "当前二维码已失效,请重新扫码"; qrText.value = t("login.qrText2");
} else if (res.code == 801) { } else if (res.code == 801) {
loginStateMessage.value = null; loginStateMessage.value = null;
qrText.value = "请打开云音乐 APP 扫码登录"; qrText.value = t("login.qrText1");
} else if (res.code == 802) { } else if (res.code == 802) {
qrText.value = "扫描成功,请在客户端确认登录"; qrText.value = t("login.qrText3");
if (!loginStateMessage.value) { if (!loginStateMessage.value) {
loginStateMessage.value = $message.loading( loginStateMessage.value = $message.loading(t("login.qrText3"), {
"扫描成功,请在客户端确认登录",
{
duration: 0, duration: 0,
} });
);
} }
} else if (res.code == 803) { } else if (res.code == 803) {
loginStateMessage.value.destroy(); loginStateMessage.value.destroy();
@@ -289,7 +298,7 @@ const tabChange = (val) => {
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle("登录"); $setSiteTitle(t("login.login"));
// 隐藏控制条 // 隐藏控制条
music.setPlayBarState(false); music.setPlayBarState(false);
// 获取二维码登录 key // 获取二维码登录 key

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="new-album"> <div class="new-album">
<div class="title"> <div class="title">
<span class="key">全部新碟</span> <span class="key">{{ $t("home.title.newAlbum") }}</span>
</div> </div>
<n-space class="category"> <n-space class="category">
<n-tag <n-tag
@@ -30,10 +30,12 @@
<script setup> <script setup>
import { getAlbumNew } from "@/api/album"; import { getAlbumNew } from "@/api/album";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js"; import { getLongTime } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 新碟数据 // 新碟数据
@@ -52,24 +54,24 @@ const albumAreaChoose = ref(
); );
const albumArea = [ const albumArea = [
{ {
label: "全部", label: t("general.type.all"),
value: "ALL", value: "ALL",
}, },
{ {
label: "华语", label: t("general.type.china"),
value: "ZH", value: "ZH",
}, },
{ {
label: "欧美", label: t("general.type.western"),
value: "EA", value: "EA",
}, },
{ {
label: "韩国", label: t("general.type.japan"),
value: "KR", value: "JP",
}, },
{ {
label: "日本", label: t("general.type.korea"),
value: "JP", value: "KR",
}, },
]; ];
@@ -92,10 +94,10 @@ const getAlbumNewData = (area, limit = 30, offset = 0) => {
}); });
}); });
} else { } else {
$message.error("全部新碟为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };
@@ -149,7 +151,7 @@ const changeArea = (area) => {
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle("全部新碟"); $setSiteTitle(t("home.title.newAlbum"));
getAlbumNewData( getAlbumNewData(
albumAreaChoose.value, albumAreaChoose.value,
pagelimit.value, pagelimit.value,
@@ -178,8 +180,8 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.9); transform: scale(0.9);

View File

@@ -2,54 +2,51 @@
<div class="playlist" v-if="playListDetail"> <div class="playlist" v-if="playListDetail">
<div class="left"> <div class="left">
<div class="cover"> <div class="cover">
<n-avatar <n-image
show-toolbar-tooltip
class="coverImg" class="coverImg"
:src=" :src="getCoverUrl(playListDetail.coverImgUrl, 1024)"
playListDetail.coverImgUrl :previewed-img-props="{ style: { borderRadius: '8px' } }"
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') + :preview-src="getCoverUrl(playListDetail.coverImgUrl)"
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
/> />
<img src="/images/pic/album.png" class="album" alt="album" /> <n-image
preview-disabled
class="shadow"
:src="getCoverUrl(playListDetail.coverImgUrl, 1024)"
fallback-src="/images/pic/default.png"
/>
</div>
<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>
<div class="intr"> <div class="intr">
<span class="name">歌单简介</span> <span class="name">{{
$t("general.name.desc", { name: $t("general.name.playlist") })
}}</span>
<span class="desc text-hidden"> <span class="desc text-hidden">
{{ {{
playListDetail.description playListDetail.description
? playListDetail.description ? playListDetail.description
: "太懒了吧,连简介都不写" : $t("other.noDesc")
}} }}
</span> </span>
<n-button <n-button
class="all-desc"
block block
strong strong
secondary secondary
v-if="playListDetail?.description?.length > 70" v-if="playListDetail?.description?.length > 70"
@click="playListDescShow = true" @click="playListDescShow = true"
> >
全部简介 {{ $t("general.name.allDesc") }}
</n-button> </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> </div>
<n-space class="tag" v-if="playListDetail.tags"> <n-space class="tag" v-if="playListDetail.tags">
<n-tag <n-tag
class="tags" class="tags"
size="large"
round round
:bordered="false" :bordered="false"
v-for="item in playListDetail.tags" v-for="item in playListDetail.tags"
@@ -59,36 +56,45 @@
{{ item }} {{ item }}
</n-tag> </n-tag>
</n-space> </n-space>
<!-- <div class="control" v-if="true"> <n-space class="control">
<n-space> <n-button strong secondary round type="primary" @click="playAllSong">
<n-button strong secondary round>
<template #icon> <template #icon>
<n-icon :component="EditNoteRound" /> <n-icon :component="MusicList" />
</template> </template>
编辑 {{ $t("general.name.play") }}
</n-button> </n-button>
<n-button strong secondary round type="primary"> <n-dropdown
placement="right-start"
trigger="click"
:show-arrow="true"
:options="dropdownOptions"
>
<n-button strong secondary circle>
<template #icon> <template #icon>
<n-icon :component="DeleteRound" /> <n-icon :component="More" />
</template> </template>
</n-button> </n-button>
</n-dropdown>
</n-space> </n-space>
</div> --> </div>
</div> </div>
<div class="right"> <div class="right">
<div class="meta"> <div class="meta">
<span class="name">{{ playListDetail.name }}</span> <n-text class="name">{{ playListDetail.name }}</n-text>
<span class="creator">{{ playListDetail.creator.nickname }}</span> <n-text class="creator">
<div class="time"> <n-icon :depth="3" :component="People" />
<div class="createTime"> {{ playListDetail.creator.nickname }}
<span class="num">创建时间</span> </n-text>
{{ getLongTime(playListDetail.createTime) }} <n-space class="time">
</div> <div class="num">
<div class="updateTime"> <n-icon :depth="3" :component="Newlybuild" />
<span class="num">更新时间</span> <n-text v-html="getLongTime(playListDetail.createTime)" />
{{ getLongTime(playListDetail.updateTime) }}
</div> </div>
<div class="num">
<n-icon :depth="3" :component="Write" />
<n-text v-html="getLongTime(playListDetail.updateTime)" />
</div> </div>
</n-space>
</div> </div>
<DataLists :listData="playListData" /> <DataLists :listData="playListData" />
<Pagination <Pagination
@@ -99,13 +105,29 @@
@pageSizeChange="pageSizeChange" @pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange" @pageNumberChange="pageNumberChange"
/> />
<!-- 歌单简介 -->
<n-modal
class="s-modal"
v-model:show="playListDescShow"
preset="card"
:title="$t('general.name.desc', { name: $t('general.name.playlist') })"
:bordered="false"
>
<n-scrollbar>
<n-text v-html="playListDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div> </div>
</div> </div>
<div class="title" v-else-if="!playListId"> <div class="title" v-else-if="!playListId || !loadingState">
<span class="key">参数不完整</span> <span class="key">{{
loadingState
? $t("general.name.noKeywords")
: $t("general.message.acquisitionFailed")
}}</span>
<br /> <br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px"> <n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<div class="loading" v-else> <div class="loading" v-else>
@@ -122,16 +144,38 @@
</template> </template>
<script setup> <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 { useRouter } from "vue-router";
import { userStore, musicStore } from "@/store"; import { userStore, musicStore, settingStore } from "@/store";
import { getSongTime, getLongTime } from "@/utils/timeTools.js"; import { getSongTime, getLongTime } from "@/utils/timeTools";
import { EditNoteRound, DeleteRound } from "@vicons/material"; import {
MusicList,
LinkTwo,
More,
DeleteFour,
Like,
Unlike,
Newlybuild,
Write,
People,
} from "@icon-park/vue-next";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
import getCoverUrl from "@/utils/getCoverUrl";
// import SpecialPlayLists from "./SpecialPlayLists.json";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const user = userStore(); const user = userStore();
const music = musicStore(); const music = musicStore();
const setting = settingStore();
// 歌单数据 // 歌单数据
const playListId = ref(router.currentRoute.value.query.id); const playListId = ref(router.currentRoute.value.query.id);
@@ -139,6 +183,7 @@ const playListDetail = ref(null);
const playListData = ref([]); const playListData = ref([]);
const playListDescShow = ref(false); const playListDescShow = ref(false);
const pagelimit = ref(30); const pagelimit = ref(30);
const loadingState = ref(true);
const pageNumber = ref( const pageNumber = ref(
router.currentRoute.value.query.page router.currentRoute.value.query.page
? Number(router.currentRoute.value.query.page) ? Number(router.currentRoute.value.query.page)
@@ -146,19 +191,114 @@ const pageNumber = ref(
); );
const totalCount = ref(0); 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: t("menu.copy", {
name: t("general.name.playlist"),
other: t("general.name.link"),
}),
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${playListId.value}`
);
$message.success(t("general.message.copySuccess"));
} catch (err) {
console.error(t("general.message.copyFailure"), err);
$message.error(t("general.message.copyFailure"));
}
} else {
$message.error(t("general.message.notSupported"));
}
},
},
icon: renderIcon(h(LinkTwo)),
},
{
key: "del",
label: t("menu.del"),
show: user.userLogin && isCanDelete(playListId.value),
props: {
onClick: () => {
toDelPlayList(playListDetail.value);
},
},
icon: renderIcon(h(DeleteFour)),
},
{
key: "like",
label: isLikeOrDislike(playListId.value)
? t("menu.collection", { name: t("general.name.playlist") })
: t("menu.cancelCollection", { name: t("general.name.playlist") }),
show: user.userLogin && !isCanDelete(playListId.value),
props: {
onClick: () => {
toChangeLike(playListId.value);
},
},
icon: renderIcon(h(isLikeOrDislike(playListId.value) ? Like : Unlike)),
},
];
};
// 获取歌单信息 // 获取歌单信息
const getPlayListDetailData = (id) => { const getPlayListDetailData = (id) => {
getPlayListDetail(id).then((res) => { getPlayListDetail(id)
.then((res) => {
console.log(res); console.log(res);
if (res.playlist) {
// 歌单总数 // 歌单总数
totalCount.value = res.playlist.trackCount; totalCount.value = res.playlist.trackCount;
// 歌单信息 // 歌单信息
playListDetail.value = res.playlist; playListDetail.value = res.playlist;
$setSiteTitle(res.playlist.name + " - 歌单"); $setSiteTitle(res.playlist.name + " - " + t("general.name.playlist"));
} else { })
$message.error("获取歌单信息失败"); .catch((err) => {
} $setSiteTitle(t("general.name.playlist"));
loadingState.value = false;
console.error(
$message.error(t("general.message.acquisitionFailed")),
err
);
$message.error(t("general.message.acquisitionFailed"));
}); });
}; };
@@ -178,18 +318,127 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
alia: v.alia, alia: v.alia,
time: getSongTime(v.dt), time: getSongTime(v.dt),
fee: v.fee, fee: v.fee,
sourceId: id,
pc: v.pc ? v.pc : null, pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null, mv: v.mv ? v.mv : null,
}); });
}); });
} else { } else {
$message.error("获取歌单内歌曲失败"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };
// 播放歌单所有歌曲
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($message.error(t("general.message.operationFailed")), err);
$message.error($message.error(t("general.message.operationFailed")));
}
};
// 删除歌单
const toDelPlayList = (data) => {
if (data.id === user.getUserPlayLists?.own[0].id) {
$message.warning(t("menu.unableToDelete"));
return false;
}
$dialog.warning({
class: "s-dialog",
title: t("general.dialog.delete"),
content: t("menu.delQuestion", {
name: data.name,
}),
positiveText: t("general.dialog.delete"),
negativeText: t("general.dialog.cancel"),
onPositiveClick: () => {
delPlayList(data.id).then((res) => {
if (res.code === 200) {
$message.success(t("general.message.deleteSuccess"));
user.setUserPlayLists();
router.push("/user/playlists");
}
});
},
});
};
// 收藏/取消收藏
const toChangeLike = async (id) => {
const type = isLikeOrDislike(id) ? 1 : 2;
const likeMsg = t("general.name.playlist");
const isThereASpace = setting.language === "zh-CN" ? "" : " ";
try {
const res = await likePlaylist(type, id);
if (res.code === 200) {
$message.success(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.success") })
: t("menu.cancelCollection", { name: t("general.dialog.success") })
}`
);
user.setUserPlayLists(() => {
setDropdownOptions();
});
} else {
$message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
}
} catch (err) {
$message.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`
);
console.error(
`${likeMsg + isThereASpace}${
type == 1
? t("menu.collection", { name: t("general.dialog.failed") })
: t("menu.cancelCollection", { name: t("general.dialog.failed") })
}`,
err
);
}
};
onMounted(() => { onMounted(() => {
if (playListId.value) { if (playListId.value) {
getPlayListDetailData(playListId.value); getPlayListDetailData(playListId.value);
@@ -198,6 +447,17 @@ onMounted(() => {
pagelimit.value, pagelimit.value,
(pageNumber.value - 1) * pagelimit.value (pageNumber.value - 1) * pagelimit.value
); );
if (
user.userLogin &&
!user.getUserPlayLists.has &&
!user.getUserPlayLists.isLoading
) {
user.setUserPlayLists(() => {
setDropdownOptions();
});
} else {
setDropdownOptions();
}
} }
}); });
@@ -265,22 +525,64 @@ watch(
align-items: flex-start; align-items: flex-start;
position: sticky; position: sticky;
top: 24px; top: 24px;
@media (max-width: 990px) {
margin-right: 0;
width: 30vw;
}
.cover { .cover {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
.n-avatar {
border-radius: 8px;
width: 80%; width: 80%;
height: 80%; height: 80%;
border-radius: 8px;
position: relative;
transition: transform 0.3s;
&:active {
transform: scale(0.95);
} }
.album { .coverImg {
border-radius: 8px;
width: 100%;
height: 100%; height: 100%;
overflow: hidden;
z-index: 1;
:deep(img) {
width: 100%;
}
}
.shadow {
position: absolute; position: absolute;
top: 0; top: 12px;
right: 4%; 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;
}
}
.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;
}
.creator {
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
} }
} }
.intr { .intr {
@@ -292,6 +594,9 @@ watch(
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
margin-bottom: 12px; margin-bottom: 12px;
@media (max-width: 990px) {
font-size: 18px;
}
} }
.desc { .desc {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
@@ -302,17 +607,23 @@ watch(
.tag { .tag {
margin-top: 20px; margin-top: 20px;
.tags { .tags {
line-height: 0;
font-size: 13px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
background-color: $mainSecondaryColor; background-color: var(--main-second-color);
color: $mainColor; color: var(--main-color);
} }
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
} }
} }
.control {
margin-top: 20px;
}
}
} }
.right { .right {
flex: 1; flex: 1;
@@ -326,6 +637,8 @@ watch(
font-weight: bold; font-weight: bold;
} }
.creator { .creator {
display: flex;
align-items: center;
margin-top: 6px; margin-top: 6px;
font-size: 16px; font-size: 16px;
opacity: 0.8; opacity: 0.8;
@@ -333,7 +646,10 @@ watch(
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: $mainColor; color: var(--main-color);
}
.n-icon {
margin-right: 6px;
} }
} }
.time { .time {
@@ -346,10 +662,13 @@ watch(
align-items: flex-start; align-items: flex-start;
} }
.num { .num {
color: #999; // color: #999;
display: flex;
flex-direction: row;
align-items: center;
.n-icon {
margin-right: 6px;
} }
div {
margin-right: 12px;
} }
} }
} }
@@ -367,21 +686,109 @@ watch(
@media (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
.left { .left {
margin-bottom: 12px; position: relative;
position: static; top: 0;
width: 60vw; width: 100%;
height: 40vw;
max-width: none; max-width: none;
.intr, display: flex;
.tag { 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; display: none;
} }
.desc {
-webkit-line-clamp: 2;
margin-bottom: 0;
}
}
.control {
position: absolute;
left: 0;
bottom: -60px;
}
}
} }
.right { .right {
margin-top: 80px;
.meta { .meta {
display: none;
}
}
}
@media (max-width: 540px) {
.left {
.cover {
margin-right: 20px;
}
.meta {
.title {
.name { .name {
font-size: 26px; 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;
} }
} }
} }

View File

@@ -0,0 +1,6 @@
[
{
"id": 3136952023,
"name": "私人雷达"
}
]

View File

@@ -14,9 +14,12 @@
<script setup> <script setup>
import { getSearchData } from "@/api/search"; import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js"; import { getLongTime } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索数据 // 搜索数据
@@ -49,10 +52,10 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 10) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -7,8 +7,10 @@
<script setup> <script setup>
import { getSearchData } from "@/api/search"; import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import ArtistLists from "@/components/DataList/ArtistLists.vue"; import ArtistLists from "@/components/DataList/ArtistLists.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索数据 // 搜索数据
@@ -35,10 +37,10 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 100) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -1,11 +1,11 @@
<template> <template>
<div class="search"> <div class="search">
<div class="title" v-if="searchKeywords"> <div class="title" v-if="searchKeywords">
<span class="key">{{ searchKeywords }}</span> <n-text class="key" v-html="searchKeywords" />
<span>的搜索结果</span> <n-text v-html="$t('nav.search.results')" />
</div> </div>
<div class="title" v-else> <div class="title" v-else>
<span class="key">未提供搜索关键字</span> <span class="key">{{ $t("general.name.noKeywords") }}</span>
<br /> <br />
<n-button <n-button
strong strong
@@ -13,7 +13,7 @@
@click="router.go(-1)" @click="router.go(-1)"
style="margin-top: 20px" style="margin-top: 20px"
> >
返回上一级 {{ $t("general.name.goBack") }}
</n-button> </n-button>
</div> </div>
<n-tabs <n-tabs
@@ -23,11 +23,11 @@
v-model:value="tabValue" v-model:value="tabValue"
v-if="searchKeywords" v-if="searchKeywords"
> >
<n-tab name="songs"> 单曲 </n-tab> <n-tab name="songs">{{ $t("general.name.song") }}</n-tab>
<n-tab name="artists"> 歌手 </n-tab> <n-tab name="artists">{{ $t("general.name.artists") }}</n-tab>
<n-tab name="albums"> 专辑 </n-tab> <n-tab name="albums">{{ $t("general.name.album") }}</n-tab>
<n-tab name="videos"> 视频 </n-tab> <n-tab name="videos">{{ $t("general.name.videos") }}</n-tab>
<n-tab name="playlists"> 歌单 </n-tab> <n-tab name="playlists">{{ $t("general.name.playlist") }}</n-tab>
</n-tabs> </n-tabs>
<main class="content" v-if="searchKeywords"> <main class="content" v-if="searchKeywords">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
@@ -43,6 +43,9 @@
<script setup> <script setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索关键词 // 搜索关键词
@@ -74,7 +77,8 @@ const tabChange = (value) => {
}; };
onMounted(() => { onMounted(() => {
$setSiteTitle(searchKeywords.value + "的搜索结果"); if (searchKeywords.value)
$setSiteTitle(searchKeywords.value + " " + t("nav.search.results"));
}); });
</script> </script>

View File

@@ -14,9 +14,12 @@
<script setup> <script setup>
import { getSearchData } from "@/api/search"; import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js"; import { formatNumber } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索数据 // 搜索数据
@@ -49,10 +52,10 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1000) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -13,11 +13,14 @@
<script setup> <script setup>
import { getSearchData } from "@/api/search"; import { getSearchData } from "@/api/search";
import { getMusicDetail } from "@/api/song"; // import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js"; import { getSongTime } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索数据 // 搜索数据
@@ -58,10 +61,10 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -14,9 +14,12 @@
<script setup> <script setup>
import { getSearchData } from "@/api/search"; import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { formatNumber, getSongTime } from "@/utils/timeTools.js"; import { formatNumber, getSongTime } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import VideoLists from "@/components/DataList/VideoLists.vue"; import VideoLists from "@/components/DataList/VideoLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// 搜索数据 // 搜索数据
@@ -50,10 +53,10 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1004) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶并结束加载条 // 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
}; };

View File

@@ -1,15 +1,15 @@
<template> <template>
<div class="setting"> <div class="setting">
<div class="title">全局设置</div> <div class="title">{{ $t("nav.avatar.setting") }}</div>
<n-tabs <n-tabs
class="main-tab" class="main-tab"
type="segment" type="segment"
@update:value="tabChange" @update:value="tabChange"
v-model:value="tabValue" v-model:value="tabValue"
> >
<n-tab name="main"> 基础 </n-tab> <n-tab name="main">{{ $t("setting.main") }}</n-tab>
<n-tab name="player"> 播放器 </n-tab> <n-tab name="player">{{ $t("setting.player") }}</n-tab>
<n-tab name="other"> 其他 </n-tab> <n-tab name="other">{{ $t("general.type.other") }}</n-tab>
</n-tabs> </n-tabs>
<main class="content"> <main class="content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
@@ -25,7 +25,9 @@
<script setup> <script setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
// Tab 默认选中 // Tab 默认选中
@@ -48,8 +50,9 @@ watch(
); );
onMounted(() => { onMounted(() => {
$setSiteTitle("全局设置"); $setSiteTitle(t("nav.avatar.setting"));
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" }); // 回顶
if (typeof $scrollToTop !== "undefined") $scrollToTop();
}); });
</script> </script>
@@ -79,6 +82,14 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-right: 20px; padding-right: 20px;
.dev {
display: flex;
flex-direction: row;
align-items: center;
.n-tag {
margin-left: 6px;
}
}
.tip { .tip {
font-size: 12px; font-size: 12px;
opacity: 0.8; opacity: 0.8;

View File

@@ -1,28 +1,85 @@
<template> <template>
<div class="set-main"> <div class="set-main">
<n-card class="set-item"> <n-card
<div class="name">明暗模式</div> class="set-item"
<n-select class="set" v-model:value="theme" :options="darkOptions" /> :content-style="{
flexDirection: 'column',
alignItems: 'flex-start',
}"
>
<div class="top">
<div class="name">
{{ $t("setting.themeType") }}
<span class="tip">{{ $t("setting.themeTypeTip") }}</span>
</div>
<n-button
v-if="themeType !== 'red'"
strong
secondary
@click="changeThemeColor(null, true)"
>
{{ $t("general.name.restore") }}
</n-button>
</div>
<n-grid
class="color-selete"
:x-gap="16"
:y-gap="16"
responsive="screen"
cols="3 s:4 m:5 l:6"
>
<n-grid-item
v-for="item in themeColorData"
:key="item"
:style="{ '--color': item.primaryColor }"
:class="item.label === themeType ? 'item check' : 'item'"
@click="changeThemeColor(item)"
>
<n-text v-html="language === 'zh-CN' ? item.name : item.label" />
</n-grid-item>
</n-grid>
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name">明暗模式跟随系统</div> <div class="name">{{ $t("setting.language") }}</div>
<n-switch v-model:value="themeAuto" :round="false" /> <n-select
class="set"
v-model:value="language"
:options="languageOptions"
@update:value="changeLanguage"
/>
</n-card>
<n-card class="set-item">
<div class="name">{{ $t("setting.theme") }}</div>
<n-select
class="set"
v-model:value="theme"
:options="themeOptions"
@update:value="themeAuto = false"
/>
</n-card>
<n-card class="set-item">
<div class="name">{{ $t("setting.themeAuto") }}</div>
<n-switch
v-model:value="themeAuto"
:round="false"
@update:value="themeAutoOpen"
/>
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
每日签到 {{ $t("setting.autoSignIn") }}
<span class="tip">是否自动进行每日签到</span> <span class="tip">{{ $t("setting.autoSignInTip") }}</span>
</div> </div>
<n-switch v-model:value="autoSignIn" :round="false" /> <n-switch v-model:value="autoSignIn" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name">显示轮播图</div> <div class="name">{{ $t("setting.bannerShow") }}</div>
<n-switch v-model:value="bannerShow" :round="false" /> <n-switch v-model:value="bannerShow" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
列表点击方式 {{ $t("setting.listClickMode") }}
<span class="tip">移动端该设置项无效单击同时生效</span> <span class="tip">{{ $t("setting.listClickModeTip") }}</span>
</div> </div>
<n-select <n-select
class="set" class="set"
@@ -31,20 +88,36 @@
/> />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name">显示搜索历史</div> <div class="name">{{ $t("setting.searchHistory") }}</div>
<n-switch v-model:value="searchHistory" :round="false" /> <n-switch v-model:value="searchHistory" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
显示底栏歌词 {{ $t("setting.bottomLyricShow") }}
<span class="tip">是否在播放时显示歌词</span> <span class="tip">{{ $t("setting.bottomLyricShowTip") }}</span>
</div> </div>
<n-switch v-model:value="bottomLyricShow" :round="false" /> <n-switch v-model:value="bottomLyricShow" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
歌曲音质 {{ $t("setting.songVolumeFade") }}
<span class="tip">无损音质及以上需要您为黑胶会员</span> <span class="tip">{{ $t("setting.songVolumeFadeTip") }}</span>
</div>
<n-switch v-model:value="songVolumeFade" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.memoryLastPlaybackPosition") }}
<span class="tip">{{
$t("setting.memoryLastPlaybackPositionTip")
}}</span>
</div>
<n-switch v-model:value="memoryLastPlaybackPosition" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.songLevel") }}
<span class="tip">{{ $t("setting.songLevelTip") }}</span>
</div> </div>
<n-select <n-select
class="set" class="set"
@@ -52,15 +125,43 @@
:options="songLevelOptions" :options="songLevelOptions"
/> />
</n-card> </n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.useUnmServerShow") }}
<span class="tip">
{{
useUnmServerShow
? $t("setting.useUnmServerShowTip1")
: $t("setting.useUnmServerShowTip2")
}}
</span>
</div>
<n-switch
v-model:value="useUnmServer"
:round="false"
:disabled="!useUnmServerShow"
/>
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.showLyricSetting") }}
<span class="tip">{{ $t("setting.showLyricSettingTip") }}</span>
</div>
<n-switch v-model:value="showLyricSetting" :round="false" />
</n-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { settingStore, userStore } from "@/store"; import { settingStore, userStore } from "@/store";
import { useI18n } from "vue-i18n";
import { useOsTheme } from "naive-ui";
import themeColorData from "@/components/Provider/themeColor.json";
const setting = settingStore(); const setting = settingStore();
const user = userStore(); const user = userStore();
const osThemeRef = useOsTheme();
const { const {
theme, theme,
themeAuto, themeAuto,
@@ -70,56 +171,201 @@ const {
bannerShow, bannerShow,
autoSignIn, autoSignIn,
searchHistory, searchHistory,
themeType,
showLyricSetting,
songVolumeFade,
useUnmServer,
memoryLastPlaybackPosition,
language,
} = storeToRefs(setting); } = storeToRefs(setting);
// 国际化
const { locale, t } = useI18n();
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
// 深浅模式 // 深浅模式
const darkOptions = [ const themeOptions = ref([]);
const themeChange = () => {
themeOptions.value = [
{ {
label: "浅色模式", label: t("nav.avatar.light"),
value: "light", value: "light",
}, },
{ {
label: "深色模式", label: t("nav.avatar.dark"),
value: "dark", value: "dark",
}, },
]; ];
};
// 开启自动跟随
const themeAutoOpen = (val) => {
console.log(osThemeRef.value);
if (val) {
theme.value = osThemeRef.value;
}
};
// 列表模式 // 列表模式
const listClickModeOptions = [ const listClickModeOptions = ref([]);
const listClickModeChange = () => {
listClickModeOptions.value = [
{ {
label: "双击播放", label: t("setting.dblclick"),
value: "dblclick", value: "dblclick",
}, },
{ {
label: "单击播放", label: t("setting.click"),
value: "click", value: "click",
}, },
];
};
// 语言
const languageOptions = [
{
label: "🇨🇳 简体中文",
value: "zh-CN",
},
{
label: "🇬🇧 English",
value: "en",
},
]; ];
// 语言切换
const changeLanguage = (value, option) => {
const html = document.documentElement;
locale.value = value;
if (html) html.setAttribute("lang", value);
changeAllOptions();
console.log(t("setting.changeLanguage", { name: value }));
$message.success(t("setting.changeLanguage", { name: option.label }));
};
// 歌曲音质 // 歌曲音质
const songLevelOptions = [ const songLevelOptions = ref([]);
const songLevelChange = () => {
songLevelOptions.value = [
{ {
label: "标准", label: t("setting.standard"),
value: "standard", value: "standard",
}, },
{ {
label: "较高", label: t("setting.higher"),
value: "higher", value: "higher",
}, },
, ,
{ {
label: "极高", label: t("setting.exhigh"),
value: "exhigh", value: "exhigh",
}, },
{ {
label: "无损", label: t("setting.lossless"),
value: "lossless", value: "lossless",
disabled: user.userData?.vipType ? false : true, disabled: user.userData?.vipType ? false : true,
}, },
{ {
label: "Hi-Res", label: t("setting.hires"),
value: "hires", value: "hires",
disabled: user.userData?.vipType ? false : true, disabled: user.userData?.vipType ? false : true,
}, },
]; {
label: t("setting.jyeffect"),
value: "jyeffect",
disabled: user.userData?.vipType ? false : true,
},
{
label: t("setting.jymaster"),
value: "jymaster",
disabled: user.userData?.vipType ? false : true,
},
];
};
// 更改所有配置
const changeAllOptions = () => {
themeChange();
listClickModeChange();
songLevelChange();
};
// 更换主题色
const changeThemeColor = (data, reset = false) => {
if (reset) {
$dialog.warning({
class: "s-dialog",
title: t("general.name.restore"),
content: t("setting.themeTypeDialog"),
positiveText: t("general.name.restore"),
negativeText: t("general.dialog.cancel"),
onPositiveClick: () => {
$message.success(t("other.cleanAll"));
themeType.value = "red";
},
});
} else {
$message.success(t("setting.themeChange", { name: data.name }));
themeType.value = data.label;
}
};
onMounted(() => {
changeAllOptions();
});
</script> </script>
<style lang="scss" scoped>
.set-item {
.top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.color-selete {
margin-top: 16px;
.item {
position: relative;
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color);
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
@media (max-width: 800px) {
height: 60px;
}
&::before {
content: "";
position: absolute;
border-radius: 12px;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid var(--color);
opacity: 0;
transition: opacity 0.3s;
}
&.check {
&::before {
opacity: 1;
}
}
&:active {
transform: scale(0.98);
}
.n-text {
color: #fff;
}
}
}
}
</style>

View File

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

View File

@@ -1,7 +1,10 @@
<template> <template>
<div class="set-player"> <div class="set-player">
<n-card class="set-item"> <n-card class="set-item">
<div class="name">播放器样式</div> <div class="name">
{{ $t("setting.playerStyle") }}
<span class="tip">{{ $t("setting.playerStyleTip") }}</span>
</div>
<n-select <n-select
class="set" class="set"
v-model:value="playerStyle" v-model:value="playerStyle"
@@ -10,43 +13,82 @@
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
替换无法播放的歌曲链接 {{ $t("setting.backgroundImageShow") }}
<span class="tip"> <span class="tip">{{
{{ backgroundImageShow === "blur"
useUnmServerShow ? $t("setting.backgroundImageShowTip1")
? "是否使用 UNM 替换无法播放的歌曲链接" : $t("setting.backgroundImageShowTip2")
: "请配置 UNM-Server 后使用解灰功能" }}</span>
}}
</span>
</div> </div>
<n-switch <n-select
v-model:value="useUnmServer" class="set"
:round="false" v-model:value="backgroundImageShow"
:disabled="!useUnmServerShow" :options="backgroundImageShowOptions"
/> />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name">显示歌词翻译</div> <div class="name">
{{ $t("setting.showTransl") }}
<span class="tip">{{ $t("setting.showTranslTip") }}</span>
</div>
<n-switch v-model:value="showTransl" :round="false" /> <n-switch v-model:value="showTransl" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
显示逐字歌词 {{ $t("setting.showRoma") }}
<span class="tip">是否在歌曲具有逐字歌词时显示实验性功能</span> <span class="tip">{{ $t("setting.showRomaTip") }}</span>
</div> </div>
<n-switch v-model:value="showYrc" :round="false" /> <n-switch v-model:value="showRoma" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
智能暂停滚动 <div class="dev">
<span class="tip">鼠标移入歌词区域是否暂停滚动</span> {{ $t("setting.showYrc") }}
<n-tag round :bordered="false" size="small" type="warning">
{{ $t("setting.dev") }}
<template #icon>
<n-icon :component="Code" />
</template>
</n-tag>
</div>
<span class="tip">{{ $t("setting.showYrcTip") }}</span>
</div>
<n-switch v-model:value="showYrc" :round="false" />
</n-card>
<template v-if="showYrc">
<n-card class="set-item">
<div class="name">
{{ $t("setting.showYrcAnimation") }}
<span class="tip">{{ $t("setting.showYrcAnimationTip") }}</span>
</div>
<n-switch v-model:value="showYrcAnimation" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.showYrcTransform") }}
<span class="tip">{{ $t("setting.showYrcTransformTip") }}</span>
</div>
<n-switch v-model:value="showYrcTransform" :round="false" />
</n-card>
</template>
<n-card class="set-item">
<div class="name">
{{ $t("setting.countDownShow") }}
<span class="tip">{{ $t("setting.countDownShowTip") }}</span>
</div>
<n-switch v-model:value="countDownShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.lrcMousePause") }}
<span class="tip">{{ $t("setting.lrcMousePauseTip") }}</span>
</div> </div>
<n-switch v-model:value="lrcMousePause" :round="false" /> <n-switch v-model:value="lrcMousePause" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
歌词滚动位置 {{ $t("setting.lyricsBlock") }}
<span class="tip">歌词高亮时所处的位置</span> <span class="tip">{{ $t("setting.lyricsBlockTip") }}</span>
</div> </div>
<n-select <n-select
class="set" class="set"
@@ -61,7 +103,7 @@
alignItems: 'flex-start', alignItems: 'flex-start',
}" }"
> >
<div class="name">歌词文本大小</div> <div class="name">{{ $t("setting.lyricsFontSize") }}</div>
<n-slider <n-slider
v-model:value="lyricsFontSize" v-model:value="lyricsFontSize"
:tooltip="false" :tooltip="false"
@@ -69,9 +111,9 @@
:min="3" :min="3"
:step="0.01" :step="0.01"
:marks="{ :marks="{
3: '最小', 3: t('setting.lyrics1'),
3.6: '默认', 3.6: t('setting.lyrics2'),
4: '最大', 4: t('setting.lyrics3'),
}" }"
/> />
<div :class="lyricsBlur ? 'more blur' : 'more'"> <div :class="lyricsBlur ? 'more blur' : 'more'">
@@ -96,7 +138,7 @@
</div> </div>
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name">默认歌词位置</div> <div class="name">{{ $t("setting.lyricsPosition") }}</div>
<n-select <n-select
class="set" class="set"
v-model:value="lyricsPosition" v-model:value="lyricsPosition"
@@ -105,12 +147,12 @@
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="name"> <div class="name">
歌词模糊 {{ $t("setting.lyricsBlur") }}
<span class="tip">未播放或已播放歌词模糊显示实验性功能</span> <span class="tip">{{ $t("setting.lyricsBlurTip") }}</span>
</div> </div>
<n-switch v-model:value="lyricsBlur" :round="false" /> <n-switch v-model:value="lyricsBlur" :round="false" />
</n-card> </n-card>
<n-card class="set-item"> <!-- <n-card class="set-item">
<div class="name"> <div class="name">
显示音乐频谱 显示音乐频谱
<span class="tip">可能会导致一些意想不到的后果实验性功能</span> <span class="tip">可能会导致一些意想不到的后果实验性功能</span>
@@ -120,13 +162,17 @@
:round="false" :round="false"
@click="changeMusicFrequency" @click="changeMusicFrequency"
/> />
</n-card> </n-card> -->
</div> </div>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { settingStore } from "@/store"; import { settingStore } from "@/store";
import { useI18n } from "vue-i18n";
import { Code } from "@icon-park/vue-next";
const { t } = useI18n();
const setting = settingStore(); const setting = settingStore();
const { const {
@@ -139,20 +185,21 @@ const {
lyricsBlur, lyricsBlur,
lrcMousePause, lrcMousePause,
showYrc, showYrc,
useUnmServer, showRoma,
backgroundImageShow,
countDownShow,
showYrcAnimation,
showYrcTransform,
} = storeToRefs(setting); } = storeToRefs(setting);
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
// 歌词位置 // 歌词位置
const lyricsPositionOptions = [ const lyricsPositionOptions = [
{ {
label: "居左", label: t("setting.positionLeft"),
value: "left", value: "left",
}, },
{ {
label: "居中", label: t("setting.positionCenter"),
value: "center", value: "center",
}, },
]; ];
@@ -160,11 +207,11 @@ const lyricsPositionOptions = [
// 歌词滚动位置 // 歌词滚动位置
const lyricsBlockOptions = [ const lyricsBlockOptions = [
{ {
label: "靠近顶部", label: t("setting.blockStart"),
value: "start", value: "start",
}, },
{ {
label: "水平居中", label: t("setting.blockCenter"),
value: "center", value: "center",
}, },
]; ];
@@ -172,35 +219,46 @@ const lyricsBlockOptions = [
// 播放器样式 // 播放器样式
const playerStyleOptions = [ const playerStyleOptions = [
{ {
label: "封面模式", label: t("setting.cover"),
value: "cover", value: "cover",
}, },
{ {
label: "唱片模式", label: t("setting.record"),
value: "record", value: "record",
}, },
]; ];
// 播放背景类型
const backgroundImageShowOptions = [
{
label: t("setting.solid"),
value: "solid",
},
{
label: t("setting.blur"),
value: "blur",
},
];
// 音乐频谱提醒 // 音乐频谱提醒
const changeMusicFrequency = () => { // const changeMusicFrequency = () => {
if (musicFrequency.value) { // if (musicFrequency.value) {
$dialog.warning({ // $dialog.warning({
class: "s-dialog", // class: "s-dialog",
title: "实验性功能", // title: "实验性功能",
content: "确认开启音乐频谱?将于刷新后生效", // content: "确认开启音乐频谱?将在重启应用后生效",
positiveText: "开启", // positiveText: "开启",
negativeText: "取消", // negativeText: "取消",
onMaskClick: () => { // onMaskClick: () => {
musicFrequency.value = false; // musicFrequency.value = false;
}, // },
onPositiveClick: () => { // onPositiveClick: () => {
musicFrequency.value = true; // musicFrequency.value = true;
location.reload(); // },
}, // onNegativeClick: () => {
onNegativeClick: () => { // musicFrequency.value = false;
musicFrequency.value = false; // },
}, // });
}); // }
} // };
};
</script> </script>

View File

@@ -2,14 +2,24 @@
<div class="song" v-if="musicDetail"> <div class="song" v-if="musicDetail">
<div class="detail"> <div class="detail">
<div class="pic"> <div class="pic">
<n-avatar <n-image
show-toolbar-tooltip
class="coverImg" class="coverImg"
:src=" :previewed-img-props="{ style: { borderRadius: '8px' } }"
musicDetail.al.picUrl :preview-src="getCoverUrl(musicDetail?.al.picUrl)"
? musicDetail.al.picUrl.replace(/^http:/, 'https:') + :src="getCoverUrl(musicDetail?.al.picUrl, 1024)"
'?param=1024y1024' fallback-src="/images/pic/default.png"
: '/images/pic/default.png' >
" <template #placeholder>
<div class="cover-loading">
<n-spin />
</div>
</template>
</n-image>
<n-image
class="shadow"
preview-disabled
:src="getCoverUrl(musicDetail?.al.picUrl, 1024)"
fallback-src="/images/pic/default.png" fallback-src="/images/pic/default.png"
/> />
</div> </div>
@@ -22,20 +32,43 @@
v-if="musicDetail.alia[0]" v-if="musicDetail.alia[0]"
v-html="musicDetail.alia[0]" v-html="musicDetail.alia[0]"
/> />
<div class="all-artist"> <n-space class="tag">
<n-text class="tip" depth="3">歌手</n-text> <n-tag
<AllArtists v-if="musicDetail.ar" :artistsData="musicDetail.ar" /> v-if="musicDetail.fee == 1 || musicDetail.fee == 4"
class="vip"
round
:bordered="false"
>
{{ musicDetail.fee == 1 ? "VIP" : "EP" }}
</n-tag>
<n-tag
v-if="musicDetail.pc"
class="cloud"
round
type="info"
:bordered="false"
>
{{ $t("general.name.cloud") }}
</n-tag>
</n-space>
<div class="item">
<n-icon :depth="3" :component="People" />
<AllArtists
v-if="musicDetail.ar"
:artistsData="musicDetail.ar"
:isDark="false"
/>
</div> </div>
<div class="album"> <div class="item">
<n-text class="tip" depth="3">专辑</n-text> <n-icon :depth="3" :component="RecordDisc" />
<n-text <n-text
class="text" class="text"
v-html="musicDetail.al.name" v-html="musicDetail.al.name"
@click="router.push(`/album?id=${musicDetail.al.id}`)" @click="router.push(`/album?id=${musicDetail.al.id}`)"
/> />
</div> </div>
<div class="time" v-if="musicDetail.publishTime"> <div class="item" v-if="musicDetail.publishTime">
<n-text class="tip" depth="3">发行日期</n-text> <n-icon :depth="3" :component="Time" />
<n-text <n-text
class="text" class="text"
v-html="getLongTime(musicDetail.publishTime)" v-html="getLongTime(musicDetail.publishTime)"
@@ -50,9 +83,9 @@
@click="addSong(musicDetail)" @click="addSong(musicDetail)"
> >
<template #icon> <template #icon>
<n-icon :component="PlayArrowRound" /> <n-icon :component="PlayOne" />
</template> </template>
播放 {{ $t("general.name.play") }}
</n-button> </n-button>
<n-button <n-button
strong strong
@@ -60,9 +93,9 @@
@click="addPlayListRef.openAddToPlaylist(musicId)" @click="addPlayListRef.openAddToPlaylist(musicId)"
> >
<template #icon> <template #icon>
<n-icon :component="PlaylistAddRound" /> <n-icon :component="ListAdd" />
</template> </template>
添加 {{ $t("general.name.add") }}
</n-button> </n-button>
<n-button <n-button
strong strong
@@ -70,9 +103,9 @@
@click="router.push(`/comment?id=${musicDetail.id}&page=1`)" @click="router.push(`/comment?id=${musicDetail.id}&page=1`)"
> >
<template #icon> <template #icon>
<n-icon :component="MessageFilled" /> <n-icon :component="Comments" />
</template> </template>
评论 {{ $t("general.name.comment") }}
</n-button> </n-button>
<n-button <n-button
strong strong
@@ -81,16 +114,22 @@
@click="router.push(`/video?id=${musicDetail.mv}`)" @click="router.push(`/video?id=${musicDetail.mv}`)"
> >
<template #icon> <template #icon>
<n-icon :component="VideocamRound" /> <n-icon :component="Youtube" />
</template> </template>
MV MV
</n-button> </n-button>
</n-space> </n-space>
</div> </div>
</div> </div>
<n-divider /> <div class="comments" v-if="commentData[0]">
<n-h6 prefix="bar"> {{ $t("general.name.hotComments") }} </n-h6>
<div class="content">
<Comment v-for="item in commentData" :key="item" :commentData="item" />
</div>
</div>
<div class="simiPlayList" v-if="simiPlayList[0]"> <div class="simiPlayList" v-if="simiPlayList[0]">
<n-h6 prefix="bar"> 包含这首歌的歌单 </n-h6> <n-divider />
<n-h6 prefix="bar"> {{ $t("other.containing") }} </n-h6>
<CoverLists :listData="simiPlayList" /> <CoverLists :listData="simiPlayList" />
</div> </div>
<!-- 添加到歌单 --> <!-- 添加到歌单 -->
@@ -100,20 +139,28 @@
<script setup> <script setup>
import { getSimiPlayList, getMusicDetail } from "@/api/song"; import { getSimiPlayList, getMusicDetail } from "@/api/song";
import { getComment } from "@/api/comment";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { musicStore } from "@/store"; import { musicStore } from "@/store";
import { getLongTime } from "@/utils/timeTools.js"; import { getLongTime } from "@/utils/timeTools";
import { import {
PlayArrowRound, PlayOne,
MessageFilled, Comments,
VideocamRound, ListAdd,
PlaylistAddRound, Youtube,
} from "@vicons/material"; People,
import { formatNumber } from "@/utils/timeTools.js"; RecordDisc,
Time,
} from "@icon-park/vue-next";
import { formatNumber } from "@/utils/timeTools";
import { useI18n } from "vue-i18n";
import AllArtists from "@/components/DataList/AllArtists.vue"; import AllArtists from "@/components/DataList/AllArtists.vue";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue"; import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
import Comment from "@/components/Comment/index.vue";
import getCoverUrl from "@/utils/getCoverUrl";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const music = musicStore(); const music = musicStore();
const addPlayListRef = ref(null); const addPlayListRef = ref(null);
@@ -122,6 +169,9 @@ const addPlayListRef = ref(null);
const musicId = ref(router.currentRoute.value.query.id); const musicId = ref(router.currentRoute.value.query.id);
const musicDetail = ref(null); const musicDetail = ref(null);
// 评论数据
const commentData = ref([]);
// 相似数据 // 相似数据
const simiPlayList = ref([]); const simiPlayList = ref([]);
@@ -132,16 +182,39 @@ const getMusicDetailData = (id) => {
if (res.songs[0]) { if (res.songs[0]) {
musicDetail.value = res.songs[0]; musicDetail.value = res.songs[0];
$setSiteTitle( $setSiteTitle(
res.songs[0].name + " - " + res.songs[0].ar[0].name + " - 单曲" res.songs[0].name +
" - " +
res.songs[0].ar[0].name +
" - " +
t("general.name.song")
); );
// 获取热门评论
getCommentData(id);
// 获取相似数据 // 获取相似数据
getSimiData(id); getSimiData(id);
// 请求后回顶
if (typeof $scrollToTop !== "undefined") $scrollToTop();
} else { } else {
$message.error("歌曲信息获取失败"); $message.error(t("general.message.acquisitionFailed"));
} }
}); });
}; };
// 获取评论数据
const getCommentData = (id) => {
getComment(id)
.then((res) => {
// 写入数据
if (res.total > 0) {
commentData.value = res.hotComments;
}
})
.catch((err) => {
console.error(t("general.message.acquisitionFailed"), err);
$message.error(t("general.message.acquisitionFailed"));
});
};
// 获取相似数据 // 获取相似数据
const getSimiData = (id) => { const getSimiData = (id) => {
getSimiPlayList(id).then((res) => { getSimiPlayList(id).then((res) => {
@@ -205,11 +278,53 @@ watch(
justify-content: center; justify-content: center;
max-width: 280px; max-width: 280px;
border-radius: 8px; border-radius: 8px;
overflow: hidden;
margin-right: 40px; margin-right: 40px;
.n-avatar { position: relative;
transition: transform 0.3s;
&:active {
transform: scale(0.95);
}
.coverImg {
border-radius: 8px;
width: 100%; width: 100%;
height: inherit; height: 100%;
overflow: hidden;
z-index: 1;
:deep(img) {
width: 100%;
}
.cover-loading {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 0;
padding-bottom: 100%;
background-color: #0001;
.n-spin-body {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.shadow {
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;
:deep(img) {
width: 100%;
}
} }
} }
.right { .right {
@@ -228,19 +343,22 @@ watch(
.alia { .alia {
font-size: 20px; font-size: 20px;
} }
.all-artist { .tag {
margin-top: 12px; margin: 12px 0;
display: flex; }
flex-direction: row; .item {
align-items: center; font-size: 16px;
display: flex;
align-items: center;
margin-bottom: 4px;
.n-icon {
margin-right: 6px;
} }
.album {
margin: 4px 0;
.text { .text {
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: $mainColor; color: var(--main-color);
} }
} }
} }
@@ -252,6 +370,7 @@ watch(
} }
@media (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
width: 100%;
.pic { .pic {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -267,5 +386,8 @@ watch(
} }
} }
} }
.comments {
margin-top: 40px;
}
} }
</style> </style>

View File

@@ -2,11 +2,13 @@
<n-result <n-result
class="error" class="error"
status="403" status="403"
title="禁止访问" :title="$t('state.prohibition')"
description="总有些门是对你关闭的" :description="$t('state.prohibitionDesc')"
> >
<template #footer> <template #footer>
<n-button type="primary" @click="router.go(-1)">回到上一页</n-button> <n-button type="primary" @click="router.go(-1)">{{
$t("general.name.goBack")
}}</n-button>
</template> </template>
</n-result> </n-result>
</template> </template>

View File

@@ -2,11 +2,13 @@
<n-result <n-result
class="error" class="error"
status="404" status="404"
title="资源不存在" :title="$t('state.notFound')"
description="怎么跑到这来了" :description="$t('state.notFoundDesc')"
> >
<template #footer> <template #footer>
<n-button type="primary" @click="router.go(-1)">回到上一页</n-button> <n-button type="primary" @click="router.go(-1)">{{
$t("general.name.goBack")
}}</n-button>
</template> </template>
</n-result> </n-result>
</template> </template>

View File

@@ -2,11 +2,13 @@
<n-result <n-result
class="error" class="error"
status="500" status="500"
title="服务器错误" :title="$t('state.error')"
description="服务器寄了,等会再试吧" :description="$t('state.errorDesc')"
> >
<template #footer> <template #footer>
<n-button type="primary" @click="router.push('/')">重新载入</n-button> <n-button type="primary" @click="router.push('/')">{{
$t("general.name.reload")
}}</n-button>
</template> </template>
</n-result> </n-result>
</template> </template>

View File

@@ -6,12 +6,14 @@
<script setup> <script setup>
import { userStore } from "@/store"; import { userStore } from "@/store";
import { useI18n } from "vue-i18n";
import CoverLists from "@/components/DataList/CoverLists.vue"; import CoverLists from "@/components/DataList/CoverLists.vue";
const { t } = useI18n();
const user = userStore(); const user = userStore();
onMounted(() => { onMounted(() => {
$setSiteTitle("音乐库 - 收藏的专辑"); $setSiteTitle(t("nav.user") + " - " + t("nav.userChildren.album"));
if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading) if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading)
user.setUserAlbumLists(); user.setUserAlbumLists();
}); });

View File

@@ -6,12 +6,14 @@
<script setup> <script setup>
import { userStore } from "@/store"; import { userStore } from "@/store";
import { useI18n } from "vue-i18n";
import ArtistLists from "@/components/DataList/ArtistLists.vue"; import ArtistLists from "@/components/DataList/ArtistLists.vue";
const { t } = useI18n();
const user = userStore(); const user = userStore();
onMounted(() => { onMounted(() => {
$setSiteTitle("音乐库 - 收藏的歌手"); $setSiteTitle(t("nav.user") + " - " + t("nav.userChildren.artist"));
if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading) if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading)
user.setUserArtistLists(); user.setUserArtistLists();
}); });

View File

@@ -12,7 +12,7 @@
<template #icon> <template #icon>
<n-icon :component="BackupRound" /> <n-icon :component="BackupRound" />
</template> </template>
上传歌曲 {{ $t("general.name.upCloud") }}
</n-button> </n-button>
<input <input
ref="upSongRef" ref="upSongRef"
@@ -27,18 +27,21 @@
<template #trigger> <template #trigger>
<n-progress <n-progress
type="line" type="line"
color="#f55e55" :color="setting.themeData.primaryColor"
class="progress" class="progress"
:show-indicator="false" :show-indicator="false"
:percentage="100 / (cloudSpace[1] / cloudSpace[0])" :percentage="100 / (cloudSpace[1] / cloudSpace[0])"
/> />
</template> </template>
<n-text> <n-text>
已用 {{ (100 / (cloudSpace[1] / cloudSpace[0])).toFixed() }}%剩余 {{
{{ cloudSpace[1] - cloudSpace[0] }} G $t("general.name.cloudUsed", {
used: (100 / (cloudSpace[1] / cloudSpace[0])).toFixed(),
remaining: cloudSpace[1] - cloudSpace[0],
})
}}
</n-text> </n-text>
</n-popover> </n-popover>
<span>{{ cloudSpace[1] }} G</span> <span>{{ cloudSpace[1] }} G</span>
</div> </div>
</div> </div>
@@ -54,7 +57,7 @@
class="s-modal close" class="s-modal close"
v-model:show="upSongModal" v-model:show="upSongModal"
preset="card" preset="card"
title="云盘上传" :title="$t('general.name.upCloud')"
:auto-focus="false" :auto-focus="false"
:bordered="false" :bordered="false"
:close-on-esc="false" :close-on-esc="false"
@@ -70,9 +73,11 @@
/> />
<template #footer> <template #footer>
<n-space justify="end" v-if="upSongType === 'error'"> <n-space justify="end" v-if="upSongType === 'error'">
<n-button @click="closeUpSongModal"> 取消 </n-button> <n-button @click="closeUpSongModal">
{{ $t("general.dialog.cancel") }}
</n-button>
<n-button type="primary" @click="resetUpSongModal"> <n-button type="primary" @click="resetUpSongModal">
重新上传 {{ $t("general.dialog.resetUp") }}
</n-button> </n-button>
</n-space> </n-space>
</template> </template>
@@ -83,12 +88,16 @@
<script setup> <script setup>
import { getCloud, upCloudSong } from "@/api/user"; import { getCloud, upCloudSong } from "@/api/user";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js"; import { settingStore } from "@/store";
import { getSongTime } from "@/utils/timeTools";
import { BackupRound } from "@vicons/material"; import { BackupRound } from "@vicons/material";
import { useI18n } from "vue-i18n";
import DataLists from "@/components/DataList/DataLists.vue"; import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue"; import Pagination from "@/components/Pagination/index.vue";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const setting = settingStore();
// 云盘数据 // 云盘数据
const cloudSpace = ref([]); const cloudSpace = ref([]);
@@ -133,11 +142,10 @@ const getCloudData = (limit = 30, offset = 0, scroll = true) => {
}); });
}); });
} else { } else {
$message.error("搜索内容为空"); $message.error(t("general.message.acquisitionFailed"));
} }
// 请求后回顶 // 请求后回顶
if ($mainContent && scroll) if (typeof $scrollToTop !== "undefined") $scrollToTop();
$mainContent.scrollIntoView({ behavior: "smooth" });
}); });
}; };
@@ -146,7 +154,6 @@ const onUploadProgress = (progressEvent) => {
const { loaded, total } = progressEvent; const { loaded, total } = progressEvent;
const percentCompleted = Math.round((loaded * 100) / total); const percentCompleted = Math.round((loaded * 100) / total);
upSongCompleted.value = Number(percentCompleted); upSongCompleted.value = Number(percentCompleted);
console.log(`上传 ${percentCompleted}% 完成`);
}; };
// 歌曲上传 // 歌曲上传
@@ -162,21 +169,25 @@ const upCloudSongData = (e) => {
if (res.code === 200) { if (res.code === 200) {
closeUpSongModal(); closeUpSongModal();
if (!res.privateCloud.simpleSong.al?.name) { if (!res.privateCloud.simpleSong.al?.name) {
$message.warning("上传歌曲详细信息获取失败,可尝试歌曲纠正"); $message.warning(t("general.message.upCloudNotHas"));
} }
$message.success(res.privateCloud.simpleSong?.name + " 上传成功"); $message.success(
t("general.message.upCloudSuccess", {
name: res.privateCloud.simpleSong?.name,
})
);
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value); getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
} else { } else {
upSongType.value = "error"; upSongType.value = "error";
$message.error("歌曲上传出错,请重试"); $message.error(t("general.message.upCloudError"));
console.error("歌曲上传出错,请重试"); console.error(t("general.message.upCloudError"));
} }
}) })
.catch((err) => { .catch((err) => {
upSongType.value = "error"; upSongType.value = "error";
closeUpSongModal(); closeUpSongModal();
$message.error("歌曲上传出现错误"); $message.error(t("general.message.upCloudFailure"));
console.error("歌曲上传出现错误:" + err); console.error(t("general.message.upCloudFailure"), err);
}); });
}; };
@@ -232,7 +243,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
$setSiteTitle("音乐库 - 音乐云盘"); $setSiteTitle(t("nav.user") + " - " + t("nav.userChildren.cloud"));
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value); getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
}); });
</script> </script>

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