Compare commits

...

16 Commits

Author SHA1 Message Date
底层用户
d48c8b1aa4 Merge pull request #81 from imsyy/dev
feat: 支持手机号登录 #67
2023-10-25 16:34:15 +08:00
imsyy
9fa5935929 feat: 更新说明 2023-09-13 16:00:25 +08:00
imsyy
ab0fb19585 feat: 支持手机号登录 #67 2023-08-21 17:27:08 +08:00
底层用户
67e69e0513 Merge pull request #66 from imsyy/dev
feat: 更新说明
2023-08-14 10:03:56 +08:00
imsyy
540b0c5855 feat: 更新说明 2023-08-08 10:03:19 +08:00
底层用户
52f15a044f Merge pull request #65 from imsyy/dev
feat: 新增一项设置 #58
2023-08-07 09:06:53 +08:00
imsyy
b5ec3aaee3 feat: 底栏歌词切换时动画 2023-08-01 15:53:54 +08:00
imsyy
91d66801a9 fix: 单曲循环无法暂停 2023-07-19 11:24:15 +08:00
imsyy
73470ef643 feat: 新增一项设置 #58 2023-07-19 10:57:42 +08:00
底层用户
6563cbf3ab Merge pull request #62 from imsyy/dev
feat: 新增自定义主题
2023-07-17 09:39:21 +08:00
imsyy
b5f193c731 fix: 私人 FM 加载动画 2023-07-17 09:36:39 +08:00
imsyy
f13eaf838f feat: 新增自定义主题 & fix: 修复提示触发位置 #60 2023-07-15 11:20:22 +08:00
imsyy
7f8dbbaaf8 style: 样式微调 2023-07-03 11:06:07 +08:00
底层用户
0a6f071bbd Merge pull request #54 from imsyy/dev
fix: 部分无信息歌曲导致异常
2023-06-16 09:58:31 +08:00
imsyy
846691f789 fix: 部分无信息歌曲导致异常 2023-06-14 17:57:29 +08:00
imsyy
17bb9f13b8 fix: 解决 Vercel 刷新 404 2023-06-14 11:44:55 +08:00
26 changed files with 800 additions and 694 deletions

13
.env
View File

@@ -1,10 +1,12 @@
# 全局 API 地址
## 需部署 API详见 https://github.com/Binaryify/NeteaseCloudMusicApi
VITE_MUSIC_API = "https://api-music.imsyy.top/"
# VITE_MUSIC_API = "https://api-music.imsyy.top/"
VITE_MUSIC_API = "http://localhost:3000/"
# 网易云解灰 API 地址(可选功能)
## 需部署 API详见 https://github.com/imsyy/UNM-Server#%E8%BF%90%E8%A1%8C
VITE_UNM_API = "https://api-unm.imsyy.top/"
# VITE_UNM_API = "http://localhost:5678/"
# 站点标题
VITE_SITE_TITLE = "SPlayer"
@@ -15,9 +17,6 @@ VITE_SITE_URL = "imsyy.top"
VITE_SITE_LOGO = "/images/logo/favicon.svg"
VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
# 百度统计(若不需要,请设为空即可)
VITE_SITE_BAIDUTONGJI = "c6579e9a33cbc5260fc90231678556ec"
# ICP 备案号
## 若不需要,请设为空即可
VITE_ICP = "豫ICP备2022018134号-1"
@@ -25,8 +24,8 @@ VITE_ICP = "豫ICP备2022018134号-1"
# 公告配置
## 若无需公告,请将任意一项设为空即可
## 公告标题
VITE_ANN_TITLE = ""
VITE_ANN_TITLE = "温馨提醒"
## 公告内容
VITE_ANN_CONTENT = ""
VITE_ANN_CONTENT = "由于演示站访问量随时可能超出,请自行部署后查看效果"
## 公告时长(毫秒)不可超过 999999
VITE_ANN_DURATION = 3000
VITE_ANN_DURATION = 8000

View File

@@ -8,6 +8,11 @@
## 说明
> **当前项目正在重构中,当前版本进入维护模式,仅在遇到重大问题时会进行修复**
> - 支持客户端与网页端
> - 支持现有版本所有功能
> - 新增支持播放与管理本地歌曲
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 `SCSS` 开发
- 目前主要以 `Web` 端为主,可能暂时不会考虑使用 `Electron` 构建客户端
- 仅对移动端做了基础适配,**不保证功能全部可用**
@@ -20,7 +25,7 @@
## 🎉 功能
- 支持扫码登录
- 支持手机号登录(上游接口暂时无法使用)
- 支持手机号登录
- 自动进行每日签到及云贝签到
- 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server),自动替换变灰歌曲
- 由于酷我音源不支持 `https`,故网页端替换可能不全面

View File

