Compare commits

..

54 Commits

Author SHA1 Message Date
底层用户
2370d237a8 Merge pull request #34 from imsyy/dev
feat: 同步开发分支
2023-05-11 14:05:25 +08:00
imsyy
ffbe6229f9 style: 调整进度条样式 2023-05-11 14:04:30 +08:00
imsyy
bf1312889d style: 部分组件样式调整 2023-05-11 11:19:20 +08:00
imsyy
2bf3d7db5a style: 样式微调 2023-05-10 18:25:58 +08:00
imsyy
06ccb969e4 feat: 新增歌单及专辑页播放全部 & 移动端体验优化 2023-05-10 18:18:31 +08:00
imsyy
6867897e02 fix: 在切换模式时播放进度异常
- 我愿称之为屎山添屎 🤡
2023-05-10 10:26:11 +08:00
imsyy
c78f94ae86 fix: 切换歌曲时多次弹窗 2023-05-09 17:57:06 +08:00
imsyy
40ed0dada1 fix: 切换歌曲时异常播放上一首 2023-05-09 17:33:13 +08:00
imsyy
f4d2c5f337 fix: 修复播放异常问题(可能) 2023-05-09 16:52:30 +08:00
imsyy
aace9e97b0 fix: 再次修复引用错误 😂 2023-05-09 16:20:15 +08:00
imsyy
b3641801df fix: 引用错误 2023-05-09 16:13:54 +08:00
imsyy
1dd877832c feat: 更换播放器为 Howler
- 新增站点标题自定义
- 暂时去除音乐频谱功能
2023-05-09 16:05:32 +08:00
imsyy
660cd33387 fix: 修复部分样式 2023-05-05 16:19:54 +08:00
imsyy
8caebf65f9 feat: 歌单页新增播放全部 2023-05-05 12:06:52 +08:00
imsyy
3b432dbd8b feat: 歌词页面快捷设置 2023-05-04 17:26:29 +08:00
imsyy
ee934c89f4 fix: 修复引用错误 2023-05-04 15:00:52 +08:00
imsyy
921b0eed0a fix: 修复歌手全部歌曲显示异常 #30 2023-05-04 14:55:27 +08:00
底层用户
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
imsyy
ecdb3b75ba fix: 修复歌单为空时的提示 2023-04-20 17:00:44 +08:00
imsyy
415cf3b3c9 feat: 播放列表重构 & fix: 修复一些样式问题 2023-04-20 16:48:28 +08:00
底层用户
bc84e11adf Merge pull request #18 from imsyy/dev
feat: 设置页面重构
2023-04-19 18:13:04 +08:00
imsyy
5425288e16 feat: 设置页面重构 2023-04-19 17:59:12 +08:00
imsyy
ae8f3696f3 fix: 修复一些样式问题 2023-04-19 16:39:57 +08:00
底层用户
6a102a1bff Merge pull request #17 from imsyy/dev
feat: 支持 UnblockNeteaseMusic
2023-04-18 17:35:42 +08:00
imsyy
80aea0826b fix: 区分网页端解灰种类 2023-04-18 17:26:34 +08:00
imsyy
a65a7224ae feat: 尝试支持 UnblockNeteaseMusic 2023-04-18 15:05:39 +08:00
底层用户
ddd12364fe Merge pull request #15 from imsyy/dev
fix: 修复逐字歌词导致的一系列问题
2023-04-17 14:28:27 +08:00
imsyy
7592296124 fix: 修复逐字歌词情况下歌词滚动异常 2023-04-17 14:26:34 +08:00
imsyy
0922735b2d fix: 修复逐字歌词导致的一系列问题 2023-04-17 11:57:15 +08:00
底层用户
144955e7c8 feat: 支持逐字歌词显示
feat: 支持逐字歌词显示
2023-04-14 17:26:45 +08:00
imsyy
7495e7af2d feat: 支持逐字歌词显示 2023-04-14 17:26:12 +08:00
底层用户
43fe04b4fc feat: 新增歌曲前奏等待提醒
feat: 新增歌曲前奏等待提醒
2023-04-14 14:58:55 +08:00
imsyy
db5aebbf89 feat: 新增歌曲前奏等待提醒 2023-04-14 14:56:05 +08:00
底层用户
535d0f7493 Merge pull request #12 from imsyy/dev
feat: 更换歌词解析方式
2023-04-13 16:51:32 +08:00
imsyy
7415f591b3 feat: 更换歌词解析方式 2023-04-13 16:50:12 +08:00
imsyy
e138d06e6f feat: 歌词滚动组件抽离 2023-04-13 14:06:08 +08:00
底层用户
cd05376e18 Merge pull request #11 from imsyy/dev
fix: 歌单无法展示
2023-04-12 16:29:23 +08:00
imsyy
45b374a0cb fix: 歌单无法展示 2023-04-12 16:28:56 +08:00
底层用户
418da81738 Merge pull request #10 from imsyy/dev
feat: 站点标题跟随页面内容
2023-04-12 14:57:14 +08:00
imsyy
dd66725d9c feat: 站点标题跟随页面内容 2023-04-12 14:40:01 +08:00
94 changed files with 5667 additions and 2283 deletions

22
.env
View File

@@ -1,6 +1,22 @@
# 全局 API 地址
## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi
VITE_MUSIC_API = "https://api-music.imsyy.top/"
# 网易云解灰 API 地址(可选功能)
## 需部署 API详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C
VITE_UNM_API = "https://api-unm.imsyy.top/"
# 站点标题
VITE_SITE_TITLE = "SPlayer"
VITE_SITE_ANTHOR = "無名"
VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器"
VITE_SITE_DES = "一个简约的在线音乐播放器具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
VITE_SITE_URL = "imsyy.top"
VITE_SITE_LOGO = "/images/logo/favicon.svg"
# 百度统计(若不需要,请设为空即可)
VITE_SITE_BAIDUTONGJI = "c6579e9a33cbc5260fc90231678556ec"
# ICP 备案号
## 若不需要,请设为空即可
VITE_ICP = "豫ICP备2022018134号-1"
@@ -8,8 +24,8 @@ VITE_ICP = "豫ICP备2022018134号-1"
# 公告配置
## 若无需公告,请将任意一项设为空即可
## 公告标题
VITE_ANN_TITLE = "即将完成"
VITE_ANN_TITLE = ""
## 公告内容
VITE_ANN_CONTENT = "进行最后完善"
## 公告时长(毫秒)
VITE_ANN_CONTENT = ""
## 公告时长(毫秒)不可超过 999999
VITE_ANN_DURATION = 3000

26
.hintrc Normal file
View File

@@ -0,0 +1,26 @@
{
"extends": [
"development"
],
"hints": {
"detect-css-reflows/composite": "off",
"detect-css-reflows/layout": "off",
"detect-css-reflows/paint": "off",
"compat-api/css": [
"default",
{
"ignore": [
"backdrop-filter"
]
}
],
"compat-api/html": [
"default",
{
"ignore": [
"meta[name=theme-color]"
]
}
]
}
}

View File

@@ -6,8 +6,12 @@
</div>
<br />
> 本项目采用 Vue 3 全家桶及 SCSS 开发
> 目前主要以 PC 端为主,移动端做了基础适配,仅保证功能
## 说明
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 `SCSS` 开发
- 目前主要以 `Web` 端为主,可能暂时不会考虑使用 `Electron` 构建客户端
- 仅对移动端做了基础适配,**不保证功能全部可用**
- 欢迎各位大佬指点和 `Star` 哦 😍
## 👀 Demo
@@ -15,37 +19,36 @@
## 🎉 功能
- 账号
- 扫码登录
- 手机号登录(目前暂时无法使用)
- 自动进行每日签到及云贝签到
- 管理
- 下载歌曲(最高 Hi-Res
- 新建歌单
- 歌单编辑
- 收藏 / 取消收藏歌单
- 收藏 / 取消收藏歌手
- 推荐
- 每日推荐歌曲
- 私人 FM
- 音乐云盘
- 云盘音乐上传
- 云盘内歌曲播放
- 云盘内歌曲纠正
- 云盘歌曲删除
- 播放
- 歌词滚动以及歌词翻译
- MV 与视频播放
- 音乐频谱显示( 实验性功能,需在设置中开启
- 音乐渐入渐出
- 其他
- 支持 PWA
- 支持评论区及评论点赞
- 明暗模式自动 / 手动切换
- 移动端基础适配
- 支持扫码登录
- 支持手机号登录(上游接口暂时无法使用)
- 自动进行每日签到及云贝签到
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
- 由于酷我音源不支持 `https`,故网页端替换可能不全面
- 下载歌曲(最高支持 Hi-Res
- 新建歌单及歌单编辑
- 收藏 / 取消收藏歌单或歌手
- 每日推荐歌曲
- 私人 FM
- 云盘音乐上传
- 云盘内歌曲播放
- 云盘内歌曲纠正
- 云盘歌曲删除
- 支持逐字歌词
- 歌词滚动以及歌词翻译
- MV 与视频播放
- 音乐频谱显示( 暂时去除,还待完善
- 音乐渐入渐出
- 支持 PWA
- 支持评论区及评论点赞
- 明暗模式自动 / 手动切换
- 移动端基础适配
- [ ] 主题换肤
#### 待办
- [ ] 电台节目支持
- [ ] 发表评论
- [ ] `i18n` 支持
- [ ] 重构(写成屎山了) 🤣
## 😍 Screenshots
@@ -83,16 +86,20 @@
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
### API 服务
### API 服务(必需)
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址
- 请在根目录下的 `.env` 文件中的 `VITE_MUSIC_API` 中填入 API 地址(必需)
```js
VITE_MUSIC_API = "your api url"
```
### 网易云解灰 API可选
如需使用网易云解灰服务,请前往 [UNM-Server](https://github.com/imsyy/UNM-Server) 部署在线 API 服务并将 `API` 地址填入 `.env` 环境变量中,该服务用于网页端替换无法播放或无版权的歌曲。如不需要该服务,请前往站点的 `全局设置` 中关闭
### 安装依赖
```bash
@@ -131,6 +138,8 @@ npm build
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
## 📜 开源许可

View File

@@ -3,23 +3,34 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/images/logo/favicon.svg">
<link rel="icon" href="<%- logo %>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
<title>SPlayer</title>
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" />
<meta name="description" content="一个简约的在线音乐播放器具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" />
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
<title><%- title %></title>
<meta name="author" content="<%- author %>" />
<meta name="keywords" content="<%- keywords %>" />
<meta name="description" content="<%- description %>" />
<meta name="theme-color" content="#ffffff">
<!-- HarmonyOS Sans -->
<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" />
<!-- IE Out -->
<script>
if ( /*@cc_on!@*/ false || (!!window.MSInputMethodContext && !!document.documentMode))
window.location.href =
"https://support.dmeng.net/upgrade-your-browser.html?referrer=" + encodeURIComponent(window.location.href)
</script>
<% if (tongji) { %>
<!-- 百度统计 -->
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?c6579e9a33cbc5260fc90231678556ec";
hm.src = "https://hm.baidu.com/hm.js?<%- tongji %>";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<% } %>
</head>
<body>

View File

@@ -1,7 +1,6 @@
{
"name": "splayer",
"version": "1.0.4",
"private": true,
"version": "1.1.6",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",
@@ -15,14 +14,19 @@
"@ivanv/vue-collapse-transition": "^1.0.2",
"artplayer": "^4.5.12",
"axios": "^1.2.0",
"colorthief": "^2.4.0",
"howler": "^2.2.3",
"pinia": "^2.0.26",
"pinia-plugin-persistedstate": "^3.0.1",
"plyr": "^3.7.3",
"qrcode.vue": "^3.3.3",
"sass": "^1.56.1",
"screenfull": "^6.0.2",
"throttle-debounce": "^5.0.0",
"vite-plugin-html": "^3.2.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"vue-slider-component": "4.1.0-beta.7"
},
"devDependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",

638
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,15 @@
:native-scrollbar="false"
embedded
>
<main ref="mainContent" class="main">
<main
ref="mainContent"
class="main"
id="main"
:class="{
playlist: music.showPlayList,
search: site.searchInputActive,
}"
>
<n-back-top
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
style="transition: all 0.3s"
@@ -30,7 +38,7 @@
</template>
<script setup>
import { musicStore, userStore, settingStore } from "@/store";
import { musicStore, userStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router";
import { getLoginState, refreshLogin } from "@/api/login";
import { userDailySignin, userYunbeiSign } from "@/api/user";
@@ -42,6 +50,7 @@ import packageJson from "@/../package.json";
const music = musicStore();
const user = userStore();
const setting = settingStore();
const site = siteStore();
const router = useRouter();
const mainContent = ref(null);
@@ -68,6 +77,20 @@ const spacePlayOrPause = (e) => {
}
};
// 更改页面标题
const setSiteTitle = (val) => {
const title = val
? val === import.meta.env.VITE_SITE_TITLE
? val
: val + " - " + import.meta.env.VITE_SITE_TITLE
: sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
site.siteTitle = title;
sessionStorage.setItem("siteTitle", title);
if (!music.getPlayState) {
window.document.title = title;
}
};
// 刷新登录
const toRefreshLogin = () => {
const today = Date.now();
@@ -121,7 +144,7 @@ const signIn = () => {
const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功");
localStorage.clear();
window.location.href = "/";
document.location.reload();
};
onMounted(() => {
@@ -129,6 +152,7 @@ onMounted(() => {
window.$mainContent = mainContent.value;
window.$cleanAll = cleanAll;
window.$signIn = signIn;
window.$setSiteTitle = setSiteTitle;
// 公告
if (annShow) {
@@ -140,7 +164,7 @@ onMounted(() => {
}
// 版权声明
const logoText = "SPlayer";
const logoText = import.meta.env.VITE_SITE_TITLE;
const copyrightNotice = `\n\n版本: ${packageJson.version}\n作者: ${packageJson.author}\n作者主页: ${packageJson.home}\nGitHub: ${packageJson.github}`;
console.info(
`%c${logoText} %c ${copyrightNotice}`,
@@ -170,6 +194,7 @@ onMounted(() => {
if (music.getPlayListMode === "cloud") {
$message.info("登录已失效,请重新登录");
music.setPlaylists([]);
music.setPlayListMode("list");
}
}
})
@@ -208,6 +233,33 @@ onMounted(() => {
.main {
max-width: 1400px;
margin: 0 auto;
div:nth-of-type(2) {
transition: all 0.3s;
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
transition: all 0.3s;
pointer-events: none;
z-index: 2;
}
}
&.playlist {
div:nth-of-type(2) {
transform: scale(0.98);
}
}
&.search {
div:nth-of-type(2) {
&::after {
pointer-events: all;
background-color: #00000040;
}
}
}
}
}
@@ -220,6 +272,7 @@ onMounted(() => {
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.98);
// transform: scale(0.98);
transform: translateX(10px);
}
</style>

View File

@@ -59,6 +59,7 @@ export const getPlayListDetail = (id) => {
return axios({
method: "GET",
url: "/playlist/detail",
withCredentials: false,
params: {
id,
timestamp: new Date().getTime(),

View File

@@ -38,6 +38,28 @@ export const getMusicUrl = (id, level = "exhigh") => {
});
};
/**
* 网易云解灰
* @param {number} id - 要替换播放链接的音乐ID
*/
export const getMusicNumUrl = async (id) => {
const server =
process.env.NODE_ENV === "development"
? "kuwo,qq,pyncmd,kugou"
: "qq,pyncmd,kugou";
const url = `${import.meta.env.VITE_UNM_API}match?id=${id}&server=${server}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
return Promise.reject(new Error());
}
return await response.json();
};
/**
* 获取指定音乐的歌词
* @param {number} id - 要获取歌词的音乐ID
@@ -53,6 +75,21 @@ export const getMusicLyric = (id) => {
});
};
/**
* 获取指定音乐的逐字歌词
* @param {number} id - 要获取逐字歌词的音乐ID
*/
export const getMusicNewLyric = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/lyric/new",
params: {
id,
},
});
};
/**
* 获取指定音乐的详情。
* @param {string} ids - 要获取详情的音乐ID多个ID用逗号分隔
@@ -111,3 +148,22 @@ export const getSongDownload = (id, br = 999000) => {
},
});
};
/**
* 听歌打卡
* @param {number} id - 音乐ID
* @param {number} sourceid - 来源ID
*/
export const songScrobble = (id, sourceid = 0, time = 0) => {
return axios({
method: "GET",
url: "/scrobble",
hiddenBar: true,
params: {
id,
sourceid,
time,
timestamp: new Date().getTime(),
},
});
};

View File

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

View File

@@ -1,71 +1,84 @@
<template>
<n-card class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="commentData.user.vipRights.musicPackage.iconUrl"
alt="redVipAnnualCount"
title="网易音乐人"
/>
</div>
<div
class="associator"
v-if="commentData.user.vipRights?.redVipLevel > 0"
>
<img
v-if="commentData.user.vipRights.associator"
:src="commentData.user.vipRights.associator.iconUrl"
alt="associator"
title="黑胶会员"
/>
</div>
</div>
<div class="review">
<div class="content">
<n-text class="name">{{ commentData.user.nickname }}</n-text>
<n-text class="text" v-html="commentData.content" />
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<n-text class="name">
@{{ commentData.beReplied[0].user.nickname }}
</n-text>
<n-text class="text">{{ commentData.beReplied[0].content }}</n-text>
</div>
<div class="thing">
<div class="item">
<n-icon size="14" :depth="3" :component="Time" />
<n-text :depth="3" v-html="getCommentTime(commentData.time)" />
</div>
<div class="item" v-if="commentData.ipLocation?.location">
<n-icon size="14" :depth="3" :component="Local" />
<n-text :depth="3" v-html="commentData.ipLocation.location" />
<Transition mode="out-in">
<n-card v-if="Object.keys(commentData).length" class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="
commentData.user.vipRights.musicPackage.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="redVipAnnualCount"
title="网易音乐人"
/>
</div>
<div
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
class="associator"
v-if="commentData.user.vipRights?.redVipLevel > 0"
>
<n-icon>
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
</n-icon>
{{ formatNumber(commentData.likedCount) }}
<img
v-if="commentData.user.vipRights.associator"
:src="
commentData.user.vipRights.associator.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="associator"
title="黑胶会员"
/>
</div>
</div>
</div>
</n-card>
<div class="review">
<div class="content">
<n-text class="name">{{ commentData.user.nickname }}</n-text>
<n-text class="text" v-html="commentData.content" />
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<n-text class="name">
@{{ commentData.beReplied[0].user.nickname }}
</n-text>
<n-text class="text">{{ commentData.beReplied[0].content }}</n-text>
</div>
<div class="thing">
<div class="item">
<n-icon size="14" :depth="3" :component="Time" />
<n-text :depth="3" v-html="getCommentTime(commentData.time)" />
</div>
<div class="item" v-if="commentData.ipLocation?.location">
<n-icon size="14" :depth="3" :component="Local" />
<n-text :depth="3" v-html="commentData.ipLocation.location" />
</div>
<div
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
>
<n-icon>
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
</n-icon>
{{ formatNumber(commentData.likedCount) }}
</div>
</div>
</div>
</n-card>
<n-skeleton v-else class="skeleton" />
</Transition>
</template>
<script setup>
import { getCommentTime, formatNumber } from "@/utils/timeTools.js";
import { getCommentTime, formatNumber } from "@/utils/timeTools";
import { Local, Time, ThumbsUp } from "@icon-park/vue-next";
import { userStore } from "@/store";
import { useRouter } from "vue-router";
@@ -77,7 +90,7 @@ const props = defineProps({
// 评论 数据
commentData: {
type: Object,
default: [],
default: {},
},
});
@@ -105,6 +118,15 @@ const toLikeComment = () => {
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.comment {
margin-bottom: 12px;
border-radius: 8px;
@@ -174,7 +196,7 @@ const toLikeComment = () => {
cursor: pointer;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}
@@ -191,7 +213,7 @@ const toLikeComment = () => {
cursor: pointer;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}
@@ -216,14 +238,14 @@ const toLikeComment = () => {
transition: all 0.3s;
opacity: 0.6;
&:hover {
color: $mainColor;
color: var(--main-color);
opacity: 1;
}
&:active {
transform: scale(0.95);
}
&.liked {
color: $mainColor;
color: var(--main-color);
opacity: 1;
}
}
@@ -235,4 +257,10 @@ const toLikeComment = () => {
}
}
}
.skeleton {
width: 100%;
height: 120px;
border-radius: 8px;
margin-bottom: 12px;
}
</style>

View File

