mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 11:29:26 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc84e11adf | ||
|
|
5425288e16 | ||
|
|
ae8f3696f3 | ||
|
|
6a102a1bff | ||
|
|
80aea0826b | ||
|
|
a65a7224ae | ||
|
|
ddd12364fe | ||
|
|
7592296124 | ||
|
|
0922735b2d | ||
|
|
144955e7c8 | ||
|
|
7495e7af2d | ||
|
|
43fe04b4fc | ||
|
|
db5aebbf89 | ||
|
|
535d0f7493 | ||
|
|
7415f591b3 | ||
|
|
e138d06e6f | ||
|
|
cd05376e18 | ||
|
|
45b374a0cb | ||
|
|
418da81738 | ||
|
|
dd66725d9c |
11
.env
11
.env
@@ -1,6 +1,11 @@
|
||||
# 全局 API 地址
|
||||
## 需部署 API,详见 https://github.com/Binaryify/NeteaseCloudMusicApi
|
||||
VITE_MUSIC_API = "https://api-music.imsyy.top/"
|
||||
|
||||
# 网易云解灰 API 地址
|
||||
## 需部署 API,详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C
|
||||
VITE_UNM_API = "https://api-unm.imsyy.top/"
|
||||
|
||||
# ICP 备案号
|
||||
## 若不需要,请设为空即可
|
||||
VITE_ICP = "豫ICP备2022018134号-1"
|
||||
@@ -8,8 +13,8 @@ VITE_ICP = "豫ICP备2022018134号-1"
|
||||
# 公告配置
|
||||
## 若无需公告,请将任意一项设为空即可
|
||||
## 公告标题
|
||||
VITE_ANN_TITLE = "即将完成"
|
||||
VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
VITE_ANN_CONTENT = "进行最后完善"
|
||||
## 公告时长(毫秒)
|
||||
VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
VITE_ANN_DURATION = 3000
|
||||
|
||||
18
.hintrc
Normal file
18
.hintrc
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"detect-css-reflows/composite": "off",
|
||||
"detect-css-reflows/layout": "off",
|
||||
"detect-css-reflows/paint": "off",
|
||||
"compat-api/css": [
|
||||
"default",
|
||||
{
|
||||
"ignore": [
|
||||
"backdrop-filter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
62
README.md
62
README.md
@@ -15,36 +15,32 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 账号
|
||||
- 扫码登录
|
||||
- 手机号登录(目前暂时无法使用)
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 管理
|
||||
- 下载歌曲(最高 Hi-Res)
|
||||
- 新建歌单
|
||||
- 歌单编辑
|
||||
- 收藏 / 取消收藏歌单
|
||||
- 收藏 / 取消收藏歌手
|
||||
- 推荐
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 音乐云盘
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 播放
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 实验性功能,需在设置中开启 )
|
||||
- 音乐渐入渐出
|
||||
- 其他
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- 移动端基础适配
|
||||
- 支持扫码登录
|
||||
- 支持手机号登录(目前暂时无法使用)
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
|
||||
- 由于酷我音源不支持 `https`,故网页端替换可能不全面
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 支持逐字歌词
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 实验性功能,需在设置中开启 )
|
||||
- 音乐渐入渐出
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- 移动端基础适配
|
||||
|
||||
#### 待办
|
||||
|
||||
- [ ] 主题换肤
|
||||
- [ ] 发表评论
|
||||
|
||||
## 😍 Screenshots
|
||||
@@ -83,16 +79,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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/images/logo/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
|
||||
<title>SPlayer</title>
|
||||
<meta name="keywords" content="SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器" />
|
||||
<meta name="description" content="一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"version": "1.1.2",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
|
||||
15
src/App.vue
15
src/App.vue
@@ -68,6 +68,20 @@ const spacePlayOrPause = (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更改页面标题
|
||||
const setSiteTitle = (val) => {
|
||||
const title = val
|
||||
? val === "SPlayer"
|
||||
? val
|
||||
: val + " - SPlayer"
|
||||
: user.siteTitle;
|
||||
user.setSiteTitle(title);
|
||||
sessionStorage.setItem("siteTitle", title);
|
||||
if (!music.getPlayState) {
|
||||
window.document.title = title;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新登录
|
||||
const toRefreshLogin = () => {
|
||||
const today = Date.now();
|
||||
@@ -129,6 +143,7 @@ onMounted(() => {
|
||||
window.$mainContent = mainContent.value;
|
||||
window.$cleanAll = cleanAll;
|
||||
window.$signIn = signIn;
|
||||
window.$setSiteTitle = setSiteTitle;
|
||||
|
||||
// 公告
|
||||
if (annShow) {
|
||||
|
||||
@@ -59,6 +59,7 @@ export const getPlayListDetail = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/detail",
|
||||
withCredentials: false,
|
||||
params: {
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
|
||||
@@ -38,6 +38,28 @@ export const getMusicUrl = (id, level = "exhigh") => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 网易云解灰
|
||||
* @param {number} id - 要替换播放链接的音乐ID
|
||||
*/
|
||||
export const getMusicNumUrl = async (id) => {
|
||||
const server =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "kuwo,qq,pyncmd,kugou"
|
||||
: "qq,pyncmd,kugou";
|
||||
const url = `${import.meta.env.VITE_UNM_API}match?id=${id}&server=${server}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error());
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定音乐的歌词
|
||||
* @param {number} id - 要获取歌词的音乐ID
|
||||
@@ -53,6 +75,21 @@ export const getMusicLyric = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定音乐的逐字歌词
|
||||
* @param {number} id - 要获取逐字歌词的音乐ID
|
||||
*/
|
||||
export const getMusicNewLyric = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
hiddenBar: true,
|
||||
url: "/lyric/new",
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定音乐的详情。
|
||||
* @param {string} ids - 要获取详情的音乐ID,多个ID用逗号分隔
|
||||
|
||||
@@ -1,67 +1,70 @@
|
||||
<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"
|
||||
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"
|
||||
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>
|
||||
@@ -77,7 +80,7 @@ const props = defineProps({
|
||||
// 评论 数据
|
||||
commentData: {
|
||||
type: Object,
|
||||
default: [],
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -105,6 +108,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;
|
||||
@@ -235,4 +247,10 @@ const toLikeComment = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -373,6 +373,7 @@ 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);
|
||||
@@ -385,6 +386,7 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
background-color: #00000030;
|
||||
font-size: 12px;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 6px;
|
||||
border-top-left-radius: 8px;
|
||||
|
||||
@@ -421,8 +421,8 @@ const openRightMenu = (e, data) => {
|
||||
icon: renderIcon(AddMusic),
|
||||
show:
|
||||
music.getPersonalFmMode || music.getPlaySongData.id == data.id
|
||||
? true
|
||||
: false,
|
||||
? false
|
||||
: true,
|
||||
props: {
|
||||
onClick: () => {
|
||||
music.addSongToNext(data);
|
||||
|
||||
@@ -115,6 +115,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 +127,7 @@ const props = defineProps({
|
||||
color: #fff;
|
||||
background-color: #00000030;
|
||||
font-size: 12px;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 4px 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
@@ -127,6 +127,7 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #00000040;
|
||||
-webkit-backdrop-filter: blur(80px);
|
||||
backdrop-filter: blur(80px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@@ -12,21 +12,43 @@
|
||||
"
|
||||
>
|
||||
<div class="gray" />
|
||||
<n-icon
|
||||
class="close"
|
||||
size="40"
|
||||
:component="KeyboardArrowDownFilled"
|
||||
@click="music.setBigPlayerState(false)"
|
||||
/>
|
||||
<n-icon
|
||||
class="screenfull"
|
||||
size="36"
|
||||
:component="screenfullIcon"
|
||||
@click="screenfullChange"
|
||||
/>
|
||||
<div class="icon-menu">
|
||||
<div class="menu-left">
|
||||
<div class="icon">
|
||||
<n-icon
|
||||
class="setting"
|
||||
size="30"
|
||||
:component="SettingsRound"
|
||||
@click="
|
||||
() => {
|
||||
music.setBigPlayerState(false);
|
||||
router.push('/setting/player');
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-right">
|
||||
<div class="icon">
|
||||
<n-icon
|
||||
class="screenfull"
|
||||
:component="screenfullIcon"
|
||||
@click="screenfullChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<n-icon
|
||||
class="close"
|
||||
:component="KeyboardArrowDownFilled"
|
||||
@click="music.setBigPlayerState(false)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="
|
||||
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
|
||||
music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4
|
||||
? 'all'
|
||||
: 'all noLrc'
|
||||
"
|
||||
@@ -50,7 +72,8 @@
|
||||
<div
|
||||
class="lrcShow"
|
||||
v-if="
|
||||
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
|
||||
music.getPlaySongLyric.lrc[0] &&
|
||||
music.getPlaySongLyric.lrc.length > 4
|
||||
"
|
||||
>
|
||||
<div class="data" v-show="setting.playerStyle === 'record'">
|
||||
@@ -82,72 +105,13 @@
|
||||
</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'"
|
||||
@@ -182,12 +146,14 @@ import {
|
||||
MessageFilled,
|
||||
FullscreenRound,
|
||||
FullscreenExitRound,
|
||||
SettingsRound,
|
||||
} from "@vicons/material";
|
||||
import { musicStore, settingStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import MusicFrequency from "@/utils/MusicFrequency.js";
|
||||
import PlayerRecord from "./PlayerRecord.vue";
|
||||
import PlayerCover from "./PlayerCover.vue";
|
||||
import RollingLyrics from "./RollingLyrics.vue";
|
||||
import screenfull from "screenfull";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -201,19 +167,10 @@ const menuShow = ref(false);
|
||||
const avBars = ref(null);
|
||||
const musicFrequency = ref(null);
|
||||
|
||||
// 歌词模糊数值
|
||||
const getFilter = (lrcIndex, index) => {
|
||||
if (lrcIndex >= index) {
|
||||
return lrcIndex - index;
|
||||
} else {
|
||||
return index - lrcIndex;
|
||||
}
|
||||
};
|
||||
|
||||
// 点击歌词跳转
|
||||
const jumpTime = (time) => {
|
||||
lrcMouseStatus.value = false;
|
||||
// 歌词文本点击事件
|
||||
const lrcTextClick = (time) => {
|
||||
if ($player) $player.currentTime = time;
|
||||
lrcMouseStatus.value = false;
|
||||
};
|
||||
|
||||
// 鼠标移出歌词区域
|
||||
@@ -253,13 +210,20 @@ 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 : 100);
|
||||
container.scrollTo({
|
||||
top: scrollDistance,
|
||||
behavior: "smooth",
|
||||
block: type,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -355,11 +319,63 @@ watch(
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #00000060;
|
||||
-webkit-backdrop-filter: blur(80px);
|
||||
backdrop-filter: blur(80px);
|
||||
z-index: -1;
|
||||
}
|
||||
.icon-menu {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
.menu-left,
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
opacity: 0.3;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #ffffff20;
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
.screenfull,
|
||||
.setting {
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-right {
|
||||
.icon {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
.close,
|
||||
.screenfull {
|
||||
.screenfull,
|
||||
.setting {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
@@ -385,6 +401,9 @@ watch(
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.setting {
|
||||
left: 24px;
|
||||
}*/
|
||||
.all {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -438,6 +457,7 @@ watch(
|
||||
height: 40px;
|
||||
border-radius: 25px;
|
||||
background-color: #ffffff20;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -495,100 +515,6 @@ 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;
|
||||
|
||||
83
src/components/Player/CountDown.vue
Normal file
83
src/components/Player/CountDown.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<Transition mode="out-in" appear>
|
||||
<div
|
||||
class="countdown"
|
||||
:style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }"
|
||||
v-if="remainingPoint <= 2 && totalDuration > 3"
|
||||
>
|
||||
<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) => {
|
||||
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) => {
|
||||
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>
|
||||
@@ -149,8 +149,6 @@ const router = useRouter();
|
||||
const music = musicStore();
|
||||
const user = userStore();
|
||||
|
||||
const canvas = ref(null);
|
||||
|
||||
// 歌曲进度条更新
|
||||
const songTimeSliderUpdate = (val) => {
|
||||
if ($player && music.getPlaySongTime && music.getPlaySongTime.duration)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
302
src/components/Player/RollingLyrics.vue
Normal file
302
src/components/Player/RollingLyrics.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<!-- 歌词滚动 -->
|
||||
<div
|
||||
v-if="music.getPlaySongLyric.lrc[0]"
|
||||
:class="[
|
||||
setting.playerStyle === 'cover' ? 'lrc-all cover' : 'lrc-all record',
|
||||
setting.lyricsBlock === 'center' ? 'center' : 'top',
|
||||
]"
|
||||
:style="
|
||||
setting.lyricsPosition === 'center'
|
||||
? { textAlign: 'center', paddingRight: '0' }
|
||||
: null
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="placeholder"
|
||||
:id="
|
||||
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? 'lrc-1' : 'yrc-1'
|
||||
"
|
||||
>
|
||||
<CountDown :style="{ fontSize: setting.lyricsFontSize + 'vh' }" />
|
||||
</div>
|
||||
<template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc">
|
||||
<div
|
||||
v-for="(item, index) in music.getPlaySongLyric.lrc"
|
||||
:class="music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'"
|
||||
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
|
||||
:key="item"
|
||||
:id="'lrc' + index"
|
||||
@click="lrcTextClick(item.time)"
|
||||
>
|
||||
<div
|
||||
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
|
||||
:style="{
|
||||
transformOrigin:
|
||||
setting.lyricsPosition === 'center' ? 'center' : null,
|
||||
filter: setting.lyricsBlur
|
||||
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
|
||||
: null,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="lyric"
|
||||
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
|
||||
>
|
||||
{{ item.content }}
|
||||
</span>
|
||||
<span
|
||||
v-show="
|
||||
music.getPlaySongLyric.hasTran &&
|
||||
setting.getShowTransl &&
|
||||
item.tran
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
|
||||
class="lyric-fy"
|
||||
>
|
||||
{{ item.tran }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(item, index) in music.getPlaySongLyric.yrc"
|
||||
:class="music.getPlaySongLyricIndex == index ? 'yrc on' : 'yrc'"
|
||||
:key="item"
|
||||
:id="'yrc' + index"
|
||||
@click="lrcTextClick(item.time)"
|
||||
>
|
||||
<div
|
||||
:class="setting.lyricsBlur ? 'yrc-text blur' : 'yrc-text'"
|
||||
:style="{
|
||||
transformOrigin:
|
||||
setting.lyricsPosition === 'center' ? 'center' : null,
|
||||
filter: setting.lyricsBlur
|
||||
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
|
||||
: null,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="lyric"
|
||||
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
|
||||
>
|
||||
<span
|
||||
v-for="(v, i) in item.content"
|
||||
:key="i"
|
||||
:style="{
|
||||
'--dur': v.duration - 0.15 + 's',
|
||||
}"
|
||||
:class="
|
||||
music.getPlaySongLyricIndex == index &&
|
||||
music.getPlaySongTime.currentTime + 0.15 >= v.time
|
||||
? 'text fill'
|
||||
: 'text'
|
||||
"
|
||||
>
|
||||
{{ v.content }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-show="
|
||||
music.getPlaySongLyric.hasTran &&
|
||||
setting.getShowTransl &&
|
||||
item.tran
|
||||
"
|
||||
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
|
||||
class="lyric-fy"
|
||||
>
|
||||
{{ item.tran }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="placeholder"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { musicStore, settingStore } from "@/store";
|
||||
import CountDown from "./CountDown.vue";
|
||||
|
||||
const music = musicStore();
|
||||
const setting = settingStore();
|
||||
|
||||
// 发送方法
|
||||
const emit = defineEmits(["lrcTextClick"]);
|
||||
|
||||
// 歌词模糊数值
|
||||
const getFilter = (lrcIndex, index) => {
|
||||
if (lrcIndex >= index) {
|
||||
return lrcIndex - index;
|
||||
} else {
|
||||
return index - lrcIndex;
|
||||
}
|
||||
};
|
||||
|
||||
// 歌词文本点击
|
||||
const lrcTextClick = (time) => {
|
||||
emit("lrcTextClick", time);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lrc-all {
|
||||
margin-right: 20%;
|
||||
scrollbar-width: none;
|
||||
// max-width: 460px;
|
||||
max-width: 52vh;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&.cover {
|
||||
height: 80vh;
|
||||
}
|
||||
&.record {
|
||||
height: 60vh;
|
||||
}
|
||||
&.center {
|
||||
mask: linear-gradient(
|
||||
180deg,
|
||||
hsla(0, 0%, 100%, 0) 0,
|
||||
hsla(0, 0%, 100%, 0.6) 15%,
|
||||
#fff 25%,
|
||||
#fff 75%,
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
-webkit-mask: linear-gradient(
|
||||
180deg,
|
||||
hsla(0, 0%, 100%, 0) 0,
|
||||
hsla(0, 0%, 100%, 0.6) 15%,
|
||||
#fff 25%,
|
||||
#fff 75%,
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
}
|
||||
&.top {
|
||||
mask: linear-gradient(
|
||||
180deg,
|
||||
hsla(0, 0%, 100%, 0) 0,
|
||||
hsla(0, 0%, 100%, 0.6) 5%,
|
||||
#fff 10%,
|
||||
#fff 75%,
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
-webkit-mask: linear-gradient(
|
||||
180deg,
|
||||
hsla(0, 0%, 100%, 0) 0,
|
||||
hsla(0, 0%, 100%, 0.6) 5%,
|
||||
#fff 10%,
|
||||
#fff 75%,
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
.placeholder {
|
||||
&:nth-of-type(1) {
|
||||
height: 16%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.lrc-text {
|
||||
&.blur {
|
||||
filter: blur(0) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
height: 70vh;
|
||||
margin-right: 0;
|
||||
}
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
&:nth-of-type(1) {
|
||||
height: 50%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 0 0 0.8vh 3vh;
|
||||
}
|
||||
&:nth-last-of-type(1) {
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
.lrc,
|
||||
.yrc {
|
||||
opacity: 0.2;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 0.8vh;
|
||||
padding: 1.8vh 4vh 1.8vh 3vh;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
transform-origin: left bottom;
|
||||
cursor: pointer;
|
||||
.lrc-text,
|
||||
.yrc-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.35s ease-in-out;
|
||||
transform: scale(0.95);
|
||||
transform-origin: left bottom;
|
||||
.lyric {
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
.text {
|
||||
transition: all var(--dur);
|
||||
color: #ffffff66;
|
||||
&.fill {
|
||||
text-shadow: 0px 0px 30px #ffffff40;
|
||||
background-image: linear-gradient(to right, #fff 0%, #fff 0%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: #ffffff66;
|
||||
animation: toRight var(--dur) forwards linear;
|
||||
}
|
||||
@keyframes toRight {
|
||||
to {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lyric-fy {
|
||||
margin-top: 4px;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
&.on {
|
||||
opacity: 1;
|
||||
.lrc-text {
|
||||
transform: scale(1.05);
|
||||
.lyric {
|
||||
text-shadow: 0px 0px 30px #ffffff40;
|
||||
}
|
||||
}
|
||||
.yrc-text {
|
||||
transform: scale(1.05);
|
||||
.lyric {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (min-width: 768px) {
|
||||
background-color: #ffffff20;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
.yrc {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -46,24 +46,45 @@
|
||||
<template v-if="setting.bottomLyricShow">
|
||||
<Transition mode="out-in">
|
||||
<AllArtists
|
||||
v-if="!music.getPlayState || !music.getPlaySongLyric[0]"
|
||||
v-if="!music.getPlayState || !music.getPlaySongLyric.lrc[0]"
|
||||
class="text-hidden"
|
||||
:artistsData="music.getPlaySongData.artist"
|
||||
/>
|
||||
<n-text
|
||||
v-else-if="
|
||||
music.getPlaySongLyric[0] &&
|
||||
music.getPlaySongLyricIndex != -1
|
||||
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[music.getPlaySongLyricIndex].lyric
|
||||
? music.getPlaySongLyric[music.getPlaySongLyricIndex]
|
||||
.lyric
|
||||
: '暂无歌词'
|
||||
music.getPlaySongLyric.lrc[music.getPlaySongLyricIndex]
|
||||
.content
|
||||
"
|
||||
/>
|
||||
<AllArtists
|
||||
v-else
|
||||
class="text-hidden"
|
||||
:artistsData="music.getPlaySongData.artist"
|
||||
/>
|
||||
</Transition>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -199,7 +220,12 @@
|
||||
</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,
|
||||
@@ -234,33 +260,77 @@ const music = musicStore();
|
||||
const { persistData } = storeToRefs(music);
|
||||
const addPlayListRef = ref(null);
|
||||
|
||||
// 重试次数
|
||||
const testNumber = ref(0);
|
||||
|
||||
// UNM 是否存在
|
||||
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
|
||||
|
||||
// 音频标签
|
||||
const player = ref(null);
|
||||
|
||||
// 获取歌曲播放数据
|
||||
const getPlaySongData = (id, level = setting.songLevel) => {
|
||||
checkMusicCanUse(id).then((res) => {
|
||||
if (res.success) {
|
||||
console.log("音乐可用");
|
||||
// 获取音乐地址
|
||||
getMusicUrl(id, level).then((res) => {
|
||||
if (res.data[0].fee == 1) {
|
||||
$message.warning("当前歌曲为 VIP 专享,仅可试听");
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
// 免费或无版权
|
||||
else {
|
||||
checkMusicCanUse(id).then((res) => {
|
||||
if (res.success) {
|
||||
console.log("当前歌曲可用");
|
||||
if (!pc && (fee === 1 || fee === 4))
|
||||
$message.info("当前歌曲为 VIP 专享,仅可试听");
|
||||
// 获取音乐地址
|
||||
getMusicUrl(id, level).then((res) => {
|
||||
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
|
||||
});
|
||||
} else {
|
||||
if (useUnmServerHas && setting.useUnmServer) {
|
||||
getMusicNumUrlData(id);
|
||||
} else {
|
||||
$message.warning("当前歌曲播放失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 获取歌词
|
||||
getMusicNewLyric(id).then((res) => {
|
||||
music.setPlaySongLyric(res);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("当前歌曲所有音源匹配失败:" + err);
|
||||
if (music.getPlayState && $player) {
|
||||
$message.warning("当前歌曲所有音源匹配失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 网易云解灰
|
||||
const getMusicNumUrlData = (id) => {
|
||||
getMusicNumUrl(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
console.log("替换成功:" + res.data.url.replace(/^http:/, ""));
|
||||
music.setPlaySongLink(res.data.url.replace(/^http:/, ""));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("解灰失败:" + err);
|
||||
$message.warning("当前歌曲解灰失败,跳至下一首");
|
||||
music.setPlaySongIndex("next");
|
||||
});
|
||||
};
|
||||
|
||||
// 歌曲进度更新事件
|
||||
@@ -281,6 +351,7 @@ const songCanplay = () => {
|
||||
|
||||
// 歌曲开始播放
|
||||
const songPlay = () => {
|
||||
testNumber.value = 0;
|
||||
if (!music.getPlaySongData) {
|
||||
$message.error("音乐数据获取失败");
|
||||
return false;
|
||||
@@ -332,7 +403,14 @@ const songPlay = () => {
|
||||
// 写入播放历史
|
||||
music.setPlayHistory(music.getPlaySongData);
|
||||
// 更改页面标题
|
||||
window.document.title = music.getPlaySongData.name + " - SPlayer";
|
||||
// $setSiteTitle(
|
||||
// music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name
|
||||
// );
|
||||
window.document.title =
|
||||
music.getPlaySongData.name +
|
||||
" - " +
|
||||
music.getPlaySongData.artist[0].name +
|
||||
" - SPlayer";
|
||||
};
|
||||
|
||||
// 音乐渐入渐出
|
||||
@@ -385,7 +463,8 @@ const songPause = () => {
|
||||
console.log("音乐暂停");
|
||||
if (!$player.ended) music.setPlayState(false);
|
||||
// 更改页面标题
|
||||
window.document.title = "SPlayer";
|
||||
// window.document.title = "SPlayer";
|
||||
$setSiteTitle();
|
||||
};
|
||||
|
||||
// 歌曲进度条更新
|
||||
@@ -397,7 +476,13 @@ const songTimeSliderUpdate = (val) => {
|
||||
// 歌曲播放失败事件
|
||||
const songError = () => {
|
||||
console.error("歌曲播放失败");
|
||||
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData.id);
|
||||
$message.error("歌曲播放失败");
|
||||
if (testNumber.value < 4) {
|
||||
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData);
|
||||
testNumber.value++;
|
||||
} else {
|
||||
$message.error("歌曲重试次数过多,请刷新后重试");
|
||||
}
|
||||
if (music.getPlayState) songInOrOut("play");
|
||||
};
|
||||
|
||||
@@ -414,7 +499,7 @@ const volumeMute = () => {
|
||||
onMounted(() => {
|
||||
// 获取音乐数据
|
||||
if (music.getPlaylists[0] && music.getPlaySongData)
|
||||
getPlaySongData(music.getPlaySongData.id);
|
||||
getPlaySongData(music.getPlaySongData);
|
||||
// 挂载播放器
|
||||
window.$player = player.value;
|
||||
// 恢复上次播放进度
|
||||
@@ -430,7 +515,7 @@ watch(
|
||||
() => music.getPlaySongData,
|
||||
(val) => {
|
||||
debounce(() => {
|
||||
getPlaySongData(val.id);
|
||||
getPlaySongData(val);
|
||||
}, 500);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -125,7 +125,25 @@ const routes = [
|
||||
meta: {
|
||||
title: "全局设置",
|
||||
},
|
||||
component: () => import("@/views/Setting/SettingView.vue"),
|
||||
component: () => import("@/views/Setting/index.vue"),
|
||||
redirect: "/setting/main",
|
||||
children: [
|
||||
{
|
||||
path: "main",
|
||||
name: "setting-main",
|
||||
component: () => import("@/views/Setting/main.vue"),
|
||||
},
|
||||
{
|
||||
path: "player",
|
||||
name: "setting-player",
|
||||
component: () => import("@/views/Setting/player.vue"),
|
||||
},
|
||||
{
|
||||
path: "other",
|
||||
name: "setting-other",
|
||||
component: () => import("@/views/Setting/other.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 登录页
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { defineStore } from "pinia";
|
||||
import useSettingDataStore from "./settingData";
|
||||
import useMusicDataStore from "./musicData";
|
||||
import useUserDataStore from "./userData";
|
||||
|
||||
@@ -3,10 +3,10 @@ import { getSongTime, getSongPlayingTime } from "@/utils/timeTools.js";
|
||||
import { getPersonalFm, setFmTrash } from "@/api/home";
|
||||
import { getLikelist, setLikeSong } from "@/api/user";
|
||||
import { getPlayListCatlist } from "@/api/playlist";
|
||||
import { userStore } 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 parseLyric from "@/utils/parseLyric";
|
||||
|
||||
const useMusicDataStore = defineStore("musicData", {
|
||||
state: () => {
|
||||
@@ -21,12 +21,16 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
playState: false,
|
||||
// 当前歌曲播放链接
|
||||
playSongLink: null,
|
||||
// 当前歌曲歌词
|
||||
playSongLyric: [],
|
||||
// 当前歌曲歌词数据
|
||||
playSongLyric: {
|
||||
lrc: [],
|
||||
yrc: [],
|
||||
hasTran: false,
|
||||
hasTran: false,
|
||||
hasYrc: false,
|
||||
},
|
||||
// 当前歌曲歌词播放索引
|
||||
playSongLyricIndex: 0,
|
||||
// 当前歌曲是否拥有翻译
|
||||
playSongTransl: false,
|
||||
// 每日推荐
|
||||
dailySongsData: [],
|
||||
// 歌单分类
|
||||
@@ -80,10 +84,6 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
getPersonalFmData(state) {
|
||||
return state.persistData.personalFmData;
|
||||
},
|
||||
// 获取是否拥有翻译
|
||||
getPlaySongTransl(state) {
|
||||
return state.playSongTransl;
|
||||
},
|
||||
// 获取每日推荐
|
||||
getDailySongs(state) {
|
||||
return state.dailySongsData;
|
||||
@@ -308,25 +308,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 = [];
|
||||
}
|
||||
},
|
||||
@@ -348,16 +337,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() {
|
||||
@@ -419,7 +403,7 @@ 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;
|
||||
|
||||
@@ -20,6 +20,8 @@ const useSettingDataStore = defineStore("settingData", {
|
||||
playerStyle: "cover",
|
||||
// 底栏歌词显示
|
||||
bottomLyricShow: true,
|
||||
// 是否显示逐字歌词
|
||||
showYrc: true,
|
||||
// 是否显示歌词翻译
|
||||
showTransl: true,
|
||||
// 歌曲音质
|
||||
@@ -27,15 +29,17 @@ const useSettingDataStore = defineStore("settingData", {
|
||||
// 歌词位置
|
||||
lyricsPosition: "left",
|
||||
// 歌词滚动位置
|
||||
lyricsBlock: "center",
|
||||
lyricsBlock: "start",
|
||||
// 歌词大小
|
||||
lyricsFontSize: 2.8,
|
||||
lyricsFontSize: 3.6,
|
||||
// 歌词模糊
|
||||
lyricsBlur: false,
|
||||
// 音乐频谱
|
||||
musicFrequency: false,
|
||||
// 鼠标移入歌词区域暂停滚动
|
||||
lrcMousePause: true,
|
||||
// 是否使用网易云解灰
|
||||
useUnmServer: true,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -12,6 +12,8 @@ import { formatNumber, getLongTime } from "@/utils/timeTools.js";
|
||||
const useUserDataStore = defineStore("userData", {
|
||||
state: () => {
|
||||
return {
|
||||
// 站点标题
|
||||
siteTitle: "SPlayer",
|
||||
// 用户登录状态
|
||||
userLogin: false,
|
||||
// 用户 cookie
|
||||
@@ -68,6 +70,10 @@ const useUserDataStore = defineStore("userData", {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 更改站点标题
|
||||
setSiteTitle(value) {
|
||||
this.siteTitle = value;
|
||||
},
|
||||
// 更改 cookie
|
||||
setCookie(value) {
|
||||
window.localStorage.setItem("cookie", value);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: "HarmonyOS_Regular", sans-serif !important;
|
||||
}
|
||||
|
||||
@@ -49,14 +49,36 @@ body,
|
||||
.n-card-header__main {
|
||||
// font-weight: bold;
|
||||
font-size: 18px;
|
||||
// position: relative;
|
||||
// padding-left: 6px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// &::before {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// left: -4px;
|
||||
// width: 4px;
|
||||
// height: 80%;
|
||||
// border-radius: 4px;
|
||||
// background-color: $mainColor;
|
||||
// }
|
||||
}
|
||||
}
|
||||
.n-card__content {
|
||||
padding-right: 28px;
|
||||
}
|
||||
.n-scrollbar {
|
||||
max-height: 60vh;
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.n-modal-body-wrapper {
|
||||
.n-modal-mask {
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
}
|
||||
|
||||
// Nscrollbar
|
||||
.n-scrollbar {
|
||||
|
||||
193
src/utils/parseLyric.js
Normal file
193
src/utils/parseLyric.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 将接口数据解析出对应数据
|
||||
* @param {string} data 接口数据
|
||||
* @returns {Array} 对应数据
|
||||
*/
|
||||
const parseLyric = (data) => {
|
||||
// 初始化数据
|
||||
const { lrc, tlyric, romalrc, yrc, ytlrc } = data;
|
||||
const lyrics = lrc ? lrc.lyric : null;
|
||||
const otherLyrics = {
|
||||
tran: tlyric ? tlyric.lyric : null,
|
||||
roma: romalrc ? romalrc.lyric : null,
|
||||
yrc: yrc ? yrc.lyric : null,
|
||||
ytlrc: ytlrc ? ytlrc.lyric : null,
|
||||
};
|
||||
// 初始化输出结果
|
||||
let result = {
|
||||
lrc: [], // 歌词数组 {time:时间,content:歌词}
|
||||
yrc: [], // 逐字歌词数据
|
||||
// 是否具有翻译
|
||||
hasTran: tlyric ? (tlyric.lyric ? true : false) : false,
|
||||
// 是否具有音译
|
||||
hasRoma: romalrc ? (romalrc.lyric ? true : false) : false,
|
||||
// 是否具有逐字歌词
|
||||
hasYrc: yrc ? (yrc.lyric ? true : false) : false,
|
||||
};
|
||||
// 普通歌词数据
|
||||
let lrcData = Lrcsplit(lyrics);
|
||||
// 翻译歌词数据
|
||||
let tranLrcData = null;
|
||||
// 循环遍历 otherLyrics 参数对象
|
||||
for (let i in otherLyrics) {
|
||||
const element = otherLyrics[i];
|
||||
if (element !== null) {
|
||||
// 若存在逐字歌词
|
||||
if (i == "yrc" && otherLyrics[i] != null) {
|
||||
result[i] = parseYrc(otherLyrics[i]);
|
||||
continue;
|
||||
}
|
||||
// 若存在翻译
|
||||
if (i == "ytlrc" && element != null) {
|
||||
tranLrcData = Lrcsplit(element);
|
||||
for (let num in tranLrcData) {
|
||||
// 翻译文本对齐
|
||||
let objNum = result["yrc"].findIndex(
|
||||
(o) => o.time == tranLrcData[num].time
|
||||
);
|
||||
if (objNum != -1)
|
||||
result["yrc"][objNum]["tran"] = tranLrcData[num].content;
|
||||
}
|
||||
}
|
||||
// 若存在其他翻译
|
||||
tranLrcData = Lrcsplit(element);
|
||||
if (tranLrcData[0]) {
|
||||
console.log(`歌曲存在 ${i} 翻译`, tranLrcData);
|
||||
for (let num in tranLrcData) {
|
||||
// 翻译文本对齐
|
||||
let objNum = lrcData.findIndex(
|
||||
(o) => o.time == tranLrcData[num].time
|
||||
);
|
||||
if (objNum != -1) lrcData[objNum][i] = tranLrcData[num].content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将歌词按时间排序
|
||||
result.lrc = lrcData.sort((a, b) => {
|
||||
return a.t - b.t;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将歌词字符串解析为歌词对象数组
|
||||
* @param {string} lrc 歌词字符串
|
||||
* @returns {Array} 歌词对象数组
|
||||
*/
|
||||
const Lrcsplit = (lrc) => {
|
||||
const lyrics = lrc.split("\n");
|
||||
const lrcData = [];
|
||||
lyrics.forEach((lyric) => {
|
||||
lyric = lyric.replace(/(^\s*)|(\s*$)/g, "");
|
||||
const time = lyric.substring(lyric.indexOf("[") + 1, lyric.indexOf("]"));
|
||||
const timeArr = time.split(":");
|
||||
if (isNaN(parseInt(timeArr[0]))) {
|
||||
for (let i in lyrics) {
|
||||
if (i != "lrc" && i == timeArr[0].toLowerCase()) {
|
||||
lyrics[i] = timeArr[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const lyricArr = lyric.match(/\[(\d+:.+?)\]/g);
|
||||
let start = 0;
|
||||
for (let k in lyricArr) {
|
||||
start += lyricArr[k].length;
|
||||
}
|
||||
const content = lyric.substring(start);
|
||||
if (content == "") {
|
||||
return false;
|
||||
}
|
||||
lyricArr.forEach((t) => {
|
||||
let time = t.substring(1, t.length - 1);
|
||||
let second = time.split(":");
|
||||
if (
|
||||
(parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3) == 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lrcData.push({
|
||||
time: (parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3),
|
||||
content: content,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
return lrcData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 逐字歌词解析
|
||||
* @param {string} lyrics 歌词字符串
|
||||
* @returns {Array} 歌词对象数组
|
||||
*/
|
||||
const parseYrc = (lyrics) => {
|
||||
// 若无内容,则返回空数组
|
||||
if (lyrics == undefined) {
|
||||
return [];
|
||||
}
|
||||
let lines = lyrics.split("\n");
|
||||
let parsedLyrics = [];
|
||||
// 解析每一句
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// 创建一个空对象,用于存放当前句的信息
|
||||
let parsedLine = {
|
||||
time: undefined, // 开始时间
|
||||
endTime: undefined, // 结束时间
|
||||
content: undefined, // 歌词内容
|
||||
};
|
||||
// 分离出时间信息,并转换为秒
|
||||
let timeInfo = line
|
||||
.substring(line.indexOf("[") + 1, line.indexOf("]"))
|
||||
.split(",");
|
||||
// 将开始时间转换为秒
|
||||
parsedLine.time = Number(timeInfo[0]) / 1000;
|
||||
// 将结束时间转换为秒
|
||||
parsedLine.endTime = Number(timeInfo[1]) / 1000;
|
||||
// 若时间信息不合法,将跳过该句
|
||||
if (isNaN(parsedLine.time) || isNaN(parsedLine.endTime)) {
|
||||
continue;
|
||||
}
|
||||
// 寻找歌词内容
|
||||
const lyricArr = line.match(/\[[1-9]\d*,[1-9]\d*]/g);
|
||||
if (!lyricArr) {
|
||||
continue;
|
||||
}
|
||||
// 去除时间信息,获取歌词内容
|
||||
let contentArray = [];
|
||||
// 分离成单个字或词,并解析时间信息
|
||||
let splitContent = line.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g);
|
||||
for (let j = 0; j < splitContent.length; j++) {
|
||||
const splitc = splitContent[j];
|
||||
if (splitc == "") {
|
||||
continue;
|
||||
}
|
||||
// 创建一个对象,用于存放当前字或词的信息,并添加到当前句的歌词内容中
|
||||
let contentObj = {
|
||||
time: undefined, // 开始时间
|
||||
duration: undefined, // 持续时间
|
||||
content: "", // 字或词的文本内容
|
||||
};
|
||||
// 提取时间和文本信息,并转换为秒
|
||||
let time = splitc.match(/\([1-9]\d*,[1-9]\d*,\d*\)/);
|
||||
if (!time) {
|
||||
continue;
|
||||
}
|
||||
let timeArray = time[0].slice(1, -1).split(",");
|
||||
// 将开始时间转换为秒
|
||||
contentObj.time = Number(timeArray[0]) / 1000;
|
||||
// 将持续时间转换为秒
|
||||
contentObj.duration = Number(timeArray[1]) / 1000;
|
||||
// 获取字或词的文本内容
|
||||
contentObj.content = splitc.slice(time[0].length);
|
||||
contentArray.push(contentObj);
|
||||
}
|
||||
parsedLine.content = contentArray;
|
||||
parsedLyrics.push(parsedLine);
|
||||
}
|
||||
|
||||
return parsedLyrics;
|
||||
};
|
||||
|
||||
export default parseLyric;
|
||||
@@ -7,7 +7,7 @@
|
||||
:src="
|
||||
albumDetail.picUrl
|
||||
? albumDetail.picUrl.replace(/^http:/, 'https:') +
|
||||
'?param=500y500'
|
||||
'?param=1024y1024'
|
||||
: null
|
||||
"
|
||||
fallback-src="/images/pic/default.png"
|
||||
@@ -55,7 +55,12 @@
|
||||
<div class="right">
|
||||
<div class="meta">
|
||||
<span class="name">{{ albumDetail.name }}</span>
|
||||
<span class="creator">{{ albumDetail.artist.name }}</span>
|
||||
<span
|
||||
class="creator"
|
||||
@click="router.push(`/artist/songs?id=${albumDetail.artist.id}`)"
|
||||
>
|
||||
{{ albumDetail.artist.name }}
|
||||
</span>
|
||||
<div class="time">
|
||||
<div class="createTime">
|
||||
<span class="num">发行时间:</span>
|
||||
@@ -109,6 +114,7 @@ const getAlbumData = (id) => {
|
||||
console.log(res);
|
||||
// 专辑信息
|
||||
albumDetail.value = res.album;
|
||||
$setSiteTitle(res.album.name + " - 专辑");
|
||||
// 专辑歌曲
|
||||
if (res.songs) {
|
||||
albumData.value = [];
|
||||
@@ -227,6 +233,12 @@ watch(
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -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" });
|
||||
})
|
||||
|
||||
@@ -134,6 +134,7 @@ const pageNumberChange = (val) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("全部评论");
|
||||
// 获取评论数据
|
||||
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ const getDailySongsData = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("每日推荐");
|
||||
if (music.getDailySongs.length === 0) getDailySongsData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -200,6 +200,7 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("发现 - 歌手");
|
||||
// 获取歌手数据
|
||||
getArtistListData(
|
||||
artistType[artistTypeNamesChoose.value],
|
||||
|
||||
@@ -313,6 +313,7 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("发现 - 歌单");
|
||||
// 获取歌单分类
|
||||
if (!music.catList.sub || !music.highqualityCatList[0])
|
||||
music.setCatList(true);
|
||||
|
||||
@@ -97,6 +97,7 @@ const getToplistData = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("发现 - 排行榜");
|
||||
getToplistData();
|
||||
});
|
||||
</script>
|
||||
@@ -157,6 +158,7 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
background-color: #00000030;
|
||||
font-size: 12px;
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px 0 8px 0;
|
||||
|
||||
@@ -32,6 +32,10 @@ import DataLists from "@/components/DataList/DataLists.vue";
|
||||
|
||||
const music = musicStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("播放历史");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -28,6 +28,10 @@ import PaDailySongs from "@/components/Personalized/PaDailySongs.vue";
|
||||
import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
|
||||
|
||||
const setting = settingStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("SPlayer");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { userStore } from "@/store";
|
||||
import { userStore, musicStore } from "@/store";
|
||||
import {
|
||||
getLoginState,
|
||||
getQrKey,
|
||||
@@ -110,6 +110,7 @@ import QrcodeVue from "qrcode.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const user = userStore();
|
||||
const music = musicStore();
|
||||
const { numberRule, mobileRule } = formRules();
|
||||
|
||||
// 二维码数据
|
||||
@@ -266,7 +267,7 @@ const phoneLogin = (e) => {
|
||||
phoneFormData._value.captcha
|
||||
).then((res) => {
|
||||
console.log(res);
|
||||
// 网易接口抽风,等好了再写
|
||||
// 暂时不支持,等支持了再写
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -288,11 +289,16 @@ const tabChange = (val) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("登录");
|
||||
// 隐藏控制条
|
||||
music.setPlayBarState(false);
|
||||
// 获取二维码登录 key
|
||||
getQrKeyData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 恢复控制条
|
||||
music.setPlayBarState(true);
|
||||
// 清除定时器
|
||||
clearInterval(qrCheckInterval.value);
|
||||
clearInterval(captchaTimeOut.value);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:src="
|
||||
playListDetail.coverImgUrl
|
||||
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
|
||||
'?param=500y500'
|
||||
'?param=1024y1024'
|
||||
: null
|
||||
"
|
||||
fallback-src="/images/pic/default.png"
|
||||
@@ -27,7 +27,7 @@
|
||||
block
|
||||
strong
|
||||
secondary
|
||||
v-if="playListDetail?.description.length > 70"
|
||||
v-if="playListDetail?.description?.length > 70"
|
||||
@click="playListDescShow = true"
|
||||
>
|
||||
全部简介
|
||||
@@ -155,6 +155,7 @@ const getPlayListDetailData = (id) => {
|
||||
totalCount.value = res.playlist.trackCount;
|
||||
// 歌单信息
|
||||
playListDetail.value = res.playlist;
|
||||
$setSiteTitle(res.playlist.name + " - 歌单");
|
||||
} else {
|
||||
$message.error("获取歌单信息失败");
|
||||
}
|
||||
@@ -328,6 +329,12 @@ watch(
|
||||
margin-top: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $mainColor;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -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(() => {
|
||||
$setSiteTitle(searchKeywords.value + "的搜索结果");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
136
src/views/Setting/index.vue
Normal file
136
src/views/Setting/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<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("全局设置");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting {
|
||||
.title {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
:deep(.set-item) {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
.n-card__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.name {
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: 20px;
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.set {
|
||||
width: 200px;
|
||||
@media (max-width: 768px) {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--n-border-color);
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
box-sizing: border-box;
|
||||
&.blur {
|
||||
.lrc {
|
||||
filter: blur(2px);
|
||||
&.on {
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lrc {
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: scale(0.95);
|
||||
transition: all 0.3s;
|
||||
&.on {
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 路由跳转动画
|
||||
.move-enter-active,
|
||||
.move-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.move-enter-from,
|
||||
.move-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
</style>
|
||||
125
src/views/Setting/main.vue
Normal file
125
src/views/Setting/main.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="set-main">
|
||||
<n-card class="set-item">
|
||||
<div class="name">明暗模式</div>
|
||||
<n-select class="set" v-model:value="theme" :options="darkOptions" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">明暗模式跟随系统</div>
|
||||
<n-switch v-model:value="themeAuto" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
每日签到
|
||||
<span class="tip">是否自动进行每日签到</span>
|
||||
</div>
|
||||
<n-switch v-model:value="autoSignIn" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">显示轮播图</div>
|
||||
<n-switch v-model:value="bannerShow" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
列表点击方式
|
||||
<span class="tip">移动端该设置项无效,单击同时生效</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="listClickMode"
|
||||
:options="listClickModeOptions"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">显示搜索历史</div>
|
||||
<n-switch v-model:value="searchHistory" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
显示底栏歌词
|
||||
<span class="tip">是否在播放时显示歌词</span>
|
||||
</div>
|
||||
<n-switch v-model:value="bottomLyricShow" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
歌曲音质
|
||||
<span class="tip">无损音质及以上需要您为黑胶会员</span>
|
||||
</div>
|
||||
<n-select
|
||||
class="set"
|
||||
v-model:value="songLevel"
|
||||
:options="songLevelOptions"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { settingStore, userStore } from "@/store";
|
||||
|
||||
const setting = settingStore();
|
||||
const user = userStore();
|
||||
const {
|
||||
theme,
|
||||
themeAuto,
|
||||
listClickMode,
|
||||
bottomLyricShow,
|
||||
songLevel,
|
||||
bannerShow,
|
||||
autoSignIn,
|
||||
searchHistory,
|
||||
} = storeToRefs(setting);
|
||||
|
||||
// 深浅模式
|
||||
const darkOptions = [
|
||||
{
|
||||
label: "浅色模式",
|
||||
value: "light",
|
||||
},
|
||||
{
|
||||
label: "深色模式",
|
||||
value: "dark",
|
||||
},
|
||||
];
|
||||
|
||||
// 列表模式
|
||||
const listClickModeOptions = [
|
||||
{
|
||||
label: "双击播放",
|
||||
value: "dblclick",
|
||||
},
|
||||
{
|
||||
label: "单击播放",
|
||||
value: "click",
|
||||
},
|
||||
];
|
||||
|
||||
// 歌曲音质
|
||||
const songLevelOptions = [
|
||||
{
|
||||
label: "标准",
|
||||
value: "standard",
|
||||
},
|
||||
{
|
||||
label: "较高",
|
||||
value: "higher",
|
||||
},
|
||||
,
|
||||
{
|
||||
label: "极高",
|
||||
value: "exhigh",
|
||||
},
|
||||
{
|
||||
label: "无损",
|
||||
value: "lossless",
|
||||
disabled: user.userData?.vipType ? false : true,
|
||||
},
|
||||
{
|
||||
label: "Hi-Res",
|
||||
value: "hires",
|
||||
disabled: user.userData?.vipType ? false : true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
33
src/views/Setting/other.vue
Normal file
33
src/views/Setting/other.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="set-other">
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
系统重置
|
||||
<span class="tip">若程序显示异常或出现问题时可尝试此操作</span>
|
||||
</div>
|
||||
<n-button strong secondary type="error" @click="resetApp">
|
||||
重置
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 系统重置
|
||||
const resetApp = () => {
|
||||
const cleanAll = () => {
|
||||
$message ? $message.success("重置成功") : alert("重置成功");
|
||||
localStorage.clear();
|
||||
window.location.href = "/";
|
||||
};
|
||||
$dialog.warning({
|
||||
title: "系统重置",
|
||||
content: "确认重置为默认状态?你的登录状态以及自定义设置都将丢失!",
|
||||
positiveText: "重置",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
$cleanAll ? $cleanAll() : cleanAll();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
206
src/views/Setting/player.vue
Normal file
206
src/views/Setting/player.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="set-player">
|
||||
<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">
|
||||
<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">显示歌词翻译</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="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,
|
||||
useUnmServer,
|
||||
} = storeToRefs(setting);
|
||||
|
||||
// UNM 开关显示
|
||||
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;
|
||||
|
||||
// 歌词位置
|
||||
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;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("403");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -24,4 +28,4 @@ const router = useRouter();
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("404");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -24,4 +28,4 @@ const router = useRouter();
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("500");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -11,6 +11,7 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
|
||||
const user = userStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 收藏的专辑");
|
||||
if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading)
|
||||
user.setUserAlbumLists();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import ArtistLists from "@/components/DataList/ArtistLists.vue";
|
||||
const user = userStore();
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 收藏的歌手");
|
||||
if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading)
|
||||
user.setUserArtistLists();
|
||||
});
|
||||
|
||||
@@ -225,13 +225,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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -86,6 +86,7 @@ const createClose = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
$setSiteTitle("音乐库 - 我的歌单");
|
||||
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
|
||||
user.setUserPlayLists();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user