@@ -1,79 +1,64 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="<%- logo %>" />
<link rel="apple-touch-icon" href="<%- appleLogo %>" />
<link rel="bookmark" href="<%- appleLogo %>" />
<link
rel="apple-touch-icon-precomposed"
sizes="200x200"
href="<%- appleLogo %>"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> -->
<title><%- title %></title>
<meta name="apple-mobile-web-app-title" content="<%- title %>" />
<meta name="author" content="<%- author %>" />
<meta name="keywords" content="<%- keywords %>" />
<meta name="description" content="<%- description %>" />
<meta name="theme-color" content="#ffffff" />
<!-- HarmonyOS Sans -->
<link
rel="stylesheet"
href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css"
/>
<!-- IE Out -->
<script>
if (
/*@cc_on!@*/ false ||
(!!window.MSInputMethodContext && !!document.documentMode)
)
window.location.href =
"https://support.dmeng.net/upgrade-your-browser.html?referrer=" +
encodeURIComponent(window.location.href);
</script>
<% if (tongji) { %>
<!-- 百度统计 -->
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?<%- tongji %>";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<% } %>
<style>
noscript {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 16px);
font-family: "HarmonyOS_Regular", sans-serif;
}
noscript .title {
font-size: 30px;
font-weight: bold;
margin-top: 50px;
}
noscript .tip {
opacity: 0.6;
margin: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<noscript>
<img src="<%- logo %>" alt="logo" />
<p class="title"><%- title %></p>
<p class="tip">请开启 JavaScript</p>
</noscript>
<script type="module" src="/src/main.js"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="%VITE_SITE_LOGO%" />
<link rel="apple-touch-icon" href="%VITE_SITE_APPLE_LOGO%" />
<link rel="bookmark" href="%VITE_SITE_APPLE_LOGO%" />
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%VITE_SITE_APPLE_LOGO%" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_SITE_TITLE%</title>
<meta name="apple-mobile-web-app-title" content="%VITE_SITE_TITLE%" />
<meta name="author" content="%VITE_SITE_ANTHOR%" />
<meta name="keywords" content="%VITE_SITE_KEYWORDS%" />
<meta name="description" content="%VITE_SITE_DES%" />
<meta name="theme-color" content="#ffffff" />
<!-- HarmonyOS Sans -->
<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" />
<!-- IE Out -->
<script>
if (
/*@cc_on!@*/
false ||
(!!window.MSInputMethodContext && !!document.documentMode)
)
window.location.href =
"https://support.dmeng.net/upgrade-your-browser.html?referrer=" +
encodeURIComponent(window.location.href);
</script>
<style>
noscript {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 16px);
font-family: "HarmonyOS_Regular", sans-serif;
}
noscript .title {
font-size: 30px;
font-weight: bold;
margin-top: 50px;
}
noscript .tip {
opacity: 0.6;
margin: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<noscript>
<img src="%VITE_SITE_LOGO%" alt="logo" />
<p class="title">%VITE_SITE_TITLE%</p>
<p class="tip">请开启 JavaScript</p>
</noscript>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "splayer",
"version": "1.1.7",
"version": "1.1.9",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",
@@ -24,7 +24,6 @@
"screenfull": "^6.0.2",
"swiper": "^9.3.2",
"throttle-debounce": "^5.0.0",
"vite-plugin-html": "^3.2.0",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
@@ -38,7 +37,8 @@
"naive-ui": "^2.34.4",
"unplugin-auto-import": "^0.12.0",
"unplugin-vue-components": "^0.22.11",
"vite": "^4.3.8",
"vite-plugin-pwa": "^0.15.0"
"vite": "^4.4.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.16.4"
}
}