@@ -31,7 +31,7 @@ const router = useRouter();
const props = defineProps({
// 歌手数据
artistsData: {
type: Object,
type: Array,
default: [],
},
});
@@ -59,7 +59,7 @@ const jumpArtist = (id) => {
cursor: pointer;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
.line {

View File

@@ -24,6 +24,12 @@
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
fallback-src="/images/pic/default.png"
/>
<n-avatar
round
class="shadow"
:src="item.cover.replace(/^http:/, 'https:') + '?param=200y200'"
fallback-src="/images/pic/default.png"
/>
<n-icon size="40" :component="PeopleSearchOne" />
</div>
<n-text class="name text-hidden">{{ item.name }}</n-text>
@@ -75,7 +81,7 @@ const router = useRouter();
const props = defineProps({
// 列表数据
listData: {
type: Object,
type: Array,
default: [],
},
// 折叠栅格
@@ -205,12 +211,26 @@ onMounted(() => {
box-shadow: 0 4px 16px 0 #00000020;
border-radius: 50%;
transition: all 0.3s;
.n-avatar {
.coverImg {
filter: brightness(1);
transform: scale(1);
width: 100%;
height: 100%;
transition: all 0.3s;
z-index: 1;
}
.shadow {
opacity: 0;
position: absolute;
top: 12px;
height: 100%;
width: 100%;
filter: blur(16px) opacity(0.6);
transform: scale(0.92, 0.96);
z-index: 0;
background-size: cover;
aspect-ratio: 1/1;
transition: opacity 0.3s;
}
.n-icon {
opacity: 0;
@@ -218,6 +238,7 @@ onMounted(() => {
position: absolute;
color: #fff;
transition: all 0.3s;
z-index: 1;
}
&:hover {
box-shadow: 0 4px 16px 0 #00000040;
@@ -225,10 +246,13 @@ onMounted(() => {
opacity: 1;
transform: scale(1);
}
.n-avatar {
.coverImg {
filter: brightness(0.8);
transform: scale(1.05);
}
.shadow {
opacity: 1;
}
}
&:active {
.n-avatar {
@@ -242,7 +266,7 @@ onMounted(() => {
transition: all 0.3s;
cursor: pointer;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -23,6 +23,11 @@
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'"
fallback-src="/images/pic/default.png"
/>
<n-avatar
class="shadow"
:src="item.cover.replace(/^http:/, 'https:') + '?param=300y300'"
fallback-src="/images/pic/default.png"
/>
<n-icon class="play" size="40">
<PlayOne theme="filled" />
</n-icon>
@@ -86,13 +91,22 @@
</template>
<script setup>
import { PlayOne, Headset } from "@icon-park/vue-next";
import { NIcon } from "naive-ui";
import {
PlayOne,
Headset,
LinkTwo,
Like,
Unlike,
Editor,
DeleteFour,
} from "@icon-park/vue-next";
import { delPlayList, likePlaylist } from "@/api/playlist";
import { likeAlbum } from "@/api/album";
import { musicStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue";
import PlaylistUpdate from "@/components/DataModel/PlaylistUpdate.vue";
import PlaylistUpdate from "@/components/DataModal/PlaylistUpdate.vue";
const router = useRouter();
const music = musicStore();
@@ -100,7 +114,7 @@ const user = userStore();
const props = defineProps({
// 列表数据
listData: {
type: Object,
type: Array,
default: [],
},
// 列表类型
@@ -131,6 +145,19 @@ const props = defineProps({
});
const playlistUpdateRef = ref(null);
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 右键菜单数据
const rightMenuX = ref(0);
const rightMenuY = ref(0);
@@ -150,9 +177,10 @@ const openRightMenu = (e, data) => {
router.currentRoute.value.name === "user-playlists" ? true : false,
props: {
onClick: () => {
playlistUpdateRef.value.openUpdateModel(data);
playlistUpdateRef.value.openUpdateModal(data);
},
},
icon: renderIcon(h(Editor)),
},
{
key: "del",
@@ -164,6 +192,7 @@ const openRightMenu = (e, data) => {
toDelPlayList(data);
},
},
icon: renderIcon(h(DeleteFour)),
},
{
key: "likePlaylist",
@@ -180,6 +209,7 @@ const openRightMenu = (e, data) => {
toChangeLike(data.id);
},
},
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
},
{
key: "likeAlbum",
@@ -195,6 +225,7 @@ const openRightMenu = (e, data) => {
toChangeLike(data.id);
},
},
icon: renderIcon(h(isLikeOrDislike(data.id) ? Like : Unlike)),
},
{
key: "copy",
@@ -221,6 +252,7 @@ const openRightMenu = (e, data) => {
}
},
},
icon: renderIcon(h(LinkTwo)),
},
];
rightMenuShow.value = true;
@@ -279,10 +311,10 @@ const isLikeOrDislike = (id) => {
const playlists = user.getUserPlayLists.like;
const albums = user.getUserAlbumLists.list;
if (listType === "playlist" && playlists.length) {
return !playlists.some((item) => item.id === id);
return !playlists.some((item) => item.id === Number(id));
}
if (listType === "album" && albums.length) {
return !albums.some((item) => item.id === id);
return !albums.some((item) => item.id === Number(id));
}
return true;
};
@@ -356,7 +388,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
// overflow: hidden;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
@@ -365,7 +397,25 @@ onMounted(() => {
border-radius: 8px;
width: 100%;
height: 100%;
transition: all 0.3s;
overflow: hidden;
transition: filter 0.3s;
z-index: 1;
:deep(img) {
transition: transform 0.3s;
}
}
.shadow {
opacity: 0;
position: absolute;
top: 12px;
height: 100%;
width: 100%;
filter: blur(16px) opacity(0.6);
transform: scale(0.92, 0.96);
z-index: 0;
background-size: cover;
aspect-ratio: 1/1;
transition: opacity 0.3s;
}
.play {
opacity: 0;
@@ -373,10 +423,12 @@ onMounted(() => {
color: #fff;
padding: 0.5vw;
background-color: #00000010;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 50%;
transform: scale(0.8);
transition: all 0.3s;
z-index: 1;
}
.description {
position: absolute;
@@ -385,10 +437,13 @@ onMounted(() => {
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 6px;
border-top-left-radius: 8px;
border-bottom-right-radius: 8px;
transition: all 0.3s;
z-index: 1;
.num {
display: flex;
flex-direction: row;
@@ -402,10 +457,11 @@ onMounted(() => {
}
}
&:hover {
box-shadow: 0 15px 30px rgb(0 0 0 / 10%);
.coverImg {
filter: brightness(0.8);
transform: scale(1.1);
:deep(img) {
transform: scale(1.1);
}
}
.play {
transform: scale(1);
@@ -414,6 +470,9 @@ onMounted(() => {
.description {
opacity: 0;
}
.shadow {
opacity: 1;
}
}
&:active {
transform: scale(0.98);
@@ -431,7 +490,7 @@ onMounted(() => {
cursor: pointer;
&:hover {
opacity: 1;
color: $mainColor;
color: var(--main-color);
}
}
.by {
@@ -441,7 +500,7 @@ onMounted(() => {
cursor: pointer;
&:hover {
opacity: 1;
color: $mainColor;
color: var(--main-color);
}
}
.artists {

View File

@@ -1,13 +1,12 @@
<template>
<Transition mode="out-in">
<div class="datalists" v-if="listData[0]">
<div class="datalists" id="datalists" v-if="listData[0]">
<n-card
hoverable
v-for="item in listData"
:key="item"
:class="
music.getPlaySongData
? music.getPlaySongData.id == item.id
? 'songs play'
: 'songs'
music.getPlaySongData && music.getPlaySongData.id == item.id
? 'songs play'
: 'songs'
"
:content-style="{
@@ -17,8 +16,7 @@
alignItems: 'center',
justifyContent: 'space-between',
}"
v-for="item in listData"
:key="item"
hoverable
@dblclick="
setting.listClickMode === 'dblclick' ? playSong(listData, item) : null
"
@@ -43,13 +41,13 @@
@click.stop="jumpLink(item.id, 1)"
/>
<n-tag
v-if="item.fee == 1"
v-if="item.fee == 1 || item.fee == 4"
class="vip"
round
:bordered="false"
size="small"
>
VIP
{{ item.fee == 1 ? "VIP" : "数字专辑" }}
</n-tag>
<n-tag
v-if="item.pc"
@@ -110,7 +108,7 @@
<n-icon
class="download"
size="20"
@click.stop="downloadSongRef.openDownloadModel(item)"
@click.stop="downloadSongRef.openDownloadModal(item)"
>
<DownloadFour theme="filled" />
</n-icon>
@@ -204,7 +202,7 @@
class="item"
@click="
() => {
downloadSongRef.openDownloadModel(drawerData);
downloadSongRef.openDownloadModal(drawerData);
drawerShow = false;
}
"
@@ -333,10 +331,11 @@ import { musicStore, settingStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import { setCloudDel } from "@/api/user";
import { NIcon } from "naive-ui";
import { soundStop } from "@/utils/Player";
import AllArtists from "./AllArtists.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import CloudMatch from "@/components/DataModel/CloudMatch.vue";
import DownloadSong from "@/components/DataModel/DownloadSong.vue";
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
import CloudMatch from "@/components/DataModal/CloudMatch.vue";
import DownloadSong from "@/components/DataModal/DownloadSong.vue";
import SmallSongData from "./SmallSongData.vue";
const router = useRouter();
@@ -350,7 +349,7 @@ const downloadSongRef = ref(null);
const props = defineProps({
// 列表数据
listData: {
type: Object,
type: Array,
default: [],
},
// 专辑隐藏
@@ -387,13 +386,13 @@ const copySongData = (id, url = true) => {
};
// 图标渲染
const renderIcon = (icon) => {
const renderIcon = (icon, filled = true) => {
return () => {
return h(
NIcon,
{ depth: 2, style: { transform: "translateX(2px)" } },
{
default: () => h(icon, { theme: "filled" }),
default: () => h(icon, { theme: filled ? "filled" : "outline" }),
}
);
};
@@ -420,9 +419,9 @@ const openRightMenu = (e, data) => {
label: "下一首播放",
icon: renderIcon(AddMusic),
show:
music.getPersonalFmMode || music.getPlaySongData.id == data.id
? true
: false,
music.getPersonalFmMode || music.getPlaySongData?.id == data.id
? false
: true,
props: {
onClick: () => {
music.addSongToNext(data);
@@ -446,14 +445,14 @@ const openRightMenu = (e, data) => {
icon: renderIcon(DownloadFour),
props: {
onClick: () => {
downloadSongRef.value.openDownloadModel(data);
downloadSongRef.value.openDownloadModal(data);
},
},
},
{
key: "comment",
label: "前往评论区",
icon: renderIcon(Comments),
icon: renderIcon(Comments, false),
props: {
onClick: () => {
router.push(`/comment?id=${data.id}`);
@@ -463,7 +462,7 @@ const openRightMenu = (e, data) => {
{
key: "mv",
label: "观看 MV",
icon: renderIcon(Video),
icon: renderIcon(Video, false),
show: data.mv && data.mv != 0 ? true : false,
props: {
onClick: () => {
@@ -505,7 +504,7 @@ const openRightMenu = (e, data) => {
{
key: "search",
label: "同名搜索",
icon: renderIcon(Search),
icon: renderIcon(Search, false),
props: {
onClick: () => {
router.push({
@@ -521,7 +520,7 @@ const openRightMenu = (e, data) => {
{
key: "copyId",
label: "复制歌曲 ID",
icon: renderIcon(FileMusic),
icon: renderIcon(FileMusic, false),
props: {
onClick: () => {
copySongData(data.id, false);
@@ -583,7 +582,11 @@ const openDrawer = (data) => {
// 播放并添加
const playSong = (data, song) => {
console.log(data, song);
music.setPersonalFmMode(false);
if (music.getPersonalFmMode) {
soundStop($player);
music.setPersonalFmMode(false);
}
music.setPlayState(true);
if (router.currentRoute.value.name !== "history") music.setPlaylists(data);
// 检查是否为云盘歌曲
if (router.currentRoute.value.name === "user-cloud") {
@@ -637,9 +640,10 @@ const jumpLink = (id, type) => {
transition: all 0.3s;
cursor: pointer;
&:hover {
border-color: $mainColor;
box-shadow: 0 1px 2px -2px #f55e5526, 0 3px 6px 0 #f55e5530,
0 5px 12px 4px #f55e5505;
border-color: var(--main-color);
box-shadow: 0 1px 2px -2px var(--main-boxshadow-color),
0 3px 6px 0 var(--main-boxshadow-color),
0 5px 12px 4px var(--main-boxshadow-hover-color);
.action {
.like,
.download {
@@ -652,18 +656,18 @@ const jumpLink = (id, type) => {
// transform: scale(0.99);
// }
&.play {
background-color: $mainSecondaryColor;
border-color: $mainColor;
background-color: var(--main-second-color);
border-color: var(--main-color);
a,
span,
.n-icon {
color: $mainColor;
color: var(--main-color);
}
.artists {
:deep(.artist) {
.name,
.line {
color: $mainColor;
color: var(--main-color);
}
}
}
@@ -703,7 +707,7 @@ const jumpLink = (id, type) => {
font-weight: bold;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
.n-tag {
@@ -712,8 +716,8 @@ const jumpLink = (id, type) => {
height: 18px;
}
.vip {
color: $mainColor;
background-color: $mainSecondaryColor;
color: var(--main-color);
background-color: var(--main-second-color);
}
.mv {
cursor: pointer;
@@ -744,7 +748,7 @@ const jumpLink = (id, type) => {
.n-text {
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}
@@ -770,7 +774,7 @@ const jumpLink = (id, type) => {
cursor: pointer;
opacity: 0;
transform: scale(0.8);
color: $mainColor;
color: var(--main-color);
transition: all 0.3s;
&:hover {
transform: scale(1.1);

View File

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

View File

@@ -105,6 +105,7 @@ onMounted(() => {
.pic {
margin-right: 12px;
border-radius: 8px;
min-width: 48px;
}
.name {
line-height: 1.6;
@@ -113,7 +114,7 @@ onMounted(() => {
transition: all 0.3s;
cursor: pointer;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
.artists {

View File

@@ -66,7 +66,6 @@
<script setup>
import { PlayOne, Youtube } from "@icon-park/vue-next";
import { PlayArrowRound, OndemandVideoFilled } from "@vicons/material";
import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue";
@@ -74,7 +73,7 @@ const router = useRouter();
const props = defineProps({
// 列表数据
listData: {
type: Object,
type: Array,
default: [],
},
});
@@ -115,6 +114,7 @@ const props = defineProps({
color: #fff;
padding: 0.5vw;
background-color: #00000010;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 50%;
transform: scale(0.8);
@@ -126,6 +126,7 @@ const props = defineProps({
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 4px 8px;
transition: all 0.3s;
@@ -175,7 +176,7 @@ const props = defineProps({
cursor: pointer;
&:hover {
opacity: 1;
color: $mainColor;
color: var(--main-color);
}
}
.by {
@@ -185,7 +186,7 @@ const props = defineProps({
cursor: pointer;
&:hover {
opacity: 1;
color: $mainColor;
color: var(--main-color);
}
}
.artists {

View File

@@ -10,7 +10,7 @@
>
<div class="copyright">
<div class="desc">
<n-text class="name">SPlayer</n-text>
<n-text class="name">{{ siteTitle }}</n-text>
<n-text class="version" :depth="3">
v&nbsp;{{ packageJson.version }}
</n-text>
@@ -53,6 +53,7 @@ import { GithubOne } from "@icon-park/vue-next";
import packageJson from "@/../package.json";
//
const siteTitle = import.meta.env.VITE_SITE_TITLE;
const showAboutModal = ref(false);
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);

View File

@@ -1,16 +1,33 @@
<template>
<n-modal
class="add-playlist s-modal"
v-model:show="addToPlaylistModel"
v-model:show="addToPlaylistModal"
preset="card"
title="添加到歌单"
:bordered="false"
:on-after-leave="closeAddToPlaylist"
>
<template #header>
添加到歌单
<n-tag
round
class="tag"
type="primary"
:style="{
marginLeft: '12px',
fontSize: '13px',
transform: 'translateY(-2px)',
cursor: 'pointer',
}"
:bordered="false"
@click="createPlaylistRef.openCreatePlaylist()"
>
新建歌单
</n-tag>
</template>
<n-space vertical class="list" v-if="user.getUserPlayLists.own[0]">
<div
class="item"
v-for="item in user.getUserPlayLists.own"
v-for="item in user.getUserPlayLists.own.slice(1)"
:key="item"
@click="addToPlayList(item.id, addToPlaylistId)"
>
@@ -31,16 +48,20 @@
</n-space>
<n-text v-else>歌单列表加载中</n-text>
</n-modal>
<!-- 新建歌单 -->
<CreatePlaylist ref="createPlaylistRef" />
</template>
<script setup>
import { addSongToPlayList } from "@/api/playlist";
import { userStore } from "@/store";
import CreatePlaylist from "./CreatePlaylist.vue";
const user = userStore();
const createPlaylistRef = ref(null);
//
const addToPlaylistModel = ref(false);
const addToPlaylistModal = ref(false);
const addToPlaylistId = ref(null);
//
@@ -67,14 +88,14 @@ const openAddToPlaylist = (id) => {
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
user.setUserPlayLists();
}
addToPlaylistModel.value = true;
addToPlaylistModal.value = true;
addToPlaylistId.value = id;
console.log("开启", addToPlaylistModel.value, addToPlaylistId.value);
console.log("开启", addToPlaylistModal.value, addToPlaylistId.value);
};
//
const closeAddToPlaylist = () => {
addToPlaylistModel.value = false;
addToPlaylistModal.value = false;
};
//

View File

@@ -1,7 +1,7 @@
<template>
<n-modal
class="s-modal"
v-model:show="cloudMatchModel"
v-model:show="cloudMatchModal"
preset="card"
title="歌曲信息纠正"
:bordered="false"
@@ -65,7 +65,7 @@ const user = userStore();
//
const cloudDataLoad = inject("cloudDataLoad", null);
const smallSongDataRef = ref(null);
const cloudMatchModel = ref(false);
const cloudMatchModal = ref(false);
const cloudMatchBeforeData = ref(null);
const cloudMatchId = ref(null);
const cloudMatchValue = ref({
@@ -102,7 +102,7 @@ const setCloudMatchBtn = (data) => {
const openCloudMatch = (data) => {
cloudMatchValue.value.sid = data.id;
cloudMatchBeforeData.value = data;
cloudMatchModel.value = true;
cloudMatchModal.value = true;
};
//
@@ -110,7 +110,7 @@ const closeCloudMatch = () => {
cloudMatchBeforeData.value = null;
cloudMatchId.value = null;
cloudMatchValue.value.asid = null;
cloudMatchModel.value = false;
cloudMatchModal.value = false;
};
//

View File

@@ -0,0 +1,75 @@
<template>
<n-modal
class="s-modal"
v-model:show="createPlaylistShow"
preset="card"
title="新建歌单"
:bordered="false"
:on-after-leave="closeCreatePlaylist"
>
<n-input
style="margin-bottom: 12px"
v-model:value="createName"
type="text"
placeholder="请输入新歌单标题"
/>
<n-checkbox v-model:checked="createPrivacy"> 设置为隐私歌单 </n-checkbox>
<template #footer>
<n-space justify="end">
<n-button @click="closeCreatePlaylist"> 取消 </n-button>
<n-button
type="primary"
@click="createPlaylistBtn(createName, createPrivacy)"
>
新建
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { userStore } from "@/store";
import { createPlaylist } from "@/api/playlist";
const user = userStore();
// 新建歌单数据
const createPlaylistShow = ref(false);
const createPrivacy = ref(false);
const createName = ref(null);
// 新建歌单
const createPlaylistBtn = (name, privacy = false) => {
if (createName.value) {
createPlaylist(name, privacy ? "10" : null).then((res) => {
if (res.code === 200) {
closeCreatePlaylist();
$message.success("歌单新建成功");
user.setUserPlayLists();
} else {
$message.error("歌单新建失败,请重试");
}
});
} else {
$message.info("请输入歌单名称");
}
};
// 开启新建歌单
const openCreatePlaylist = () => {
createPlaylistShow.value = true;
};
// 取消新建歌单
const closeCreatePlaylist = () => {
createName.value = null;
createPrivacy.value = false;
createPlaylistShow.value = false;
};
// 暴露方法
defineExpose({
openCreatePlaylist,
});
</script>

View File

@@ -1,11 +1,11 @@
<template>
<n-modal
class="s-modal downloadModel"
v-model:show="downloadModel"
class="s-modal downloadModal"
v-model:show="downloadModal"
preset="card"
title="歌曲下载"
:bordered="false"
:on-after-leave="closeDownloadModel"
:on-after-leave="closeDownloadModal"
>
<Transition mode="out-in">
<div v-if="songData">
@@ -42,7 +42,7 @@
</Transition>
<template #footer>
<n-space justify="end">
<n-button @click="closeDownloadModel"> 取消 </n-button>
<n-button @click="closeDownloadModal"> 取消 </n-button>
<n-button
:disabled="!downloadChoose"
:loading="downloadStatus"
@@ -75,7 +75,7 @@ const router = useRouter();
const songId = ref(null);
const songData = ref(null);
const downloadStatus = ref(false);
const downloadModel = ref(false);
const downloadModal = ref(false);
const downloadChoose = ref(null);
const downloadLevel = ref(null);
@@ -98,7 +98,7 @@ const toSongDownload = (id, br, name) => {
document.body.appendChild(a);
a.click();
a.remove();
closeDownloadModel();
closeDownloadModal();
downloadStatus.value = false;
$message.success(name + " 下载完成");
});
@@ -108,7 +108,7 @@ const toSongDownload = (id, br, name) => {
}
})
.catch((err) => {
closeDownloadModel();
closeDownloadModal();
console.error("下载出现错误:" + err);
$message.error("下载出现错误,请重试");
});
@@ -133,7 +133,7 @@ const getMusicDetailData = (id) => {
}
})
.catch((err) => {
closeDownloadModel();
closeDownloadModal();
console.error("歌曲信息获取出现错误:" + err);
$message.error("歌曲信息获取出现错误,请重试");
});
@@ -200,7 +200,7 @@ const getSongSize = (data, type) => {
};
//
const openDownloadModel = (data) => {
const openDownloadModal = (data) => {
if (user.userLogin) {
if (
router.currentRoute.value.name === "user-cloud" ||
@@ -209,7 +209,7 @@ const openDownloadModel = (data) => {
data?.pc
) {
songId.value = data.id;
downloadModel.value = true;
downloadModal.value = true;
getMusicDetailData(data.id);
} else {
$message.error("该歌曲需使用黑胶会员下载");
@@ -220,22 +220,22 @@ const openDownloadModel = (data) => {
};
//
const closeDownloadModel = () => {
const closeDownloadModal = () => {
songId.value = null;
songData.value = null;
downloadStatus.value = false;
downloadModel.value = false;
downloadModal.value = false;
downloadChoose.value = null;
};
//
defineExpose({
openDownloadModel,
openDownloadModal,
});
</script>
<style lang="scss" scoped>
.downloadModel {
.downloadModal {
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;

View File

@@ -0,0 +1,189 @@
<template>
<!-- 歌词设置 -->
<n-modal
:bordered="false"
:z-index="10000"
class="s-modal lyric-set"
v-model:show="LyricSettingModal"
preset="card"
title="歌词设置"
>
<n-scrollbar>
<div class="set">
<n-card class="set-item">
<div class="name">
播放页快捷设置
<span class="tip">关闭后需在设置页开启</span>
</div>
<n-switch v-model:value="showLyricSetting" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示逐字歌词
<span class="tip">是否在歌曲具有逐字歌词时显示实验性功能</span>
</div>
<n-switch v-model:value="showYrc" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示歌词翻译
<span class="tip">是否在具有翻译歌词时显示</span>
</div>
<n-switch v-model:value="showTransl" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示歌词音译
<span class="tip">是否在具有音译歌词时显示</span>
</div>
<n-switch v-model:value="showRoma" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示前奏等待
<span class="tip">部分歌曲前奏可能存在显示错误</span>
</div>
<n-switch v-model:value="countDownShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
智能暂停滚动
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
</div>
<n-switch v-model:value="lrcMousePause" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌词模糊
<span class="tip">未播放或已播放歌词模糊显示实验性功能</span>
</div>
<n-switch v-model:value="lyricsBlur" :round="false" />
</n-card>
<n-space justify="center">
<n-button
class="more"
size="large"
strong
secondary
round
@click="
() => {
LyricSettingModal = false;
music.setBigPlayerState(false);
router.push('/setting/player');
}
"
>
更多设置
</n-button>
</n-space>
</div>
</n-scrollbar>
</n-modal>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { settingStore } from "@/store";
import { useRouter } from "vue-router";
import { musicStore } from "@/store";
const setting = settingStore();
const router = useRouter();
const music = musicStore();
// 歌词设置弹窗
const LyricSettingModal = ref(false);
// 设置数据
const {
showTransl,
lyricsBlur,
lrcMousePause,
showYrc,
showRoma,
countDownShow,
showLyricSetting,
} = storeToRefs(setting);
// 开启歌词设置弹窗
const openLyricSetting = () => {
LyricSettingModal.value = true;
};
// 暴露方法
defineExpose({
openLyricSetting,
});
</script>
<style lang="scss">
.n-card {
&.lyric-set {
background-color: #ffffff40;
color: #fff;
.n-card-header {
.n-card-header__main,
.n-card-header__close {
color: #fff;
}
}
}
}
</style>
<style lang="scss" scoped>
.set {
width: 100%;
:deep(.set-item) {
width: 100%;
color: #fff;
border-radius: 8px;
margin-bottom: 12px;
background-color: #ffffff40;
border-color: transparent;
.n-card__content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.name {
font-size: 16px;
display: flex;
flex-direction: column;
padding-right: 20px;
.tip {
font-size: 12px;
opacity: 0.8;
}
}
.n-switch {
--n-box-shadow-focus: none;
&.n-switch--active {
.n-switch__rail {
background-color: #ffffff78;
}
}
.n-switch__rail {
background-color: #ffffff24;
}
}
.set {
width: 200px;
@media (max-width: 768px) {
width: 140px;
min-width: 140px;
}
}
}
}
.more {
margin: 12px 0;
color: #fff;
background-color: #ffffff40;
&:hover {
background-color: #ffffff20;
}
}
}
</style>

View File

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

View File

@@ -1,11 +1,11 @@
<template>
<n-modal
class="s-modal"
v-model:show="playlistUpdateModel"
v-model:show="playlistUpdateModal"
preset="card"
title="歌单编辑"
:bordered="false"
:on-after-leave="closeUpdateModel"
:on-after-leave="closeUpdateModal"
>
<n-form
ref="playlistUpdateRef"
@@ -42,7 +42,7 @@
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="closeUpdateModel"> 取消 </n-button>
<n-button @click="closeUpdateModal"> 取消 </n-button>
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button>
</n-space>
</template>
@@ -51,7 +51,7 @@
<script setup>
import { playlistUpdate } from "@/api/playlist";
import { formRules } from "@/utils/formRules.js";
import { formRules } from "@/utils/formRules";
import { musicStore, userStore } from "@/store";
const { textRule } = formRules();
@@ -61,7 +61,7 @@ const user = userStore();
//
const playlistUpdateId = ref(null);
const playlistUpdateRef = ref(null);
const playlistUpdateModel = ref(false);
const playlistUpdateModal = ref(false);
const playlistUpdateRules = {
name: textRule,
};
@@ -86,7 +86,7 @@ const toUpdatePlayList = (e) => {
console.log(res);
if (res.code === 200) {
$message.success("编辑成功");
closeUpdateModel();
closeUpdateModal();
user.setUserPlayLists();
} else {
$message.error("编辑失败,请重试");
@@ -113,24 +113,24 @@ const openSelect = () => {
};
//
const openUpdateModel = (data) => {
const openUpdateModal = (data) => {
playlistUpdateValue.value = {
name: data.name,
desc: data.desc,
tags: data.tags,
};
playlistUpdateId.value = data.id;
playlistUpdateModel.value = true;
playlistUpdateModal.value = true;
};
//
const closeUpdateModel = () => {
playlistUpdateModel.value = false;
const closeUpdateModal = () => {
playlistUpdateModal.value = false;
playlistUpdateId.value = null;
};
//
defineExpose({
openUpdateModel,
openUpdateModal,
});
</script>

View File

@@ -2,12 +2,14 @@
<nav>
<div class="left">
<div class="logo" @click="router.push('/')">
<img src="/images/logo/favicon.svg" alt="logo" />
</div>
<div class="controls">
<n-icon size="22" :component="Left" @click="router.go(-1)" />
<n-icon size="22" :component="Right" @click="router.go(1)" />
<img :src="logoUrl" alt="logo" />
</div>
<Transition name="fade" mode="out-in">
<div v-show="!site.searchInputActive" class="controls">
<n-icon size="22" :component="Left" @click="router.go(-1)" />
<n-icon size="22" :component="Right" @click="router.go(1)" />
</div>
</Transition>
</div>
<div class="center">
<router-link class="link" to="/">首页</router-link>
@@ -18,19 +20,22 @@
>
<router-link class="link" to="/discover">发现</router-link>
</n-dropdown>
<n-dropdown
trigger="hover"
:options="userOptions"
@select="menuSelect"
>
<n-dropdown trigger="hover" :options="userOptions" @select="menuSelect">
<router-link class="link" to="/user">我的</router-link>
</n-dropdown>
</div>
<div class="right">
<SearchInp />
<!-- 移动端菜单 -->
<n-dropdown trigger="click" :options="mbMenuOptions" @select="menuSelect">
<n-button class="mb-menu" circle>
<template #icon>
<n-icon :component="HamburgerButton" />
</template>
</n-button>
</n-dropdown>
<!-- 下拉菜单 -->
<n-dropdown
class="dropdown"
placement="bottom-end"
:show="showDropdown"
:show-arrow="true"
@@ -44,7 +49,8 @@
size="small"
:src="
user.getUserData.avatarUrl
? user.getUserData.avatarUrl
? user.getUserData.avatarUrl.replace(/^http:/, 'https:') +
'?param=60y60'
: '/images/ico/user-filling.svg'
"
:img-props="{ class: 'avatarImg' }"
@@ -70,16 +76,23 @@ import {
History,
SunOne,
Moon,
HamburgerButton,
HomeTwo,
FindOne,
Me,
} from "@icon-park/vue-next";
import { userStore, settingStore } from "@/store";
import { userStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router";
import AboutSite from "@/components/DataModel/AboutSite.vue";
import AboutSite from "@/components/DataModal/AboutSite.vue";
import SearchInp from "@/components/SearchInp/index.vue";
const router = useRouter();
const user = userStore();
const site = siteStore();
const setting = settingStore();
const aboutSiteRef = ref(null);
const timeOut = ref(null);
const logoUrl = import.meta.env.VITE_SITE_LOGO;
// 下拉菜单显隐
const showDropdown = ref(false);
@@ -92,6 +105,19 @@ const closeDropdown = (event) => {
}
};
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 用户数据模块
const userDataRender = () => {
return h(
@@ -109,7 +135,8 @@ const userDataRender = () => {
round: true,
style: "margin-right: 12px",
src: user.userLogin
? user.getUserData.avatarUrl
? user.getUserData.avatarUrl.replace(/^http:/, "https:") +
"?param=60y60"
: "/images/ico/user-filling.svg",
fallbackSrc: "/images/ico/user-filling.svg",
}),
@@ -139,7 +166,7 @@ const userDataRender = () => {
type: "line",
percentage:
user.getUserOtherData.level.progress * 100,
color: "#f55e55",
color: setting.themeData.primaryColor,
},
{
default: () =>
@@ -171,8 +198,9 @@ const discoverOptions = ref([
key: "/discover/artists",
},
]);
const userOptions = ref(
user.userLogin
const userOptions = ref([]);
const changeUserOptions = (val) => {
userOptions.value = val
? [
{
label: "我的歌单",
@@ -200,8 +228,8 @@ const userOptions = ref(
label: "登录账号",
key: "/login",
},
]
);
];
};
const dropdownOptions = ref([
{
key: "header",
@@ -238,28 +266,12 @@ const dropdownOptions = ref([
{
label: "播放历史",
key: "history",
icon: () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(History),
}
);
},
icon: renderIcon(h(History)),
},
{
label: "全局设置",
key: "setting",
icon: () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(SettingTwo),
}
);
},
icon: renderIcon(h(SettingTwo)),
},
{
label: () => {
@@ -285,19 +297,30 @@ const dropdownOptions = ref([
{
label: "关于本站",
key: "about",
icon: () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(Info),
}
);
},
icon: renderIcon(h(Info)),
},
]);
// 下拉框事件
// 移动端菜单
const mbMenuOptions = ref([
{
label: "首页",
key: "/",
icon: renderIcon(h(HomeTwo)),
},
{
label: "发现",
key: "/discover",
icon: renderIcon(h(FindOne)),
},
{
label: "我的",
key: "/user",
icon: renderIcon(h(Me)),
},
]);
// 下拉框点击事件
const menuSelect = (key) => {
router.push(key);
};
@@ -331,6 +354,10 @@ const dropdownSelect = (key) => {
onPositiveClick: () => {
user.userLogOut();
$message.success("已退出登录");
// 刷新页面
timeOut.value = setTimeout(() => {
document.location.reload();
}, 1000);
},
});
} else {
@@ -346,6 +373,22 @@ const dropdownSelect = (key) => {
break;
}
};
// 监听登录状态变化
watch(
() => user.userLogin,
(val) => {
changeUserOptions(val);
}
);
onMounted(() => {
changeUserOptions(user.userLogin);
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
</script>
<style lang="scss" scoped>
@@ -358,6 +401,17 @@ nav {
align-items: center;
max-width: 1400px;
margin: 0 auto;
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-active {
transition-delay: 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.left {
flex: 1;
max-width: 300px;
@@ -371,27 +425,35 @@ nav {
width: 30px;
height: 30px;
margin-right: 12px;
transition: all 0.3s;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
@media (min-width: 640px) {
&:hover {
transform: scale(1.15);
}
}
&:active {
transform: scale(1);
}
}
.controls {
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 520px) {
display: none;
}
.n-icon {
margin: 0 4px;
border-radius: 8px;
padding: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--n-border-color);
@media (min-width: 640px) {
&:hover {
background-color: var(--n-border-color);
}
}
&:active {
transform: scale(0.95);
@@ -418,8 +480,8 @@ nav {
transition: all 0.3s;
cursor: pointer;
&:hover {
background-color: $mainColor;
color: var(--n-color);
background-color: var(--main-color);
color: rgba(255, 255, 255, 0.9);
}
&:active {
transform: scale(0.95);
@@ -427,8 +489,8 @@ nav {
}
.router-link-active {
background-color: $mainColor;
color: var(--n-color);
background-color: var(--main-color);
color: rgba(255, 255, 255, 0.9);
}
}
.right {
@@ -438,6 +500,10 @@ nav {
flex-direction: row;
align-items: center;
justify-content: flex-end;
@media (max-width: 520px) {
position: absolute;
right: 12px;
}
.avatar {
width: 30px;
min-width: 30px;
@@ -446,6 +512,13 @@ nav {
box-shadow: 0 4px 12px -2px rgb(0 0 0 / 10%);
cursor: pointer;
}
.mb-menu {
margin-left: 12px;
display: none;
@media (max-width: 768px) {
display: flex;
}
}
}
}
</style>

View File

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

View File

@@ -16,7 +16,7 @@
<script setup>
import { getNewAlbum } from "@/api/home";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
const router = useRouter();
@@ -66,7 +66,7 @@ onMounted(() => {
margin-left: 6px;
}
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -92,7 +92,7 @@ onMounted(() => {
margin-left: 6px;
}
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -127,6 +127,7 @@ onMounted(() => {
width: 100%;
height: 100%;
background-color: #00000040;
-webkit-backdrop-filter: blur(80px);
backdrop-filter: blur(80px);
z-index: -1;
}
@@ -189,8 +190,10 @@ onMounted(() => {
border-radius: 8px;
transform: scale(1);
transition: all 0.3s;
&:hover {
background-color: #ffffff30;
@media (min-width: 640px) {
&:hover {
background-color: #ffffff30;
}
}
&:active {
transform: scale(0.9);

View File

@@ -17,7 +17,7 @@
<script setup>
import { getPersonalized } from "@/api/home";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
const router = useRouter();
@@ -66,7 +66,7 @@ onMounted(() => {
margin-left: 6px;
}
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -1,32 +1,51 @@
<template>
<Transition name="up">
<div
v-show="music.showBigPlayer"
v-if="music.showBigPlayer"
class="bplayer"
:style="
music.getPlaySongData
:style="[
music.getPlaySongData && setting.backgroundImageShow === 'blur'
? 'background-image: url(' +
music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
'?param=50y50)'
: ''
"
: '',
`backgroundColor: ${site.songPicColor}`,
]"
>
<div class="gray" />
<n-icon
class="close"
size="40"
:component="KeyboardArrowDownFilled"
@click="music.setBigPlayerState(false)"
/>
<n-icon
class="screenfull"
size="36"
:component="screenfullIcon"
@click="screenfullChange"
<div
:class="setting.backgroundImageShow === 'blur' ? 'gray blur' : 'gray'"
/>
<div class="icon-menu">
<div class="menu-left">
<div v-if="setting.showLyricSetting" class="icon">
<n-icon
class="setting"
size="30"
:component="SettingsRound"
@click="LyricSettingRef.openLyricSetting()"
/>
</div>
</div>
<div class="menu-right">
<div class="icon">
<n-icon
class="screenfull"
:component="screenfullIcon"
@click="screenfullChange"
/>
</div>
<div class="icon">
<n-icon
class="close"
:component="KeyboardArrowDownFilled"
@click="music.setBigPlayerState(false)"
/>
</div>
</div>
</div>
<div
:class="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4
? 'all'
: 'all noLrc'
"
@@ -50,7 +69,8 @@
<div
class="lrcShow"
v-if="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] &&
music.getPlaySongLyric.lrc.length > 4
"
>
<div class="data" v-show="setting.playerStyle === 'record'">
@@ -82,81 +102,22 @@
</span>
</div>
</div>
<div
:class="
setting.playerStyle === 'cover'
? 'lrc-all cover'
: 'lrc-all record'
"
v-if="music.getPlaySongLyric[0]"
:style="
setting.lyricsPosition === 'center'
? { textAlign: 'center', paddingRight: '0' }
: null
"
<RollingLyrics
@mouseenter="
lrcMouseStatus = setting.lrcMousePause ? true : false
"
@mouseleave="lrcAllLeave"
>
<!-- 提示文本 -->
<div class="tip">
<n-text>点击选中的歌词以调整播放进度</n-text>
</div>
<div class="placeholder"></div>
<div
:class="
music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'
"
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
v-for="(item, index) in music.getPlaySongLyric"
:key="item"
:id="'lrc' + index"
@click="jumpTime(item.time)"
>
<div
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(
music.getPlaySongLyricIndex,
index
)}px)`
: null,
}"
>
<span
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
{{ item.lyric }}
</span>
<span
v-show="
music.getPlaySongTransl &&
setting.getShowTransl &&
item.lyricFy
"
:style="{ fontSize: setting.lyricsFontSize - 0.4 + 'vh' }"
class="lyric-fy"
>
{{ item.lyricFy }}</span
>
</div>
</div>
<div class="placeholder"></div>
</div>
@lrcTextClick="lrcTextClick"
/>
<div
:class="menuShow ? 'menu show' : 'menu'"
v-show="setting.playerStyle === 'record'"
>
<n-icon
v-if="music.getPlaySongTransl"
:class="setting.getShowTransl ? 'open' : ''"
:class="setting.showTransl ? 'open' : ''"
:component="GTranslateFilled"
@click="setting.setShowTransl(!setting.getShowTransl)"
@click="setting.setShowTransl(!setting.showTransl)"
/>
<n-icon
class="open"
@@ -168,9 +129,10 @@
</Transition>
</div>
</div>
<div class="canvas">
<canvas v-if="setting.musicFrequency" class="avBars" ref="avBars" />
</div>
<!-- 音乐频谱 -->
<!-- <Spectrum v-if="setting.musicFrequency" /> -->
<!-- 歌词设置 -->
<LyricSetting ref="LyricSettingRef" />
</div>
</Transition>
</template>
@@ -182,38 +144,34 @@ import {
MessageFilled,
FullscreenRound,
FullscreenExitRound,
SettingsRound,
} from "@vicons/material";
import { musicStore, settingStore } from "@/store";
import { musicStore, settingStore, siteStore } from "@/store";
import { useRouter } from "vue-router";
import MusicFrequency from "@/utils/MusicFrequency.js";
import { setSeek } from "@/utils/Player";
import PlayerRecord from "./PlayerRecord.vue";
import PlayerCover from "./PlayerCover.vue";
import RollingLyrics from "./RollingLyrics.vue";
// import Spectrum from "./Spectrum.vue";
import LyricSetting from "@/components/DataModal/LyricSetting.vue";
import screenfull from "screenfull";
const router = useRouter();
const music = musicStore();
const site = siteStore();
const setting = settingStore();
// 工具栏显隐
const menuShow = ref(false);
// 音乐频谱
const avBars = ref(null);
const musicFrequency = ref(null);
// 歌词设置弹窗
const LyricSettingRef = ref(null);
// 歌词模糊数值
const getFilter = (lrcIndex, index) => {
if (lrcIndex >= index) {
return lrcIndex - index;
} else {
return index - lrcIndex;
}
};
// 点击歌词跳转
const jumpTime = (time) => {
// 歌词文本点击事件
const lrcTextClick = (time) => {
if (typeof $player !== "undefined") setSeek($player, time);
music.setPlayState(true);
lrcMouseStatus.value = false;
if ($player) $player.currentTime = time;
};
// 鼠标移出歌词区域
@@ -224,6 +182,7 @@ const lrcAllLeave = () => {
};
// 全屏切换
const timeOut = ref(null);
const screenfullIcon = shallowRef(FullscreenRound);
const screenfullChange = () => {
if (screenfull.isEnabled) {
@@ -232,7 +191,7 @@ const screenfullChange = () => {
? FullscreenRound
: FullscreenExitRound;
// 延迟一段时间执行列表滚动
setTimeout(() => {
timeOut.value = setTimeout(() => {
lrcMouseStatus.value = false;
lyricsScroll(music.getPlaySongLyricIndex);
}, 500);
@@ -253,44 +212,58 @@ const toComment = () => {
// 歌词滚动
const lyricsScroll = (index) => {
const type = setting.lyricsBlock;
const el = document.getElementById(
`lrc${type === "center" ? index : index - 2}`
);
const lrcType =
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? "lrc" : "yrc";
const el = document.getElementById(lrcType + index);
if (el && !lrcMouseStatus.value) {
el.scrollIntoView({
const container = el.parentElement;
const containerHeight = container.clientHeight;
// 调整滚动的距离
const scrollDistance =
el.offsetTop -
container.offsetTop -
(type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 80);
container.scrollTo({
top: scrollDistance,
behavior: "smooth",
block: type,
});
}
};
onMounted(() => {
nextTick(() => {
if (setting.musicFrequency) {
$player.crossOrigin = "anonymous";
musicFrequency.value = new MusicFrequency(
avBars.value,
$player,
null,
50,
null,
null,
5
);
musicFrequency.value.drawSpectrum();
// 改变 PWA 应用标题栏颜色
const changePwaColor = () => {
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (music.showBigPlayer) {
themeColorMeta.setAttribute("content", site.songPicColor);
} else {
if (setting.getSiteTheme === "light") {
themeColorMeta.setAttribute("content", "#ffffff");
} else if (setting.getSiteTheme === "dark") {
themeColorMeta.setAttribute("content", "#18181c");
}
}
};
onMounted(() => {
nextTick().then(() => {
// 滚动歌词
lyricsScroll(music.getPlaySongLyricIndex);
});
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
// 监听页面是否打开
watch(
() => music.showBigPlayer,
(val) => {
changePwaColor();
if (val) {
console.log("开启播放器", music.getPlaySongLyricIndex);
nextTick(() => {
nextTick().then(() => {
music.showPlayList = false;
lyricsScroll(music.getPlaySongLyricIndex);
});
}
@@ -302,6 +275,12 @@ watch(
() => music.getPlaySongLyricIndex,
(val) => lyricsScroll(val)
);
// 监听主题色改变
watch(
() => site.songPicColor,
() => changePwaColor()
);
</script>
<style lang="scss" scoped>
@@ -354,35 +333,61 @@ watch(
left: 0;
width: 100%;
height: 100%;
background-color: #00000060;
background-color: #00000030;
-webkit-backdrop-filter: blur(80px);
backdrop-filter: blur(80px);
z-index: -1;
&.blur {
background-color: #00000060;
}
}
.close,
.screenfull {
.icon-menu {
padding: 20px;
width: 100%;
height: 80px;
position: absolute;
top: 24px;
right: 24px;
opacity: 0.3;
color: #fff;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 2;
&:hover {
background-color: #ffffff20;
transform: scale(1.05);
opacity: 1;
box-sizing: border-box;
.menu-left,
.menu-right {
display: flex;
align-items: center;
.icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
opacity: 0.3;
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
&:hover {
background-color: #ffffff20;
transform: scale(1.05);
opacity: 1;
}
&:active {
transform: scale(1);
}
.screenfull,
.setting {
@media (max-width: 768px) {
display: none;
}
}
}
}
&:active {
transform: scale(1);
}
}
.screenfull {
right: 80px;
padding: 2px;
@media (max-width: 768px) {
display: none;
.menu-right {
.icon {
margin-left: 12px;
}
}
}
.all {
@@ -397,6 +402,12 @@ watch(
.left {
padding-right: 0;
transform: translateX(25vh);
@media (max-width: 1200px) {
transform: translateX(22.2vh);
}
@media (min-width: 769px) and (max-width: 869px) {
transform: translateX(20.1vh);
}
}
@media (max-width: 768px) {
.left {
@@ -438,6 +449,7 @@ watch(
height: 40px;
border-radius: 25px;
background-color: #ffffff20;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
display: flex;
align-items: center;
@@ -468,7 +480,7 @@ watch(
justify-content: center;
flex-direction: column;
.data {
padding: 0 20px;
padding: 0 3vh;
margin-bottom: 8px;
.name {
font-size: 3vh;
@@ -495,103 +507,9 @@ watch(
}
}
}
.lrc-all {
margin-right: 20%;
scrollbar-width: none;
// max-width: 460px;
max-width: 52vh;
overflow: auto;
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
&::-webkit-scrollbar {
display: none;
}
&.cover {
height: 80vh;
}
&.record {
height: 60vh;
}
&:hover {
.lrc-text {
&.blur {
filter: blur(0) !important;
}
}
}
.placeholder {
width: 100%;
height: 50%;
}
.lrc {
opacity: 0.4;
transition: all 0.3s;
// display: flex;
// flex-direction: column;
// margin-bottom: 4px;
// padding: 12px 20px;
margin-bottom: 0.8vh;
padding: 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: left center;
cursor: pointer;
.lrc-text {
display: flex;
flex-direction: column;
transition: all 0.35s ease-in-out;
transform: scale(0.95);
transform-origin: left center;
.lyric {
transition: all 0.3s;
// font-size: 2.4vh;
}
.lyric-fy {
margin-top: 2px;
transition: all 0.3s;
opacity: 0.8;
// font-size: 2vh;
}
}
&.on {
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
text-shadow: 0px 0px 30px #ffffff40;
}
}
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
}
}
&:active {
transform: scale(0.95);
}
}
}
.menu {
opacity: 0;
padding: 0 20px;
padding: 0 3vh;
margin-top: 20px;
display: flex;
flex-direction: row;

View File

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

View File

@@ -11,6 +11,16 @@
"
alt="cover"
/>
<img
class="shadow"
:src="
music.getPlaySongData
? music.getPlaySongData.album.picUrl.replace(/^http:/, 'https:') +
'?param=1024y1024'
: '/images/pic/default.png'
"
alt="shadow"
/>
</div>
<div class="control">
<div class="data">
@@ -61,11 +71,14 @@
</div>
<div class="time">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
<n-slider
v-model:value="music.getPlaySongTime.barMoveDistance"
class="progress"
:step="0.01"
@update:value="songTimeSliderUpdate"
<vue-slider
v-model="music.getPlaySongTime.barMoveDistance"
@drag-start="music.setPlayState(false)"
@drag-end="sliderDragEnd"
@click.stop="
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
"
:tooltip="'none'"
/>
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div>
@@ -98,6 +111,7 @@
v-else
class="dislike"
size="20"
:style="!user.userLogin ? 'opacity: 0.2;pointer-events: none;' : null"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
@@ -143,18 +157,25 @@ import {
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { musicStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import { setSeek } from "@/utils/Player";
import AllArtists from "@/components/DataList/AllArtists.vue";
import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/default.css";
const router = useRouter();
const music = musicStore();
const user = userStore();
const canvas = ref(null);
// 歌曲进度条更新
const sliderDragEnd = () => {
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
music.setPlayState(true);
};
const songTimeSliderUpdate = (val) => {
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
$player.currentTime = (music.getPlaySongTime.duration / 100) * val;
if (typeof $player !== "undefined" && music.getPlaySongTime?.duration) {
const currentTime = (music.getPlaySongTime.duration / 100) * val;
setSeek($player, currentTime);
}
};
// 页面跳转
@@ -170,11 +191,11 @@ const routerJump = (url, query) => {
<style lang="scss" scoped>
.cover {
.pic {
position: relative;
width: 50vh;
height: 50vh;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 40px 14px rgb(0 0 0 / 20%);
z-index: 1;
// overflow: hidden;
@media (max-width: 1200px) {
width: 44vh;
height: 44vh;
@@ -186,6 +207,19 @@ const routerJump = (url, query) => {
.album {
width: 100%;
height: 100%;
border-radius: 8px;
}
.shadow {
position: absolute;
left: 0;
top: 12px;
height: 100%;
width: 100%;
filter: blur(16px) opacity(0.6);
transform: scale(0.92, 0.96);
z-index: -1;
background-size: cover;
aspect-ratio: 1/1;
}
}
.control {
@@ -270,13 +304,26 @@ const routerJump = (url, query) => {
span {
opacity: 0.8;
}
.progress {
margin: 0 12px;
--n-handle-size: 12px;
--n-fill-color: #fff;
--n-fill-color-hover: #fff;
--n-rail-color: #ffffff20;
--n-rail-color-hover: #ffffff30;
.vue-slider {
margin: 0 10px;
width: 100% !important;
transform: translateY(-1px);
cursor: pointer;
:deep(.vue-slider-rail) {
background-color: #ffffff20;
border-radius: 25px;
.vue-slider-process {
background-color: #fff;
}
.vue-slider-dot {
width: 12px !important;
height: 12px !important;
box-shadow: none;
}
.vue-slider-dot-handle-focus {
box-shadow: none;
}
}
}
}
.buttons {

View File

@@ -190,6 +190,7 @@ const music = musicStore();
height: 68%;
border-radius: 50%;
background-color: #00000050;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
transition: all 0.5s;
.n-icon {

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

@@ -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,209 +1,239 @@
<template>
<n-card
:class="
music.getPlaylists[0] && music.showPlayBar ? 'player show' : 'player'
"
content-style="padding: 0"
>
<div class="slider">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
<n-slider
v-model:value="music.getPlaySongTime.barMoveDistance"
class="progress"
:step="0.01"
:tooltip="false"
@update:value="songTimeSliderUpdate"
@click.stop
/>
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div>
<div class="all">
<div class="data">
<div class="pic" @click.stop="music.setBigPlayerState(true)">
<img
:src="
music.getPlaySongData
? music.getPlaySongData.album.picUrl.replace(
/^http:/,
'https:'
) + '?param=50y50'
: '/images/pic/default.png'
"
alt="pic"
/>
<n-icon class="open" size="30" :component="KeyboardArrowUpFilled" />
</div>
<div class="name">
<div
class="song text-hidden"
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
>
{{
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲"
}}
<Transition name="show">
<n-card
v-show="music.getPlaylists[0] && music.showPlayBar"
class="player"
content-style="padding: 0"
>
<div class="slider">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
<vue-slider
v-model="music.getPlaySongTime.barMoveDistance"
@drag-start="music.setPlayState(false)"
@drag-end="sliderDragEnd"
@click.stop="
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance)
"
:tooltip="'active'"
:use-keyboard="false"
>
<template v-slot:tooltip>
<div class="slider-tooltip">
{{
getSongPlayingTime(
(music.getPlaySongTime.duration / 100) *
music.getPlaySongTime.barMoveDistance
)
}}
</div>
</template>
</vue-slider>
<span>{{ music.getPlaySongTime.songTimeDuration }}</span>
</div>
<div class="all">
<div class="data">
<div class="pic" @click.stop="music.setBigPlayerState(true)">
<img
:src="
music.getPlaySongData
? music.getPlaySongData.album.picUrl.replace(
/^http:/,
'https:'
) + '?param=50y50'
: '/images/pic/default.png'
"
alt="pic"
/>
<n-icon class="open" size="30" :component="KeyboardArrowUpFilled" />
</div>
<div class="artisrOrLrc" v-if="music.getPlaySongData">
<template v-if="setting.bottomLyricShow">
<Transition mode="out-in">
<div class="name">
<div
class="song text-hidden"
@click.stop="router.push(`/song?id=${music.getPlaySongData.id}`)"
>
{{
music.getPlaySongData ? music.getPlaySongData.name : "暂无歌曲"
}}
</div>
<div class="artisrOrLrc" v-if="music.getPlaySongData">
<template v-if="setting.bottomLyricShow">
<Transition mode="out-in">
<AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric.lrc[0]"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
<n-text
v-else-if="
setting.showYrc &&
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.hasYrc
"
class="lrc text-hidden"
>
<n-text
v-for="item in music.getPlaySongLyric.yrc[
music.getPlaySongLyricIndex
].content"
:key="item"
:depth="3"
>
{{ item.content }}
</n-text>
</n-text>
<n-text
v-else-if="
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.lrc[0]
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric.lrc[music.getPlaySongLyricIndex]
.content
"
/>
<AllArtists
v-else
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</Transition>
</template>
<template v-else>
<AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric[0]"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
<n-text
v-else-if="
music.getPlaySongLyric[0] &&
music.getPlaySongLyricIndex != -1
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric[music.getPlaySongLyricIndex].lyric
? music.getPlaySongLyric[music.getPlaySongLyricIndex]
.lyric
: '暂无歌词'
"
/>
</Transition>
</template>
<template v-else>
<AllArtists
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</template>
</template>
</div>
</div>
</div>
</div>
<div class="control">
<n-icon
v-if="!music.getPersonalFmMode"
title="上一曲"
class="prev"
size="30"
:component="SkipPreviousRound"
@click.stop="music.setPlaySongIndex('prev')"
/>
<n-icon
v-else
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
<div class="play-state">
<div class="control">
<n-icon
size="46"
:title="music.getPlayState ? '暂停' : '播放'"
:component="
music.getPlayState ? PauseCircleFilled : PlayCircleFilled
"
@click.stop="music.setPlayState(!music.getPlayState)"
/>
</div>
<n-icon
class="next"
size="30"
:component="SkipNextRound"
@click.stop="music.setPlaySongIndex('next')"
/>
</div>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<div class="like" v-if="music.getPlaySongData">
<n-icon
class="like-icon"
size="24"
:component="
music.getSongIsLike(music.getPlaySongData.id)
? FavoriteRound
: FavoriteBorderRound
"
@click.stop="
music.getSongIsLike(music.getPlaySongData.id)
? music.changeLikeList(music.getPlaySongData.id, false)
: music.changeLikeList(music.getPlaySongData.id, true)
"
/>
</div>
<div class="add-playlist">
<n-icon
class="add-icon"
v-if="!music.getPersonalFmMode"
title="上一曲"
class="prev"
size="30"
:component="PlaylistAddRound"
@click.stop="
addPlayListRef.openAddToPlaylist(music.getPlaySongData.id)
"
:component="SkipPreviousRound"
@click.stop="music.setPlaySongIndex('prev')"
/>
</div>
<div class="pattern">
<n-icon
:component="
persistData.playSongMode === 'normal'
? PlayCycle
: persistData.playSongMode === 'random'
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
v-else
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
</div>
<div class="playlist">
<PlayList />
<div class="play-state">
<n-icon
size="46"
:title="music.getPlayState ? '暂停' : '播放'"
:component="
music.getPlayState ? PauseCircleFilled : PlayCircleFilled
"
@click.stop="music.setPlayState(!music.getPlayState)"
/>
</div>
<n-icon
class="next"
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
:component="SkipNextRound"
@click.stop="music.setPlaySongIndex('next')"
/>
</div>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"
:tooltip="false"
:min="0"
:max="1"
:step="0.01"
@click.stop
/>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<div class="like" v-if="music.getPlaySongData">
<n-icon
class="like-icon"
size="24"
:component="
music.getSongIsLike(music.getPlaySongData.id)
? FavoriteRound
: FavoriteBorderRound
"
@click.stop="
music.getSongIsLike(music.getPlaySongData.id)
? music.changeLikeList(music.getPlaySongData.id, false)
: music.changeLikeList(music.getPlaySongData.id, true)
"
/>
</div>
<div class="add-playlist">
<n-icon
class="add-icon"
size="30"
:component="PlaylistAddRound"
@click.stop="
addPlayListRef.openAddToPlaylist(music.getPlaySongData.id)
"
/>
</div>
<div class="pattern">
<n-icon
:component="
persistData.playSongMode === 'normal'
? PlayCycle
: persistData.playSongMode === 'random'
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
/>
</div>
<div :class="music.showPlayList ? 'playlist open' : 'playlist'">
<n-icon
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
/>
</div>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"
:tooltip="false"
:min="0"
:max="1"
:step="0.01"
@click.stop
/>
</div>
</div>
</div>
</div>
<audio
ref="player"
:autoplay="music.getPlayState"
@timeupdate="songUpdate"
@play="songPlay"
@pause="songPause"
@canplay="songCanplay"
@error="songError"
@ended="music.setPlaySongIndex('next')"
:src="music.getPlaySongLink"
></audio>
</n-card>
</n-card>
</Transition>
<!-- 播放列表 -->
<PlayListDrawer ref="PlayListDrawerRef" />
<!-- 添加到歌单 -->
<AddPlaylist ref="addPlayListRef" />
<!-- 播放器 -->
<BigPlayer />
</template>
<script setup>
import { checkMusicCanUse, getMusicUrl, getMusicLyric } from "@/api/song";
import {
checkMusicCanUse,
getMusicUrl,
getMusicNumUrl,
getMusicNewLyric,
} from "@/api/song";
import { NIcon } from "naive-ui";
import {
KeyboardArrowUpFilled,
MusicNoteFilled,
PlayCircleFilled,
PauseCircleFilled,
SkipNextRound,
@@ -220,185 +250,114 @@ import {
} from "@vicons/material";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import { storeToRefs } from "pinia";
import { musicStore, settingStore } from "@/store";
import { musicStore, settingStore, siteStore } from "@/store";
import {
createSound,
setVolume,
setSeek,
fadePlayOrPause,
} from "@/utils/Player";
import { getSongPlayingTime } from "@/utils/timeTools";
import { useRouter } from "vue-router";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import { debounce } from "throttle-debounce";
import VueSlider from "vue-slider-component";
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
import PlayListDrawer from "@/components/DataModal/PlayListDrawer.vue";
import AllArtists from "@/components/DataList/AllArtists.vue";
import PlayList from "@/components/DataList/PlayList.vue";
import ColorThief from "colorthief";
import BigPlayer from "./BigPlayer.vue";
import debounce from "@/utils/debounce";
import "vue-slider-component/theme/default.css";
const router = useRouter();
const setting = settingStore();
const music = musicStore();
const site = siteStore();
const { persistData } = storeToRefs(music);
const addPlayListRef = ref(null);
const PlayListDrawerRef = ref(null);
// UNM 是否存在
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
// 音频标签
const player = ref(null);
// 获取歌曲播放数据
const getPlaySongData = (id, level = setting.songLevel) => {
checkMusicCanUse(id).then((res) => {
if (res.success) {
console.log("音乐可用");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
if (res.data[0].fee == 1) {
$message.warning("当前歌曲为 VIP 专享,仅可试听");
}
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
});
// 获取歌词
getMusicLyric(id).then((res) => {
music.setPlaySongLyric(res);
});
} else {
console.log("无法播放");
if (music.getPlayState && $player) {
$message.error("当前歌曲无法播放,已跳至下一首");
music.setPlaySongIndex("next");
}
const getPlaySongData = (data, level = setting.songLevel) => {
try {
const { id, fee, pc } = data;
// VIP 歌曲或需要购买专辑
if (
useUnmServerHas &&
setting.useUnmServer &&
!pc &&
(fee === 1 || fee === 4)
) {
console.log("网易云解灰");
getMusicNumUrlData(id);
}
});
};
// 歌曲进度更新事件
const songUpdate = (e) => {
const currentTime = e.target.currentTime;
const duration = e.target.duration;
music.setPlaySongTime({ currentTime, duration });
};
// 歌曲缓冲完毕
const songCanplay = () => {
console.log("缓冲完毕", music.getPlayState);
if (music.getPlayState && $player) {
music.setPlayState(true);
songInOrOut("play");
}
};
// 歌曲开始播放
const songPlay = () => {
if (!music.getPlaySongData) {
$message.error("音乐数据获取失败");
return false;
}
music.setPlayState(true);
// 兼容 mediaSession
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: music.getPlaySongData.name,
artist: music.getPlaySongData.artist[0].name,
album: music.getPlaySongData.album.name,
artwork: [
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=96y96",
sizes: "96x96",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=128y128",
sizes: "128x128",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=192y192",
sizes: "192x192",
},
],
// 免费或无版权
else {
checkMusicCanUse(id).then((res) => {
if (res.success) {
console.log("当前歌曲可用");
if (!pc && (fee === 1 || fee === 4))
$message.info("当前歌曲为 VIP 专享,仅可试听");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
player.value = createSound(
res.data[0].url.replace(/^http:/, "https:")
);
});
} else {
if (useUnmServerHas && setting.useUnmServer) {
getMusicNumUrlData(id);
} else {
$message.warning("当前歌曲播放失败,跳至下一首");
music.setPlaySongIndex("next");
}
}
});
}
// 获取歌词
getMusicNewLyric(id).then((res) => {
music.setPlaySongLyric(res);
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
} catch (err) {
if (music.getPlaylists[0] && music.getPlayState) {
console.log("当前歌曲所有音源匹配失败:" + err);
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
music.setPlaySongIndex("next");
}
}
};
// 网易云解灰
const getMusicNumUrlData = (id) => {
getMusicNumUrl(id)
.then((res) => {
if (res.code === 200) {
console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
player.value = createSound(res.data.url.replace(/^http:/, ""));
}
})
.catch((err) => {
console.log("解灰失败:" + err);
$message.warning("当前歌曲解灰失败,跳至下一首");
music.setPlaySongIndex("next");
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
music.setPlaySongIndex("prev");
});
}
$message.info(
music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name,
{
icon: () =>
h(NIcon, null, {
default: () => h(MusicNoteFilled),
}),
}
);
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 更改页面标题
window.document.title = music.getPlaySongData.name + " - SPlayer";
};
// 音乐渐入渐出
const isFading = ref(false);
const songInOrOut = (type) => {
if (isFading.value) {
return;
}
isFading.value = true;
if (type === "play") {
let volume = 0;
$player.play();
const interval = setInterval(() => {
// 如果音量已经到达当前音量,则停止渐入
if (volume >= persistData.value.playVolume) {
clearInterval(interval);
isFading.value = false;
return;
}
// 增加音量
volume += 0.1;
if (volume > persistData.value.playVolume) {
volume = persistData.value.playVolume;
}
$player.volume = volume;
}, 30);
} else if (type === "pause") {
let volume = persistData.value.playVolume;
const interval = setInterval(() => {
// 如果音量已经到达最小值,则停止渐出
if (volume <= 0) {
clearInterval(interval);
$player.pause();
isFading.value = false;
return;
}
// 减小音量
volume -= 0.1;
if (volume < 0) {
volume = 0;
}
$player.volume = volume;
}, 30);
}
};
// 歌曲暂停
const songPause = () => {
console.log("音乐暂停");
if (!$player.ended) music.setPlayState(false);
// 更改页面标题
window.document.title = "SPlayer";
};
// 歌曲进度条更新
const songTimeSliderUpdate = (val) => {
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
$player.currentTime = (music.getPlaySongTime.duration / 100) * val;
const sliderDragEnd = () => {
songTimeSliderUpdate(music.getPlaySongTime.barMoveDistance);
music.setPlayState(true);
};
// 歌曲播放失败事件
const songError = () => {
console.error("歌曲播放失败");
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData.id);
if (music.getPlayState) songInOrOut("play");
const songTimeSliderUpdate = (val) => {
if (player.value && music.getPlaySongTime?.duration) {
const currentTime = (music.getPlaySongTime.duration / 100) * val;
setSeek(player.value, currentTime);
}
};
// 静音事件
@@ -411,27 +370,55 @@ const volumeMute = () => {
}
};
onMounted(() => {
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData)
getPlaySongData(music.getPlaySongData.id);
// 挂载播放器
window.$player = player.value;
// 恢复上次播放进度
if (music.getPlaySongTime && music.getPlaySongTime.currentTime) {
$player.currentTime = music.getPlaySongTime.currentTime;
// 歌曲更换事件
const songChange = debounce(500, (val) => {
if (val === undefined) {
window.document.title =
sessionStorage.getItem("siteTitle") ?? import.meta.env.VITE_SITE_TITLE;
}
// 加载数据
getPlaySongData(val);
getPicColor(val?.album.picUrl);
});
// 获取封面图主色
const getPicColor = (url) => {
if (!url) return false;
const imgUrl = url.replace(/^http:/, "https:") + "?param=50y50";
const img = new Image();
fetch(imgUrl)
.then((res) => res.blob())
.then((blob) => {
img.src = URL.createObjectURL(blob);
img.addEventListener("load", async () => {
const colorThief = new ColorThief();
const color = await colorThief.getColor(img);
console.log(`当前封面主色rgb(${color.join(",")})`);
site.songPicColor = `rgb(${color.join(",")})`;
});
})
.catch((err) => {
console.error("图像处理出错:" + err);
site.songPicColor = "rgb(128,128,128)";
});
};
onMounted(() => {
// 挂载方法
window.$getPlaySongData = getPlaySongData;
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData) {
getPlaySongData(music.getPlaySongData);
getPicColor(music.getPlaySongData.album.picUrl);
}
// 设置音量
if ($player) $player.volume = persistData.value.playVolume;
});
// 监听当前音乐数据变化
watch(
() => music.getPlaySongData,
(val) => {
debounce(() => {
getPlaySongData(val.id);
}, 500);
music.setPlaySongTime({ currentTime: 0, duration: 0 });
songChange(val);
}
);
@@ -439,7 +426,7 @@ watch(
watch(
() => persistData.value.playVolume,
(val) => {
if ($player) $player.volume = val;
if (player.value) setVolume(player.value, val);
}
);
@@ -447,40 +434,35 @@ watch(
watch(
() => music.getPlayState,
(val) => {
nextTick(() => {
// $player ? (val ? $player.play() : $player.pause()) : null;
if ($player) {
// val ? $player.play() : $player.pause();
val ? songInOrOut("play") : songInOrOut("pause");
} else {
$message.error("播放器初始化失败,请重试");
nextTick().then(() => {
if (player.value && !music.isLoadingSong) {
fadePlayOrPause(
player.value,
val ? "play" : "pause",
persistData.value.playVolume
);
}
});
}
);
// 监听歌曲进度更新
// watch(
// () => music.getPlaySongTime,
// (val) => {
// if (val.barMoveDistance) {
// songTimeVal.value = val.barMoveDistance;
// }
// }
// );
</script>
<style lang="scss" scoped>
.show-enter-active,
.show-leave-active {
transform: translateY(0);
transition: all 0.3s cubic-bezier(0.65, 0.05, 0.36, 1);
}
.show-enter-from,
.show-leave-to {
transform: translateY(80px);
}
.player {
height: 70px;
position: fixed;
bottom: -90px;
bottom: 0;
left: 0;
transition: all 0.3s;
z-index: 2;
&.show {
bottom: 0;
}
.slider {
position: absolute;
top: -12px;
@@ -488,19 +470,15 @@ watch(
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@media (max-width: 640px) {
top: -6px;
top: -8px;
> {
span {
display: none;
}
}
}
.progress {
--n-handle-size: 12px;
--n-rail-height: 3px;
}
> {
span {
font-size: 12px;
@@ -512,6 +490,33 @@ watch(
margin: 0 2px;
}
}
.vue-slider {
width: 100% !important;
height: 3px !important;
cursor: pointer;
.slider-tooltip {
font-size: 12px;
white-space: nowrap;
background-color: var(--n-color);
outline: 1px solid var(--n-border-color);
padding: 2px 8px;
border-radius: 25px;
}
:deep(.vue-slider-rail) {
background-color: var(--n-border-color);
border-radius: 25px;
.vue-slider-process {
background-color: var(--main-color);
}
.vue-slider-dot {
width: 12px !important;
height: 12px !important;
}
.vue-slider-dot-handle-focus {
box-shadow: 0px 0px 1px 2px var(--main-color);
}
}
}
}
.all {
@@ -521,23 +526,6 @@ watch(
align-items: center;
max-width: 1400px;
margin: 0 auto;
@media (max-width: 620px) {
display: flex;
flex-direction: row;
justify-content: space-between;
.data {
.time {
display: none;
}
}
.control {
margin-left: auto;
.prev,
.next {
display: none;
}
}
}
.data {
display: flex;
flex-direction: row;
@@ -588,7 +576,7 @@ watch(
cursor: pointer;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
.artisrOrLrc {
@@ -614,7 +602,7 @@ watch(
.next,
.prev,
.dislike {
color: $mainColor;
color: var(--main-color);
cursor: pointer;
padding: 4px;
border-radius: 50%;
@@ -622,7 +610,7 @@ watch(
transition: all 0.3s;
&:hover {
color: var(--n-color-embedded);
background-color: $mainColor;
background-color: var(--main-color);
}
&:active {
transform: scale(0.9);
@@ -634,7 +622,7 @@ watch(
.play-state {
width: 46px;
height: 46px;
color: $mainColor;
color: var(--main-color);
margin: 0 12px;
cursor: pointer;
transform: scale(1);
@@ -654,7 +642,7 @@ watch(
flex-direction: row;
align-items: center;
justify-content: flex-end;
color: $mainColor;
color: var(--main-color);
@media (max-width: 640px) {
.volume,
.like,
@@ -674,9 +662,11 @@ watch(
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainColor;
color: var(--n-color-embedded);
@media (min-width: 640px) {
&:hover {
background-color: var(--main-color);
color: var(--n-color-embedded);
}
}
&:active {
transform: scale(0.95);
@@ -709,6 +699,12 @@ watch(
display: flex;
align-items: center;
justify-content: center;
&.open {
.n-icon {
background-color: var(--main-color);
color: var(--n-color-embedded);
}
}
}
.volume {
display: flex;
@@ -725,6 +721,26 @@ watch(
}
}
}
@media (max-width: 620px) {
display: flex;
flex-direction: row;
justify-content: space-between;
.data {
.time {
display: none;
}
}
.control {
margin-left: auto;
.prev,
.next {
display: none;
}
.play-state {
margin: 0;
}
}
}
}
}
</style>

View File

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

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,8 +1,9 @@
<template>
<div class="searchInp">
<n-input
:class="inputActive ? 'input focus' : 'input'"
:class="site.searchInputActive ? 'input focus' : 'input'"
:input-props="{ autoComplete: false }"
ref="searchInpRef"
round
clearable
placeholder="搜索音乐/视频"
@@ -14,7 +15,7 @@
<template #prefix>
<n-icon
size="16"
:color="inputActive ? '#f55e55' : ''"
:class="site.searchInputActive ? 'active' : ''"
:component="Search"
/>
</template>
@@ -23,7 +24,7 @@
<n-card
class="list"
v-show="
inputActive &&
site.searchInputActive &&
!inputValue &&
(music.getSearchHistory[0] || searchData.hot[0])
"
@@ -93,7 +94,7 @@
<CollapseTransition easing="ease-in-out">
<n-card
class="list"
v-show="inputActive && inputValue && searchData.suggest"
v-show="site.searchInputActive && inputValue && searchData.suggest"
content-style="padding: 0"
>
<n-scrollbar>
@@ -195,20 +196,21 @@ import {
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import debounce from "@/utils/debounce";
import { useRouter } from "vue-router";
import { musicStore, settingStore } from "@/store";
import { musicStore, settingStore, siteStore } from "@/store";
const router = useRouter();
const music = musicStore();
const setting = settingStore();
const site = siteStore();
// 输入框内容
const inputValue = ref(null);
// 输入框激活状态
const inputActive = ref(false);
const searchInpRef = ref(null);
// 输入框激活事件
const inputFocus = () => {
inputActive.value = true;
searchInpRef.value?.focus();
site.searchInputActive = true;
music.showPlayList = false;
getSearchHotData();
};
@@ -274,7 +276,8 @@ const toSearch = (val, type) => {
const inputkeydown = (e) => {
if (e.key === "Enter" && inputValue.value != null) {
console.log("执行搜索" + inputValue.value.trim());
inputActive.value = false;
searchInpRef.value?.blur();
site.searchInputActive = false;
// 写入搜索历史
music.setSearchHistory(inputValue.value.trim());
router.push({
@@ -306,13 +309,15 @@ onMounted(() => {
getSearchHotData();
// 搜索框失焦
document.addEventListener("click", () => {
inputActive.value = false;
searchInpRef.value?.blur();
site.searchInputActive = false;
});
});
onUnmounted(() => {
document.removeEventListener("click", () => {
inputActive.value = false;
searchInpRef.value?.blur();
site.searchInputActive = false;
});
});
@@ -328,6 +333,17 @@ watch(
}
}
);
// 监听播放列表显隐
watch(
() => music.showPlayList,
(val) => {
if (val) {
searchInpRef.value?.blur();
site.searchInputActive = false;
}
}
);
</script>
<style lang="scss" scoped>
@@ -339,10 +355,30 @@ watch(
.input {
width: 200px;
transition: all 0.3s;
@media (max-width: 450px) {
width: 40px;
}
&.focus {
width: 280px;
:deep(input) {
color: $mainColor;
color: var(--main-color);
}
@media (max-width: 450px) {
width: 60vw;
}
@media (max-width: 380px) {
width: 54vw;
}
@media (max-width: 320px) {
width: 50vw;
}
}
:deep(.n-input__prefix) {
.n-icon {
transition: color 0.3s;
&.active {
color: var(--main-color);
}
}
}
}
@@ -353,125 +389,142 @@ watch(
border-radius: 8px;
width: 280px;
z-index: 3;
@media (max-width: 450px) {
position: fixed;
width: 100%;
top: 58px;
right: 0;
left: 0;
border-radius: 0 0 8px 8px;
z-index: 2006;
&::after {
position: absolute;
right: 0;
top: 0;
content: "收起";
padding: 4px 12px;
font-size: 12px;
background-color: #efefef;
border-radius: 0 0 0 14px;
}
}
:deep(.n-scrollbar) {
max-height: 80vh;
.n-scrollbar-rail {
width: 0;
@media (max-width: 450px) {
max-height: calc(100vh - 60px);
min-height: calc(100vh - 60px);
box-sizing: border-box;
}
.n-scrollbar-content {
padding: 12px;
.list-title {
color: $mainColor;
display: flex;
align-items: center;
margin-bottom: 8px;
.n-text {
margin-left: 4px;
font-size: 14px;
color: $mainColor;
line-height: 0;
}
}
.history-list {
margin-bottom: 18px;
.n-space {
margin: 12px 0;
.n-tag {
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
}
&:active {
transform: scale(0.95);
}
}
}
.del {
.n-scrollbar-rail {
width: 4px;
}
.n-scrollbar-container {
padding-top: 8px;
.n-scrollbar-content {
padding: 12px;
.list-title {
color: var(--main-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
cursor: pointer;
.n-icon {
margin-right: 4px;
}
}
}
.hot-list {
margin-top: 6px;
.hot-item {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
border-radius: 8px;
padding: 6px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
.n-text {
margin-left: 4px;
font-size: 14px;
color: var(--main-color);
line-height: 0;
}
&:hover {
background-color: var(--n-border-color);
}
.num {
width: 30px;
height: 30px;
min-width: 30px;
text-align: center;
line-height: 30px;
font-size: 16px;
font-weight: bold;
margin-right: 8px;
&.hot {
color: #ff5656;
}
.history-list {
margin-bottom: 18px;
.n-space {
margin: 12px 0;
.n-tag {
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
}
}
}
.title {
.del {
display: flex;
flex-direction: column;
.name {
font-size: 16px;
display: flex;
flex-direction: row;
align-items: center;
img {
height: 16px;
width: auto;
margin-left: 6px;
margin-bottom: 2px;
}
.tag {
transform: scale(0.9);
margin-left: 6px;
height: 18px;
color: $mainColor;
background-color: $mainSecondaryColor;
border-color: $mainColor;
}
}
.tip {
font-size: 12px;
align-items: center;
justify-content: center;
font-size: 13px;
cursor: pointer;
.n-icon {
margin-right: 4px;
}
}
}
}
.suggest-tip {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.n-icon {
margin-right: 6px;
.hot-list {
margin-top: 6px;
.hot-item {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
border-radius: 8px;
padding: 6px;
transition: all 0.3s;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
&:hover {
background-color: var(--n-border-color);
}
.num {
width: 30px;
height: 30px;
min-width: 30px;
text-align: center;
line-height: 30px;
font-size: 16px;
font-weight: bold;
margin-right: 8px;
&.hot {
color: var(--main-color);
}
}
.title {
display: flex;
flex-direction: column;
.name {
font-size: 16px;
display: flex;
flex-direction: row;
align-items: center;
img {
height: 16px;
width: auto;
margin-left: 6px;
margin-bottom: 2px;
}
.tag {
transform: scale(0.9);
margin-left: 6px;
height: 18px;
color: var(--main-color);
background-color: var(--main-second-color);
border-color: var(--main-color);
}
}
.tip {
font-size: 12px;
}
}
}
}
}
.suggest-all {
.loading {
.suggest-tip {
display: flex;
flex-direction: row;
justify-content: center;
@@ -480,32 +533,43 @@ watch(
margin-right: 6px;
}
}
.suggest-item {
margin-bottom: 12px;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.type {
color: #ff5656;
.suggest-all {
.loading {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 4px;
.n-icon {
margin-bottom: 2px;
}
.name {
margin-left: 4px;
margin-right: 6px;
}
}
.names {
display: block;
padding: 14px 18px 14px 22px;
cursor: pointer;
transition: all 0.3s;
border-radius: 8px;
&:hover {
background-color: var(--n-border-color);
.suggest-item {
margin-bottom: 12px;
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.type {
color: var(--main-color);
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4px;
.n-icon {
margin-bottom: 2px;
}
.name {
margin-left: 4px;
}
}
.names {
display: block;
padding: 14px 18px 14px 22px;
cursor: pointer;
transition: all 0.3s;
border-radius: 8px;
&:hover {
background-color: var(--n-border-color);
}
}
}
}

View File

@@ -18,10 +18,19 @@ app.use(router);
app.mount("#app");
// PWA
// 检测到更新提醒
let pwaMessage = null;
navigator.serviceWorker.addEventListener("onupdatefound", () => {
console.info("发现站点更新,正在下载新版本");
pwaMessage = $message.loading("发现站点更新,正在下载新版本", {
closable: true,
duration: 0,
});
});
// 更新完成提醒
navigator.serviceWorker.addEventListener("controllerchange", () => {
// 弹出更新提醒
console.log("站点已更新,刷新后生效");
console.info("站点已更新,刷新后生效");
if (pwaMessage) pwaMessage?.destroy();
$message.info("站点已更新,刷新后生效", {
closable: true,
duration: 0,

View File

@@ -125,7 +125,25 @@ const routes = [
meta: {
title: "全局设置",
},
component: () => import("@/views/Setting/SettingView.vue"),
component: () => import("@/views/Setting/index.vue"),
redirect: "/setting/main",
children: [
{
path: "main",
name: "setting-main",
component: () => import("@/views/Setting/main.vue"),
},
{
path: "player",
name: "setting-player",
component: () => import("@/views/Setting/player.vue"),
},
{
path: "other",
name: "setting-other",
component: () => import("@/views/Setting/other.vue"),
},
],
},
// 登录页
{

View File

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

View File

@@ -1,12 +1,14 @@
import { defineStore } from "pinia";
import { getSongTime, getSongPlayingTime } from "@/utils/timeTools.js";
import { nextTick } from "vue";
import { getSongTime, getSongPlayingTime } from "@/utils/timeTools";
import { getPersonalFm, setFmTrash } from "@/api/home";
import { getLikelist, setLikeSong } from "@/api/user";
import { getPlayListCatlist } from "@/api/playlist";
import { userStore } from "@/store";
import { userStore, settingStore } from "@/store";
import { NIcon } from "naive-ui";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import lyricFormat from "@/utils/lyricFormat";
import { soundStop, fadePlayOrPause } from "@/utils/Player";
import parseLyric from "@/utils/parseLyric";
const useMusicDataStore = defineStore("musicData", {
state: () => {
@@ -20,19 +22,32 @@ const useMusicDataStore = defineStore("musicData", {
// 播放状态
playState: false,
// 当前歌曲播放链接
playSongLink: null,
// 当前歌曲歌词
playSongLyric: [],
// playSongLink: null,
// 当前歌曲歌词数据
playSongLyric: {
lrc: [],
yrc: [],
hasTran: false,
hasTran: false,
hasYrc: false,
},
// 当前歌曲歌词播放索引
playSongLyricIndex: 0,
// 当前歌曲是否拥有翻译
playSongTransl: false,
// 每日推荐
dailySongsData: [],
// 歌单分类
catList: {},
// 精品歌单分类
highqualityCatList: [],
// 音乐频谱数据
spectrumsData: {
data: [],
audio: null,
analyser: null,
audioCtx: null,
},
// 是否正在加载数据
isLoadingSong: false,
// 持久化数据
persistData: {
// 搜索历史
@@ -80,10 +95,6 @@ const useMusicDataStore = defineStore("musicData", {
getPersonalFmData(state) {
return state.persistData.personalFmData;
},
// 获取是否拥有翻译
getPlaySongTransl(state) {
return state.playSongTransl;
},
// 获取每日推荐
getDailySongs(state) {
return state.dailySongsData;
@@ -116,10 +127,6 @@ const useMusicDataStore = defineStore("musicData", {
getPlayState(state) {
return state.playState;
},
// 获取播放链接
getPlaySongLink(state) {
return state.playSongLink;
},
// 获取喜欢音乐列表
getLikeList(state) {
return state.persistData.likeList;
@@ -142,8 +149,8 @@ const useMusicDataStore = defineStore("musicData", {
setPersonalFmMode(value) {
this.persistData.personalFmMode = value;
if (value) {
this.playSongLink = null;
if (this.persistData.personalFmData.id) {
soundStop($player);
if (this.persistData.personalFmData?.id) {
this.persistData.playlists = [];
this.persistData.playlists.push(this.persistData.personalFmData);
this.persistData.playSongIndex = 0;
@@ -175,7 +182,7 @@ const useMusicDataStore = defineStore("musicData", {
} else {
this.persistData.personalFmData = fmData;
if (this.persistData.personalFmMode) {
this.playSongLink = null;
soundStop($player);
this.persistData.playlists = [];
this.persistData.playlists.push(fmData);
this.persistData.playSongIndex = 0;
@@ -232,22 +239,22 @@ const useMusicDataStore = defineStore("musicData", {
setLikeSong(id, like).then((res) => {
if (res.code == 200) {
list.push(id);
$message.info("成功喜欢歌曲");
$message.info("已添加到我喜欢的音乐");
} else {
$message.error("喜欢歌曲时发生错误");
$message.error("喜欢音乐时发生错误");
}
});
} else {
$message.info("我喜欢的列表中已存在该歌曲");
$message.info("我喜欢的音乐中已存在该歌曲");
}
} else {
if (exists) {
setLikeSong(id, like).then((res) => {
if (res.code == 200) {
list.splice(list.indexOf(id), 1);
$message.info("成功取消喜欢歌曲");
$message.info("已从我喜欢的音乐中移除");
} else {
$message.error("取消喜欢歌曲时发生错误");
$message.error("取消喜欢音乐时发生错误");
}
});
} else {
@@ -270,18 +277,13 @@ const useMusicDataStore = defineStore("musicData", {
setPlayBarState(value) {
this.showPlayBar = value;
},
// 更改歌曲播放链接
setPlaySongLink(value) {
this.playSongLink = value;
},
// 更改播放列表模式
setPlayListMode(value) {
console.log(value);
this.persistData.playListMode = value;
},
// 添加歌单至播放列表
setPlaylists(value) {
this.persistData.playlists = value;
this.persistData.playlists = value.slice();
console.log(`已添加${value.length}首歌曲至播放列表`);
},
// 更改每日推荐数据
@@ -308,25 +310,14 @@ const useMusicDataStore = defineStore("musicData", {
// 歌词处理
setPlaySongLyric(value) {
if (value.lrc) {
this.playSongLyric = lyricFormat(value.lrc.lyric);
if (value.tlyric && value.tlyric.lyric) {
console.log("歌词有翻译");
this.playSongTransl = true;
let playSongLyric = this.playSongLyric;
let playSongLyricFy = lyricFormat(value.tlyric.lyric);
playSongLyric.forEach((v) => {
playSongLyricFy.forEach((x) => {
if (v.time === x.time) {
v.lyricFy = x.lyric;
}
});
});
this.playSongLyric = playSongLyric;
} else {
this.playSongTransl = false;
try {
this.playSongLyric = parseLyric(value);
} catch (err) {
$message.error("歌词处理出错");
console.error("歌词处理出错:" + err);
}
} else {
console.log("无歌词");
console.log("该歌曲暂无歌词");
this.playSongLyric = [];
}
},
@@ -335,9 +326,13 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongTime.currentTime = value.currentTime;
this.persistData.playSongTime.duration = value.duration;
// 计算进度条应该移动的距离
this.persistData.playSongTime.barMoveDistance = Number(
(value.currentTime / (value.duration / 100)).toFixed(2)
);
if (value.duration === 0) {
this.persistData.playSongTime.barMoveDistance = 0;
} else {
this.persistData.playSongTime.barMoveDistance = Number(
(value.currentTime / (value.duration / 100)).toFixed(2)
);
}
if (this.persistData.playSongTime.barMoveDistance) {
// 歌曲播放进度转换
this.persistData.playSongTime.songTimePlayed = getSongPlayingTime(
@@ -348,16 +343,11 @@ const useMusicDataStore = defineStore("musicData", {
);
}
// 计算当前歌词播放索引
const index = this.playSongLyric.findIndex(
(v) => v.time >= value.currentTime
);
if (index === -1) {
// 如果没有找到合适的歌词,则返回最后一句歌词
this.playSongLyricIndex =
this.playSongLyric.length - 1;
} else {
this.playSongLyricIndex = (index ? index : index + 1) - 1;
}
const setting = settingStore();
const lrcType = !this.playSongLyric.hasYrc || !setting.showYrc;
const lyrics = lrcType ? this.playSongLyric.lrc : this.playSongLyric.yrc;
const index = lyrics.findIndex((v) => v.time >= value.currentTime);
this.playSongLyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
},
// 设置当前播放模式
setPlaySongMode() {
@@ -380,7 +370,9 @@ const useMusicDataStore = defineStore("musicData", {
},
// 上下曲调整
setPlaySongIndex(type) {
this.playState = false;
// this.playState = false;
soundStop($player);
this.isLoadingSong = true;
if (this.persistData.personalFmMode) {
this.setPersonalFmData();
} else {
@@ -393,8 +385,9 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongIndex = Math.floor(
Math.random() * listLength
);
} else if (listMode === "single" && $player) {
$player.currentTime = 0;
} else if (listMode === "single" && typeof $player !== "undefined") {
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
} else {
$message.error("播放出错,请刷新后重试");
}
@@ -403,11 +396,15 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playSongIndex = listLength - 1;
} else if (this.persistData.playSongIndex >= listLength) {
this.persistData.playSongIndex = 0;
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
}
if (listMode !== "single") {
this.playSongLink = null;
if (listMode !== "single" && listLength > 1) {
soundStop($player);
}
this.playState = true;
nextTick().then(() => {
this.setPlayState(true);
});
}
},
// 添加歌曲至播放列表
@@ -419,10 +416,11 @@ const useMusicDataStore = defineStore("musicData", {
try {
if (
value.id !==
this.persistData.playlists[this.persistData.playSongIndex].id
this.persistData.playlists[this.persistData.playSongIndex]?.id
) {
console.log("播放歌曲与上一次不一致");
this.playSongLink = null;
soundStop($player);
this.isLoadingSong = true;
}
} catch (error) {
console.error("出现错误:" + error);
@@ -433,7 +431,7 @@ const useMusicDataStore = defineStore("musicData", {
this.persistData.playlists.push(value);
this.persistData.playSongIndex = this.persistData.playlists.length - 1;
}
play ? (this.playState = true) : null;
play ? this.setPlayState(true) : null;
},
// 在当前播放歌曲后添加
addSongToNext(value) {
@@ -465,9 +463,17 @@ const useMusicDataStore = defineStore("musicData", {
const name = this.persistData.playlists[index].name;
if (index < this.persistData.playSongIndex) {
this.persistData.playSongIndex--;
} else if (index === this.persistData.playSongIndex) {
// 如果删除的是当前播放歌曲,则重置播放器
soundStop($player);
}
$message.success(name + " 已从播放列表中移除");
this.persistData.playlists.splice(index, 1);
// 检查当前播放歌曲的索引是否超出了列表范围
if (this.persistData.playSongIndex >= this.persistData.playlists.length) {
this.persistData.playSongIndex = 0;
soundStop($player);
}
},
// 获取歌单分类
setCatList(highquality = false) {

View File

@@ -8,6 +8,8 @@ const useSettingDataStore = defineStore("settingData", {
// 全局主题
theme: "light",
themeAuto: true,
themeType: "red",
themeData: {},
// 搜索历史
searchHistory: true,
// 轮播图显示
@@ -20,22 +22,38 @@ const useSettingDataStore = defineStore("settingData", {
playerStyle: "cover",
// 底栏歌词显示
bottomLyricShow: true,
// 是否显示逐字歌词
showYrc: true,
// 是否显示歌词翻译
showTransl: true,
// 是否显示歌词音译
showRoma: true,
// 歌曲音质
songLevel: "exhigh",
// 歌词位置
lyricsPosition: "left",
// 歌词滚动位置
lyricsBlock: "center",
lyricsBlock: "start",
// 歌词大小
lyricsFontSize: 2.8,
lyricsFontSize: 3.6,
// 歌词模糊
lyricsBlur: false,
// 音乐频谱
musicFrequency: false,
// 鼠标移入歌词区域暂停滚动
lrcMousePause: true,
// 是否使用网易云解灰
useUnmServer: true,
// 播放背景是否显示图片
backgroundImageShow: "blur",
// 是否显示前奏等待
countDownShow: true,
// 是否显示歌词设置
showLyricSetting: true,
// 歌曲渐入渐出
songVolumeFade: true,
// 列表默认数量
listNumber: 30,
};
},
getters: {
@@ -43,10 +61,6 @@ const useSettingDataStore = defineStore("settingData", {
getSiteTheme(state) {
return state.theme;
},
// 获取是否开启翻译
getShowTransl(state) {
return state.showTransl;
},
},
actions: {
// 切换明暗模式

25
src/store/siteData.js Normal file
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,7 +7,7 @@ import {
getUserArtistlist,
getUserAlbum,
} from "@/api/user";
import { formatNumber, getLongTime } from "@/utils/timeTools.js";
import { formatNumber, getLongTime } from "@/utils/timeTools";
const useUserDataStore = defineStore("userData", {
state: () => {
@@ -101,10 +101,11 @@ const useUserDataStore = defineStore("userData", {
this.userData = {};
this.userOtherData = {};
localStorage.removeItem("cookie");
// 调用退出登录接口
userLogOut();
},
// 更改用户歌单
async setUserPlayLists() {
async setUserPlayLists(callback) {
if (this.userLogin) {
try {
if (!Object.keys(this.userOtherData).length) {
@@ -144,6 +145,9 @@ const useUserDataStore = defineStore("userData", {
});
}
});
if (typeof callback === "function") {
callback();
}
this.userPlayLists.isLoading = false;
} else {
this.userPlayLists.isLoading = false;
@@ -194,7 +198,7 @@ const useUserDataStore = defineStore("userData", {
}
},
// 更改用户收藏专辑
async setUserAlbumLists() {
async setUserAlbumLists(callback) {
if (this.userLogin) {
try {
let offset = 0;
@@ -216,6 +220,9 @@ const useUserDataStore = defineStore("userData", {
offset += 30;
console.log(totalCount, offset, this.userAlbum.list);
}
if (typeof callback === "function") {
callback();
}
this.userAlbum.isLoading = false;
this.userAlbum.has = true;
} catch (err) {

View File

@@ -9,7 +9,7 @@
html,
body,
#app {
-webkit-font-smoothing: subpixel-antialiased;
-webkit-font-smoothing: antialiased;
font-family: "HarmonyOS_Regular", sans-serif !important;
}
@@ -47,16 +47,27 @@ body,
}
.n-card-header {
.n-card-header__main {
// font-weight: bold;
font-size: 18px;
}
}
.n-card__content {
padding-right: 28px;
}
.n-scrollbar {
max-height: 60vh;
font-size: 16px;
line-height: 32px;
}
}
.n-modal-container {
// z-index: 2006 !important;
.n-modal-body-wrapper {
.n-modal-mask {
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
}
}
}
// Nscrollbar
.n-scrollbar {
@@ -90,6 +101,18 @@ body,
border-radius: 8px !important;
}
// Drawer
.n-drawer-container {
.n-drawer {
border-radius: 8px 0 0 8px;
transition: all 0.3s;
@media (max-width: 450px) {
width: 100% !important;
border-radius: 0;
}
}
}
// 文本超出隐藏
.text-hidden {
display: -webkit-box !important;
@@ -98,3 +121,26 @@ body,
overflow: hidden;
word-break: break-all;
}
// 响应式布局
@mixin changeWidth($padding: 10vw) {
.n-layout-header {
padding: 0 $padding;
}
.n-layout-content {
.main {
padding: 24px $padding 54px $padding;
.player {
padding: 0 $padding;
}
}
}
}
@include changeWidth;
/* 小于1200px时 */
@media (max-width: 1200px) {
@include changeWidth($padding: 5vw);
}

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

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

@@ -0,0 +1,294 @@
import { Howl, Howler } from "howler";
import { songScrobble } from "@/api/song";
import { musicStore } from "@/store";
import { NIcon } from "naive-ui";
import { MusicNoteFilled } from "@vicons/material";
// 歌曲信息更新定时器
let timeupdateInterval = null;
// 重试次数
let testNumber = 0;
/**
* 创建音频对象
* @param {string} src - 音频文件地址
* @param {number} volume - 音量默认为0.7
* @param {number} seek - 初始播放进度默认为0
* @return {Howl} - 音频对象
*/
export const createSound = (src, autoPlay = true) => {
try {
Howler.unload();
const music = musicStore();
const sound = new Howl({
src: [src],
format: ["mp3", "flac"],
html5: true,
preload: true,
volume: music.persistData.playVolume,
});
if (autoPlay && music.getPlayState) {
fadePlayOrPause(sound, "play", music.persistData.playVolume);
}
// 首次加载事件
sound?.once("load", () => {
const songId = music.getPlaySongData?.id;
const sourceId = music.getPlaySongData?.sourceId
? music.getPlaySongData.sourceId
: 0;
const isLogin = JSON.parse(localStorage.getItem("userData")).userLogin;
console.log("首次缓冲完成:" + songId + " / 来源:" + sourceId);
sound?.seek(music.persistData.playSongTime.currentTime);
// 取消加载状态
music.isLoadingSong = false;
// 听歌打卡
if (isLogin) {
songScrobble(songId, sourceId).catch((err) => {
console.error("歌曲打卡失败:" + err);
});
}
});
// 播放事件
sound?.on("play", () => {
if (!Object.keys(music.getPlaySongData).length) {
$message.error("音乐数据获取失败");
return false;
}
testNumber = 0;
music.setPlayState(true);
const songName = music.getPlaySongData.name;
const songArtist = music.getPlaySongData.artist[0].name;
$message.info(songName + " - " + songArtist, {
icon: () =>
h(NIcon, null, {
default: () => h(MusicNoteFilled),
}),
});
console.log("开始播放:" + songName + " - " + songArtist);
// 获取播放器信息
timeupdateInterval = setInterval(() => checkAudioTime(sound, music), 250);
setMediaSession(music);
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 播放时页面标题
window.document.title =
music.getPlaySongData.name +
" - " +
music.getPlaySongData.artist[0].name +
" - " +
import.meta.env.VITE_SITE_TITLE;
});
// 暂停事件
sound?.on("pause", () => {
clearInterval(timeupdateInterval);
console.log("音乐暂停");
music.setPlayState(false);
// 更改页面标题
$setSiteTitle();
});
// 结束事件
sound?.on("end", () => {
console.log("歌曲播放结束");
music.setPlaySongIndex("next");
});
// 错误事件
sound?.on("loaderror", () => {
if (testNumber > 2) {
$message.error("歌曲播放失败");
console.error("歌曲播放失败");
music.setPlayState(false);
}
if (testNumber < 4) {
if (music.getPlaylists[0]) $getPlaySongData(music.getPlaySongData);
testNumber++;
} else {
$message.error("歌曲重试次数过多,请刷新后重试", {
closable: true,
duration: 0,
});
}
});
sound?.on("playerror", () => {
$message.error("歌曲播放出错");
console.error("歌曲播放出错");
music.setPlayState(false);
});
// 生成频谱
// createSpectrums(sound, music);
// 返回音频对象
return (window.$player = sound);
} catch (err) {
$message.error("音乐播放器初始化失败");
console.error("音乐播放器初始化失败:" + err);
}
};
/**
* 设置音量
* @param {number} volume - 设置的音量值0-1之间的浮点数
*/
export const setVolume = (sound, volume) => {
sound?.volume(volume);
};
/**
* 设置进度
* @param {number} seek - 设置的进度值0-1之间的浮点数
*/
export const setSeek = (sound, seek) => {
// const music = musicStore();
// music.persistData.playSongTime.currentTime = seek;
sound?.seek(seek);
};
/**
* 音频渐入渐出
* @param {Howl} sound - 音频对象
* @param {String} type - 渐入还是渐出
* @param {number} volume - 渐出音量的大小0-1之间的浮点数
* @param {number} duration - 渐出音量的时长,单位为毫秒
*/
export const fadePlayOrPause = (sound, type, volume, duration = 300) => {
const isFade =
JSON.parse(localStorage.getItem("settingData")).songVolumeFade ?? true;
if (isFade) {
if (type === "play") {
if (sound?.playing()) return;
sound?.play();
sound?.once("play", () => {
sound?.fade(0, volume, duration);
});
} else if (type === "pause") {
sound?.fade(volume, 0, duration);
sound?.once("fade", () => {
sound?.pause();
});
}
} else {
type === "play" ? sound?.play() : sound?.pause();
}
};
/**
* 停止播放器
* @param {Howl} sound - 音频对象
*/
export const soundStop = (sound) => {
sound?.stop();
setSeek(sound, 0);
};
/**
* 获取播放进度
* @param {Howl} sound - 音频对象
* @param {music} music - pinia
*/
const checkAudioTime = (sound, music) => {
if (sound.playing()) {
const currentTime = sound.seek();
const duration = sound._duration;
music.setPlaySongTime({ currentTime, duration });
}
};
/**
* 生成 MediaSession
* @param {music} music - pinia
*/
const setMediaSession = (music) => {
if (
"mediaSession" in navigator &&
Object.keys(music.getPlaySongData).length
) {
const artists = music.getPlaySongData.artist.map((a) => a.name);
navigator.mediaSession.metadata = new MediaMetadata({
title: music.getPlaySongData.name,
artist: artists.join(" & "),
album: music.getPlaySongData.album.name,
artwork: [
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=96y96",
sizes: "96x96",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=128y128",
sizes: "128x128",
},
{
src:
music.getPlaySongData.album.picUrl.replace(/^http:/, "https:") +
"?param=512x512",
sizes: "512x512",
},
],
length: music.getPlaySongTime?.duration,
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
music.setPlaySongIndex("next");
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
music.setPlaySongIndex("prev");
});
navigator.mediaSession.setActionHandler("play", () => {
music.setPlayState(true);
});
navigator.mediaSession.setActionHandler("pause", () => {
music.setPlayState(false);
});
}
};
/**
* 生成频谱数据 - 快速傅里叶变换FFT
* @param {Howl} sound - 音频对象
* @param {music} music - pinia
*/
const createSpectrums = (sound, music) => {
try {
if (!music.spectrumsData.audioCtx) {
// 断开之前的连接
music.spectrumsData.audio?.disconnect();
music.spectrumsData.analyser?.disconnect();
music.spectrumsData.audioCtx?.close();
// 创建新的连接
music.spectrumsData.audioCtx = new (window.AudioContext ||
window.webkitAudioContext)();
const audioDom = sound._sounds[0]._node;
audioDom.crossOrigin = "anonymous";
const source =
music.spectrumsData.audioCtx.createMediaElementSource(audioDom);
const analyser = music.spectrumsData.audioCtx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
analyser.connect(music.spectrumsData.audioCtx.destination);
// 更新频谱数据
const dataArray = new Uint8Array(analyser.frequencyBinCount);
updateSpectrums(analyser, dataArray, music);
// 保存当前链接
music.spectrumsData.audio = source;
music.spectrumsData.analyser = analyser;
}
} catch (err) {
console.error("音乐频谱生成失败:" + err);
}
};
/**
* 更新音乐频谱数据
*
* @param {Object} analyser - 音频分析器
* @param {Uint8Array} dataArray - 频谱数据数组
* @param {Object} music - pinia
*/
const updateSpectrums = (analyser, dataArray, music) => {
analyser.getByteFrequencyData(dataArray);
music.spectrumsData.data = [...dataArray];
// 递归调用,持续更新频谱数据
requestAnimationFrame(() => {
updateSpectrums(analyser, dataArray, music);
});
};

View File

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

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

@@ -0,0 +1,191 @@
/**
* 将歌词接口数据解析出对应数据
* @param {string} data 接口数据
* @returns {Array} 对应数据
*/
const parseLyric = (data) => {
// 判断是否具有内容
const checkLyric = (lyric) => (lyric ? (lyric.lyric ? true : false) : false);
// 初始化数据
const { lrc, tlyric, romalrc, yrc, ytlrc, yromalrc } = data;
const lrcData = {
lrc: lrc?.lyric || null,
tlyric: tlyric?.lyric || null,
romalrc: romalrc?.lyric || null,
yrc: yrc?.lyric || null,
ytlrc: ytlrc?.lyric || null,
yromalrc: yromalrc?.lyric || null,
};
// 初始化输出结果
const result = {
// 是否具有普通翻译
hasLrcTran: checkLyric(tlyric),
// 是否具有普通音译
hasLrcRoma: checkLyric(romalrc),
// 是否具有逐字歌词
hasYrc: checkLyric(yrc),
// 是否具有逐字翻译
hasYrcTran: checkLyric(ytlrc),
// 是否具有逐字音译
hasYrcRoma: checkLyric(yromalrc),
// 普通歌词数组
lrc: [],
// 逐字歌词数据
yrc: [],
};
// 普通歌词
if (lrcData.lrc) {
result.lrc = parseLrc(lrcData.lrc);
//判断是否有其他翻译
result.lrc = lrcData.tlyric
? parseOtherLrc(result.lrc, parseLrc(lrcData.tlyric), "tran")
: result.lrc;
result.lrc = lrcData.romalrc
? parseOtherLrc(result.lrc, parseLrc(lrcData.romalrc), "roma")
: result.lrc;
}
// 逐字歌词
if (lrcData.yrc) {
result.yrc = parseYrc(lrcData.yrc);
//判断是否有其他翻译
result.yrc = lrcData.ytlrc
? parseOtherLrc(result.yrc, parseLrc(lrcData.ytlrc), "tran")
: result.yrc;
result.yrc = lrcData.yromalrc
? parseOtherLrc(result.yrc, parseLrc(lrcData.yromalrc), "roma")
: result.yrc;
}
return result;
};
/**
* 翻译文本对齐
* @param {string} lrc 歌词字符串
* @param {string} tranLrc 翻译歌词字符串
* @returns {Array} 包含翻译的歌词对象数组
*/
const parseOtherLrc = (lrc, tranLrc, name) => {
const lyric = lrc;
const tranLyric = tranLrc;
if (lyric[0] && tranLyric[0]) {
lyric.forEach((v) => {
tranLyric.forEach((x) => {
if (
Number(v.time) === Number(x.time) ||
Math.abs(Number(v.time) - Number(x.time)) < 0.6
) {
v[name] = x.content;
}
});
});
}
return lyric;
};
/**
* 普通歌词解析
* @param {string} lyrics 歌词字符串
* @returns {Array} 歌词对象数组
*/
const parseLrc = (lyrics) => {
if (!lyrics) return [];
try {
// 匹配时间轴和歌词文本的正则表达式
const regex = /^\[([^\]]+)\]\s*(.+?)\s*$/;
// 将歌词字符串按行分割为数组
const lines = lyrics.split("\n");
// 对每一行进行转换
const parsedLyrics = lines
// 筛选出包含时间轴和歌词文本的行
.filter((line) => regex.test(line))
// 转换时间轴和歌词文本为对象
.map((line) => {
const [, time, text] = line.match(regex);
const parts = time.split(":");
const seconds =
Number(parts[0]) * 60 +
Number(parts[1]) +
(parts.length > 2 ? Number(parts[2]) / 1000 : 0);
return { time: Number(seconds.toFixed(2)), content: text.trim() };
})
.filter((c) => c.content.trim() !== "");
// 检查是否为纯音乐,是则返回空数组
if (parsedLyrics.length && /纯音乐,请欣赏/.test(parsedLyrics[0].content)) {
console.log("该歌曲为纯音乐");
return [];
}
return parsedLyrics;
} catch (err) {
console.error("普通歌词处理出错:" + err);
return [];
}
};
/**
* 逐字歌词解析
* @param {string} lyrics 逐字歌词字符串
* @returns {Array} 歌词对象数组
*/
const parseYrc = (lyrics) => {
if (!lyrics) return [];
try {
// 遍历每一行逐字歌词
const parsedLyrics = lyrics
.split("\n")
.map((line) => {
// 匹配每一行中的时间戳信息
const timeReg = /\[(\d+),(\d+)\]/;
const timeMatch = line.match(timeReg);
if (!timeMatch) {
return null;
}
// 解构出起始时间和结束时间
const [_, startTime, endTime] = timeMatch;
if (isNaN(startTime) || isNaN(endTime)) {
return null;
}
// 去除当前行中的时间戳信息,得到歌词内容
const content = line.replace(timeReg, "");
if (!content) {
return null;
}
// 对歌词内容中的时间戳和歌词内容分离
const contentArray = content
.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g)
.filter((c) => c.trim())
.map((c) => {
// 匹配当前片段中的时间戳信息
const timeReg = /\((\d+),(\d+),(\d+)\)/;
const timeMatch = c.match(timeReg);
if (!timeMatch) {
return null;
}
// 解构出时间戳,持续时间和歌词内容
const [_, time, duration] = timeMatch;
const content = c.replace(timeReg, "");
if (!content) {
return null;
}
return {
time: Number(time) / 1000,
duration: Number(duration) / 1000,
content,
};
})
.filter((c) => c);
// 返回当前行解析出的时间信息和歌词内容信息
return {
time: Number(startTime) / 1000,
endTime: Number(endTime) / 1000,
content: contentArray,
};
})
.filter((line) => line);
return parsedLyrics;
} catch (err) {
console.error("逐字歌词处理出错:" + err);
return [];
}
};
export default parseLyric;

View File

@@ -41,7 +41,7 @@ axios.interceptors.response.use(
const data = error.response.data;
switch (error.response.status) {
case 401:
console.error("您未登录");
console.error("无权限访问");
break;
case 301:
console.error("请求发生重定向");

View File

@@ -7,55 +7,92 @@
:src="
albumDetail.picUrl
? albumDetail.picUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
/>
<img src="/images/pic/album.png" class="album" alt="album" />
</div>
<div class="intr">
<span class="name">歌单简介</span>
<span class="desc text-hidden">
{{ albumDetail.description }}
</span>
<n-button
block
strong
secondary
v-if="albumDetail?.description.length > 70"
@click="albumDescShow = true"
>
全部简介
</n-button>
<n-modal
class="s-modal"
v-model:show="albumDescShow"
preset="card"
title="歌单简介"
:bordered="false"
>
<n-scrollbar>
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div>
<div class="tag" v-if="albumDetail.tags">
<n-tag
class="tags"
round
:bordered="false"
v-for="item in albumDetail.tags"
:key="item"
>
{{ item }}
</n-tag>
<div class="meta">
<div class="title">
<n-text class="name">{{ albumDetail.name }}</n-text>
<n-text
class="creator"
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
>
{{ albumDetail.artist.name }}
</n-text>
</div>
<div class="intr">
<span class="name">专辑简介</span>
<span class="desc text-hidden">
{{ albumDetail.description }}
</span>
<n-button
class="all-desc"
block
strong
secondary
v-if="albumDetail?.description.length > 70"
@click="albumDescShow = true"
>
全部简介
</n-button>
<n-modal
class="s-modal"
v-model:show="albumDescShow"
preset="card"
title="歌单简介"
:bordered="false"
>
<n-scrollbar>
<n-text v-html="albumDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div>
<n-space class="tag" v-if="albumDetail.tags">
<n-tag
class="tags"
round
:bordered="false"
v-for="item in albumDetail.tags"
:key="item"
>
{{ item }}
</n-tag>
</n-space>
<n-space class="control">
<n-button strong secondary round type="primary" @click="playAllSong">
<template #icon>
<n-icon :component="MusicList" />
</template>
播放
</n-button>
<n-dropdown
placement="right-start"
trigger="click"
:show-arrow="true"
:options="dropdownOptions"
>
<n-button strong secondary circle>
<template #icon>
<n-icon :component="More" />
</template>
</n-button>
</n-dropdown>
</n-space>
</div>
</div>
<div class="right">
<div class="meta">
<span class="name">{{ albumDetail.name }}</span>
<span class="creator">{{ albumDetail.artist.name }}</span>
<n-text class="name">{{ albumDetail.name }}</n-text>
<n-text
class="creator"
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
>
{{ albumDetail.artist.name }}
</n-text>
<div class="time">
<div class="createTime">
<span class="num">发行时间</span>
@@ -91,24 +128,95 @@
</template>
<script setup>
import { getAlbum } from "@/api/album";
import { NIcon, NAvatar, NText } from "naive-ui";
import { getAlbum, likeAlbum } from "@/api/album";
import { useRouter } from "vue-router";
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
import { getSongTime, getLongTime } from "@/utils/timeTools";
import { MusicList, LinkTwo, More, Like, Unlike } from "@icon-park/vue-next";
import { userStore, musicStore, settingStore } from "@/store";
import DataLists from "@/components/DataList/DataLists.vue";
const router = useRouter();
// 歌单数据
const router = useRouter();
const user = userStore();
const music = musicStore();
const setting = settingStore();
// 专辑数据
const albumId = ref(router.currentRoute.value.query.id);
const albumDetail = ref(null);
const albumData = ref([]);
const albumDescShow = ref(false);
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
const playlists = user.getUserAlbumLists.list;
if (playlists.length) {
return !playlists.some((item) => item.id === Number(id));
}
return true;
};
// 专辑下拉菜单数据
const dropdownOptions = ref([]);
// 更改专辑下拉菜单数据
const setDropdownOptions = () => {
dropdownOptions.value = [
{
key: "copy",
label: "复制专辑链接",
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${albumId.value}`
);
$message.success("专辑链接复制成功");
} catch (err) {
$message.error("复制失败:", err);
}
} else {
$message.error("您的浏览器暂不支持该操作");
}
},
},
icon: renderIcon(h(LinkTwo)),
},
{
key: "like",
label: isLikeOrDislike(albumId.value) ? "收藏专辑" : "取消收藏专辑",
show: user.userLogin,
props: {
onClick: () => {
toChangeLike(albumId.value);
},
},
icon: renderIcon(h(isLikeOrDislike(albumId.value) ? Like : Unlike)),
},
];
};
// 获取歌单信息
const getAlbumData = (id) => {
getAlbum(id).then((res) => {
console.log(res);
// 专辑信息
albumDetail.value = res.album;
$setSiteTitle(res.album.name + " - 专辑");
// 专辑歌曲
if (res.songs) {
albumData.value = [];
@@ -122,6 +230,7 @@ const getAlbumData = (id) => {
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
sourceId: id,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
@@ -132,9 +241,75 @@ const getAlbumData = (id) => {
});
};
// 播放专辑所有歌曲
const playAllSong = () => {
try {
// 获取元素
const songDom = document.getElementById("datalists").firstElementChild;
const allSongDom = document.querySelectorAll("#datalists > *");
// 是否有元素存在 play
let isHasPlay = false;
// 遍历
allSongDom.forEach((child) => {
if (child.classList.contains("play")) {
isHasPlay = true;
}
});
if (!isHasPlay) {
// 双击操作
const event = new MouseEvent("dblclick", {
bubbles: true,
cancelable: true,
view: window,
});
// 双击或单击
if (setting.listClickMode === "dblclick") {
songDom.dispatchEvent(event);
} else if (setting.listClickMode === "click") {
songDom.click();
}
} else {
music.setPlayState(true);
}
} catch (err) {
console.error("播放全部歌曲失败:" + err);
$message.error("播放全部歌曲失败,请重试");
}
};
// 收藏/取消收藏
const toChangeLike = async (id) => {
const type = isLikeOrDislike(id) ? 1 : 2;
try {
const res = await likeAlbum(type, id);
if (res.code === 200) {
$message.success(`专辑${type == 1 ? "收藏成功" : "取消收藏成功"}`);
user.setUserAlbumLists(() => {
setDropdownOptions();
});
} else {
$message.error(`专辑${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
} catch (err) {
$message.error(`专辑${type == 1 ? "收藏失败" : "取消收藏失败"}`);
console.error(`专辑${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err);
}
};
onMounted(() => {
if (albumId.value) {
getAlbumData(albumId.value);
if (
user.userLogin &&
!user.getUserAlbumLists.has &&
!user.getUserAlbumLists.isLoading
) {
user.setUserAlbumLists(() => {
setDropdownOptions();
});
} else {
setDropdownOptions();
}
}
});
@@ -166,49 +341,84 @@ watch(
align-items: flex-start;
position: sticky;
top: 24px;
@media (max-width: 990px) {
margin-right: 0;
width: 30vw;
}
.cover {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
width: 80%;
height: 80%;
.n-avatar {
border-radius: 8px;
width: 80%;
height: 80%;
width: 100%;
height: 100%;
}
.album {
height: 100%;
position: absolute;
top: 0;
right: 4%;
right: -20%;
}
}
.intr {
margin-top: 24px;
width: 80%;
padding-left: 4px;
.name {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 12px;
}
.desc {
-webkit-line-clamp: 4;
line-height: 26px;
margin-bottom: 16px;
}
}
.tag {
margin-top: 20px;
.meta {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
.tags {
margin-right: 8px;
font-size: 13px;
flex-direction: column;
justify-content: flex-start;
.title {
display: none;
flex-direction: column;
margin-top: 0;
.name {
font-size: 28px;
font-weight: bold;
-webkit-line-clamp: 2;
}
.creator {
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
}
}
.intr {
margin-top: 24px;
width: 80%;
padding-left: 4px;
.name {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 12px;
}
.desc {
-webkit-line-clamp: 4;
line-height: 26px;
margin-bottom: 16px;
}
}
.tag {
margin-top: 20px;
.tags {
line-height: 0;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
}
}
}
.control {
margin-top: 20px;
}
}
}
@@ -227,13 +437,19 @@ watch(
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 1;
color: var(--main-color);
}
}
.time {
margin-top: 8px;
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 768px) {
@media (max-width: 1100px) {
flex-direction: column;
align-items: flex-start;
}
@@ -259,21 +475,111 @@ watch(
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static;
width: 60vw;
position: relative;
top: 0;
width: 100%;
height: 30vw;
max-width: none;
.intr {
display: none;
display: flex;
flex-direction: row;
.cover {
height: 100%;
min-width: 30vw;
width: 30vw;
margin-right: 60px;
}
.meta {
.title {
display: flex;
margin-bottom: 16px;
.name {
font-size: 25px;
}
.creator {
font-size: 14px;
}
}
.intr {
margin-top: 0;
padding-left: 0;
.name,
.all-desc {
display: none;
}
.desc {
-webkit-line-clamp: 3;
margin-bottom: 0;
}
}
.control {
position: absolute;
left: 0;
bottom: -60px;
}
}
}
.right {
margin-top: 80px;
.meta {
.name {
font-size: 26px;
display: none;
}
}
}
@media (max-width: 540px) {
.left {
.cover {
margin-right: 44px;
}
.meta {
.title {
margin-bottom: 0;
.name {
font-size: 24px;
}
}
.intr,
.tag {
display: none !important;
}
.control {
position: static;
}
}
}
.right {
margin-top: 30px;
}
}
@media (max-width: 520px) {
.left {
.meta {
.title {
.name {
font-size: 20px;
}
.creator {
font-size: 12px;
}
}
}
}
}
@media (max-width: 370px) {
.left {
.meta {
.title {
.name {
-webkit-line-clamp: 3;
}
}
.control {
position: absolute;
}
}
}
.right {
margin-top: 80px;
}
}
}
.title {

View File

@@ -13,7 +13,7 @@
<script setup>
import { getArtistAblums } from "@/api/artist";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();

View File

@@ -30,10 +30,10 @@
</template>
<script setup>
import { getArtistAllSongs } from "@/api/artist";
import { getArtistDetail, getArtistAllSongs } from "@/api/artist";
import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import { getSongTime } from "@/utils/timeTools";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";
@@ -51,41 +51,56 @@ const pageNumber = ref(
: 1
);
// 获取歌手名称
const getArtistDetailData = (id) => {
getArtistDetail(id).then((res) => {
artistName.value = res.data.artist.name;
});
};
// 获取歌手信息
const getArtistAllSongsData = (id, limit = 30, offset = 0, order = "hot") => {
getArtistAllSongs(id, limit, offset, order).then((res) => {
console.log(res);
if (res.songs[0]) {
// 数据总数
totalCount.value = res.total;
// 歌手名称
artistName.value = res.songs[0].ar[0].name;
// 列表数据
const ids = res.songs.map((obj) => obj.id);
getMusicDetail(ids.join(",")).then((res) => {
console.log(res);
artistData.value = [];
res.songs.forEach((v, i) => {
artistData.value.push({
id: v.id,
num: i + 1 + (pageNumber.value - 1) * pagelimit.value,
name: v.name,
artist: v.ar,
album: v.al,
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
if (!id) return false;
getArtistAllSongs(id, limit, offset, order)
.then((res) => {
console.log(res);
// 获取歌手名称
getArtistDetailData(id);
// 全部歌曲数据
if (res.songs[0]) {
// 数据总数
totalCount.value = res.total;
// 列表数据
const ids = res.songs.map((obj) => obj.id);
getMusicDetail(ids.join(",")).then((res) => {
console.log(res);
artistData.value = [];
res.songs.forEach((v, i) => {
artistData.value.push({
id: v.id,
num: i + 1 + (pageNumber.value - 1) * pagelimit.value,
name: v.name,
artist: v.ar,
album: v.al,
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
});
});
});
} else {
$message.error("歌手全部歌曲为空");
}
// 请求后回顶并结束加载条
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
});
} else {
$message.error("歌手全部歌曲为空");
}
// 请求后回顶并结束加载条
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
})
.catch((err) => {
router.go(-1);
console.error("歌手全部歌曲获取失败:" + err);
$message.error("歌手全部歌曲获取失败");
});
};
// 监听路由参数变化

View File

@@ -146,6 +146,7 @@ const getArtistDetailData = (id) => {
musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize,
};
$setSiteTitle(res.data.artist.name + " - 歌手");
// 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
})
@@ -307,13 +308,13 @@ watch(
cursor: pointer;
transition: all 0.3s;
.n-icon {
color: $mainColor;
color: var(--main-color);
transform: translateY(-1px);
font-size: 16px;
margin-right: 4px;
}
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -19,7 +19,7 @@
<script setup>
import { getArtistSongs } from "@/api/artist";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import { getSongTime } from "@/utils/timeTools";
import DataLists from "@/components/DataList/DataLists.vue";
const router = useRouter();
@@ -73,8 +73,8 @@ watch(
font-size: 16px;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);

View File

@@ -13,7 +13,7 @@
<script setup>
import { getArtistVideos } from "@/api/artist";
import { useRouter } from "vue-router";
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
import { formatNumber, getSongTime } from "@/utils/timeTools";
import VideoLists from "@/components/DataList/VideoLists.vue";
import Pagination from "@/components/Pagination/index.vue";

View File

@@ -134,6 +134,7 @@ const pageNumberChange = (val) => {
};
onMounted(() => {
$setSiteTitle("全部评论");
// 获取评论数据
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
});

View File

@@ -36,6 +36,7 @@ const getDailySongsData = () => {
};
onMounted(() => {
$setSiteTitle("每日推荐");
if (music.getDailySongs.length === 0) getDailySongsData();
});
</script>

View File

@@ -200,6 +200,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌手");
// 获取歌手数据
getArtistListData(
artistType[artistTypeNamesChoose.value],
@@ -239,8 +240,8 @@ onMounted(() => {
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.9);
@@ -261,8 +262,8 @@ onMounted(() => {
font-size: 16px;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);

View File

@@ -8,7 +8,7 @@
class="cat"
icon-placement="right"
round
@click="catModelShow = true"
@click="catModalShow = true"
>
<template #icon>
<n-icon class="up" :component="ChevronRightRound" />
@@ -17,7 +17,7 @@
</n-button>
<n-modal
class="s-modal"
v-model:show="catModelShow"
v-model:show="catModalShow"
preset="card"
title="歌单分类"
:bordered="false"
@@ -32,6 +32,7 @@
marginLeft: '12px',
fontSize: '12px',
transform: 'translateY(-2px)',
cursor: 'pointer',
}"
:bordered="false"
@click="changeTagName('全部歌单')"
@@ -39,7 +40,7 @@
全部歌单
</n-tag>
</template>
<n-scrollbar style="max-height: 80vh">
<n-scrollbar>
<div class="all-cat" v-if="music.catList?.sub[0]">
<n-list>
<n-list-item
@@ -126,7 +127,7 @@ import { ChevronRightRound, LocalFireDepartmentRound } from "@vicons/material";
import { useRouter } from "vue-router";
import { musicStore } from "@/store";
import { getHighqualityPlaylist, getTopPlaylist } from "@/api/playlist";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
@@ -134,7 +135,7 @@ const router = useRouter();
const music = musicStore();
// 分类数据
const catModelShow = ref(false);
const catModalShow = ref(false);
const catName = ref(
router.currentRoute.value.query.cat
? router.currentRoute.value.query.cat
@@ -254,7 +255,7 @@ const changeTagName = (name) => {
page: 1,
},
});
catModelShow.value = false;
catModalShow.value = false;
};
// 排序方式变化
@@ -299,6 +300,7 @@ watch(
: false;
if (val.name == "dsc-playlists") {
if (hqPLayListOpen.value) {
playlistsData.value = [];
getHqPlaylistData(catName.value);
} else {
pageNumber.value = val.query.page ? Number(val.query.page) : 1;
@@ -313,6 +315,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌单");
// 获取歌单分类
if (!music.catList.sub || !music.highqualityCatList[0])
music.setCatList(true);
@@ -347,8 +350,8 @@ onMounted(() => {
font-size: 16px;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
@@ -372,14 +375,14 @@ onMounted(() => {
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
}
.icon {
color: $mainColor;
color: var(--main-color);
font-size: 16px;
margin-left: 4px;
transform: translateY(-1px);

View File

@@ -57,7 +57,7 @@
<script setup>
import { getToplist } from "@/api/album";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
const router = useRouter();
@@ -97,6 +97,7 @@ const getToplistData = () => {
};
onMounted(() => {
$setSiteTitle("发现 - 排行榜");
getToplistData();
});
</script>
@@ -157,6 +158,7 @@ onMounted(() => {
color: #fff;
background-color: #00000030;
font-size: 12px;
-webkit-backdrop-filter: blur(40px);
backdrop-filter: blur(40px);
padding: 4px 8px;
border-radius: 8px 0 8px 0;

View File

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

View File

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

View File

@@ -32,7 +32,7 @@
:size="180"
level="H"
background="#00000000"
foreground="#f55e55"
:foreground="setting.themeData.primaryColor"
/>
</n-card>
<span class="tip">{{ qrText }}</span>
@@ -94,7 +94,7 @@
</template>
<script setup>
import { userStore } from "@/store";
import { userStore, musicStore, settingStore } from "@/store";
import {
getLoginState,
getQrKey,
@@ -105,11 +105,13 @@ import {
} from "@/api/login";
import { useRouter } from "vue-router";
import { PhoneAndroidRound, PasswordRound } from "@vicons/material";
import { formRules } from "@/utils/formRules.js";
import { formRules } from "@/utils/formRules";
import QrcodeVue from "qrcode.vue";
const router = useRouter();
const user = userStore();
const music = musicStore();
const setting = settingStore();
const { numberRule, mobileRule } = formRules();
// 二维码数据
@@ -138,6 +140,7 @@ const loginStateMessage = ref(null);
// 储存登录信息
const saveLoginData = (data) => {
data.cookie = data.cookie.replaceAll(" HTTPOnly", "");
user.setCookie(data.cookie);
// 验证用户登录信息
getLoginState().then((res) => {
@@ -149,7 +152,7 @@ const saveLoginData = (data) => {
// 自动签到
if ($signIn) $signIn();
clearInterval(qrCheckInterval.value);
router.go(-1);
router.push("/user");
} else {
user.userLogOut();
$message.error("登录出错,请重试");
@@ -165,7 +168,7 @@ const getQrKeyData = () => {
if (res.data.profile && window.localStorage.getItem("cookie")) {
$message.info("已登录,请勿重复登录");
user.userLogin = true;
router.go(-1);
router.push("/user");
} else {
user.userLogOut();
clearInterval(qrCheckInterval.value);
@@ -266,7 +269,7 @@ const phoneLogin = (e) => {
phoneFormData._value.captcha
).then((res) => {
console.log(res);
// 网易接口抽风,等好了再写
// 暂时不支持,等支持了再写
});
}
});
@@ -288,11 +291,16 @@ const tabChange = (val) => {
};
onMounted(() => {
$setSiteTitle("登录");
// 隐藏控制条
music.setPlayBarState(false);
// 获取二维码登录 key
getQrKeyData();
});
onBeforeUnmount(() => {
// 恢复控制条
music.setPlayBarState(true);
// 清除定时器
clearInterval(qrCheckInterval.value);
clearInterval(captchaTimeOut.value);

View File

@@ -30,7 +30,7 @@
<script setup>
import { getAlbumNew } from "@/api/album";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
@@ -103,7 +103,7 @@ const getAlbumNewData = (area, limit = 30, offset = 0) => {
watch(
() => router.currentRoute.value,
(val) => {
albumAreaChoose.value = val.query.area;
albumAreaChoose.value = val.query.area ? val.query.area : "ALL";
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "new-album") {
getAlbumNewData(
@@ -149,6 +149,7 @@ const changeArea = (area) => {
};
onMounted(() => {
$setSiteTitle("全部新碟");
getAlbumNewData(
albumAreaChoose.value,
pagelimit.value,
@@ -177,8 +178,8 @@ onMounted(() => {
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.9);

View File

@@ -7,78 +7,85 @@
:src="
playListDetail.coverImgUrl
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
/>
<n-avatar
class="shadow"
:src="
playListDetail.coverImgUrl
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
/>
<img src="/images/pic/album.png" class="album" alt="album" />
</div>
<div class="intr">
<span class="name">歌单简介</span>
<span class="desc text-hidden">
{{
playListDetail.description
? playListDetail.description
: "太懒了吧,连简介都不写"
}}
</span>
<n-button
block
strong
secondary
v-if="playListDetail?.description.length > 70"
@click="playListDescShow = true"
>
全部简介
</n-button>
<n-modal
class="s-modal"
v-model:show="playListDescShow"
preset="card"
title="歌单简介"
:bordered="false"
>
<n-scrollbar>
<n-text
v-html="playListDetail.description.replace(/\n/g, '<br>')"
/>
</n-scrollbar>
</n-modal>
</div>
<n-space class="tag" v-if="playListDetail.tags">
<n-tag
class="tags"
size="large"
round
:bordered="false"
v-for="item in playListDetail.tags"
:key="item"
@click="router.push(`/discover/playlists?cat=${item}&page=1`)"
>
{{ item }}
</n-tag>
</n-space>
<!-- <div class="control" v-if="true">
<n-space>
<n-button strong secondary round>
<template #icon>
<n-icon :component="EditNoteRound" />
</template>
编辑
</n-button>
<n-button strong secondary round type="primary">
<template #icon>
<n-icon :component="DeleteRound" />
</template>
<div class="meta">
<div class="title">
<n-text class="name text-hidden">{{ playListDetail.name }}</n-text>
<n-text class="creator">{{ playListDetail.creator.nickname }}</n-text>
</div>
<div class="intr">
<span class="name">歌单简介</span>
<span class="desc text-hidden">
{{
playListDetail.description
? playListDetail.description
: "太懒了吧,连简介都不写"
}}
</span>
<n-button
class="all-desc"
block
strong
secondary
v-if="playListDetail?.description?.length > 70"
@click="playListDescShow = true"
>
全部简介
</n-button>
</div>
<n-space class="tag" v-if="playListDetail.tags">
<n-tag
class="tags"
round
:bordered="false"
v-for="item in playListDetail.tags"
:key="item"
@click="router.push(`/discover/playlists?cat=${item}&page=1`)"
>
{{ item }}
</n-tag>
</n-space>
</div> -->
<n-space class="control">
<n-button strong secondary round type="primary" @click="playAllSong">
<template #icon>
<n-icon :component="MusicList" />
</template>
播放
</n-button>
<n-dropdown
placement="right-start"
trigger="click"
:show-arrow="true"
:options="dropdownOptions"
>
<n-button strong secondary circle>
<template #icon>
<n-icon :component="More" />
</template>
</n-button>
</n-dropdown>
</n-space>
</div>
</div>
<div class="right">
<div class="meta">
<span class="name">{{ playListDetail.name }}</span>
<span class="creator">{{ playListDetail.creator.nickname }}</span>
<n-text class="name">{{ playListDetail.name }}</n-text>
<n-text class="creator">{{ playListDetail.creator.nickname }}</n-text>
<div class="time">
<div class="createTime">
<span class="num">创建时间</span>
@@ -99,13 +106,27 @@
@pageSizeChange="pageSizeChange"
@pageNumberChange="pageNumberChange"
/>
<!-- 歌单简介 -->
<n-modal
class="s-modal"
v-model:show="playListDescShow"
preset="card"
title="歌单简介"
:bordered="false"
>
<n-scrollbar>
<n-text v-html="playListDetail.description.replace(/\n/g, '<br>')" />
</n-scrollbar>
</n-modal>
</div>
</div>
<div class="title" v-else-if="!playListId">
<span class="key">参数不完整</span>
<div class="title" v-else-if="!playListId || !loadingState">
<span class="key">{{
loadingState ? "参数不完整" : "歌单信息加载失败"
}}</span>
<br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一
返回上一
</n-button>
</div>
<div class="loading" v-else>
@@ -122,16 +143,31 @@
</template>
<script setup>
import { getPlayListDetail, getAllPlayList } from "@/api/playlist";
import { NIcon, NAvatar, NText } from "naive-ui";
import {
getPlayListDetail,
getAllPlayList,
delPlayList,
likePlaylist,
} from "@/api/playlist";
import { useRouter } from "vue-router";
import { userStore, musicStore } from "@/store";
import { getSongTime, getLongTime } from "@/utils/timeTools.js";
import { EditNoteRound, DeleteRound } from "@vicons/material";
import { userStore, musicStore, settingStore } from "@/store";
import { getSongTime, getLongTime } from "@/utils/timeTools";
import {
MusicList,
LinkTwo,
More,
DeleteFour,
Like,
Unlike,
} from "@icon-park/vue-next";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
const user = userStore();
const music = musicStore();
const setting = settingStore();
// 歌单数据
const playListId = ref(router.currentRoute.value.query.id);
@@ -139,6 +175,7 @@ const playListDetail = ref(null);
const playListData = ref([]);
const playListDescShow = ref(false);
const pagelimit = ref(30);
const loadingState = ref(true);
const pageNumber = ref(
router.currentRoute.value.query.page
? Number(router.currentRoute.value.query.page)
@@ -146,19 +183,105 @@ const pageNumber = ref(
);
const totalCount = ref(0);
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => icon,
}
);
};
};
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
const playlists = user.getUserPlayLists.like;
if (playlists.length) {
return !playlists.some((item) => item.id === Number(id));
}
return true;
};
// 判断是否可删除
const isCanDelete = (id) => {
const playlists = user.getUserPlayLists.own;
if (playlists.length) {
return playlists.some((item) => item.id === Number(id));
}
return false;
};
// 歌单下拉菜单数据
const dropdownOptions = ref([]);
// 更改歌单下拉菜单数据
const setDropdownOptions = () => {
dropdownOptions.value = [
{
key: "copy",
label: "复制歌单链接",
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${playListId.value}`
);
$message.success("歌单链接复制成功");
} catch (err) {
$message.error("复制失败:", err);
}
} else {
$message.error("您的浏览器暂不支持该操作");
}
},
},
icon: renderIcon(h(LinkTwo)),
},
{
key: "del",
label: "删除歌单",
show: user.userLogin && isCanDelete(playListId.value),
props: {
onClick: () => {
toDelPlayList(playListDetail.value);
},
},
icon: renderIcon(h(DeleteFour)),
},
{
key: "like",
label: isLikeOrDislike(playListId.value) ? "收藏歌单" : "取消收藏歌单",
show: user.userLogin && !isCanDelete(playListId.value),
props: {
onClick: () => {
toChangeLike(playListId.value);
},
},
icon: renderIcon(h(isLikeOrDislike(playListId.value) ? Like : Unlike)),
},
];
};
// 获取歌单信息
const getPlayListDetailData = (id) => {
getPlayListDetail(id).then((res) => {
console.log(res);
if (res.playlist) {
getPlayListDetail(id)
.then((res) => {
console.log(res);
// 歌单总数
totalCount.value = res.playlist.trackCount;
// 歌单信息
playListDetail.value = res.playlist;
} else {
$message.error("获取歌单信息失败");
}
});
$setSiteTitle(res.playlist.name + " - 歌单");
})
.catch((err) => {
$setSiteTitle("歌单详情");
loadingState.value = false;
console.error("获取歌单信息失败:" + err);
});
};
// 获取歌单所有歌曲
@@ -177,6 +300,7 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
alia: v.alia,
time: getSongTime(v.dt),
fee: v.fee,
sourceId: id,
pc: v.pc ? v.pc : null,
mv: v.mv ? v.mv : null,
});
@@ -189,6 +313,85 @@ const getAllPlayListData = (id, limit = 30, offset = 0) => {
});
};
// 播放歌单所有歌曲
const playAllSong = () => {
try {
// 获取元素
const songDom = document.getElementById("datalists").firstElementChild;
const allSongDom = document.querySelectorAll("#datalists > *");
// 是否有元素存在 play
let isHasPlay = false;
// 遍历
allSongDom.forEach((child) => {
if (child.classList.contains("play")) {
isHasPlay = true;
}
});
if (!isHasPlay) {
// 双击操作
const event = new MouseEvent("dblclick", {
bubbles: true,
cancelable: true,
view: window,
});
// 双击或单击
if (setting.listClickMode === "dblclick") {
songDom.dispatchEvent(event);
} else if (setting.listClickMode === "click") {
songDom.click();
}
} else {
music.setPlayState(true);
}
} catch (err) {
console.error("播放全部歌曲失败:" + err);
$message.error("播放全部歌曲失败,请重试");
}
};
// 删除歌单
const toDelPlayList = (data) => {
if (data.id === user.getUserPlayLists?.own[0].id) {
$message.warning("默认歌单无法删除");
return false;
}
$dialog.warning({
class: "s-dialog",
title: "删除歌单",
content: "确认删除歌单 " + data.name + "?删除后将不可恢复!",
positiveText: "删除",
negativeText: "取消",
onPositiveClick: () => {
delPlayList(data.id).then((res) => {
if (res.code === 200) {
$message.success("删除成功");
user.setUserPlayLists();
router.push("/user/playlists");
}
});
},
});
};
// 收藏/取消收藏
const toChangeLike = async (id) => {
const type = isLikeOrDislike(id) ? 1 : 2;
try {
const res = await likePlaylist(type, id);
if (res.code === 200) {
$message.success(`歌单${type == 1 ? "收藏成功" : "取消收藏成功"}`);
user.setUserPlayLists(() => {
setDropdownOptions();
});
} else {
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
} catch (err) {
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
console.error(`歌单${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err);
}
};
onMounted(() => {
if (playListId.value) {
getPlayListDetailData(playListId.value);
@@ -197,6 +400,17 @@ onMounted(() => {
pagelimit.value,
(pageNumber.value - 1) * pagelimit.value
);
if (
user.userLogin &&
!user.getUserPlayLists.has &&
!user.getUserPlayLists.isLoading
) {
user.setUserPlayLists(() => {
setDropdownOptions();
});
} else {
setDropdownOptions();
}
}
});
@@ -264,53 +478,94 @@ watch(
align-items: flex-start;
position: sticky;
top: 24px;
@media (max-width: 990px) {
margin-right: 0;
width: 30vw;
}
.cover {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
// box-shadow: 0 0 16px 0px rgb(0 0 0 / 20%);
width: 80%;
height: 80%;
.n-avatar {
border-radius: 8px;
width: 80%;
height: 80%;
}
.album {
width: 100%;
height: 100%;
z-index: 1;
}
.shadow {
position: absolute;
top: 0;
right: 4%;
top: 12px;
height: 100%;
width: 100%;
filter: blur(16px) opacity(0.6);
transform: scale(0.92, 0.96);
z-index: 0;
background-size: cover;
aspect-ratio: 1/1;
}
}
.intr {
margin-top: 24px;
width: 80%;
padding-left: 4px;
.name {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 12px;
}
.desc {
-webkit-line-clamp: 4;
line-height: 26px;
margin-bottom: 16px;
}
}
.tag {
margin-top: 20px;
.tags {
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: $mainSecondaryColor;
color: $mainColor;
.meta {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.title {
display: none;
flex-direction: column;
margin-top: 0;
.name {
font-size: 28px;
font-weight: bold;
-webkit-line-clamp: 2;
}
&:active {
transform: scale(0.95);
.creator {
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
}
}
.intr {
margin-top: 24px;
width: 80%;
padding-left: 4px;
.name {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 12px;
@media (max-width: 990px) {
font-size: 18px;
}
}
.desc {
-webkit-line-clamp: 4;
line-height: 26px;
margin-bottom: 16px;
}
}
.tag {
margin-top: 20px;
.tags {
line-height: 0;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
}
}
}
.control {
margin-top: 20px;
}
}
}
.right {
@@ -328,6 +583,12 @@ watch(
margin-top: 6px;
font-size: 16px;
opacity: 0.8;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 1;
color: var(--main-color);
}
}
.time {
margin-top: 8px;
@@ -360,22 +621,110 @@ watch(
@media (max-width: 768px) {
flex-direction: column;
.left {
margin-bottom: 12px;
position: static;
width: 60vw;
position: relative;
top: 0;
width: 100%;
height: 40vw;
max-width: none;
.intr,
.tag {
display: none;
display: flex;
flex-direction: row;
.cover {
height: 100%;
min-width: 40vw;
margin-right: 30px;
}
.meta {
.title {
display: flex;
margin-bottom: 16px;
.name {
font-size: 25px;
}
.creator {
font-size: 15px;
}
}
.intr {
margin-top: 0;
padding-left: 0;
.name,
.all-desc {
display: none;
}
.desc {
-webkit-line-clamp: 2;
margin-bottom: 0;
}
}
.control {
position: absolute;
left: 0;
bottom: -60px;
}
}
}
.right {
margin-top: 80px;
.meta {
.name {
font-size: 26px;
display: none;
}
}
}
@media (max-width: 540px) {
.left {
.cover {
margin-right: 20px;
}
.meta {
.title {
.name {
font-size: 24px;
}
}
.intr,
.tag {
display: none !important;
}
.control {
position: static;
}
}
}
.right {
margin-top: 30px;
}
}
@media (max-width: 520px) {
.left {
.meta {
.title {
margin-bottom: 0;
.name {
font-size: 20px;
}
.creator {
font-size: 12px;
}
}
}
}
}
@media (max-width: 370px) {
.left {
.meta {
.title {
.name {
-webkit-line-clamp: 3;
}
}
.control {
position: absolute;
}
}
}
.right {
margin-top: 80px;
}
}
}
.title {

View File

@@ -14,7 +14,7 @@
<script setup>
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { getLongTime } from "@/utils/timeTools.js";
import { getLongTime } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();

View File

@@ -55,6 +55,7 @@ const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
watch(
() => router.currentRoute.value,
(val) => {
$setSiteTitle(val.query.keywords + "的搜索结果");
searchKeywords.value = val.query.keywords;
tabValue.value = val.path.split("/")[2];
}
@@ -71,6 +72,10 @@ const tabChange = (value) => {
},
});
};
onMounted(() => {
if (searchKeywords.value) $setSiteTitle(searchKeywords.value + "的搜索结果");
});
</script>
<style lang="scss" scoped>

View File

@@ -14,7 +14,7 @@
<script setup>
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber } from "@/utils/timeTools";
import CoverLists from "@/components/DataList/CoverLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();

View File

@@ -13,9 +13,9 @@
<script setup>
import { getSearchData } from "@/api/search";
import { getMusicDetail } from "@/api/song";
// import { getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import { getSongTime } from "@/utils/timeTools";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
@@ -41,7 +41,7 @@ const getSearchDataList = (keywords, limit = 30, offset = 0, type = 1) => {
totalCount.value = res.result.songCount;
// const ids = res.result.songs.map((obj) => obj.id);
// getMusicDetail(ids.join(",")).then((res) => {});
console.log(res);
// console.log(res);
searchData.value = [];
res.result.songs.forEach((v, i) => {
searchData.value.push({

View File

@@ -14,7 +14,7 @@
<script setup>
import { getSearchData } from "@/api/search";
import { useRouter } from "vue-router";
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
import { formatNumber, getSongTime } from "@/utils/timeTools";
import VideoLists from "@/components/DataList/VideoLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();

View File

@@ -1,399 +0,0 @@
<template>
<div class="setting">
<div class="title">全局设置</div>
<n-h6 prefix="bar"> 基础设置 </n-h6>
<n-card class="set-item">
<div class="name">明暗模式</div>
<n-select class="set" v-model:value="theme" :options="darkOptions" />
</n-card>
<n-card class="set-item">
<div class="name">明暗模式跟随系统</div>
<n-switch v-model:value="themeAuto" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
每日签到
<span class="tip">是否自动进行每日签到</span>
</div>
<n-switch v-model:value="autoSignIn" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
列表点击方式
<span class="tip">移动端该设置项无效单击同时生效</span>
</div>
<n-select
class="set"
v-model:value="listClickMode"
:options="listClickModeOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">显示轮播图</div>
<n-switch v-model:value="bannerShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">显示搜索历史</div>
<n-switch v-model:value="searchHistory" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示底栏歌词
<span class="tip">是否在播放时显示歌词</span>
</div>
<n-switch v-model:value="bottomLyricShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌曲音质
<span class="tip">无损音质及以上需要您为黑胶会员</span>
</div>
<n-select
class="set"
v-model:value="songLevel"
:options="songLevelOptions"
/>
</n-card>
<n-h6 prefix="bar"> 歌词设置 </n-h6>
<n-card class="set-item">
<div class="name">显示歌词翻译</div>
<n-switch v-model:value="showTransl" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
智能暂停滚动
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
</div>
<n-switch v-model:value="lrcMousePause" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">播放器样式</div>
<n-select
class="set"
v-model:value="playerStyle"
:options="playerStyleOptions"
/>
</n-card>
<n-card
class="set-item"
:content-style="{
flexDirection: 'column',
alignItems: 'flex-start',
}"
>
<div class="name">歌词文本大小</div>
<n-slider
v-model:value="lyricsFontSize"
:tooltip="false"
:max="3.4"
:min="2.2"
:step="0.01"
:marks="{
2.2: '最小',
2.8: '默认',
3.4: '最大',
}"
/>
<div :class="lyricsBlur ? 'more blur' : 'more'">
<div
v-for="n in 3"
:key="n"
:class="n === 2 ? 'lrc on' : 'lrc'"
:style="{
margin: n === 2 ? '12px 0' : null,
alignItems: lyricsPosition == 'center' ? 'center' : null,
transformOrigin:
lyricsPosition == 'center' ? 'center' : 'center left',
}"
>
<span :style="{ fontSize: lyricsFontSize + 'vh' }"
>这是一句歌词
</span>
<span :style="{ fontSize: lyricsFontSize - 0.4 + 'vh' }"
>This is a lyric
</span>
</div>
</div>
</n-card>
<n-card class="set-item">
<div class="name">默认歌词位置</div>
<n-select
class="set"
v-model:value="lyricsPosition"
:options="lyricsPositionOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
歌词滚动位置
<span class="tip">歌词高亮时所处的位置</span>
</div>
<n-select
class="set"
v-model:value="lyricsBlock"
:options="lyricsBlockOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
歌词模糊
<span class="tip">实验性功能未播放或已播放歌词模糊显示</span>
</div>
<n-switch v-model:value="lyricsBlur" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示音乐频谱
<span class="tip">实验性功能可能会导致一些意想不到的后果</span>
</div>
<n-switch
v-model:value="musicFrequency"
:round="false"
@click="changeMusicFrequency"
/>
</n-card>
<n-h6 prefix="bar"> 其他设置 </n-h6>
<n-card class="set-item">
<div class="name">
系统重置
<span class="tip">若程序显示异常或出现问题时可尝试此操作</span>
</div>
<n-button strong secondary type="error" @click="resetApp">
重置
</n-button>
</n-card>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { settingStore, userStore } from "@/store";
const setting = settingStore();
const user = userStore();
const {
theme,
themeAuto,
showTransl,
lyricsPosition,
playerStyle,
musicFrequency,
listClickMode,
lyricsFontSize,
bottomLyricShow,
lyricsBlock,
songLevel,
bannerShow,
lyricsBlur,
autoSignIn,
lrcMousePause,
searchHistory,
} = storeToRefs(setting);
// 深浅模式
const darkOptions = [
{
label: "浅色模式",
value: "light",
},
{
label: "深色模式",
value: "dark",
},
];
// 列表模式
const listClickModeOptions = [
{
label: "双击播放",
value: "dblclick",
},
{
label: "单击播放",
value: "click",
},
];
// 歌曲音质
const songLevelOptions = [
{
label: "标准",
value: "standard",
},
{
label: "较高",
value: "higher",
},
,
{
label: "极高",
value: "exhigh",
},
,
{
label: "无损",
value: "lossless",
disabled: user.userData?.vipType ? false : true,
},
,
{
label: "Hi-Res",
value: "hires",
disabled: user.userData?.vipType ? false : true,
},
];
// 歌词位置
const lyricsPositionOptions = [
{
label: "居左",
value: "left",
},
{
label: "居中",
value: "center",
},
];
// 歌词滚动位置
const lyricsBlockOptions = [
{
label: "靠近顶部",
value: "start",
},
{
label: "水平居中",
value: "center",
},
];
// 播放器样式
const playerStyleOptions = [
{
label: "封面模式",
value: "cover",
},
{
label: "唱片模式",
value: "record",
},
];
// 音乐频谱提醒
const changeMusicFrequency = () => {
if (musicFrequency.value) {
$dialog.warning({
class: "s-dialog",
title: "实验性功能",
content: "确认开启音乐频谱?将于刷新后生效",
positiveText: "开启",
negativeText: "取消",
onMaskClick: () => {
musicFrequency.value = false;
},
onPositiveClick: () => {
musicFrequency.value = true;
location.reload();
},
onNegativeClick: () => {
musicFrequency.value = false;
},
});
}
};
// 系统重置
const resetApp = () => {
const cleanAll = () => {
$message ? $message.success("重置成功") : alert("重置成功");
localStorage.clear();
window.location.href = "/";
};
$dialog.warning({
title: "系统重置",
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
positiveText: "重置",
negativeText: "取消",
onPositiveClick: () => {
$cleanAll ? $cleanAll() : cleanAll();
},
});
};
</script>
<style lang="scss" scoped>
.setting {
padding: 0 10vw;
max-width: 1200px;
margin: 0 auto;
@media (max-width: 768px) {
padding: 0;
}
.title {
margin-top: 30px;
margin-bottom: 20px;
font-size: 40px;
font-weight: bold;
}
.n-h {
padding-left: 16px;
font-size: 20px;
margin-left: 4px;
}
.set-item {
width: 100%;
border-radius: 8px;
margin-bottom: 12px;
:deep(.n-card__content) {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.name {
font-size: 16px;
display: flex;
flex-direction: column;
padding-right: 20px;
.tip {
font-size: 12px;
opacity: 0.8;
}
}
.set {
width: 200px;
@media (max-width: 768px) {
width: 140px;
min-width: 140px;
}
}
.more {
padding: 12px;
border-radius: 8px;
background-color: var(--n-border-color);
width: 100%;
margin-top: 12px;
box-sizing: border-box;
&.blur {
.lrc {
filter: blur(2px);
&.on {
filter: blur(0);
}
}
}
.lrc {
opacity: 0.6;
display: flex;
flex-direction: column;
transform: scale(0.95);
transition: all 0.3s;
&.on {
font-weight: bold;
opacity: 1;
transform: scale(1.05);
}
}
}
}
}
}
</style>

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

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

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

@@ -0,0 +1,286 @@
<template>
<div class="set-main">
<n-card
class="set-item"
:content-style="{
flexDirection: 'column',
alignItems: 'flex-start',
}"
>
<div class="top">
<div class="name">
主题色选择
<span class="tip">更换全站主题色即时生效</span>
</div>
<n-button
v-if="themeType !== 'red'"
strong
secondary
@click="changeThemeColor(null, true)"
>
恢复默认
</n-button>
</div>
<n-grid
class="color-selete"
:x-gap="16"
:y-gap="16"
responsive="screen"
cols="3 s:4 m:5 l:6"
>
<n-grid-item
v-for="item in themeColorData"
:key="item"
:style="{ '--color': item.primaryColor }"
:class="item.label === themeType ? 'item check' : 'item'"
@click="changeThemeColor(item)"
>
<n-text v-html="item.name" />
</n-grid-item>
</n-grid>
</n-card>
<n-card class="set-item">
<div class="name">明暗模式</div>
<n-select class="set" v-model:value="theme" :options="darkOptions" />
</n-card>
<n-card class="set-item">
<div class="name">明暗模式跟随系统</div>
<n-switch v-model:value="themeAuto" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
每日签到
<span class="tip">是否自动进行每日签到</span>
</div>
<n-switch v-model:value="autoSignIn" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">显示轮播图</div>
<n-switch v-model:value="bannerShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
列表点击方式
<span class="tip">移动端该设置项无效单击同时生效</span>
</div>
<n-select
class="set"
v-model:value="listClickMode"
:options="listClickModeOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">显示搜索历史</div>
<n-switch v-model:value="searchHistory" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示底栏歌词
<span class="tip">是否在播放时显示歌词</span>
</div>
<n-switch v-model:value="bottomLyricShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌曲渐入渐出
<span class="tip">是否在歌曲暂停 / 播放时渐入渐出</span>
</div>
<n-switch v-model:value="songVolumeFade" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌曲音质
<span class="tip">无损音质及以上需要您为黑胶会员</span>
</div>
<n-select
class="set"
v-model:value="songLevel"
:options="songLevelOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
尝试替换无法播放的歌曲
<span class="tip">
{{
useUnmServerShow
? "是否使用 UNM 替换无法播放的歌曲链接"
: "请配置 UNM-Server 后使用解灰功能"
}}
</span>
</div>
<n-switch
v-model:value="useUnmServer"
:round="false"
:disabled="!useUnmServerShow"
/>
</n-card>
<n-card class="set-item">
<div class="name">
播放页快捷设置
<span class="tip">是否在播放页面显示快捷设置</span>
</div>
<n-switch v-model:value="showLyricSetting" :round="false" />
</n-card>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { settingStore, userStore } from "@/store";
import themeColorData from "@/components/Provider/themeColor.json";
const setting = settingStore();
const user = userStore();
const {
theme,
themeAuto,
listClickMode,
bottomLyricShow,
songLevel,
bannerShow,
autoSignIn,
searchHistory,
themeType,
showLyricSetting,
songVolumeFade,
useUnmServer,
} = storeToRefs(setting);
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
// 深浅模式
const darkOptions = [
{
label: "浅色模式",
value: "light",
},
{
label: "深色模式",
value: "dark",
},
];
// 列表模式
const listClickModeOptions = [
{
label: "双击播放",
value: "dblclick",
},
{
label: "单击播放",
value: "click",
},
];
// 歌曲音质
const songLevelOptions = [
{
label: "标准",
value: "standard",
},
{
label: "较高",
value: "higher",
},
,
{
label: "极高",
value: "exhigh",
},
{
label: "无损",
value: "lossless",
disabled: user.userData?.vipType ? false : true,
},
{
label: "Hi-Res",
value: "hires",
disabled: user.userData?.vipType ? false : true,
},
{
label: "鲸云臻音",
value: "jyeffect",
disabled: user.userData?.vipType ? false : true,
},
{
label: "鲸云母带",
value: "jymaster",
disabled: user.userData?.vipType ? false : true,
},
];
// 更换主题色
const changeThemeColor = (data, reset = false) => {
if (reset) {
$dialog.warning({
class: "s-dialog",
title: "恢复默认",
content: "确认恢复全站主题色为默认?",
positiveText: "确认",
negativeText: "取消",
onPositiveClick: () => {
$message.success("主题色已重置");
themeType.value = "red";
},
});
} else {
$message.success("主题色更换为" + data.name);
themeType.value = data.label;
}
};
</script>
<style lang="scss" scoped>
.set-item {
.top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.color-selete {
margin-top: 16px;
.item {
position: relative;
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color);
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
@media (max-width: 800px) {
height: 60px;
}
&::before {
content: "";
position: absolute;
border-radius: 12px;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid var(--color);
opacity: 0;
transition: opacity 0.3s;
}
&.check {
&::before {
opacity: 1;
}
}
&:active {
transform: scale(0.98);
}
.n-text {
color: #fff;
}
}
}
}
</style>

View File

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

View File

@@ -0,0 +1,234 @@
<template>
<div class="set-player">
<n-card class="set-item">
<div class="name">
播放器样式
<span class="tip">播放器左侧功能区样式</span>
</div>
<n-select
class="set"
v-model:value="playerStyle"
:options="playerStyleOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
播放背景样式
<span class="tip">{{
backgroundImageShow === "blur"
? "将专辑封面模糊显示"
: "提取专辑主色作为背景颜色"
}}</span>
</div>
<n-select
class="set"
v-model:value="backgroundImageShow"
:options="backgroundImageShowOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
显示歌词翻译
<span class="tip">是否在具有翻译歌词时显示</span>
</div>
<n-switch v-model:value="showTransl" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示歌词音译
<span class="tip">是否在具有音译歌词时显示</span>
</div>
<n-switch v-model:value="showRoma" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示前奏等待
<span class="tip">部分歌曲前奏可能存在显示错误</span>
</div>
<n-switch v-model:value="countDownShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示逐字歌词
<span class="tip">是否在歌曲具有逐字歌词时显示实验性功能</span>
</div>
<n-switch v-model:value="showYrc" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
智能暂停滚动
<span class="tip">鼠标移入歌词区域是否暂停滚动</span>
</div>
<n-switch v-model:value="lrcMousePause" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
歌词滚动位置
<span class="tip">歌词高亮时所处的位置</span>
</div>
<n-select
class="set"
v-model:value="lyricsBlock"
:options="lyricsBlockOptions"
/>
</n-card>
<n-card
class="set-item"
:content-style="{
flexDirection: 'column',
alignItems: 'flex-start',
}"
>
<div class="name">歌词文本大小</div>
<n-slider
v-model:value="lyricsFontSize"
:tooltip="false"
:max="4"
:min="3"
:step="0.01"
:marks="{
3: '最小',
3.6: '默认',
4: '最大',
}"
/>
<div :class="lyricsBlur ? 'more blur' : 'more'">
<div
v-for="n in 3"
:key="n"
:class="n === 2 ? 'lrc on' : 'lrc'"
:style="{
margin: n === 2 ? '12px 0' : null,
alignItems: lyricsPosition == 'center' ? 'center' : null,
transformOrigin:
lyricsPosition == 'center' ? 'center' : 'center left',
}"
>
<span :style="{ fontSize: lyricsFontSize + 'vh' }"
>这是一句歌词
</span>
<span :style="{ fontSize: lyricsFontSize - 0.4 + 'vh' }"
>This is a lyric
</span>
</div>
</div>
</n-card>
<n-card class="set-item">
<div class="name">默认歌词位置</div>
<n-select
class="set"
v-model:value="lyricsPosition"
:options="lyricsPositionOptions"
/>
</n-card>
<n-card class="set-item">
<div class="name">
歌词模糊
<span class="tip">未播放或已播放歌词模糊显示实验性功能</span>
</div>
<n-switch v-model:value="lyricsBlur" :round="false" />
</n-card>
<!-- <n-card class="set-item">
<div class="name">
显示音乐频谱
<span class="tip">可能会导致一些意想不到的后果实验性功能</span>
</div>
<n-switch
v-model:value="musicFrequency"
:round="false"
@click="changeMusicFrequency"
/>
</n-card> -->
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { settingStore } from "@/store";
const setting = settingStore();
const {
showTransl,
lyricsPosition,
playerStyle,
musicFrequency,
lyricsFontSize,
lyricsBlock,
lyricsBlur,
lrcMousePause,
showYrc,
showRoma,
backgroundImageShow,
countDownShow,
} = storeToRefs(setting);
// 歌词位置
const lyricsPositionOptions = [
{
label: "居左",
value: "left",
},
{
label: "居中",
value: "center",
},
];
// 歌词滚动位置
const lyricsBlockOptions = [
{
label: "靠近顶部",
value: "start",
},
{
label: "水平居中",
value: "center",
},
];
// 播放器样式
const playerStyleOptions = [
{
label: "封面模式",
value: "cover",
},
{
label: "唱片模式",
value: "record",
},
];
// 播放背景类型
const backgroundImageShowOptions = [
{
label: "封面主色",
value: "solid",
},
{
label: "封面模糊",
value: "blur",
},
];
// 音乐频谱提醒
// const changeMusicFrequency = () => {
// if (musicFrequency.value) {
// $dialog.warning({
// class: "s-dialog",
// title: "实验性功能",
// content: "确认开启音乐频谱?将在重启应用后生效",
// positiveText: "开启",
// negativeText: "取消",
// onMaskClick: () => {
// musicFrequency.value = false;
// },
// onPositiveClick: () => {
// musicFrequency.value = true;
// },
// onNegativeClick: () => {
// musicFrequency.value = false;
// },
// });
// }
// };
</script>

View File

@@ -102,17 +102,17 @@
import { getSimiPlayList, getMusicDetail } from "@/api/song";
import { useRouter } from "vue-router";
import { musicStore } from "@/store";
import { getLongTime } from "@/utils/timeTools.js";
import { getLongTime } from "@/utils/timeTools";
import {
PlayArrowRound,
MessageFilled,
VideocamRound,
PlaylistAddRound,
} from "@vicons/material";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber } from "@/utils/timeTools";
import AllArtists from "@/components/DataList/AllArtists.vue";
import CoverLists from "@/components/DataList/CoverLists.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import AddPlaylist from "@/components/DataModal/AddPlaylist.vue";
const router = useRouter();
const music = musicStore();
@@ -131,6 +131,9 @@ const getMusicDetailData = (id) => {
console.log(res);
if (res.songs[0]) {
musicDetail.value = res.songs[0];
$setSiteTitle(
res.songs[0].name + " - " + res.songs[0].ar[0].name + " - 单曲"
);
// 获取相似数据
getSimiData(id);
} else {
@@ -237,7 +240,7 @@ watch(
cursor: pointer;
transition: all 0.3s;
&:hover {
color: $mainColor;
color: var(--main-color);
}
}
}

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("403");
});
</script>
<style lang="scss" scoped>
@@ -24,4 +28,4 @@ const router = useRouter();
align-items: center;
flex-direction: column;
}
</style>
</style>

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("404");
});
</script>
<style lang="scss" scoped>
@@ -24,4 +28,4 @@ const router = useRouter();
align-items: center;
flex-direction: column;
}
</style>
</style>

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("500");
});
</script>
<style lang="scss" scoped>

View File

@@ -11,6 +11,7 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
const user = userStore();
onMounted(() => {
$setSiteTitle("音乐库 - 收藏的专辑");
if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading)
user.setUserAlbumLists();
});

View File

@@ -11,6 +11,7 @@ import ArtistLists from "@/components/DataList/ArtistLists.vue";
const user = userStore();
onMounted(() => {
$setSiteTitle("音乐库 - 收藏的歌手");
if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading)
user.setUserArtistLists();
});

View File

@@ -27,7 +27,7 @@
<template #trigger>
<n-progress
type="line"
color="#f55e55"
:color="setting.themeData.primaryColor"
class="progress"
:show-indicator="false"
:percentage="100 / (cloudSpace[1] / cloudSpace[0])"
@@ -83,12 +83,14 @@
<script setup>
import { getCloud, upCloudSong } from "@/api/user";
import { useRouter } from "vue-router";
import { getSongTime } from "@/utils/timeTools.js";
import { settingStore } from "@/store";
import { getSongTime } from "@/utils/timeTools";
import { BackupRound } from "@vicons/material";
import DataLists from "@/components/DataList/DataLists.vue";
import Pagination from "@/components/Pagination/index.vue";
const router = useRouter();
const setting = settingStore();
// 云盘数据
const cloudSpace = ref([]);
@@ -225,13 +227,14 @@ watch(
() => router.currentRoute.value,
(val) => {
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "cloud") {
if (val.name == "user-cloud") {
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
}
}
);
onMounted(() => {
$setSiteTitle("音乐库 - 音乐云盘");
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
});
</script>

View File

@@ -6,13 +6,15 @@
round
:src="
user.getUserData.avatarUrl
? user.getUserData.avatarUrl
? user.getUserData.avatarUrl.replace(/^http:/, 'https:')
: '/images/ico/user-filling.svg'
"
fallback-src="/images/ico/user-filling.svg"
/>
<n-text class="key">{{ user.getUserData.nickname }}</n-text>
<n-text class="tip">的音乐库</n-text>
<div class="text">
<n-text class="key">{{ user.getUserData.nickname }}</n-text>
<n-text class="tip">的音乐库</n-text>
</div>
</div>
<n-tabs
class="main-tab"
@@ -76,16 +78,33 @@ watch(
.avatar {
width: 80px;
height: 80px;
min-width: 80px;
margin-right: 16px;
box-shadow: 0 6px 8px -2px rgb(0 0 0 / 16%);
}
.key {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
.tip {
transform: translateY(8px);
.text {
display: flex;
align-items: center;
.key {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
.tip {
transform: translateY(8px);
}
@media (max-width: 620px) {
flex-direction: column;
align-items: flex-start;
.key {
font-size: 30px;
margin-right: 0;
}
.tip {
font-size: 18px;
transform: translateY(0);
}
}
}
}
.content {

View File

@@ -11,6 +11,7 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
const user = userStore();
onMounted(() => {
$setSiteTitle("音乐库 - 收藏的歌单");
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
user.setUserPlayLists();
});

View File

@@ -7,85 +7,31 @@
strong
secondary
round
@click="createModelShow = true"
@click="createPlaylistRef.openCreatePlaylist()"
>
<template #icon>
<n-icon :component="AddCircleRound" />
</template>
新建歌单
</n-button>
<n-modal
class="s-modal"
v-model:show="createModelShow"
preset="card"
title="新建歌单"
:bordered="false"
:on-after-leave="createClose"
>
<n-input
style="margin-bottom: 12px"
v-model:value="createName"
type="text"
placeholder="请输入新歌单标题"
/>
<n-checkbox v-model:checked="createPrivacy">
设置为隐私歌单
</n-checkbox>
<template #footer>
<n-space justify="end">
<n-button @click="createClose"> 取消 </n-button>
<n-button
type="primary"
@click="createPlaylistBtn(createName, createPrivacy)"
>
新建
</n-button>
</n-space>
</template>
</n-modal>
<!-- 新建歌单 -->
<CreatePlaylist ref="createPlaylistRef" />
</div>
<CoverLists :listData="user.getUserPlayLists.own" />
</div>
</template>
<script setup>
import { createPlaylist } from "@/api/playlist";
import { AddCircleRound } from "@vicons/material";
import { userStore } from "@/store";
import CoverLists from "@/components/DataList/CoverLists.vue";
import CreatePlaylist from "@/components/DataModal/CreatePlaylist.vue";
const user = userStore();
// 新建歌单数据
const createModelShow = ref(false);
const createPrivacy = ref(false);
const createName = ref(null);
// 新建歌单
const createPlaylistBtn = (name, privacy = false) => {
if (createName.value) {
createPlaylist(name, privacy ? "10" : null).then((res) => {
if (res.code === 200) {
createClose();
$message.success("歌单新建成功");
user.setUserPlayLists();
} else {
$message.error("歌单新建失败,请重试");
}
});
} else {
$message.info("请输入歌单名称");
}
};
// 取消新建歌单
const createClose = () => {
createName.value = null;
createPrivacy.value = false;
createModelShow.value = false;
};
const createPlaylistRef = ref(null);
onMounted(() => {
$setSiteTitle("音乐库 - 我的歌单");
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
user.setUserPlayLists();
});

View File

@@ -79,7 +79,7 @@ import { useRouter } from "vue-router";
import { musicStore } from "@/store";
import { getVideoDetail, getVideoUrl, getSimiVideo } from "@/api/video";
import { getComment } from "@/api/comment";
import { formatNumber, getSongTime } from "@/utils/timeTools.js";
import { formatNumber, getSongTime } from "@/utils/timeTools";
import {
OndemandVideoFilled,
ShareFilled,
@@ -142,6 +142,7 @@ const commentsCount = ref(0);
const getVideoData = (id) => {
getVideoDetail(id).then((res) => {
videoData.value = res.data;
$setSiteTitle(res.data.name + " - " + res.data.artists[0].name + " - 视频");
const requests = res.data.brs.map((v) => {
return getVideoUrl(id, v.br);
});
@@ -212,8 +213,6 @@ const pageNumberChange = (val) => {
};
onMounted(() => {
// 隐藏控制条
music.setPlayBarState(false);
// 初始化播放器
player.value = new Plyr(videoRef.value, playerOptions);
// 获取视频数据
@@ -223,6 +222,8 @@ onMounted(() => {
// 播放器事件
player.value.on("playing", () => {
console.log("视频开始播放");
// 隐藏控制条及暂停音乐
music.setPlayBarState(false);
music.setPlayState(false);
});
});
@@ -247,7 +248,7 @@ watch(
<style lang="scss" scoped>
.video {
--plyr-color-main: #f55e55;
--plyr-color-main: var(--main-color);
--plyr-control-radius: 8px;
display: flex;
@@ -296,11 +297,10 @@ watch(
}
}
&:hover {
color: $mainColor;
color: var(--main-color);
}
.n-icon {
margin-right: 6px;
transform: translateY(-1px);
}
}
@media (max-width: 578px) {

View File

@@ -5,6 +5,7 @@ import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import { VitePWA } from "vite-plugin-pwa";
import { createHtmlPlugin } from "vite-plugin-html";
// https://vitejs.dev/config/
export default ({ mode }) =>
@@ -27,6 +28,20 @@ export default ({ mode }) =>
Components({
resolvers: [NaiveUiResolver()],
}),
createHtmlPlugin({
minify: true,
template: "index.html",
inject: {
data: {
logo: loadEnv(mode, process.cwd()).VITE_SITE_LOGO,
title: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
author: loadEnv(mode, process.cwd()).VITE_SITE_ANTHOR,
keywords: loadEnv(mode, process.cwd()).VITE_SITE_KEYWORDS,
description: loadEnv(mode, process.cwd()).VITE_SITE_DES,
tongji: loadEnv(mode, process.cwd()).VITE_SITE_BAIDUTONGJI,
},
},
}),
// PWA
VitePWA({
registerType: "autoUpdate",
@@ -53,9 +68,9 @@ export default ({ mode }) =>
],
},
manifest: {
name: "SPlayer",
short_name: "SPlayer",
description: "一个简约的在线音乐播放器",
name: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
short_name: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
description: loadEnv(mode, process.cwd()).VITE_SITE_DES,
display: "standalone",
start_url: "/",
theme_color: "#fff",
@@ -83,13 +98,6 @@ export default ({ mode }) =>
},
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "src/style/index.scss";',
},
},
},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),