Compare commits

..

6 Commits

Author SHA1 Message Date
imsyy
ecdb3b75ba fix: 修复歌单为空时的提示 2023-04-20 17:00:44 +08:00
imsyy
415cf3b3c9 feat: 播放列表重构 & fix: 修复一些样式问题 2023-04-20 16:48:28 +08:00
imsyy
5425288e16 feat: 设置页面重构 2023-04-19 17:59:12 +08:00
imsyy
ae8f3696f3 fix: 修复一些样式问题 2023-04-19 16:39:57 +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
38 changed files with 1218 additions and 728 deletions

11
.env
View File

@@ -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
View 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"
]
}
]
}
}

View File

@@ -15,36 +15,32 @@
## 🎉 功能
- 账号
- 扫码登录
- 手机号登录(目前暂时无法使用)
- 支持扫码登录
- 支持手机号登录(目前暂时无法使用)
- 自动进行每日签到及云贝签到
- 管理
- 下载歌曲(最高 Hi-Res
- 新建歌单
- 歌单编辑
- 收藏 / 取消收藏歌单
- 收藏 / 取消收藏歌手
- 推荐
- 支持 [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

View File

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

View File

@@ -1,7 +1,6 @@
{
"name": "splayer",
"version": "1.1.0",
"private": true,
"version": "1.1.3",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",

View File

@@ -10,7 +10,12 @@
:native-scrollbar="false"
embedded
>
<main ref="mainContent" class="main">
<main
ref="mainContent"
class="main"
id="main"
:class="[music.showPlayList ? 'playlist' : null]"
>
<n-back-top
:bottom="music.getPlaylists[0] && music.showPlayBar ? 100 : 40"
style="transition: all 0.3s"
@@ -223,6 +228,14 @@ onMounted(() => {
.main {
max-width: 1400px;
margin: 0 auto;
div:nth-of-type(2) {
transition: all 0.3s;
}
&.playlist {
div:nth-of-type(2) {
transform: scale(0.98);
}
}
}
}

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

View File

@@ -14,7 +14,12 @@
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="commentData.user.vipRights.musicPackage.iconUrl"
:src="
commentData.user.vipRights.musicPackage.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="redVipAnnualCount"
title="网易音乐人"
/>
@@ -25,7 +30,12 @@
>
<img
v-if="commentData.user.vipRights.associator"
:src="commentData.user.vipRights.associator.iconUrl"
:src="
commentData.user.vipRights.associator.iconUrl.replace(
/^http:/,
'https:'
)
"
alt="associator"
title="黑胶会员"
/>

View File

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

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

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

View File

@@ -0,0 +1,239 @@
<template>
<n-drawer
class="playlist-drawer"
v-model:show="playListShow"
:z-index="2000"
: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 title="播放列表" :native-scrollbar="false" closable>
<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">
<div v-if="index !== music.persistData.playSongIndex" class="num">
{{ index + 1 }}
</div>
<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 AllArtists from "@/components/DataList/AllArtists.vue";
const music = musicStore();
// 播放列表显隐
const playListShow = ref(false);
// 改变播放索引
const changeIndex = (index) => {
music.persistData.playSongIndex = index;
music.setPlayState(true);
};
// 监听播放列表显隐
const timeOut = ref(null);
watch(
() => music.showPlayList,
(val) => {
playListShow.value = val;
nextTick(() => {
if (val && music.getPlaylists[0]) {
const el = document.getElementById(
`playlist${music.persistData.playSongIndex}`
);
if (el) {
timeOut.value = setTimeout(() => {
el.scrollIntoView({
behavior: "smooth",
block: "center",
});
}, 300);
}
} 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;
}
.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;
}
:deep(span) {
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;
.bar {
display: flex;
justify-content: space-evenly;
align-items: flex-end;
width: 20px;
height: 20px;
.line {
width: 3px;
height: 16px;
background-color: $mainColor;
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: $mainColor;
background-color: var(--n-border-color);
}
}
}
}
}
</style>

View File

@@ -18,11 +18,7 @@
>
<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>
@@ -44,7 +40,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' }"
@@ -109,7 +106,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",
}),

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

View File

@@ -12,18 +12,40 @@
"
>
<div class="gray" />
<div class="icon-menu">
<div class="menu-left">
<div class="icon">
<n-icon
class="close"
size="40"
:component="KeyboardArrowDownFilled"
@click="music.setBigPlayerState(false)"
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"
size="36"
: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.lrc[0] && music.getPlaySongLyric.lrc.length > 4
@@ -124,6 +146,7 @@ import {
MessageFilled,
FullscreenRound,
FullscreenExitRound,
SettingsRound,
} from "@vicons/material";
import { musicStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
@@ -158,6 +181,7 @@ const lrcAllLeave = () => {
};
// 全屏切换
const timeOut = ref(null);
const screenfullIcon = shallowRef(FullscreenRound);
const screenfullChange = () => {
if (screenfull.isEnabled) {
@@ -166,7 +190,7 @@ const screenfullChange = () => {
? FullscreenRound
: FullscreenExitRound;
// 延迟一段时间执行列表滚动
setTimeout(() => {
timeOut.value = setTimeout(() => {
lrcMouseStatus.value = false;
lyricsScroll(music.getPlaySongLyricIndex);
}, 500);
@@ -225,6 +249,10 @@ onMounted(() => {
});
});
onBeforeUnmount(() => {
clearTimeout(timeOut.value);
});
// 监听页面是否打开
watch(
() => music.showBigPlayer,
@@ -233,6 +261,7 @@ watch(
console.log("开启播放器", music.getPlaySongLyricIndex);
nextTick(() => {
lyricsScroll(music.getPlaySongLyricIndex);
music.showPlayList = false;
});
}
}
@@ -296,11 +325,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;
@@ -326,6 +407,9 @@ watch(
display: none;
}
}
.setting {
left: 24px;
}*/
.all {
width: 100%;
height: 100%;
@@ -379,6 +463,7 @@ watch(
height: 40px;
border-radius: 25px;
background-color: #ffffff20;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
display: flex;
align-items: center;

View File

@@ -1,9 +1,13 @@
<template>
<Transition mode="out-in">
<Transition mode="out-in" appear>
<div
class="countdown"
:style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }"
v-if="remainingPoint <= 2"
v-if="
remainingPoint <= 2 &&
totalDuration > 3 &&
music.getPlaySongLyric.lrc[0]
"
>
<span class="point" :class="remainingPoint > 2 ? 'hidden' : null">●</span>
<span class="point" :class="remainingPoint > 1 ? 'hidden' : null">●</span>
@@ -30,21 +34,25 @@ const totalDuration = ref(
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>

View File

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

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

@@ -85,11 +85,11 @@
v-for="(v, i) in item.content"
:key="i"
:style="{
'--dur': v.duration - 0.1 + 's',
'--dur': v.duration - 0.15 + 's',
}"
:class="
music.getPlaySongLyricIndex == index &&
music.getPlaySongTime.currentTime + 0.2 > v.time
music.getPlaySongTime.currentTime + 0.15 >= v.time
? 'text fill'
: 'text'
"
@@ -196,8 +196,9 @@ const lrcTextClick = (time) => {
hsla(0, 0%, 100%, 0)
);
.placeholder {
width: 100%;
height: 16% !important;
&:nth-of-type(1) {
height: 16%;
}
}
}
&:hover {
@@ -213,16 +214,19 @@ const lrcTextClick = (time) => {
}
.placeholder {
width: 100%;
height: 50%;
&: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.4;
opacity: 0.2;
transition: all 0.3s;
margin-bottom: 0.8vh;
padding: 1.8vh 4vh 1.8vh 3vh;
@@ -241,7 +245,7 @@ const lrcTextClick = (time) => {
font-weight: bold;
transition: all 0.3s;
.text {
transition: all 0.3s;
transition: all var(--dur);
color: #ffffff66;
&.fill {
text-shadow: 0px 0px 30px #ffffff40;
@@ -284,6 +288,7 @@ const lrcTextClick = (time) => {
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
opacity: 1;
}
}
&:active {

View File

@@ -168,10 +168,8 @@
@click="music.setPlaySongMode()"
/>
</div>
<div class="playlist">
<PlayList />
<div :class="music.showPlayList ? 'playlist open' : 'playlist'">
<n-icon
class="next"
size="30"
:component="PlaylistPlayRound"
@click.stop="music.showPlayList = !music.showPlayList"
@@ -215,12 +213,21 @@
:src="music.getPlaySongLink"
></audio>
</n-card>
<!-- 播放列表 -->
<PlayListDrawer ref="PlayListDrawerRef" />
<!-- 添加到歌单 -->
<AddPlaylist ref="addPlayListRef" />
<!-- 播放器 -->
<BigPlayer />
</template>
<script setup>
import { checkMusicCanUse, getMusicUrl, getMusicNewLyric } from "@/api/song";
import {
checkMusicCanUse,
getMusicUrl,
getMusicNumUrl,
getMusicNewLyric,
} from "@/api/song";
import { NIcon } from "naive-ui";
import {
KeyboardArrowUpFilled,
@@ -244,8 +251,8 @@ import { storeToRefs } from "pinia";
import { musicStore, settingStore } from "@/store";
import { useRouter } from "vue-router";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import PlayListDrawer from "@/components/DataModel/PlayListDrawer.vue";
import AllArtists from "@/components/DataList/AllArtists.vue";
import PlayList from "@/components/DataList/PlayList.vue";
import BigPlayer from "./BigPlayer.vue";
import debounce from "@/utils/debounce";
@@ -254,33 +261,78 @@ const setting = settingStore();
const music = musicStore();
const { persistData } = storeToRefs(music);
const addPlayListRef = ref(null);
const PlayListDrawerRef = ref(null);
// 重试次数
const testNumber = ref(0);
// UNM 是否存在
const useUnmServerHas = import.meta.env.VITE_UNM_API ? true : false;
// 音频标签
const player = ref(null);
// 获取歌曲播放数据
const getPlaySongData = (id, level = setting.songLevel) => {
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("音乐可用");
console.log("当前歌曲可用");
if (!pc && (fee === 1 || fee === 4))
$message.info("当前歌曲为 VIP 专享,仅可试听");
// 获取音乐地址
getMusicUrl(id, level).then((res) => {
if (res.data[0].fee == 1) {
$message.warning("当前歌曲为 VIP 专享,仅可试听");
}
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);
});
} else {
console.log("无法播放");
} catch (err) {
console.log("当前歌曲所有音源匹配失败:" + err);
if (music.getPlayState && $player) {
$message.error("当前歌曲无法播放,已跳至下一首");
$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");
});
};
@@ -302,6 +354,7 @@ const songCanplay = () => {
// 歌曲开始播放
const songPlay = () => {
testNumber.value = 0;
if (!music.getPlaySongData) {
$message.error("音乐数据获取失败");
return false;
@@ -426,8 +479,13 @@ const songTimeSliderUpdate = (val) => {
// 歌曲播放失败事件
const songError = () => {
console.error("歌曲播放失败");
$message.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");
};
@@ -444,7 +502,7 @@ const volumeMute = () => {
onMounted(() => {
// 获取音乐数据
if (music.getPlaylists[0] && music.getPlaySongData)
getPlaySongData(music.getPlaySongData.id);
getPlaySongData(music.getPlaySongData);
// 挂载播放器
window.$player = player.value;
// 恢复上次播放进度
@@ -460,7 +518,7 @@ watch(
() => music.getPlaySongData,
(val) => {
debounce(() => {
getPlaySongData(val.id);
getPlaySongData(val);
}, 500);
}
);
@@ -507,7 +565,7 @@ watch(
bottom: -90px;
left: 0;
transition: all 0.3s;
z-index: 2;
z-index: 2004;
&.show {
bottom: 0;
}
@@ -551,23 +609,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;
@@ -739,6 +780,12 @@ watch(
display: flex;
align-items: center;
justify-content: center;
&.open {
.n-icon {
background-color: $mainColor;
color: var(--n-color-embedded);
}
}
}
.volume {
display: flex;
@@ -755,6 +802,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

@@ -328,6 +328,14 @@ watch(
}
}
);
// 监听播放列表显隐
watch(
() => music.showPlayList,
(val) => {
if (val) inputActive.value = false;
}
);
</script>
<style lang="scss" scoped>
@@ -339,11 +347,17 @@ watch(
.input {
width: 200px;
transition: all 0.3s;
@media (max-width: 450px) {
width: 40px;
}
&.focus {
width: 280px;
:deep(input) {
color: $mainColor;
}
@media (max-width: 450px) {
width: 60vw;
}
}
}
.list {
@@ -353,10 +367,34 @@ watch(
border-radius: 8px;
width: 280px;
z-index: 3;
@media (max-width: 450px) {
padding-top: 12px;
position: fixed;
width: 100%;
top: 58px;
right: 0;
left: 0;
border-radius: 0 0 8px 8px;
&::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;
@media (max-width: 450px) {
max-height: calc(100vh - 130px);
min-height: calc(100vh - 130px);
}
.n-scrollbar-rail {
width: 0;
width: 4px;
}
.n-scrollbar-content {
padding: 12px;

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

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

View File

@@ -38,6 +38,8 @@ const useSettingDataStore = defineStore("settingData", {
musicFrequency: false,
// 鼠标移入歌词区域暂停滚动
lrcMousePause: true,
// 是否使用网易云解灰
useUnmServer: true,
};
},
getters: {

View File

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

View File

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

View File

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

@@ -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);
// 网易接口抽风,等好了再写
// 暂时不支持,等支持了再写
});
}
});
@@ -289,11 +290,15 @@ const tabChange = (val) => {
onMounted(() => {
$setSiteTitle("登录");
// 隐藏控制条
music.setPlayBarState(false);
// 获取二维码登录 key
getQrKeyData();
});
onBeforeUnmount(() => {
// 恢复控制条
music.setPlayBarState(true);
// 清除定时器
clearInterval(qrCheckInterval.value);
clearInterval(captchaTimeOut.value);

View File

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

View File

@@ -27,7 +27,7 @@
block
strong
secondary
v-if="playListDetail?.description.length > 70"
v-if="playListDetail?.description?.length > 70"
@click="playListDescShow = true"
>
全部简介
@@ -329,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;

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];
}

View File

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

@@ -1,411 +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="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">播放器样式</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="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-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,
showYrc,
} = 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();
},
});
};
onMounted(() => {
$setSiteTitle("全局设置");
});
</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>

125
src/views/Setting/main.vue Normal file
View 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>

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

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

View File

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