638
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@ export const getMusicNewLyric = (id) => {
export const getMusicDetail = (ids) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/song/detail",
params: {
ids,
@@ -142,6 +143,7 @@ export const getSimiSong = (id) => {
export const getSongDownload = (id, br = 999000) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/song/download/url",
params: {
id,

View File

@@ -2,10 +2,11 @@
<Transition mode="out-in">
<div class="datalists" id="datalists" v-if="listData[0]">
<n-card
v-for="item in listData"
v-for="(item, index) in listData"
:key="item"
:id="'song' + index"
:class="
music.getPlaySongData && music.getPlaySongData.id == item.id
music.getPlaySongData && music.getPlaySongData?.id == item?.id
? 'songs play'
: 'songs'
"
@@ -30,28 +31,28 @@
:src="item.album.picUrl.replace(/^http:/, 'https:') + '?param=60y60'"
fallback-src="/images/pic/default.png"
/>
<div class="num" v-else-if="item.num">
<n-text :depth="2" v-html="item.num" />
<div class="num" v-else-if="item?.num">
<n-text :depth="2" v-html="item?.num" />
</div>
<div class="name">
<div class="title">
<n-text
class="text-hidden"
depth="2"
v-html="item.name"
@click.stop="jumpLink(item.id, 1)"
v-html="item?.name"
@click.stop="jumpLink(item?.id, 1)"
/>
<n-tag
v-if="item.fee == 1 || item.fee == 4"
v-if="item?.fee == 1 || item?.fee == 4"
class="vip"
round
:bordered="false"
size="small"
>
{{ item.fee == 1 ? "VIP" : "EP" }}
{{ item?.fee == 1 ? "VIP" : "EP" }}
</n-tag>
<n-tag
v-if="item.pc"
v-if="item?.pc"
class="cloud"
round
type="info"
@@ -61,7 +62,7 @@
{{ $t("general.name.cloud") }}
</n-tag>
<n-tag
v-if="item.mv"
v-if="item?.mv"
class="mv"
round
type="warning"
@@ -74,19 +75,19 @@
</div>
<div class="meta">
<AllArtists
v-if="item.artist"
v-if="item?.artist"
class="text-hidden"
:artistsData="item.artist"
:artistsData="item?.artist"
/>
<n-text
class="alia text-hidden"
depth="3"
v-if="item.alia && item.alia[0]"
v-if="item?.alia[0]"
v-html="item.alia[0]"
/>
</div>
</div>
<div class="album" v-if="!hideAlbum && item.album">
<div class="album" v-if="!hideAlbum && item?.album">
<n-text
v-html="item.album.name"
@click.stop="jumpLink(item.album.id, 10)"
@@ -97,13 +98,13 @@
class="like"
size="20"
@click.stop="
music.getSongIsLike(item.id)
? music.changeLikeList(item.id, false)
: music.changeLikeList(item.id, true)
music.getSongIsLike(item?.id)
? music.changeLikeList(item?.id, false)
: music.changeLikeList(item?.id, true)
"
>
<Like
:theme="music.getSongIsLike(item.id) ? 'filled' : 'outline'"
:theme="music.getSongIsLike(item?.id) ? 'filled' : 'outline'"
/>
</n-icon>
<n-icon
@@ -269,6 +270,27 @@
{{ $t("general.name.album") }}: {{ drawerData.album.name }}
</n-text>
</div>
<div
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@click="
() => {
router.push({
path: '/search/songs',
query: {
keywords: drawerData.name,
page: 1,
},
});
drawerShow = false;
}
"
>
<n-icon size="20">
<Search theme="filled" />
</n-icon>
<n-text>{{ $t("menu.search") }}</n-text>
</div>
<div
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@@ -596,7 +618,7 @@ const openDrawer = (data) => {
// 播放并添加
const playSong = (data, song) => {
console.log(data, song);
if (music.getPersonalFmMode) {
if (music.getPersonalFmMode && typeof $player !== "undefined") {
soundStop($player);
music.setPersonalFmMode(false);
}

View File

@@ -19,6 +19,19 @@
:placeholder="$t('other.asIdDes')"
:show-button="false"
/>
<n-popover trigger="hover" :keep-alive-on-hover="false">
<template #trigger>
<n-button
style="margin-left: 12px"
@click="toSearch(cloudMatchBeforeData.name)"
>
<template #icon>
<n-icon :component="Search" />
</template>
</n-button>
</template>
{{ $t("menu.search") }}
</n-popover>
<n-button
style="margin-left: 12px"
:disabled="!cloudMatchValue.asid"
@@ -61,6 +74,7 @@
import { setCloudMatch } from "@/api/user";
import { userStore } from "@/store";
import { useI18n } from "vue-i18n";
import { Search } from "@icon-park/vue-next";
import SmallSongData from "@/components/DataList/SmallSongData.vue";
const { t } = useI18n();
@@ -102,6 +116,11 @@ const setCloudMatchBtn = (data) => {
}
};
// 同名搜索
const toSearch = (name) => {
window.open(`/search/songs?keywords=${name}&page=1`);
};
// 开启歌曲纠正
const openCloudMatch = (data) => {
cloudMatchValue.value.sid = data.id;

View File

@@ -114,7 +114,7 @@ const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{ style: { transform: "translateX(2px) translateY(1px)" } },
{
default: () => icon,
}
@@ -257,7 +257,7 @@ const changeDropdownOptions = () => {
label: () => {
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{ style: { transform: "translateX(2px) translateY(1px)" } },
{
default: () =>
setting.getSiteTheme == "light"
@@ -270,7 +270,7 @@ const changeDropdownOptions = () => {
icon: () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{ style: { transform: "translateX(2px) translateY(1px)" } },
{
default: () =>
setting.getSiteTheme == "light" ? h(Moon) : h(SunOne),
@@ -292,7 +292,7 @@ const changeDropdownOptions = () => {
label: () => {
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{ style: { transform: "translateX(2px) translateY(1px)" } },
{
default: () =>
user.userLogin ? t("nav.avatar.logout") : t("nav.avatar.login"),
@@ -303,7 +303,7 @@ const changeDropdownOptions = () => {
icon: () => {
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{ style: { transform: "translateX(2px) translateY(1px)" } },
{
default: () => (user.userLogin ? h(Logout) : h(Login)),
}

View File

@@ -1,71 +1,74 @@
<template>
<div
class="papersonalfm"
v-if="music.getPersonalFmData?.id"
:style="`background-image: url(${music.getPersonalFmData.album.picUrl.replace(
/^http:/,
'https:'
)}?param=300y300)`"
>
<div class="gray" />
<img
class="pic"
:src="
music.getPersonalFmData.album.picUrl.replace(/^http:/, 'https:') +
'?param=300y300'
"
alt="pic"
/>
<div class="data">
<div class="info">
<span
class="name text-hidden"
@click="router.push(`/song?id=${music.getPersonalFmData.id}`)"
>
{{ music.getPersonalFmData.name }}
</span>
<AllArtists
class="text-hidden"
:artistsData="music.getPersonalFmData.artist"
/>
</div>
<div class="controls">
<n-icon
class="state"
size="46"
:component="
music.getPersonalFmMode
? music.getPlayState
? PauseCircleFilled
: PlayCircleFilled
: PlayCircleFilled
"
@click="fmPlayOrPause"
/>
<n-icon
class="next"
size="30"
:component="SkipNextRound"
@click="fmNext"
/>
<n-icon
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
<div class="radio">
<div class="icon">
<n-icon size="20" :component="RadioFilled" />
<span>{{ $t("home.modules.papersonalfm.title") }}</span>
</div>
<span class="tip" v-if="!user.userLogin">
{{ $t("home.modules.papersonalfm.subtitle") }}
<Transition mode="out-in">
<div
v-if="music.getPersonalFmData?.id"
class="papersonalfm"
:style="`background-image: url(${music.getPersonalFmData.album.picUrl.replace(
/^http:/,
'https:'
)}?param=300y300)`"
>
<div class="gray" />
<img
class="pic"
:src="
music.getPersonalFmData.album.picUrl.replace(/^http:/, 'https:') +
'?param=300y300'
"
alt="pic"
/>
<div class="data">
<div class="info">
<span
class="name text-hidden"
@click="router.push(`/song?id=${music.getPersonalFmData.id}`)"
>
{{ music.getPersonalFmData.name }}
</span>
<AllArtists
class="text-hidden"
:artistsData="music.getPersonalFmData.artist"
/>
</div>
<div class="controls">
<n-icon
class="state"
size="46"
:component="
music.getPersonalFmMode
? music.getPlayState
? PauseCircleFilled
: PlayCircleFilled
: PlayCircleFilled
"
@click="fmPlayOrPause"
/>
<n-icon
class="next"
size="30"
:component="SkipNextRound"
@click="fmNext"
/>
<n-icon
class="dislike"
size="20"
:component="ThumbDownRound"
@click="music.setFmDislike(music.getPersonalFmData.id)"
/>
<div class="radio">
<div class="icon">
<n-icon size="20" :component="RadioFilled" />
<span>{{ $t("home.modules.papersonalfm.title") }}</span>
</div>
<span class="tip" v-if="!user.userLogin">
{{ $t("home.modules.papersonalfm.subtitle") }}
</span>
</div>
</div>
</div>
</div>
</div>
<n-skeleton v-else class="papersonalfm" />
</Transition>
</template>
<script setup>

View File

@@ -4,6 +4,7 @@
v-show="music.getPlaylists[0] && music.showPlayBar"
class="player"
content-style="padding: 0"
@click.stop="setting.bottomClick ? music.setBigPlayerState(true) : null"
>
<div class="slider">
<span>{{ music.getPlaySongTime.songTimePlayed }}</span>
@@ -64,7 +65,7 @@
<Transition name="fade" mode="out-in">
<AllArtists
v-if="
!music.getPlayState || !music.getPlaySongLyric.lrc[0]
!music.getPlayState || !music.getPlaySongLyric?.lrc[0]
"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
@@ -90,15 +91,24 @@
<n-text
v-else-if="
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.lrc[0]
music.getPlaySongLyric?.lrc[0]
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric.lrc[music.getPlaySongLyricIndex]
.content
"
/>
class="lrc"
>
<Transition name="fade" mode="out-in">
<n-text
class="text-hidden"
:key="music.getPlaySongLyricIndex"
:depth="3"
>
{{
music.getPlaySongLyric.lrc[
music.getPlaySongLyricIndex
]?.content
}}
</n-text>
</Transition>
</n-text>
<AllArtists
v-else
class="text-hidden"
@@ -148,7 +158,11 @@
/>
</div>
<div :class="music.getPersonalFmMode ? 'menu fm' : 'menu'">
<n-popover v-if="music.getPlaySongData" trigger="hover" :keep-alive-on-hover="false">
<n-popover
v-if="music.getPlaySongData"
trigger="hover"
:keep-alive-on-hover="false"
>
<template #trigger>
<div class="like">
<n-icon
@@ -203,7 +217,7 @@
? ShuffleOne
: PlayOnce
"
@click="music.setPlaySongMode()"
@click.stop="music.setPlaySongMode()"
/>
</div>
</n-dropdown>
@@ -220,19 +234,32 @@
{{ $t("general.name.playlists") }}
</n-popover>
<div class="volume">
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
<n-popover
trigger="hover"
placement="top-start"
:keep-alive-on-hover="false"
>
<template #trigger>
<n-icon
size="28"
:component="
persistData.playVolume == 0
? VolumeOffRound
: persistData.playVolume < 0.4
? VolumeMuteRound
: persistData.playVolume < 0.7
? VolumeDownRound
: VolumeUpRound
"
@click.stop="volumeMute"
/>
</template>
{{
persistData.playVolume > 0
? $t("general.name.mute")
: $t("general.name.unmute")
}}
</n-popover>
<n-slider
class="volmePg"
v-model:value="persistData.playVolume"

View File

@@ -71,12 +71,21 @@ const osThemeChange = (val) => {
// 配置主题色
const changeThemeColor = (val) => {
const color = themeColorData[val];
console.log("当前主题色:" + val, color);
themeOverrides.value = {
common: color,
};
setting.themeData = color;
let color = null;
if (val !== "custom") {
color = themeColorData[val];
console.log("当前主题色:" + val, color);
themeOverrides.value = {
common: color,
};
setting.themeData = color;
} else {
color = setting.themeData;
console.log("当前主题色为自定义:" + val, color);
themeOverrides.value = {
common: color,
};
}
setCssVariable("--main-color", color.primaryColor);
setCssVariable("--main-second-color", color.primaryColor + "1f");
setCssVariable("--main-boxshadow-color", color.primaryColor + "26");
@@ -125,6 +134,10 @@ watch(
() => setting.themeType,
(val) => changeThemeColor(val)
);
watch(
() => setting.themeData,
(val) => changeThemeColor(val.label)
);
onMounted(() => {
changeTheme();

View File

@@ -83,14 +83,18 @@ export default {
qr: "QR",
phone: "Captcha",
email: "Email",
canNotUse: "This login method is temporarily unavailable",
getCode: "Get the verification code",
getCodeAgain: "Try again",
codeSuccess: "Verification code sent successfully",
codeError: "Failed to send verification code, please try again",
canNotUse: "This login mode is not secure and is temporarily disabled",
loggedIn: "Already logged in, please don't log in again",
qrText1: "Please open APP and scan the code to login",
qrText2: "The current QR code is invalid, please scan it again",
qrText3: "Scan successfully, please confirm login in the client",
qrText4: "Login successfully",
qrText5: "Login error, please try again",
qrText6: "Login QR code generation failed",
loginStatus1: "Please open APP and scan the code to login",
loginStatus2: "The current QR code is invalid, please scan it again",
loginStatus3: "Scan successfully, please confirm login in the client",
loginStatus4: "Login successfully",
loginStatus5: "Login error, please try again",
loginStatus6: "Login QR code generation failed",
},
// Menu
menu: {
@@ -187,6 +191,10 @@ export default {
random: "Random play",
single: "Single loop",
normal: "list loop",
mute: "Mute",
unmute: "Unmute",
customTheme: "Custom theme",
primaryColor: "Primary Color",
},
dialog: {
check: "Check",
@@ -366,5 +374,7 @@ export default {
lyricsBlur: "Lyric Blur",
lyricsBlurTip:
"Blur lyrics other than the currently playing ones, experimental feature",
bottomClick: "Bottomclick to expand Player ",
bottomClickTip: "It may cause mistouch, please open with caution ",
},
};

View File

@@ -83,14 +83,18 @@ export default {
qr: "扫码登录",
phone: "验证码登录",
email: "邮箱登录",
canNotUse: "该登录方式暂时无法使用",
getCode: "获取验证码",
getCodeAgain: "重新获取",
codeSuccess: "验证码发送成功",
codeError: "验证码发送失败,请重试",
canNotUse: "该登录方式不安全,暂时禁用",
loggedIn: "已登录,请勿重复登录",
qrText1: "请打开云音乐 APP 扫码登录",
qrText2: "当前二维码已失效,请重新扫码",
qrText3: "扫描成功,请在客户端确认登录",
qrText4: "登录成功",
qrText5: "登录出错,请重试",
qrText6: "登录二维码生成失败",
loginStatus1: "请打开云音乐 APP 扫码登录",
loginStatus2: "当前二维码已失效,请重新扫码",
loginStatus3: "扫描成功,请在客户端确认登录",
loginStatus4: "登录成功",
loginStatus5: "登录出错,请重试",
loginStatus6: "登录二维码生成失败",
},
// 菜单
menu: {
@@ -185,6 +189,10 @@ export default {
random: "随机播放",
single: "单曲循环",
normal: "列表循环",
mute: "静音",
unmute: "取消静音",
customTheme: "自定义主题",
primaryColor: "主色",
},
dialog: {
check: "检查",
@@ -208,7 +216,7 @@ export default {
createFailed: "歌单新建失败,请重试",
deleteSuccess: "删除成功",
deleteFailure: "删除失败",
downloadSuccess: "{name}下载完成",
downloadSuccess: "{name} 下载完成",
downloadFailure: "下载失败,请尝试其他音质",
downloadError: "下载出现错误,请重试",
upCloudSuccess: "{name} 上传成功",
@@ -349,5 +357,7 @@ export default {
positionCenter: "居中",
lyricsBlur: "歌词模糊",
lyricsBlurTip: "除当前播放歌词外模糊显示,实验性功能",
bottomClick: "点击底栏展开播放器",
bottomClickTip: "可能会造成误触,请谨慎开启",
},
};

View File

@@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes";
import { getLoginState } from "@/api/login";
import { userStore } from "@/store";
import { userStore, musicStore } from "@/store";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -11,6 +11,10 @@ const router = createRouter({
// 路由守卫
router.beforeEach((to, from, next) => {
const user = userStore();
const music = musicStore();
// 关闭播放器
music.setBigPlayerState(false);
// 开始进度条
if (typeof $loadingBar !== "undefined") $loadingBar.start();
// 判断是否需要登录
if (to.meta.needLogin) {

View File

@@ -311,7 +311,13 @@ const useMusicDataStore = defineStore("musicData", {
}
} else {
console.log("该歌曲暂无歌词");
this.playSongLyric = [];
this.playSongLyric = {
lrc: [],
yrc: [],
hasTran: false,
hasTran: false,
hasYrc: false,
};
}
},
// 歌曲播放进度
@@ -339,7 +345,7 @@ const useMusicDataStore = defineStore("musicData", {
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);
const index = lyrics?.findIndex((v) => v?.time >= value?.currentTime);
this.playSongLyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
},
// 设置当前播放模式
@@ -376,49 +382,63 @@ const useMusicDataStore = defineStore("musicData", {
},
// 上下曲调整
setPlaySongIndex(type) {
// 如果 $player 未定义,返回 false
if (typeof $player === "undefined") return false;
// 停止播放当前歌曲
soundStop($player);
this.isLoadingSong = true;
// 根据播放模式设置加载状态
if (this.persistData.playSongMode !== "single") {
this.isLoadingSong = true;
}
// 如果是个人 FM 模式,设置个人 FM 数据
if (this.persistData.personalFmMode) {
this.setPersonalFmData();
} else {
let listLength = this.persistData.playlists.length;
let listMode = this.persistData.playSongMode;
// 根据当前播放模式调整
const listLength = this.persistData.playlists.length;
const listMode = this.persistData.playSongMode;
// 根据当前播放模式调整播放索引
if (listMode === "normal") {
this.persistData.playSongIndex += type === "next" ? 1 : -1;
} else if (listMode === "random") {
this.persistData.playSongIndex = Math.floor(
Math.random() * listLength
);
} else if (listMode === "single" && typeof $player !== "undefined") {
soundStop($player);
} else if (listMode === "single") {
// 单曲循环模式
console.log("单曲循环模式");
fadePlayOrPause($player, "play", this.persistData.playVolume);
} else {
// 未知播放模式,显示错误消息
$message.error(getLanguageData("playError"));
}
// 判断是否处于最后/第一首
if (this.persistData.playSongIndex < 0) {
this.persistData.playSongIndex = listLength - 1;
} else if (this.persistData.playSongIndex >= listLength) {
this.persistData.playSongIndex = 0;
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
// 检查播放索引是否越界,并根据情况进行处理
if (listMode !== "single") {
if (this.persistData.playSongIndex < 0) {
this.persistData.playSongIndex = listLength - 1;
} else if (this.persistData.playSongIndex >= listLength) {
this.persistData.playSongIndex = 0;
soundStop($player);
fadePlayOrPause($player, "play", this.persistData.playVolume);
}
// 如果播放列表长度大于 1则停止播放当前歌曲
if (listLength > 1) {
soundStop($player);
}
// 在下一个事件循环中设置播放状态
nextTick().then(() => {
this.setPlayState(true);
});
}
if (listMode !== "single" && listLength > 1) {
soundStop($player);
}
nextTick().then(() => {
this.setPlayState(true);
});
}
},
// 添加歌曲至播放列表
addSongToPlaylists(value, play = true) {
// 停止当前播放
if (typeof $player !== "undefined") soundStop($player);
// 判断与上一次播放歌曲是否一致
const index = this.persistData.playlists.findIndex(
(o) => o.id === value.id
);
// 判断与上一次播放歌曲是否一致
try {
if (
value.id !==

View File

@@ -63,6 +63,8 @@ const useSettingDataStore = defineStore("settingData", {
memoryLastPlaybackPosition: true,
// 语言
language: "zh-CN",
// 底栏点击展开播放器
bottomClick: false,
};
},
getters: {

View File

@@ -113,6 +113,22 @@ body,
}
}
// NSlider
.n-slider {
&:hover {
.n-slider-rail {
.n-slider-rail__fill {
background-color: var(--main-color);
}
}
}
.n-slider-rail {
.n-slider-rail__fill {
background-color: var(--main-color);
}
}
}
// 文本超出隐藏
.text-hidden {
display: -webkit-box !important;

View File

@@ -79,21 +79,23 @@ export const createSound = (src, autoPlay = true) => {
}
testNumber = 0;
music.setPlayState(true);
const songName = music.getPlaySongData.name;
const songArtist = music.getPlaySongData.artist[0].name;
const songName = music.getPlaySongData?.name;
const songArtist = music.getPlaySongData.artist[0]?.name;
// 播放通知
if (typeof $message !== "undefined") {
if (typeof $message !== "undefined" && songArtist !== null) {
$message.info(songName + " - " + songArtist, {
icon: () =>
h(NIcon, null, {
default: () => h(MusicNoteFilled),
}),
});
} else {
$message.warning(getLanguageData("songNotDetails"));
}
console.log("开始播放:" + songName + " - " + songArtist);
setMediaSession(music);
// 获取播放器信息
timeupdateInterval = setInterval(() => checkAudioTime(sound, music), 250);
setMediaSession(music);
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 播放时页面标题

View File

@@ -36,6 +36,7 @@ const languageData = {
songLoadError: "音乐数据获取失败",
songPlayError: "歌曲播放失败",
songLoadTest: "歌曲重试次数过多,请刷新后重试",
songNotDetails: "歌曲详细信息获取失败,可尝试歌曲匹配",
},
en: {
million: "M",
@@ -66,6 +67,7 @@ const languageData = {
playError: "Playback error, please refresh and try again",
addSongToNext: "has been added to the next song to play",
removeSong: "has been removed from the playlist",
songNotDetails: "Song details failed to get, try song match",
},
};

View File

@@ -42,16 +42,16 @@ axios.interceptors.response.use(
const data = error.response.data;
switch (error.response.status) {
case 401:
console.error("无权限访问");
console.error(data.message ? data.message : "无权限访问");
break;
case 301:
console.error("请求发生重定向");
console.error(data.message ? data.message : "请求发生重定向");
break;
case 404:
console.error("请求资源不存在");
console.error(data.message ? data.message : "请求资源不存在");
break;
case 500:
console.error("内部服务器错误");
console.error(data.message ? data.message : "内部服务器错误");
break;
default:
console.error(data.message ? data.message : "请求失败,请稍后重试");

View File

@@ -35,16 +35,10 @@
:foreground="setting.themeData.primaryColor"
/>
</n-card>
<span class="tip">{{ qrText }}</span>
<span class="tip">{{ loginStatus }}</span>
</n-tab-pane>
<n-tab-pane name="phone" :tab="$t('login.phone')">
<n-alert
style="width: 100%; margin-top: -20px; margin-bottom: 12px"
type="warning"
>
{{ $t("login.canNotUse") }}
</n-alert>
<!-- <n-form
<n-form
class="phone"
ref="phoneFormRef"
:model="phoneFormData"
@@ -83,10 +77,10 @@
</n-form-item>
<n-form-item>
<n-button style="width: 100%" type="primary" @click="phoneLogin">
{{$t("login.login")}}
{{ $t("login.login") }}
</n-button>
</n-form-item>
</n-form> -->
</n-form>
</n-tab-pane>
<n-tab-pane name="email" :tab="$t('login.email')">
<n-alert
@@ -126,7 +120,7 @@ const { numberRule, mobileRule } = formRules();
// 二维码数据
const qrImg = ref(null);
const qrText = ref(t("login.qrText1"));
const loginStatus = ref(t("login.loginStatus1"));
// 手机号登录数据
const phoneFormRef = ref(null);
@@ -139,7 +133,7 @@ const phoneFormRules = {
captcha: numberRule,
};
const captchaTimeOut = ref(null);
const captchaText = ref("获取验证码");
const captchaText = ref(t("login.getCode"));
const captchaDisabled = ref(false);
// 定时器
@@ -157,15 +151,15 @@ const saveLoginData = (data) => {
if (res.data.profile) {
user.setUserData(res.data.profile);
user.userLogin = true;
qrText.value = t("login.qrText4");
$message.success(t("login.qrText4"));
loginStatus.value = t("login.loginStatus4");
$message.success(t("login.loginStatus4"));
// 自动签到
if ($signIn) $signIn();
clearInterval(qrCheckInterval.value);
router.push("/user");
} else {
user.userLogOut();
$message.error(t("login.qrText5"));
$message.error(t("login.loginStatus5"));
getQrKeyData();
}
});
@@ -188,7 +182,7 @@ const getQrKeyData = () => {
qrImg.value = `https://music.163.com/login?codekey=${res.data.unikey}`;
checkQrState(res.data.unikey);
} else {
$message.error(t("login.qrText6"));
$message.error(t("login.loginStatus6"));
}
});
}
@@ -204,14 +198,14 @@ const checkQrState = (key) => {
if (res.code == 800) {
getQrKeyData();
loginStateMessage.value = null;
qrText.value = t("login.qrText2");
loginStatus.value = t("login.loginStatus2");
} else if (res.code == 801) {
loginStateMessage.value = null;
qrText.value = t("login.qrText1");
loginStatus.value = t("login.loginStatus1");
} else if (res.code == 802) {
qrText.value = t("login.qrText3");
loginStatus.value = t("login.loginStatus3");
if (!loginStateMessage.value) {
loginStateMessage.value = $message.loading(t("login.qrText3"), {
loginStateMessage.value = $message.loading(t("login.loginStatus3"), {
duration: 0,
});
}
@@ -229,13 +223,13 @@ const getCaptcha = (data) => {
phoneFormRef.value?.validate(
(errors) => {
if (errors) {
$message.error("请输入正确的手机号");
$message.error(t("general.message.needCheck"));
} else {
console.log(data + "发送验证码");
sentCaptcha(data).then((res) => {
console.log(res);
if (res.code == 200) {
$message.success("验证码发送成功");
$message.success(t("login.codeSuccess"));
let countDown = 60;
captchaDisabled.value = true;
captchaTimeOut.value = setInterval(() => {
@@ -243,12 +237,12 @@ const getCaptcha = (data) => {
captchaText.value = countDown + "s";
if (countDown === 0) {
clearInterval(captchaTimeOut.value);
captchaText.value = "重新获取";
captchaText.value = t("login.getCodeAgain");
captchaDisabled.value = false;
}
}, 1000);
} else {
$message.error("验证码发送失败,请重试");
$message.error(t("login.codeError"));
}
});
}
@@ -265,24 +259,39 @@ const phoneLogin = (e) => {
phoneFormRef.value?.validate((errors) => {
if (!errors) {
console.log("通过");
verifyCaptcha(
phoneFormData._value.phone,
phoneFormData._value.captcha
).then((res) => {
console.log(res);
if (res.code == 200) {
toLogin(
phoneFormData._value.phone,
phoneFormData._value.captcha
).then((res) => {
console.log(res);
// 暂时不支持,等支持了再写
});
}
});
verifyCaptcha(phoneFormData._value.phone, phoneFormData._value.captcha)
.then((res) => {
console.log(res);
if (res.code == 200) {
toLogin(
phoneFormData._value.phone,
phoneFormData._value.captcha
).then((res) => {
console.log(res);
if (res.profile) {
saveLoginData(res);
user.setUserData(res.profile);
user.userLogin = true;
$message.success(t("login.loginStatus4"));
// 自动签到
if ($signIn) $signIn();
router.push("/user");
} else {
user.userLogOut();
$message.error(t("login.loginStatus5"));
phoneFormData.value.captcha = null;
}
});
}
})
.catch((err) => {
console.error(err);
$loadingBar.error();
$message.error(t("login.loginStatus5"));
});
} else {
$loadingBar.error();
$message.error("请检查您的输入");
$message.error(t("general.message.needCheck"));
}
});
};

View File

@@ -37,6 +37,13 @@
>
<n-text v-html="language === 'zh-CN' ? item.name : item.label" />
</n-grid-item>
<n-grid-item
:class="themeType === 'custom' ? 'item check' : 'item'"
:style="{ '--color': themeData.primaryColor }"
@click="openThemeCustom()"
>
<n-text v-html="$t('general.name.customTheme')" />
</n-grid-item>
</n-grid>
</n-card>
<n-card class="set-item">
@@ -98,6 +105,13 @@
</div>
<n-switch v-model:value="bottomLyricShow" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.bottomClick") }}
<span class="tip">{{ $t("setting.bottomClickTip") }}</span>
</div>
<n-switch v-model:value="bottomClick" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
{{ $t("setting.songVolumeFade") }}
@@ -149,6 +163,51 @@
</div>
<n-switch v-model:value="showLyricSetting" :round="false" />
</n-card>
<!-- 自定义主题 -->
<n-modal
class="s-modal"
v-model:show="showThemeCustom"
preset="card"
:title="$t('general.name.customTheme')"
:bordered="false"
>
<n-form class="color-custom" :model="customColorData">
<n-form-item
:label="$t('general.name.primaryColor')"
path="primaryColor"
>
<n-color-picker v-model:value="customColorData.primaryColor" />
</n-form-item>
<n-form-item
:label="$t('general.name.primaryColor') + ' Hover'"
path="primaryColorHover"
>
<n-color-picker v-model:value="customColorData.primaryColorHover" />
</n-form-item>
<n-form-item
:label="$t('general.name.primaryColor') + ' Suppl'"
path="primaryColorSuppl"
>
<n-color-picker v-model:value="customColorData.primaryColorSuppl" />
</n-form-item>
<n-form-item
:label="$t('general.name.primaryColor') + ' Pressed'"
path="primaryColorPressed"
>
<n-color-picker v-model:value="customColorData.primaryColorPressed" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showThemeCustom = false">
{{ $t("general.dialog.cancel") }}
</n-button>
<n-button type="primary" @click="setThemeCustom">
{{ $t("general.name.customTheme") }}
</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
@@ -172,16 +231,49 @@ const {
autoSignIn,
searchHistory,
themeType,
themeData,
showLyricSetting,
songVolumeFade,
useUnmServer,
memoryLastPlaybackPosition,
language,
bottomClick,
} = storeToRefs(setting);
// 国际化
const { locale, t } = useI18n();
// 自定义主题
const showThemeCustom = ref(false);
const customColorData = ref({
primaryColor: null,
primaryColorHover: null,
primaryColorSuppl: null,
primaryColorPressed: null,
});
const openThemeCustom = () => {
showThemeCustom.value = true;
customColorData.value = {
primaryColor: themeData.value.primaryColor,
primaryColorHover: themeData.value.primaryColorHover,
primaryColorSuppl: themeData.value.primaryColorSuppl,
primaryColorPressed: themeData.value.primaryColorPressed,
};
};
// 确认自定义颜色
const setThemeCustom = () => {
console.log(customColorData.value);
themeType.value = "custom";
themeData.value = {
label: "custom",
name: t("general.name.customTheme"),
...customColorData.value,
};
showThemeCustom.value = false;
};
// UNM 开关显示
const useUnmServerShow = import.meta.env.VITE_UNM_API ? true : false;

View File

@@ -37,7 +37,7 @@
{{
$t("general.name.cloudUsed", {
used: (100 / (cloudSpace[1] / cloudSpace[0])).toFixed(),
remaining: cloudSpace[1] - cloudSpace[0],
remaining: (cloudSpace[1] - cloudSpace[0]).toFixed(),
})
}}
</n-text>

3
vercel.json Normal file
View File

@@ -0,0 +1,3 @@
{
"rewrites": [{ "source": "/:path*", "destination": "/index.html" }]
}

View File

@@ -1,11 +1,11 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import { VitePWA } from "vite-plugin-pwa";
import { createHtmlPlugin } from "vite-plugin-html";
import vue from "@vitejs/plugin-vue";
import viteCompression from "vite-plugin-compression";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
// https://vitejs.dev/config/
export default ({ mode }) =>
@@ -28,21 +28,6 @@ export default ({ mode }) =>
Components({
resolvers: [NaiveUiResolver()],
}),
createHtmlPlugin({
minify: true,
template: "index.html",
inject: {
data: {
logo: loadEnv(mode, process.cwd()).VITE_SITE_LOGO,
appleLogo: loadEnv(mode, process.cwd()).VITE_SITE_APPLE_LOGO,
title: loadEnv(mode, process.cwd()).VITE_SITE_TITLE,
author: loadEnv(mode, process.cwd()).VITE_SITE_ANTHOR,
keywords: loadEnv(mode, process.cwd()).VITE_SITE_KEYWORDS,
description: loadEnv(mode, process.cwd()).VITE_SITE_DES,
tongji: loadEnv(mode, process.cwd()).VITE_SITE_BAIDUTONGJI,
},
},
}),
// PWA
VitePWA({
registerType: "autoUpdate",
@@ -85,9 +70,11 @@ export default ({ mode }) =>
],
},
}),
// viteCompression
viteCompression(),
],
server: {
port: 2048,
port: 25536,
open: true,
http: true,
ssr: false,