Compare commits
41 Commits
v2.0.0-bet
...
v2.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 | ||
|
|
ee9bbf0687 | ||
|
|
693dc65b07 | ||
|
|
354d271582 | ||
|
|
eaaeb0f5d3 | ||
|
|
e4e8deec59 | ||
|
|
3f21704b82 | ||
|
|
7ad2bb8bde | ||
|
|
3123e4f5f8 | ||
|
|
60e43c9f40 | ||
|
|
298813a057 | ||
|
|
2db74f3a39 | ||
|
|
024ff1773e | ||
|
|
6046e5a153 | ||
|
|
3c39dbd87f | ||
|
|
c5747b6a3e | ||
|
|
750d570c3d | ||
|
|
b811b00b9f | ||
|
|
a372570038 | ||
|
|
6d5fa15098 | ||
|
|
b65369a8a6 | ||
|
|
0af0ac3cce | ||
|
|
f0ed78eed5 | ||
|
|
b1cda68c75 | ||
|
|
dd1081cfa2 | ||
|
|
046b8f3a92 | ||
|
|
72650a5419 | ||
|
|
d471e686b5 | ||
|
|
41c4342f76 | ||
|
|
16802aaac7 |
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
@@ -21,9 +21,18 @@ RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
RENDERER_VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
RENDERER_VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
|
||||
# 公告配置
|
||||
## 若无需公告,请将标题或内容任意一项设为空即可
|
||||
## 公告类型
|
||||
RENDERER_VITE_ANN_TYPE = "info"
|
||||
## 公告标题
|
||||
RENDERER_VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
RENDERER_VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
RENDERER_VITE_ANN_DURATION = 8000
|
||||
@@ -49,5 +49,7 @@ module.exports = {
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
$refreshCloudList: true,
|
||||
$cleanAll: true,
|
||||
},
|
||||
};
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
8
.github/workflows/build.yml
vendored
@@ -21,6 +21,14 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
55
.github/workflows/release.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -21,6 +22,14 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
@@ -36,24 +45,21 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -63,7 +69,15 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 安装项目依赖
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
@@ -78,24 +92,21 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -108,6 +119,14 @@ jobs:
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
run: sudo apt-get update
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
@@ -123,19 +142,13 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -14,6 +14,7 @@ dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
out
|
||||
.env
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
5
.npmrc
@@ -1,2 +1,5 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
shamefully-hoist=true
|
||||
|
||||
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# build
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.20.2-alpine as app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
340
README.md
@@ -1,13 +1,5 @@
|
||||
> [!IMPORTANT]
|
||||
> ## 🎉 当前项目正在重构中 🎉
|
||||
>
|
||||
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
|
||||
> - 支持客户端与网页端
|
||||
> - 支持现有版本所有功能
|
||||
> - 新增支持播放与管理本地歌曲
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<img alt="logo" height="80" src="./public/images/icons/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
@@ -16,6 +8,16 @@
|
||||
|
||||
## 说明
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ### 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行构建
|
||||
- ~~仅对移动端做了基础适配,**不保证功能全部可用**~~
|
||||
@@ -118,68 +120,106 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ 部署
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
### API 服务(客户端无需理会,如果需要网页端,则必需部署)
|
||||
### 本地构建
|
||||
|
||||
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
|
||||
|
||||
- 请在根目录下的 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 中填入 API 地址(必需)
|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "your api url";
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
> 请尽量拉取最新分支后使用本地构建方式,在线部署的仓库可能更新不及时
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
# 构建
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 开发
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或者
|
||||
yarn dev
|
||||
# 或者
|
||||
npm dev
|
||||
# 拉取
|
||||
docker pull imsyy/splayer:2.0.0-beta.5
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:2.0.0-beta.5
|
||||
```
|
||||
|
||||
### 构建网页端
|
||||
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
构建完成后可将生成的 `out/renderer` 文件夹内的文件上传至服务器
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
|
||||
若使用的为第三方部署平台,比如 `Vercel`,请将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
|
||||
|
||||

|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
```
|
||||
|
||||
### 构建客户端
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||
```bash
|
||||
# win
|
||||
pnpm build:win
|
||||
# linux
|
||||
pnpm build:linux
|
||||
# mac
|
||||
pnpm build:mac
|
||||
```
|
||||

|
||||
|
||||
构建完成后可在 `dist` 文件夹中打开可执行文件来完成安装操作
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
|
||||
|
||||
```bash
|
||||
git clone https://github.com/example/repository.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 编译打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
## ⚙️ 本地部署
|
||||
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
```bash
|
||||
npm install pnpm -g
|
||||
```
|
||||
|
||||
3. 克隆仓库并拉取至本地,此处不再赘述
|
||||
4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述)
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -191,16 +231,6 @@ pnpm build:mac
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 GPL Version 3 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 GPL Version 3,并在适当的地方注明原始项目的许可证
|
||||
3. **免责声明:** 根据 GPL Version 3,本项目不提供任何明示或暗示的担保。请详细阅读 GPL Version 3以了解完整的免责声明内容
|
||||
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
5. **许可证链接:** 请阅读 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 了解更多详情
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
本项目部分功能使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
@@ -210,3 +240,187 @@ pnpm build:mac
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
|
||||
感谢您的理解
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证
|
||||
3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
<details>
|
||||
<summary>查看目录结构详情</summary>
|
||||
|
||||
|
||||
> ChatGPT 写的,如有错误,请见谅
|
||||
|
||||
```dir
|
||||
├── auto-imports.d.ts # 自动导入
|
||||
├── components.d.ts # 自动导入
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker
|
||||
├── electron # Electron
|
||||
│ ├── main # Electron 主进程
|
||||
│ │ ├── index.js # 主进程入口
|
||||
│ │ ├── mainIpcMain.js # 主进程与渲染进程通信
|
||||
│ │ ├── startMainServer.js # 启动主进程服务器
|
||||
│ │ ├── startNcmServer.js # 启动网易云音乐服务
|
||||
│ │ └── utils # 主进程工具函数
|
||||
│ │ ├── checkUpdates.js # 检查更新
|
||||
│ │ ├── createGlobalShortcut.js # 创建全局快捷键
|
||||
│ │ ├── createSystemTray.js # 创建系统托盘
|
||||
│ │ ├── getNeteaseMusicUrl.js # 解灰
|
||||
│ │ ├── kwDES.js # DES加密算法
|
||||
│ │ └── readDirAsync.js # 异步读取目录
|
||||
│ └── preload # Electron 预加载脚本
|
||||
│ └── index.js # 预加载脚本入口文件
|
||||
├── electron-builder.yml # Electron Builder
|
||||
├── electron.vite.config.js # Electron Vite
|
||||
├── index.html # 主页面 HTML
|
||||
├── LICENSE # 项目许可证
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── src # 项目源代码
|
||||
│ ├── api # API 相关
|
||||
│ │ ├── ./..
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── assets # 静态资源
|
||||
│ │ ├── emoji.json # 表情数据
|
||||
│ │ ├── icon.json # 图标数据
|
||||
│ │ └── themeColor.json # 主题颜色数据
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── Cover # 封面相关组件目录
|
||||
│ │ │ ├── CoverDropdown.vue # 封面下拉组件
|
||||
│ │ │ ├── MainCover.vue # 主封面组件
|
||||
│ │ │ ├── SpecialCoverCard.vue # 特殊封面卡片组件
|
||||
│ │ │ └── SpecialCover.vue # 特殊封面组件
|
||||
│ │ ├── Global # 全局组件目录
|
||||
│ │ │ ├── MainLayout.vue # 主布局组件
|
||||
│ │ │ ├── Menu.vue # 菜单组件
|
||||
│ │ │ ├── Pagination.vue # 分页组件
|
||||
│ │ │ ├── Playlist.vue # 歌单组件
|
||||
│ │ │ ├── Provider.vue # 全局化配置组件
|
||||
│ │ │ └── SvgIcon.vue # SVG 图标组件
|
||||
│ │ ├── List # 列表组件目录
|
||||
│ │ │ ├── CommentList.vue # 评论列表组件
|
||||
│ │ │ ├── SongListDropdown.vue # 歌曲下拉组件
|
||||
│ │ │ └── SongList.vue # 歌曲列表组件
|
||||
│ │ ├── Modal # 弹窗相关组件目录
|
||||
│ │ │ ├── AddPlaylist.vue # 添加歌单组件
|
||||
│ │ │ ├── CloudSongMatch.vue # 云盘歌曲匹配组件
|
||||
│ │ │ ├── CreatePlaylist.vue # 创建歌单组件
|
||||
│ │ │ ├── DownloadSong.vue # 下载歌曲组件
|
||||
│ │ │ ├── LoginPhone.vue # 手机登录组件
|
||||
│ │ │ ├── LoginQRCode.vue # 二维码登录组件
|
||||
│ │ │ ├── Login.vue # 登录组件
|
||||
│ │ │ ├── PlaylistUpdate.vue # 歌单编辑组件
|
||||
│ │ │ └── UpCloudSong.vue # 上传云盘歌曲组件
|
||||
│ │ ├── Nav # 导航相关组件目录
|
||||
│ │ │ ├── MainNav.vue # 主导航组件
|
||||
│ │ │ └── UserData.vue # 用户数据组件
|
||||
│ │ ├── Player # 播放器相关组件目录
|
||||
│ │ │ ├── CountDown.vue # 倒计时组件
|
||||
│ │ │ ├── FullPlayer.vue # 全屏播放器组件
|
||||
│ │ │ ├── Lyric.vue # 歌词组件
|
||||
│ │ │ ├── MainControl.vue # 主控制组件
|
||||
│ │ │ ├── PlayerControl.vue # 播放器控制组件
|
||||
│ │ │ ├── PlayerCover.vue # 播放器封面组件
|
||||
│ │ │ └── PrivateFm.vue # 私人 FM 组件
|
||||
│ │ ├── Search # 搜索相关组件
|
||||
│ │ │ ├── SearchHot.vue # 热门搜索组件
|
||||
│ │ │ ├── SearchInp.vue # 搜索输入组件
|
||||
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
|
||||
│ │ └── WinDom # 窗口 DOM 相关组件
|
||||
│ │ └── TitleBar.vue # 标题栏组件
|
||||
│ ├── main.js # Vue 应用的入口文件
|
||||
│ ├── router # Vue Router 相关文件夹
|
||||
│ │ ├── index.js # Vue Router 入口文件
|
||||
│ │ └── routes.js # 路由配置文件
|
||||
│ ├── stores # Vuex Store 相关文件夹
|
||||
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
|
||||
│ │ ├── index.js # Vuex Store 入口文件
|
||||
│ │ ├── musicData.js # 音乐数据相关文件
|
||||
│ │ ├── siteData.js # 网站数据相关文件
|
||||
│ │ ├── siteSettings.js # 网站设置相关文件
|
||||
│ │ └── siteStatus.js # 网站状态相关文件
|
||||
│ ├── style # 样式相关文件夹
|
||||
│ │ ├── animate.scss # 动画样式文件
|
||||
│ │ └── main.scss # 主样式文件
|
||||
│ ├── utils # 工具函数文件夹
|
||||
│ │ ├── auth.js # 认证相关函数
|
||||
│ │ ├── base64.js # Base64编码解码相关函数
|
||||
│ │ ├── color-utils.js # 颜色工具函数
|
||||
│ │ ├── cover-color.js # 封面颜色相关函数
|
||||
│ │ ├── debounce.js # 防抖函数
|
||||
│ │ ├── formatData.js # 数据格式化函数
|
||||
│ │ ├── formRules.js # 表单验证规则
|
||||
│ │ ├── globalEvents.js # 全局事件处理函数
|
||||
│ │ ├── globalShortcut.js # 全局快捷键相关函数
|
||||
│ │ ├── helper.js # 辅助函数
|
||||
│ │ ├── parseLyric.js # 解析歌词函数
|
||||
│ │ ├── Player.js # 播放器控制相关函数
|
||||
│ │ ├── request.js # 网络请求相关函数
|
||||
│ │ ├── throttle.js # 节流函数
|
||||
│ │ ├── timeTools.js # 时间工具函数
|
||||
│ │ └── userSignIn.js # 用户登录相关函数
|
||||
│ └── views # Vue组件文件夹
|
||||
│ ├── Artist # 艺术家相关组件
|
||||
│ │ ├── albums.vue # 艺术家专辑组件
|
||||
│ │ ├── hot.vue # 艺术家热门组件
|
||||
│ │ ├── index.vue # 艺术家主组件
|
||||
│ │ ├── songs.vue # 艺术家歌曲组件
|
||||
│ │ └── videos.vue # 艺术家视频组件
|
||||
│ ├── Cloud.vue # 云盘组件
|
||||
│ ├── Comment.vue # 评论组件
|
||||
│ ├── DailySongs.vue # 每日推荐组件
|
||||
│ ├── Discover # 发现音乐相关组件
|
||||
│ │ ├── artists.vue # 发现音乐艺术家组件
|
||||
│ │ ├── index.vue # 发现音乐主组件
|
||||
│ │ ├── new.vue # 发现音乐新歌组件
|
||||
│ │ ├── playlists.vue # 发现音乐歌单组件
|
||||
│ │ └── toplists.vue # 发现音乐排行榜组件
|
||||
│ ├── History.vue # 历史记录组件
|
||||
│ ├── Home.vue # 主页组件
|
||||
│ ├── Like # 我喜欢的相关组件
|
||||
│ │ ├── albums.vue # 我喜欢的专辑组件
|
||||
│ │ ├── artists.vue # 我喜欢的艺术家组件
|
||||
│ │ ├── index.vue # 我喜欢的主组件
|
||||
│ │ ├── playlists.vue # 我喜欢的歌单组件
|
||||
│ │ └── videos.vue # 我喜欢的视频组件
|
||||
│ ├── List # 列表相关组件
|
||||
│ │ ├── album.vue # 专辑组件
|
||||
│ │ └── playlist.vue # 歌单组件
|
||||
│ │ └── dj.vue # 电台组件
|
||||
│ ├── Local # 本地音乐相关组件
|
||||
│ │ ├── albums.vue # 本地音乐专辑组件
|
||||
│ │ ├── artists.vue # 本地音乐艺术家组件
|
||||
│ │ ├── index.vue # 本地音乐主组件
|
||||
│ │ └── songs.vue # 本地音乐歌曲组件
|
||||
│ ├── Player.vue # 视频播放器组件
|
||||
│ ├── Dj # 电台相关组件
|
||||
│ │ └── index.vue # 电台主组件
|
||||
│ │ └── type.vue # 电台分类组件
|
||||
│ ├── Search # 搜索相关组件
|
||||
│ │ ├── albums.vue # 搜索专辑组件
|
||||
│ │ ├── artists.vue # 搜索艺术家组件
|
||||
│ │ ├── index.vue # 搜索主组件
|
||||
│ │ ├── playlists.vue # 搜索歌单组件
|
||||
│ │ ├── songs.vue # 搜索歌曲组件
|
||||
│ │ └── videos.vue # 搜索视频组件
|
||||
│ │ └── djs.vue # 搜索电台组件
|
||||
│ ├── Setting # 设置相关组件
|
||||
│ │ └── index.vue # 设置主组件
|
||||
│ ├── Song.vue
|
||||
│ ├── State
|
||||
│ │ ├── 403.vue
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ └── Test.vue
|
||||
└── vercel.json # Vercel 部署配置
|
||||
```
|
||||
</details>
|
||||
|
||||
12
components.d.ts
vendored
@@ -8,6 +8,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
|
||||
CloudSongMatch: typeof import('./src/components/Modal/CloudSongMatch.vue')['default']
|
||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
|
||||
@@ -15,20 +16,22 @@ declare module 'vue' {
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
||||
LoginPhone: typeof import('./src/components/Modal/LoginPhone.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
|
||||
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
|
||||
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
|
||||
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
|
||||
MainLayout: typeof import('./src/components/Global/MainLayout.vue')['default']
|
||||
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
|
||||
Menu: typeof import('./src/components/Global/Menu.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@@ -41,6 +44,7 @@ declare module 'vue' {
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
@@ -48,7 +52,9 @@ declare module 'vue' {
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
@@ -58,9 +64,11 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
@@ -77,8 +85,10 @@ declare module 'vue' {
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
|
||||
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
|
||||
PrivateFm: typeof import('./src/components/Player/PrivateFm.vue')['default']
|
||||
|
||||
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 7899:7899
|
||||
restart: always
|
||||
@@ -19,15 +19,11 @@ asarUnpack:
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
target: nsis
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
@@ -44,12 +40,16 @@ nsis:
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/images/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/images/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_512.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
@@ -69,13 +69,12 @@ dmg:
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
@@ -71,6 +72,62 @@ export default defineConfig(({ mode }) => {
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
// PWA
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
workbox: {
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "file-cache",
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "image-cache",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
name: loadEnv(mode, process.cwd()).RENDERER_VITE_SITE_TITLE,
|
||||
short_name: loadEnv(mode, process.cwd()).RENDERER_VITE_SITE_TITLE,
|
||||
description: loadEnv(mode, process.cwd()).RENDERER_VITE_SITE_DES,
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
background_color: "#efefef",
|
||||
icons: [
|
||||
{
|
||||
src: "/images/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
@@ -100,9 +157,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,154 +1,253 @@
|
||||
import { join } from "path";
|
||||
import { app, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
|
||||
import { platform, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createSystemTray from "@main/utils/createSystemTray";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import Store from "electron-store";
|
||||
import log from "electron-log";
|
||||
|
||||
// 主窗口
|
||||
let mainWindow;
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 单例锁
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/splayer-log.txt");
|
||||
// 设置日志文件的最大大小为 10 MB
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024;
|
||||
// 绑定 console.log
|
||||
console.log = log.log.bind(log);
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
// 设置日志文件的最大大小为 2 MB
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
||||
// 绑定 console 事件
|
||||
console.error = log.error.bind(log);
|
||||
console.warn = log.warn.bind(log);
|
||||
console.info = log.info.bind(log);
|
||||
console.debug = log.debug.bind(log);
|
||||
|
||||
// 创建主窗口
|
||||
const createWindow = () => {
|
||||
// 创建浏览器窗口
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280, // 窗口宽度
|
||||
height: 720, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
});
|
||||
|
||||
// 设置窗口打开处理程序
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
constructor() {
|
||||
// 主窗口
|
||||
this.mainWindow = null;
|
||||
// 主代理
|
||||
this.mainServer = null;
|
||||
// 网易云 API
|
||||
this.ncmServer = null;
|
||||
// Store
|
||||
this.store = new Store({
|
||||
// 窗口大小
|
||||
windowSize: {
|
||||
width: { type: "number", default: 1280 },
|
||||
height: { type: "number", default: 740 },
|
||||
},
|
||||
});
|
||||
// 设置应用程序名称
|
||||
if (process.platform === "win32") app.setAppUserModelId(app.getName());
|
||||
// 初始化
|
||||
this.checkApp().then(async (lockObtained) => {
|
||||
if (lockObtained) {
|
||||
await this.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听关闭
|
||||
mainWindow.on("close", (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
// 单例锁
|
||||
async checkApp() {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("已有一个程序正在运行,本次启动阻止");
|
||||
app.quit();
|
||||
// 未获得锁
|
||||
return false;
|
||||
}
|
||||
// 聚焦到当前程序
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
});
|
||||
// 获得锁
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化完成
|
||||
app.whenReady().then(async () => {
|
||||
// 尝试获取单例锁
|
||||
if (!gotTheLock) {
|
||||
// 如果获取不到单例锁,表示已经有一个实例在运行
|
||||
app.quit();
|
||||
log.error("已有一个程序正在运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("splayer");
|
||||
// 初始化程序
|
||||
async init() {
|
||||
log.info("主进程初始化");
|
||||
|
||||
// 初始化完成并准备创建浏览器窗口
|
||||
// 为 Windows 设置应用程序用户模型 ID
|
||||
electronApp.setAppUserModelId("com.electron");
|
||||
// 启动网易云 API
|
||||
try {
|
||||
this.ncmServer = await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("启动网易云 API 失败:", error);
|
||||
}
|
||||
|
||||
// 在开发模式下默认通过 F12 打开或关闭 DevTools
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) {
|
||||
this.mainServer = await startMainServer();
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("SPlayer");
|
||||
// 应用程序准备好之前注册
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
]);
|
||||
|
||||
// 主应用程序事件
|
||||
this.mainAppEvents();
|
||||
}
|
||||
|
||||
// 创建主窗口
|
||||
createWindow();
|
||||
createWindow() {
|
||||
// 创建浏览器窗口
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: app.getName() || "SPlayer",
|
||||
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
|
||||
height: this.store.get("windowSize.height") || 740, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
nodeIntegration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 创建系统信息
|
||||
createSystemInfo(mainWindow);
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.once("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
// 主窗口事件
|
||||
this.mainWindowEvents();
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
// 设置窗口打开处理程序
|
||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 启动网易云 API
|
||||
await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(mainWindow);
|
||||
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) await startMainServer();
|
||||
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(mainWindow);
|
||||
|
||||
// 检测更新
|
||||
configureAutoUpdater(process.platform);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
this.mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
this.mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.whenReady().then(async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 检测更新
|
||||
configureAutoUpdater();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow);
|
||||
// 系统托盘
|
||||
createSystemTray(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
|
||||
// 开发环境下 F12 打开控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) this.createWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (!platform.isMacOS) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主窗口事件
|
||||
mainWindowEvents() {
|
||||
this.mainWindow.on("show", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("hide", () => {
|
||||
// console.info("窗口隐藏");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("blur", () => {
|
||||
// console.info("窗口失去焦点");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", true);
|
||||
});
|
||||
|
||||
this.mainWindow.on("unmaximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", false);
|
||||
});
|
||||
|
||||
this.mainWindow.on("resized", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
this.mainWindow.on("moved", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 窗口关闭
|
||||
this.mainWindow.on("close", (event) => {
|
||||
if (platform.isLinux) {
|
||||
app.quit();
|
||||
} else {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new MainProcess();
|
||||
|
||||
@@ -23,7 +23,7 @@ const mainIpcMain = (win) => {
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("window-state", win.isMaximized());
|
||||
ev.reply("windowState", win.isMaximized());
|
||||
});
|
||||
ipcMain.on("window-restore", () => {
|
||||
win.restore();
|
||||
|
||||
@@ -5,7 +5,7 @@ const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=12141] - 服务器端口
|
||||
* @param {number} [options.port=11451] - 服务器端口
|
||||
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
|
||||
* @returns {Promise<void>} 返回一个 Promise,在 API 服务器成功启动后 resolve
|
||||
*/
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
import { dialog, shell } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
const checkForUpdates = () => {
|
||||
autoUpdater.checkForUpdates();
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否前往 GitHub 下载新版本安装包?",
|
||||
buttons: ["前往", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
shell.openExternal("https://github.com/imsyy/SPlayer/releases");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
checkForUpdates();
|
||||
|
||||
// 监听检查更新的事件
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("Checking for update...");
|
||||
});
|
||||
|
||||
if (is.dev) return false;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
console.log("Update available:", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("Update not available.");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
console.log("Update downloaded. Ready to install.");
|
||||
|
||||
// 在需要的时候,触发安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
hasNewVersion(info);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,8 +6,18 @@ import { globalShortcut } from "electron";
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CommandOrControl+R", () => {
|
||||
win?.reload();
|
||||
globalShortcut.register("CmdOrCtrl+Shift+R", () => {
|
||||
if (win && win.isFocused()) win?.reload();
|
||||
});
|
||||
|
||||
// 打开开发者工具
|
||||
globalShortcut.register("CmdOrCtrl+Shift+I", () => {
|
||||
if (win && win.isFocused()) {
|
||||
win?.webContents.openDevTools({
|
||||
mode: "right",
|
||||
activate: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,59 +1,78 @@
|
||||
import { join } from "path";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
// 当前歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统自定义信息
|
||||
* 创建系统托盘
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemInfo = (win) => {
|
||||
// 弹出列表
|
||||
app.setUserTasks([]);
|
||||
const createSystemTray = (win) => {
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 给托盘图标设置气球提示
|
||||
const mainTray = new Tray(
|
||||
nativeImage
|
||||
.createFromPath(
|
||||
join(
|
||||
__dirname,
|
||||
process.platform === "win32"
|
||||
? "../../public/images/icons/favicon.ico"
|
||||
: "../../public/images/icons/favicon-32x32.png",
|
||||
),
|
||||
)
|
||||
.resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
}),
|
||||
);
|
||||
// 应用内菜单
|
||||
Menu.setApplicationMenu(createTrayMenu(win));
|
||||
// 默认名称
|
||||
win.setTitle(app.getName());
|
||||
mainTray.setTitle(app.getName());
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 歌曲数据改变时
|
||||
// 左键事件
|
||||
mainTray.on("click", () => win.show());
|
||||
// 托盘菜单
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
// 系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放歌曲改变
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
// 托盘图标标题
|
||||
mainTray.setToolTip(val);
|
||||
// 更改应用标题
|
||||
win.setTitle(val);
|
||||
mainTray.setTitle(val);
|
||||
mainTray.setToolTip(val);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放状态改变
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
// 显示窗口
|
||||
win.show();
|
||||
});
|
||||
// 右键事件
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icons/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icon/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icon/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
// 返回菜单
|
||||
return [
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
@@ -69,6 +88,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
@@ -76,6 +96,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
accelerator: "CmdOrCtrl+Space",
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
@@ -83,6 +104,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
@@ -94,7 +116,9 @@ const createTrayMenu = (win) => {
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.webContents.send("setting");
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("open-setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -109,7 +133,7 @@ const createTrayMenu = (win) => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
]);
|
||||
};
|
||||
|
||||
export default createSystemInfo;
|
||||
export default createSystemTray;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { encryptQuery } from "@main/utils/kwDES";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 网易云音乐解灰
|
||||
* 目前音源采用 咪咕音乐
|
||||
*/
|
||||
|
||||
// 请求头
|
||||
// 咪咕音乐请求头
|
||||
const requestHeader = {
|
||||
Origin: "http://music.migu.cn/",
|
||||
Referer: "http://m.music.migu.cn/v3/",
|
||||
@@ -19,48 +19,149 @@ const requestHeader = {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongId = async (keyword) => {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
console.log(result.data.musics[0]);
|
||||
const oldName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(oldName[0])) {
|
||||
console.log("匹配失败");
|
||||
try {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("咪咕解灰歌曲 ID:", songId);
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
const songUrl = result.data.data.url;
|
||||
console.info("咪咕解灰歌曲 URL:", songUrl);
|
||||
return songUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongId = async (keyword) => {
|
||||
try {
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8" +
|
||||
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword.toString();
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("酷我解灰歌曲 ID:", songId);
|
||||
const url = encryptQuery
|
||||
? "http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
"corp=kuwo&source=kwplayer_ar_8.5.5.0_apk_keluze.apk&p2p=1&type=convert_url2&sig=0&format=mp3" +
|
||||
"&rid=" +
|
||||
songId,
|
||||
)
|
||||
: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_" +
|
||||
songId;
|
||||
const result = await axios.get(url, { "user-agent": "okhttp/3.10.0" });
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
console.info("酷我解灰歌曲 URL:", urlMatch);
|
||||
return urlMatch;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取给定关键字的音乐 URL
|
||||
* @param {string} keyword - 关键字
|
||||
* @returns {Promise<?string>} 如果找到,则解析为音乐 URL 的 Promise;如果未找到,则为 null
|
||||
* @throws {Error} 抛出错误
|
||||
* @returns {Promise<?string>} 音乐 URL
|
||||
*/
|
||||
const getNeteaseMusicUrl = async (keyword) => {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
return result.data.data.url;
|
||||
try {
|
||||
const [kuwoSongUrl, miguSongUrl] = await Promise.all([
|
||||
getKuwoSongUrl(keyword),
|
||||
getMiguSongUrl(keyword),
|
||||
]);
|
||||
if (kuwoSongUrl) {
|
||||
return kuwoSongUrl;
|
||||
}
|
||||
if (miguSongUrl) {
|
||||
return miguSongUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取解灰 URL 全部失败:", error);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getNeteaseMusicUrl;
|
||||
|
||||
574
electron/main/utils/kwDES.js
Normal file
@@ -0,0 +1,574 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*
|
||||
Thanks to
|
||||
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
|
||||
https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
|
||||
*/
|
||||
|
||||
const Long = (n) => {
|
||||
const bN = BigInt(n);
|
||||
|
||||
return {
|
||||
low: Number(bN),
|
||||
valueOf: () => bN.valueOf(),
|
||||
toString: () => bN.toString(),
|
||||
not: () => Long(~bN),
|
||||
isNegative: () => bN < 0,
|
||||
or: (x) => Long(bN | BigInt(x)),
|
||||
and: (x) => Long(bN & BigInt(x)),
|
||||
xor: (x) => Long(bN ^ BigInt(x)),
|
||||
equals: (x) => bN === BigInt(x),
|
||||
multiply: (x) => Long(bN * BigInt(x)),
|
||||
shiftLeft: (x) => Long(bN << BigInt(x)),
|
||||
shiftRight: (x) => Long(bN >> BigInt(x)),
|
||||
};
|
||||
};
|
||||
|
||||
const range = (n) => Array.from(new Array(n).keys());
|
||||
const power = (base, index) =>
|
||||
Array(index)
|
||||
.fill(null)
|
||||
.reduce((result) => result.multiply(base), Long(1));
|
||||
const LongArray = (...array) => array.map((n) => (n === -1 ? Long(-1, -1) : Long(n)));
|
||||
|
||||
// EXPANSION
|
||||
const arrayE = LongArray(
|
||||
31,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
-1,
|
||||
-1,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
-1,
|
||||
-1,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
-1,
|
||||
-1,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
-1,
|
||||
-1,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
-1,
|
||||
-1,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
// INITIAL_PERMUTATION
|
||||
const arrayIP = LongArray(
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
63,
|
||||
55,
|
||||
47,
|
||||
39,
|
||||
31,
|
||||
23,
|
||||
15,
|
||||
7,
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
);
|
||||
|
||||
// INVERSE_PERMUTATION
|
||||
const arrayIP_1 = LongArray(
|
||||
39,
|
||||
7,
|
||||
47,
|
||||
15,
|
||||
55,
|
||||
23,
|
||||
63,
|
||||
31,
|
||||
38,
|
||||
6,
|
||||
46,
|
||||
14,
|
||||
54,
|
||||
22,
|
||||
62,
|
||||
30,
|
||||
37,
|
||||
5,
|
||||
45,
|
||||
13,
|
||||
53,
|
||||
21,
|
||||
61,
|
||||
29,
|
||||
36,
|
||||
4,
|
||||
44,
|
||||
12,
|
||||
52,
|
||||
20,
|
||||
60,
|
||||
28,
|
||||
35,
|
||||
3,
|
||||
43,
|
||||
11,
|
||||
51,
|
||||
19,
|
||||
59,
|
||||
27,
|
||||
34,
|
||||
2,
|
||||
42,
|
||||
10,
|
||||
50,
|
||||
18,
|
||||
58,
|
||||
26,
|
||||
33,
|
||||
1,
|
||||
41,
|
||||
9,
|
||||
49,
|
||||
17,
|
||||
57,
|
||||
25,
|
||||
32,
|
||||
0,
|
||||
40,
|
||||
8,
|
||||
48,
|
||||
16,
|
||||
56,
|
||||
24,
|
||||
);
|
||||
|
||||
// ROTATES
|
||||
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
|
||||
const arrayLsMask = LongArray(0, 0x100001, 0x300003);
|
||||
const arrayMask = range(64).map((n) => power(2, n));
|
||||
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1);
|
||||
|
||||
// PERMUTATION
|
||||
const arrayP = LongArray(
|
||||
15,
|
||||
6,
|
||||
19,
|
||||
20,
|
||||
28,
|
||||
11,
|
||||
27,
|
||||
16,
|
||||
0,
|
||||
14,
|
||||
22,
|
||||
25,
|
||||
4,
|
||||
17,
|
||||
30,
|
||||
9,
|
||||
1,
|
||||
7,
|
||||
23,
|
||||
13,
|
||||
31,
|
||||
26,
|
||||
2,
|
||||
8,
|
||||
18,
|
||||
12,
|
||||
29,
|
||||
5,
|
||||
21,
|
||||
10,
|
||||
3,
|
||||
24,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE1
|
||||
const arrayPC_1 = LongArray(
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE2
|
||||
const arrayPC_2 = LongArray(
|
||||
13,
|
||||
16,
|
||||
10,
|
||||
23,
|
||||
0,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
2,
|
||||
27,
|
||||
14,
|
||||
5,
|
||||
20,
|
||||
9,
|
||||
-1,
|
||||
-1,
|
||||
22,
|
||||
18,
|
||||
11,
|
||||
3,
|
||||
25,
|
||||
7,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
6,
|
||||
26,
|
||||
19,
|
||||
12,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
40,
|
||||
51,
|
||||
30,
|
||||
36,
|
||||
46,
|
||||
54,
|
||||
-1,
|
||||
-1,
|
||||
29,
|
||||
39,
|
||||
50,
|
||||
44,
|
||||
32,
|
||||
47,
|
||||
-1,
|
||||
-1,
|
||||
43,
|
||||
48,
|
||||
38,
|
||||
55,
|
||||
33,
|
||||
52,
|
||||
-1,
|
||||
-1,
|
||||
45,
|
||||
41,
|
||||
49,
|
||||
35,
|
||||
28,
|
||||
31,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
const matrixNSBox = [
|
||||
[
|
||||
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12, 7,
|
||||
8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4,
|
||||
2, 11, 14, 1, 7, 8, 13,
|
||||
],
|
||||
[
|
||||
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13, 6, 4,
|
||||
1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7,
|
||||
1, 10, 12, 14, 2, 5, 9,
|
||||
],
|
||||
[
|
||||
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7, 12,
|
||||
5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9,
|
||||
0, 14, 3, 10, 7, 1, 12,
|
||||
],
|
||||
[
|
||||
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5, 14, 10,
|
||||
13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5,
|
||||
6, 12, 11, 3, 8, 9, 14,
|
||||
],
|
||||
[
|
||||
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15, 5,
|
||||
6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4,
|
||||
12, 7, 10, 9, 1, 13, 6, 3,
|
||||
],
|
||||
[
|
||||
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4, 10,
|
||||
8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2,
|
||||
12, 14, 7, 5, 10, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7, 8,
|
||||
13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7,
|
||||
8, 12, 15, 10, 7, 6, 12,
|
||||
],
|
||||
[
|
||||
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14, 13,
|
||||
1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8,
|
||||
7, 11, 0, 4, 13, 2, 11,
|
||||
],
|
||||
];
|
||||
|
||||
const bitTransform = (arrInt, n, l) => {
|
||||
// int[], int, long : long
|
||||
let l2 = Long(0);
|
||||
range(n).forEach((i) => {
|
||||
if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0)) return;
|
||||
l2 = l2.or(arrayMask[i]);
|
||||
});
|
||||
return l2;
|
||||
};
|
||||
|
||||
const DES64 = (longs, l) => {
|
||||
const pR = range(8).map(() => Long(0));
|
||||
const pSource = [Long(0), Long(0)];
|
||||
let L = Long(0);
|
||||
let R = Long(0);
|
||||
let out = bitTransform(arrayIP, 64, l);
|
||||
pSource[0] = out.and(0xffffffff);
|
||||
pSource[1] = out.and(-4294967296).shiftRight(32);
|
||||
|
||||
range(16).forEach((i) => {
|
||||
let SOut = Long(0);
|
||||
|
||||
R = Long(pSource[1]);
|
||||
R = bitTransform(arrayE, 64, R);
|
||||
R = R.xor(longs[i]);
|
||||
range(8).forEach((j) => {
|
||||
pR[j] = R.shiftRight(j * 8).and(255);
|
||||
});
|
||||
range(8)
|
||||
.reverse()
|
||||
.forEach((sbi) => {
|
||||
SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]);
|
||||
});
|
||||
R = bitTransform(arrayP, 32, SOut);
|
||||
L = Long(pSource[0]);
|
||||
pSource[0] = Long(pSource[1]);
|
||||
pSource[1] = L.xor(R);
|
||||
});
|
||||
pSource.reverse();
|
||||
out = pSource[1].shiftLeft(32).and(-4294967296).or(pSource[0].and(0xffffffff));
|
||||
out = bitTransform(arrayIP_1, 64, out);
|
||||
return out;
|
||||
};
|
||||
|
||||
const subKeys = (l, longs, n) => {
|
||||
// long, long[], int
|
||||
let l2 = bitTransform(arrayPC_1, 56, l);
|
||||
range(16).forEach((i) => {
|
||||
l2 = l2
|
||||
.and(arrayLsMask[arrayLs[i]])
|
||||
.shiftLeft(28 - arrayLs[i])
|
||||
.or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]));
|
||||
longs[i] = bitTransform(arrayPC_2, 64, l2);
|
||||
});
|
||||
if (n === 1) {
|
||||
range(8).forEach((j) => {
|
||||
[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const crypt = (msg, key, mode) => {
|
||||
// 处理密钥块
|
||||
let l = Long(0);
|
||||
range(8).forEach((i) => {
|
||||
l = Long(key[i])
|
||||
.shiftLeft(i * 8)
|
||||
.or(l);
|
||||
});
|
||||
|
||||
const j = Math.floor(msg.length / 8);
|
||||
// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
|
||||
|
||||
const arrLong1 = range(16).map(() => Long(0));
|
||||
subKeys(l, arrLong1, mode);
|
||||
|
||||
// arrLong2 存放的是前部分的明文
|
||||
const arrLong2 = range(j).map(() => Long(0));
|
||||
|
||||
range(j).forEach((m) => {
|
||||
range(8).forEach((n) => {
|
||||
arrLong2[m] = Long(msg[n + m * 8])
|
||||
.shiftLeft(n * 8)
|
||||
.or(arrLong2[m]);
|
||||
});
|
||||
});
|
||||
|
||||
// 用于存放密文
|
||||
const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0));
|
||||
|
||||
// 计算前部的数据块(除了最后一部分)
|
||||
range(j).forEach((i1) => {
|
||||
arrLong3[i1] = DES64(arrLong1, arrLong2[i1]);
|
||||
});
|
||||
|
||||
// 保存多出来的字节
|
||||
const arrByte1 = msg.slice(j * 8);
|
||||
let l2 = Long(0);
|
||||
|
||||
range(msg.length % 8).forEach((i1) => {
|
||||
l2 = Long(arrByte1[i1])
|
||||
.shiftLeft(i1 * 8)
|
||||
.or(l2);
|
||||
});
|
||||
|
||||
// 计算多出的那一位(最后一位)
|
||||
if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2); // 解密不需要
|
||||
|
||||
// 将密文转为字节型
|
||||
const arrByte2 = range(8 * arrLong3.length).map(() => 0);
|
||||
let i4 = 0;
|
||||
arrLong3.forEach((l3) => {
|
||||
range(8).forEach((i6) => {
|
||||
arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low;
|
||||
i4 += 1;
|
||||
});
|
||||
});
|
||||
return Buffer.from(arrByte2);
|
||||
};
|
||||
|
||||
const SECRET_KEY = Buffer.from("ylzsxkwm");
|
||||
export const encrypt = (msg) => crypt(msg, SECRET_KEY, 0);
|
||||
export const decrypt = (msg) => crypt(msg, SECRET_KEY, 1);
|
||||
export const encryptQuery = (query) => encrypt(Buffer.from(query)).toString("base64");
|
||||
@@ -3,16 +3,17 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="%RENDERER_VITE_SITE_LOGO%" />
|
||||
<link rel="apple-touch-icon" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="bookmark" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
|
||||
28
nginx.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 7899;
|
||||
listen [::]:7899;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_buffers 16 32k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 128k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
20
package.json
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.2",
|
||||
"version": "2.0.0-beta.5",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.12.0",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "chcp 65001 && electron-vite dev --watch",
|
||||
"dev": "electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
@@ -24,10 +26,11 @@
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.13.5",
|
||||
"NeteaseCloudMusicApi": "^4.14.0",
|
||||
"axios": "^1.4.0",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
@@ -39,7 +42,6 @@
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"plyr": "^3.7.8",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
@@ -49,13 +51,14 @@
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"ajv": "^8.12.0",
|
||||
"electron": "^27.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.27",
|
||||
"electron-vite": "^1.0.29",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"naive-ui": "^2.34.4",
|
||||
"naive-ui": "^2.36.0",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
@@ -63,6 +66,7 @@
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vue": "^3.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
2456
pnpm-lock.yaml
generated
BIN
public/images/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/images/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icons/favicon-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/images/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/icons/favicon-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/images/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
25
public/images/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2400 4444 c-14 -2 -63 -8 -110 -14 -407 -50 -737 -184 -1077 -436
|
||||
-124 -93 -348 -309 -406 -394 -100 -144 -190 -286 -223 -352 -101 -200 -199
|
||||
-506 -226 -707 -17 -127 -22 -436 -8 -546 40 -338 161 -687 327 -942 138 -213
|
||||
226 -319 372 -450 237 -211 471 -353 761 -460 147 -54 156 -57 330 -88 383
|
||||
-71 516 -67 965 25 76 16 288 91 370 132 28 13 66 31 85 38 39 16 158 88 270
|
||||
164 242 164 474 410 628 668 254 422 362 920 308 1408 -16 139 -23 172 -73
|
||||
344 -80 273 -188 496 -346 711 -104 143 -302 348 -404 421 l-48 34 -192 0
|
||||
c-184 0 -193 -1 -203 -21 -8 -14 -11 -271 -10 -882 1 -474 -1 -896 -5 -937
|
||||
-29 -306 -228 -596 -507 -737 -207 -104 -426 -126 -663 -66 -168 43 -300 125
|
||||
-453 283 -118 122 -203 310 -223 495 -10 86 -3 271 11 320 41 135 58 180 95
|
||||
248 103 193 330 376 545 441 157 47 364 49 513 5 38 -11 78 -19 90 -17 l22 3
|
||||
-3 635 c-2 349 -7 638 -10 642 -22 22 -413 47 -502 32z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
public/images/pic/pointer.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
155
src/App.vue
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<Provider>
|
||||
<!-- 主框架 -->
|
||||
<n-layout :class="['all-layout', status.showFullPlayer ? 'full-player' : null]">
|
||||
<n-layout :class="['all-layout', { 'full-player': showFullPlayer }]">
|
||||
<!-- 导航栏 -->
|
||||
<n-layout-header bordered>
|
||||
<MainNav />
|
||||
<TitleBar v-if="checkPlatform.electron()" />
|
||||
</n-layout-header>
|
||||
<!-- 主内容 -->
|
||||
<!-- 主内容 - 有侧边栏 -->
|
||||
<n-layout
|
||||
v-if="showSider"
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': Object.keys(music.playSongData)?.length && status.showPlayBar,
|
||||
'player-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
}"
|
||||
position="absolute"
|
||||
has-sider
|
||||
>
|
||||
<!-- 侧边栏 -->
|
||||
<n-layout-sider
|
||||
class="main-sider"
|
||||
:collapsed="status.asideMenuCollapsed"
|
||||
:collapsed="asideMenuCollapsed"
|
||||
:native-scrollbar="false"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
class="main-sider"
|
||||
show-trigger="bar"
|
||||
collapse-mode="width"
|
||||
bordered
|
||||
@collapse="status.asideMenuCollapsed = true"
|
||||
@expand="status.asideMenuCollapsed = false"
|
||||
@collapse="asideMenuCollapsed = true"
|
||||
@expand="asideMenuCollapsed = false"
|
||||
>
|
||||
<div class="sider-all">
|
||||
<Menu />
|
||||
@@ -35,32 +35,29 @@
|
||||
</n-layout-sider>
|
||||
<!-- 页面区 -->
|
||||
<n-layout :native-scrollbar="false" embedded>
|
||||
<main id="main-layout" class="main-layout">
|
||||
{{ music.getplaySongData }}
|
||||
<!-- 回顶 -->
|
||||
<n-back-top bottom="110">
|
||||
<n-icon size="26">
|
||||
<SvgIcon icon="chevron-up" />
|
||||
</n-icon>
|
||||
</n-back-top>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</main>
|
||||
<MainLayout />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
<!-- 主内容 - 无侧边栏 -->
|
||||
<n-layout-content
|
||||
v-else
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
}"
|
||||
:native-scrollbar="false"
|
||||
position="absolute"
|
||||
embedded
|
||||
>
|
||||
<MainLayout />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
<!-- 主播放器 -->
|
||||
<MainControl />
|
||||
<!-- 全屏播放器 -->
|
||||
<FullPlayer />
|
||||
<!-- 全局播放列表 -->
|
||||
<n-config-provider v-if="status.showFullPlayer" :theme="darkTheme">
|
||||
<n-config-provider v-if="showFullPlayer" :theme="darkTheme">
|
||||
<Playlist />
|
||||
</n-config-provider>
|
||||
<Playlist v-else />
|
||||
@@ -81,18 +78,83 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { darkTheme } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { darkTheme, NButton } from "naive-ui";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
import globalShortcut from "@/utils/globalShortcut";
|
||||
import globalEvents from "@/utils/globalEvents";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { autoPlay, showSider, autoSignIn } = storeToRefs(settings);
|
||||
const { showPlayBar, asideMenuCollapsed, showFullPlayer } = storeToRefs(status);
|
||||
|
||||
// 公告数据
|
||||
const annShow =
|
||||
import.meta.env.RENDERER_VITE_ANN_TITLE && import.meta.env.RENDERER_VITE_ANN_CONTENT
|
||||
? true
|
||||
: false;
|
||||
const annType = import.meta.env.RENDERER_VITE_ANN_TYPE;
|
||||
const annTitle = import.meta.env.RENDERER_VITE_ANN_TITLE;
|
||||
const annContene = import.meta.env.RENDERER_VITE_ANN_CONTENT;
|
||||
const annDuration = Number(import.meta.env.RENDERER_VITE_ANN_DURATION);
|
||||
|
||||
// PWA
|
||||
if ("serviceWorker" in navigator) {
|
||||
// 更新完成提醒
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (checkPlatform.electron()) {
|
||||
$notification.create({
|
||||
title: "🎉 有更新啦",
|
||||
content: "检测到软件内资源有更新,是否重新启动软件以应用更新?",
|
||||
meta: "当前版本 v " + (packageJson.version || "1.0.0"),
|
||||
action: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: "primary",
|
||||
onClick: () => {
|
||||
electron.ipcRenderer.send("window-relaunch");
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => "更新",
|
||||
},
|
||||
),
|
||||
onAfterLeave: () => {
|
||||
$message.info("已取消本次更新,更新将在下次启动软件后生效", {
|
||||
duration: 6000,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.info("站点资源有更新,请刷新以应用更新");
|
||||
$message.info("站点资源有更新,请刷新以应用更新", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示公告
|
||||
const showAnnouncements = () => {
|
||||
if (annShow) {
|
||||
$notification[annType]({
|
||||
content: annTitle,
|
||||
meta: annContene,
|
||||
duration: annDuration,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 网络无法连接
|
||||
const canNotConnect = (error) => {
|
||||
@@ -102,36 +164,49 @@ const canNotConnect = (error) => {
|
||||
title: "网络连接错误",
|
||||
content: "网络连接错误,请检查您当前的网络状态",
|
||||
positiveText: "重试",
|
||||
negativeText: "前往本地歌曲",
|
||||
negativeText: checkPlatform.electron() ? "前往本地歌曲" : "取消",
|
||||
onPositiveClick: () => {
|
||||
location.reload();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
router.push("/local");
|
||||
if (checkPlatform.electron()) router.push("/local");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 网页端键盘事件
|
||||
const handleKeyUp = (event) => {
|
||||
globalShortcut(event, router);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 挂载方法
|
||||
window.$canNotConnect = canNotConnect;
|
||||
// 主播放器
|
||||
initPlayer(settings.autoPlay);
|
||||
await initPlayer(autoPlay.value);
|
||||
// 全局事件
|
||||
globalEvents(router);
|
||||
// 键盘监听
|
||||
window.addEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) {
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
// 自动签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 显示公告
|
||||
showAnnouncements();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) window.removeEventListener("keyup", handleKeyUp);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.all-layout {
|
||||
height: 100%;
|
||||
transition: transform 0.3s;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
.n-layout-header {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
@@ -142,9 +217,6 @@ onUnmounted(() => {
|
||||
.body-layout {
|
||||
top: 60px;
|
||||
transition: bottom 0.3s;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
.main-sider {
|
||||
:deep(.n-scrollbar-content) {
|
||||
height: 100%;
|
||||
@@ -156,12 +228,13 @@ onUnmounted(() => {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.main-layout {
|
||||
padding: 24px;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
&.full-player {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
136
src/api/dj.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import axios from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 电台部分
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取电台 - 分类
|
||||
*/
|
||||
export const getDjCatelist = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/catelist",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐
|
||||
*/
|
||||
export const getDjRecommend = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台个性推荐
|
||||
*/
|
||||
export const getDjPersonalRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/personalize/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐类型
|
||||
*/
|
||||
export const getDjCategoryRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/category/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 私人 DJ
|
||||
*/
|
||||
export const getPrivateDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/aidj/content/rcmd",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 类别热门电台
|
||||
* @param {string} cateId - 类别 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getRadioHot = (cateId, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/radio/hot",
|
||||
params: {
|
||||
cateId,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 分类推荐
|
||||
* @param {string} type - 类别 id
|
||||
*/
|
||||
export const getRecType = (type) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend/type",
|
||||
params: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 详情
|
||||
* @param {string} rid - 电台 的 id
|
||||
*/
|
||||
export const getDjDetail = (rid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/detail",
|
||||
params: {
|
||||
rid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 节目
|
||||
* @param {string} rid - 电台 的 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getDjProgram = (rid, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/program",
|
||||
params: {
|
||||
rid,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 订阅
|
||||
* @param {number} rid - 电台 的 id
|
||||
* @param {number} t - 操作类型,1为收藏,0为取消收藏
|
||||
*/
|
||||
export const likeDj = (rid, t) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sub",
|
||||
params: {
|
||||
rid,
|
||||
t,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -119,6 +119,19 @@ export const getUserMv = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户电台的订阅列表
|
||||
*/
|
||||
export const getUserDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sublist",
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户喜欢的音乐列表
|
||||
* @param {string} uid 用户的id
|
||||
@@ -151,80 +164,6 @@ export const setLikeSong = (id, like = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户云盘数据
|
||||
* @param {number} [limit=30] - 返回数量,默认30
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getCloud = (limit = 30, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/user/cloud",
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户云盘歌曲删除
|
||||
* @param {string} id - 歌曲的id
|
||||
*/
|
||||
export const setCloudDel = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/user/cloud/del",
|
||||
params: {
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 云盘歌曲信息匹配纠正
|
||||
* @param {string} uid - 用户id
|
||||
* @param {string} sid - 原歌曲id
|
||||
* @param {string} asid - 要匹配的歌曲id
|
||||
*/
|
||||
export const setCloudMatch = (uid, sid, asid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/cloud/match",
|
||||
params: {
|
||||
uid,
|
||||
sid,
|
||||
asid,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户云盘上传
|
||||
* @param {File} file - 要上传的文件
|
||||
*/
|
||||
export const upCloudSong = (file, onUploadProgress) => {
|
||||
const formData = new FormData();
|
||||
formData.append("songFile", file);
|
||||
return axios({
|
||||
url: "/cloud",
|
||||
method: "POST",
|
||||
hiddenBar: true,
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
timeout: 200000,
|
||||
onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
|
||||
@@ -63,3 +63,37 @@ export const getSimiVideo = (mvid) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏视频
|
||||
* @param {number} t - 操作类型,1为收藏,2为取消收藏
|
||||
* @param {number} id - 视频id
|
||||
*/
|
||||
export const likeVideo = (t, id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/video/sub",
|
||||
params: {
|
||||
t,
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏 MV
|
||||
* @param {number} t - 操作类型,1为收藏,2为取消收藏
|
||||
* @param {number} mvid - MV id
|
||||
*/
|
||||
export const likeMv = (t, mvid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/mv/sub",
|
||||
params: {
|
||||
t,
|
||||
mvid,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"album": "M12 11a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m0 5.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5s4.5 2 4.5 4.5s-2 4.5-4.5 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"chevron-up": "M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z",
|
||||
"chevron-down": "M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z",
|
||||
"play": "M9.525 18.025q-.5.325-1.012.038T8 17.175V6.825q0-.6.513-.888t1.012.038l8.15 5.175q.45.3.45.85t-.45.85l-8.15 5.175Z",
|
||||
"play-circle": "M10 16.5v-9l6 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"play-next": "m10 16.5l6-4.5l-6-4.5M22 12c0-5.54-4.46-10-10-10c-1.17 0-2.3.19-3.38.56l.7 1.94c.85-.34 1.74-.53 2.68-.53c4.41 0 8.03 3.62 8.03 8.03c0 4.41-3.62 8.03-8.03 8.03c-4.41 0-8.03-3.62-8.03-8.03c0-.94.19-1.88.53-2.72l-1.94-.66C2.19 9.7 2 10.83 2 12c0 5.54 4.46 10 10 10s10-4.46 10-10M5.47 3.97c.85 0 1.53.71 1.53 1.5C7 6.32 6.32 7 5.47 7c-.79 0-1.5-.68-1.5-1.53c0-.79.71-1.5 1.5-1.5Z",
|
||||
@@ -22,6 +23,7 @@
|
||||
"chevron-right": "M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.42Z",
|
||||
"account-circle": "M12 19.2c-2.5 0-4.71-1.28-6-3.2c.03-2 4-3.1 6-3.1s5.97 1.1 6 3.1a7.232 7.232 0 0 1-6 3.2M12 5a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m0-3A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10c0-5.53-4.5-10-10-10Z",
|
||||
"menu-down": "m7 10l5 5l5-5H7Z",
|
||||
"menu": "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5s1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5S5.5 6.83 5.5 6S4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5s1.5-.68 1.5-1.5s-.67-1.5-1.5-1.5zM8 19h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1s.45 1 1 1zm0-6h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1s.45 1 1 1zM7 6c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1z",
|
||||
"calendar-check": "M19 19H5V8h14m0-5h-1V1h-2v2H8V1H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-2.47 8.06L15.47 10l-4.88 4.88l-2.12-2.12l-1.06 1.06L10.59 17l5.94-5.94Z",
|
||||
"calendar-badge": "M19.5 16c-1.9 0-3.5 1.6-3.5 3.5s1.6 3.5 3.5 3.5s3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5m-5.29 5H5a2 2 0 0 1-2-2V5c0-1.11.89-2 2-2h1V1h2v2h8V1h2v2h1a2 2 0 0 1 2 2v9.21c-.5-.13-1-.21-1.5-.21c-.17 0-.33 0-.5.03V8H5v11h9.03c-.03.17-.03.33-.03.5c0 .5.08 1 .21 1.5Z",
|
||||
"round-wb-sunny": "m6.05 4.14l-.39-.39a.993.993 0 0 0-1.4 0l-.01.01a.984.984 0 0 0 0 1.4l.39.39c.39.39 1.01.39 1.4 0l.01-.01a.984.984 0 0 0 0-1.4zM3.01 10.5H1.99c-.55 0-.99.44-.99.99v.01c0 .55.44.99.99.99H3c.56.01 1-.43 1-.98v-.01c0-.56-.44-1-.99-1zm9-9.95H12c-.56 0-1 .44-1 .99v.96c0 .55.44.99.99.99H12c.56.01 1-.43 1-.98v-.97c0-.55-.44-.99-.99-.99zm7.74 3.21c-.39-.39-1.02-.39-1.41-.01l-.39.39a.984.984 0 0 0 0 1.4l.01.01c.39.39 1.02.39 1.4 0l.39-.39a.984.984 0 0 0 0-1.4zm-1.81 15.1l.39.39a.996.996 0 1 0 1.41-1.41l-.39-.39a.993.993 0 0 0-1.4 0c-.4.4-.4 1.02-.01 1.41zM20 11.49v.01c0 .55.44.99.99.99H22c.55 0 .99-.44.99-.99v-.01c0-.55-.44-.99-.99-.99h-1.01c-.55 0-.99.44-.99.99zM12 5.5c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6s-2.69-6-6-6zm-.01 16.95H12c.55 0 .99-.44.99-.99v-.96c0-.55-.44-.99-.99-.99h-.01c-.55 0-.99.44-.99.99v.96c0 .55.44.99.99.99zm-7.74-3.21c.39.39 1.02.39 1.41 0l.39-.39a.993.993 0 0 0 0-1.4l-.01-.01a.996.996 0 0 0-1.41 0l-.39.39c-.38.4-.38 1.02.01 1.41z",
|
||||
@@ -47,6 +49,7 @@
|
||||
"favorite-rounded": "M12 20.325q-.35 0-.713-.125t-.637-.4l-1.725-1.575q-2.65-2.425-4.788-4.813T2 8.15Q2 5.8 3.575 4.225T7.5 2.65q1.325 0 2.5.562t2 1.538q.825-.975 2-1.538t2.5-.562q2.35 0 3.925 1.575T22 8.15q0 2.875-2.125 5.275T15.05 18.25l-1.7 1.55q-.275.275-.637.4t-.713.125Z",
|
||||
"history": "M12 21q-3.15 0-5.575-1.913T3.275 14.2q-.1-.375.15-.687t.675-.363q.4-.05.725.15t.45.6q.6 2.25 2.475 3.675T12 19q2.925 0 4.963-2.038T19 12q0-2.925-2.038-4.963T12 5q-1.725 0-3.225.8T6.25 8H8q.425 0 .713.288T9 9q0 .425-.288.713T8 10H4q-.425 0-.713-.288T3 9V5q0-.425.288-.713T4 4q.425 0 .713.288T5 5v1.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924q1.212 1.213 1.925 2.85T21 12q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 21Zm1-9.4l2.5 2.5q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-2.8-2.8q-.15-.15-.225-.337T11 11.975V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v3.6Z",
|
||||
"delete": "M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12Z",
|
||||
"delete-sweep": "M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3zM14 5h-3l-1-1H6L5 5H2v2h12z",
|
||||
"fire": "M17.66 11.2c-.23-.3-.51-.56-.77-.82c-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32c-2.59 2.08-3.61 5.75-2.39 8.9c.04.1.08.2.08.33c0 .22-.15.42-.35.5c-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5c.14.6.41 1.2.71 1.73c1.08 1.73 2.95 2.97 4.96 3.22c2.14.27 4.43-.12 6.07-1.6c1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6c-1.12.4-2.24-.16-2.9-.82c1.19-.28 1.9-1.16 2.11-2.05c.17-.8-.15-1.46-.28-2.23c-.12-.74-.1-1.37.17-2.06c.19.38.39.76.63 1.06c.77 1 1.98 1.44 2.24 2.8c.04.14.06.28.06.43c.03.82-.33 1.72-.93 2.27Z",
|
||||
"search-rounded": "M9.5 16q-2.725 0-4.612-1.888T3 9.5q0-2.725 1.888-4.612T9.5 3q2.725 0 4.612 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l5.6 5.6q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-5.6-5.6q-.75.6-1.725.95T9.5 16Zm0-2q1.875 0 3.188-1.313T14 9.5q0-1.875-1.313-3.188T9.5 5Q7.625 5 6.312 6.313T5 9.5q0 1.875 1.313 3.188T9.5 14Z",
|
||||
"search-off": "m7 17.7l1.4 1.425q.15.15.35.15t.35-.15q.15-.15.15-.363T9.1 18.4L7.7 17l1.425-1.425q.15-.15.15-.35t-.15-.35q-.15-.15-.35-.15t-.35.15L7 16.3l-1.425-1.425q-.15-.15-.35-.15t-.35.15q-.15.15-.15.35t.15.35L6.3 17l-1.425 1.425q-.15.15-.15.35t.15.35q.15.15.35.15t.35-.15L7 17.7ZM7 22q-2.075 0-3.538-1.463T2 17q0-2.075 1.463-3.538T7 12q2.075 0 3.538 1.463T12 17q0 2.075-1.463 3.538T7 22Zm7.2-7.4q-.3-.325-.638-.663T12.9 13.3q.95-.6 1.525-1.6T15 9.5q0-1.875-1.313-3.188T10.5 5Q8.625 5 7.312 6.313T6 9.5q0 .15.013.288t.037.287q-.45.05-.987.2t-.963.35q-.05-.275-.075-.55T4 9.5q0-2.725 1.888-4.612T10.5 3q2.725 0 4.612 1.888T17 9.5q0 1.075-.338 2.038t-.937 1.762l5.575 5.6q.275.275.288.688t-.288.712q-.275.275-.7.275t-.7-.275l-5.7-5.7Z",
|
||||
@@ -80,5 +83,11 @@
|
||||
"download": "M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z",
|
||||
"radio": "M3.24 6.15C2.51 6.43 2 7.17 2 8v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.89-2-2-2H8.3l8.26-3.34L15.88 1L3.24 6.15zM7 20c-1.66 0-3-1.34-3-3s1.34-3 3-3s3 1.34 3 3s-1.34 3-3 3zm13-8h-2v-2h-2v2H4V8h16v4z",
|
||||
"person-add": "M15.39 14.56C13.71 13.7 11.53 13 9 13s-4.71.7-6.39 1.56A2.97 2.97 0 0 0 1 17.22V20h16v-2.78c0-1.12-.61-2.15-1.61-2.66zM9 12c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4zm11-3V7c0-.55-.45-1-1-1s-1 .45-1 1v2h-2c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z",
|
||||
"person-remove": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4s4-1.79 4-4zM2 18v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4s-8 1.34-8 4zm16-8h4c.55 0 1 .45 1 1s-.45 1-1 1h-4c-.55 0-1-.45-1-1s.45-1 1-1z"
|
||||
"person-remove": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4s4-1.79 4-4zM2 18v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4s-8 1.34-8 4zm16-8h4c.55 0 1 .45 1 1s-.45 1-1 1h-4c-.55 0-1-.45-1-1s.45-1 1-1z",
|
||||
"github": "M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z",
|
||||
"phone": "M16 1H8C6.34 1 5 2.34 5 4v16c0 1.66 1.34 3 3 3h8c1.66 0 3-1.34 3-3V4c0-1.66-1.34-3-3-3zm1 17H7V4h10v14zm-3 3h-4v-1h4v1z",
|
||||
"password": "M2 17h20v2H2v-2zm1.15-4.05L4 11.47l.85 1.48l1.3-.75l-.85-1.48H7v-1.5H5.3l.85-1.47L4.85 7L4 8.47L3.15 7l-1.3.75l.85 1.47H1v1.5h1.7l-.85 1.48l1.3.75zm6.7-.75l1.3.75l.85-1.48l.85 1.48l1.3-.75l-.85-1.48H15v-1.5h-1.7l.85-1.47l-1.3-.75L12 8.47L11.15 7l-1.3.75l.85 1.47H9v1.5h1.7l-.85 1.48zM23 9.22h-1.7l.85-1.47l-1.3-.75L20 8.47L19.15 7l-1.3.75l.85 1.47H17v1.5h1.7l-.85 1.48l1.3.75l.85-1.48l.85 1.48l1.3-.75l-.85-1.48H23v-1.5z",
|
||||
"star": "m12 17.27l4.15 2.51c.76.46 1.69-.22 1.49-1.08l-1.1-4.72l3.67-3.18c.67-.58.31-1.68-.57-1.75l-4.83-.41l-1.89-4.46c-.34-.81-1.5-.81-1.84 0L9.19 8.63l-4.83.41c-.88.07-1.24 1.17-.57 1.75l3.67 3.18l-1.1 4.72c-.2.86.73 1.54 1.49 1.08l4.15-2.5z",
|
||||
"record": "M17 18.25v3.25H7v-3.25c0-1.38 2.24-2.5 5-2.5s5 1.12 5 2.5M12 5.5a6.5 6.5 0 0 1 6.5 6.5c0 1.25-.35 2.42-.96 3.41L16 14.04c.32-.61.5-1.31.5-2.04c0-2.5-2-4.5-4.5-4.5s-4.5 2-4.5 4.5c0 .73.18 1.43.5 2.04l-1.54 1.37c-.61-.99-.96-2.16-.96-3.41A6.5 6.5 0 0 1 12 5.5m0-4A10.5 10.5 0 0 1 22.5 12c0 2.28-.73 4.39-1.96 6.11l-1.5-1.35c.92-1.36 1.46-3 1.46-4.76A8.5 8.5 0 0 0 12 3.5A8.5 8.5 0 0 0 3.5 12c0 1.76.54 3.4 1.46 4.76l-1.5 1.35A10.473 10.473 0 0 1 1.5 12A10.5 10.5 0 0 1 12 1.5m0 8a2.5 2.5 0 0 1 2.5 2.5a2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 9.5 12A2.5 2.5 0 0 1 12 9.5Z",
|
||||
"storage": "M4 20h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2m0-3h2v2H4zM2 6c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2m4 1H4V5h2zm-2 7h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2m0-3h2v2H4z"
|
||||
}
|
||||
|
||||
@@ -64,13 +64,22 @@
|
||||
<div class="cover-content">
|
||||
<n-text class="name">{{ item.name }}</n-text>
|
||||
<!-- 创建者 -->
|
||||
<n-text v-if="item.creator" class="creator" depth="3">
|
||||
<n-text v-if="item?.creator" class="creator" depth="3">
|
||||
{{ item.creator?.nickname || item.creator || "未知" }}
|
||||
</n-text>
|
||||
<!-- 电台简介 -->
|
||||
<n-text v-if="item?.rcmdText" class="creator" depth="3">
|
||||
{{ item.rcmdText || "未知简介" }}
|
||||
</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="item.artists" class="artists">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" class="ar">
|
||||
{{ ar.name }}
|
||||
<div v-if="item.artists && type !== 'dj'" class="artists">
|
||||
<n-text
|
||||
v-for="ar in item.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
>
|
||||
{{ ar.name || ar.userName }}
|
||||
</n-text>
|
||||
</div>
|
||||
<!-- 歌曲数量 -->
|
||||
@@ -193,6 +202,14 @@ const jumpLink = (data, type) => {
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "dj":
|
||||
router.push({
|
||||
path: "/dj",
|
||||
query: {
|
||||
id: data?.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -255,7 +272,9 @@ const jumpLink = (data, type) => {
|
||||
top: -80px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
box-sizing: border-box;
|
||||
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
opacity: 0;
|
||||
transition:
|
||||
@@ -297,8 +316,10 @@ const jumpLink = (data, type) => {
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
display: inline-flex;
|
||||
color: var(--n-close-icon-color);
|
||||
transition: color 0.3s;
|
||||
opacity: 0.6;
|
||||
transition:
|
||||
color 0.3s,
|
||||
opacity 0.3s;
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
content: "/";
|
||||
@@ -311,10 +332,7 @@ const jumpLink = (data, type) => {
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--n-code-text-color);
|
||||
&::after {
|
||||
color: var(--n-close-icon-color);
|
||||
}
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
src/components/Global/MainLayout.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<main id="main-layout" :class="['main-layout', { 'no-sider': !showSider }]">
|
||||
<!-- 回顶 -->
|
||||
<n-back-top
|
||||
:bottom="Object.keys(music.getPlaySongData)?.length && showPlayBar ? 110 : 50"
|
||||
style="transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
>
|
||||
<n-icon size="26">
|
||||
<SvgIcon icon="chevron-up" />
|
||||
</n-icon>
|
||||
</n-back-top>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }" class="main-router">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { showPlayBar } = storeToRefs(status);
|
||||
const { showSider } = storeToRefs(settings);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
padding: 24px;
|
||||
&.no-sider {
|
||||
padding: 0;
|
||||
background-color: var(--n-color);
|
||||
.main-router {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 10vw;
|
||||
@media (max-width: 1200px) {
|
||||
padding: 24px 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,14 +4,15 @@
|
||||
ref="mainMenuRef"
|
||||
v-model:value="menuActiveKey"
|
||||
class="main-menu"
|
||||
:root-indent="36"
|
||||
:root-indent="showSider ? 36 : 28"
|
||||
:indent="0"
|
||||
:collapsed="status.asideMenuCollapsed"
|
||||
:collapsed="asideMenuCollapsed.value"
|
||||
:defaultExpandedKeys="['user-playlists', 'favorite-playlists']"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
@contextmenu="openSideDropdown($event)"
|
||||
@update:value="checkMenuItem"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<CoverDropdown ref="coverDropdownRef" />
|
||||
@@ -36,9 +37,17 @@ const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
const { playList, playListOld, playIndex, playSongData, playHeartbeatMode, playMode } =
|
||||
storeToRefs(music);
|
||||
const { asideMenuCollapsed, showSider, showFullPlayer } = storeToRefs(status);
|
||||
const { userData, userLikeData, userLoginStatus } = storeToRefs(data);
|
||||
const {
|
||||
playList,
|
||||
playListOld,
|
||||
playIndex,
|
||||
playSongData,
|
||||
playHeartbeatMode,
|
||||
playMode,
|
||||
privateFmSong,
|
||||
} = storeToRefs(music);
|
||||
|
||||
// 子组件
|
||||
const coverDropdownRef = ref(null);
|
||||
@@ -90,7 +99,7 @@ const menuOptions = computed(() => [
|
||||
label: "在线音乐",
|
||||
key: "online",
|
||||
children: [],
|
||||
show: !status.asideMenuCollapsed,
|
||||
show: !asideMenuCollapsed.value,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
@@ -120,19 +129,24 @@ const menuOptions = computed(() => [
|
||||
key: "discover",
|
||||
icon: renderIcon("discover-fill"),
|
||||
},
|
||||
{
|
||||
label: "私人漫游",
|
||||
key: "fm",
|
||||
icon: renderIcon("radio"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: "videos",
|
||||
name: "dj-hot",
|
||||
},
|
||||
},
|
||||
() => ["视频"],
|
||||
() => ["播客电台"],
|
||||
),
|
||||
key: "videos",
|
||||
icon: renderIcon("video"),
|
||||
key: "dj-hot",
|
||||
icon: renderIcon("record"),
|
||||
},
|
||||
{
|
||||
key: "divider-1",
|
||||
@@ -143,7 +157,7 @@ const menuOptions = computed(() => [
|
||||
label: "我的音乐",
|
||||
key: "user",
|
||||
children: [],
|
||||
show: !status.asideMenuCollapsed,
|
||||
show: !asideMenuCollapsed.value,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
@@ -157,24 +171,51 @@ const menuOptions = computed(() => [
|
||||
menuid: "like-songs",
|
||||
},
|
||||
() => [
|
||||
h(NText, null, () => ["喜欢的音乐"]),
|
||||
h(NButton, {
|
||||
size: "small",
|
||||
type: "tertiary",
|
||||
round: true,
|
||||
strong: true,
|
||||
secondary: true,
|
||||
renderIcon: renderIcon("heartbit", "26"),
|
||||
onclick: (event) => {
|
||||
event.stopPropagation();
|
||||
startHeartRate();
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
},
|
||||
}),
|
||||
[
|
||||
h("span", null, ["喜欢的音乐"]),
|
||||
h(NButton, {
|
||||
size: "small",
|
||||
type: "tertiary",
|
||||
round: true,
|
||||
strong: true,
|
||||
secondary: true,
|
||||
class: asideMenuCollapsed.value ? "heart-rate-btn collapsed" : "heart-rate-btn",
|
||||
renderIcon: renderIcon("heartbit", "26"),
|
||||
onclick: () => {
|
||||
startHeartRate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
key: "like-songs",
|
||||
icon: renderIcon("favorite-rounded"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: "like",
|
||||
},
|
||||
},
|
||||
() => ["我的收藏"],
|
||||
),
|
||||
key: "like",
|
||||
icon: renderIcon("star"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
@@ -222,13 +263,18 @@ const menuOptions = computed(() => [
|
||||
key: "divider-2",
|
||||
type: "divider",
|
||||
},
|
||||
{ ...userPlaylists.value, show: !status.asideMenuCollapsed },
|
||||
{ ...favoritePlaylists.value, show: !status.asideMenuCollapsed },
|
||||
{ ...userPlaylists.value, show: !asideMenuCollapsed.value },
|
||||
{ ...favoritePlaylists.value, show: !asideMenuCollapsed.value },
|
||||
]);
|
||||
|
||||
// 更改用户的歌单
|
||||
const changeUserPlaylists = (data) => {
|
||||
if (!isLogin() || !data?.length) return false;
|
||||
// 未登录
|
||||
if (!isLogin() || !data?.length) {
|
||||
userPlaylists.value.children = [];
|
||||
favoritePlaylists.value.children = [];
|
||||
return false;
|
||||
}
|
||||
// 用户 id
|
||||
const userId = userData.value?.userId;
|
||||
// 创建的歌单
|
||||
@@ -281,10 +327,24 @@ const changeUserPlaylists = (data) => {
|
||||
};
|
||||
|
||||
// 选中菜单项
|
||||
const checkMenuItem = (key) => {
|
||||
console.log(key);
|
||||
const checkMenuItem = async (key) => {
|
||||
// 例外路由
|
||||
const otherRouter = ["search", "videos-player", "playlist", "like-songs"];
|
||||
// 私人漫游
|
||||
if (key === "fm") {
|
||||
if (!privateFmSong.value || !Object.keys(privateFmSong.value)?.length) {
|
||||
return $message.error("开启私人漫游出错,请重试");
|
||||
}
|
||||
if (playMode.value === "fm") {
|
||||
fadePlayOrPause();
|
||||
} else {
|
||||
// 更改播放模式
|
||||
playMode.value = "fm";
|
||||
await initPlayer(true);
|
||||
}
|
||||
showFullPlayer.value = true;
|
||||
$message.info("已开启私人漫游", { icon: renderIcon("radio") });
|
||||
}
|
||||
// 特殊处理
|
||||
if (!key) {
|
||||
menuActiveKey.value = "home";
|
||||
@@ -332,7 +392,7 @@ const openSideDropdown = (e) => {
|
||||
const startHeartRate = debounce(async () => {
|
||||
try {
|
||||
if (!isLogin()) return false;
|
||||
if (playHeartbeatMode.value) return await fadePlayOrPause();
|
||||
if (playHeartbeatMode.value) return fadePlayOrPause();
|
||||
// 基础数据
|
||||
const likeSongs = userLikeData.value.songs;
|
||||
const songId = playSongData.value?.id;
|
||||
@@ -386,6 +446,10 @@ watch(
|
||||
changeUserPlaylists(val);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => userLoginStatus.value,
|
||||
() => changeUserPlaylists(userLikeData.value.playlists),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
changeUserPlaylists(userLikeData.value.playlists);
|
||||
@@ -446,3 +510,22 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 特殊样式 -->
|
||||
<style lang="scss">
|
||||
.heart-rate-btn {
|
||||
&:hover {
|
||||
background-color: var(--main-second-color) !important;
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
&.collapsed {
|
||||
margin-left: 12px;
|
||||
background-color: #efefef40;
|
||||
color: #efefef;
|
||||
&:hover {
|
||||
background-color: #efefef60 !important;
|
||||
color: #efefef !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<!-- 全局播放列表 -->
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="status.playListShow"
|
||||
:class="status.showFullPlayer ? 'main-playlist player' : 'main-playlist'"
|
||||
v-model:show="playListShow"
|
||||
:class="['main-playlist', { 'full-player': showFullPlayer }]"
|
||||
:style="{
|
||||
'--color': coverColor,
|
||||
'--color-bg': coverColor + '14',
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
:auto-focus="false"
|
||||
@after-enter="playlistOpen"
|
||||
@after-leave="status.playListShow = false"
|
||||
@mask-click="status.playListShow = false"
|
||||
@after-leave="playListShow = false"
|
||||
@mask-click="playListShow = false"
|
||||
>
|
||||
<n-drawer-content :native-scrollbar="false" closable>
|
||||
<template #header>
|
||||
@@ -20,28 +21,100 @@
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 提示 -->
|
||||
<n-alert v-if="playList?.length >= 400" class="alert" :show-icon="false">
|
||||
因歌单数量过大,无法自动定位,请手动查找
|
||||
</n-alert>
|
||||
<!-- 歌曲列表 -->
|
||||
<div class="list">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-virtual-list
|
||||
v-if="playList?.length"
|
||||
ref="playListRef"
|
||||
:item-size="76"
|
||||
:items="playListData"
|
||||
:default-scroll-index="playIndex"
|
||||
style="max-height: calc(100vh - 158px)"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div
|
||||
:id="`songs-${index}`"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'songs-item',
|
||||
{ play: playSongData?.id === item?.id, player: showFullPlayer },
|
||||
]"
|
||||
@click.stop="playSong(item, index)"
|
||||
@dblclick.stop="playSong(item, index)"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<n-text v-if="playSongData?.id !== item?.id" class="num" depth="3">
|
||||
{{ index + 1 }}
|
||||
</n-text>
|
||||
<n-icon v-else class="play" size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<!-- 信息 -->
|
||||
<div class="info">
|
||||
<!-- 歌曲名 -->
|
||||
<n-text class="name" depth="2">{{ item?.name || "未知曲目" }}</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="Array.isArray(item?.artists)" class="artist">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" depth="3" class="ar">
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="playMode === 'dj'" class="artist">
|
||||
<n-text class="ar"> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item?.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除 -->
|
||||
<n-icon class="delete" size="18" @click.stop="removeSong(index)">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-data-table
|
||||
v-if="playList?.length"
|
||||
class="pl-list"
|
||||
:columns="columns"
|
||||
:data="playList"
|
||||
:bordered="false"
|
||||
:bottom-bordered="false"
|
||||
:max-height="playList?.length >= 400 ? 'calc(100vh - 162px)' : '100%'"
|
||||
virtual-scroll
|
||||
/>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<n-grid v-if="playList?.length" :cols="2" x-gap="16" class="controls">
|
||||
<n-gi>
|
||||
<!-- 定位歌曲 -->
|
||||
<n-button
|
||||
size="large"
|
||||
tag="div"
|
||||
strong
|
||||
secondary
|
||||
@click="playListRef?.scrollTo({ index: playIndex })"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="location" />
|
||||
</n-icon>
|
||||
</template>
|
||||
当前播放
|
||||
</n-button>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<!-- 清空列表 -->
|
||||
<n-button size="large" tag="div" strong secondary @click="cleanPlaylists">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="delete-sweep" />
|
||||
</n-icon>
|
||||
</template>
|
||||
清空列表
|
||||
</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</Transition>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
@@ -51,18 +124,26 @@
|
||||
import { NText, NIcon } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus } from "@/stores";
|
||||
import {
|
||||
initPlayer,
|
||||
fadePlayOrPause,
|
||||
changePlayIndex,
|
||||
soundStop,
|
||||
checkPlayer,
|
||||
} from "@/utils/Player";
|
||||
import { initPlayer, fadePlayOrPause, changePlayIndex, soundStop } from "@/utils/Player";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { coverColor, playSongData, playList, playIndex, playMode } = storeToRefs(music);
|
||||
const { playSongData, playList, playIndex, playMode } = storeToRefs(music);
|
||||
const { coverTheme, showFullPlayer, playListShow } = storeToRefs(status);
|
||||
|
||||
const playListRef = ref(null);
|
||||
|
||||
// 播放列表数据
|
||||
const playListData = computed(() => {
|
||||
return playList.value?.[0]
|
||||
? playList.value.slice().map((v, i) => {
|
||||
v.key = `${i}`;
|
||||
return v;
|
||||
})
|
||||
: [];
|
||||
});
|
||||
|
||||
// 抽屉开启
|
||||
const playlistOpen = () => {
|
||||
@@ -75,73 +156,8 @@ const playlistOpen = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 表格数据
|
||||
const columns = computed(() => [
|
||||
{
|
||||
key: "songs",
|
||||
className: "songs-item",
|
||||
render(song, index) {
|
||||
return createSongs(song, index);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 列表歌曲模块
|
||||
const createSongs = (song, index) => {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
id: `pl-song-${index}`,
|
||||
class: {
|
||||
songs: true,
|
||||
play: playSongData.value?.id === song?.id,
|
||||
player: status.showFullPlayer,
|
||||
},
|
||||
onClick: () => {
|
||||
playSong(song, index);
|
||||
},
|
||||
},
|
||||
[
|
||||
// 序号
|
||||
playSongData.value?.id !== song?.id
|
||||
? h(NText, { class: "num", depth: "3" }, () => [index + 1])
|
||||
: h(NIcon, { class: "play", size: "18" }, () => [h(SvgIcon, { icon: "music-note" })]),
|
||||
// 信息
|
||||
h("div", { class: "info" }, [
|
||||
// 名称
|
||||
h(NText, { class: "name", depth: "2" }, () => [song?.name || "未知曲目"]),
|
||||
// 歌手
|
||||
Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "artist" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { class: "ar", depth: "3", key: ar.id }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h("div", { class: "artist" }, [
|
||||
h(NText, { class: "ar", depth: "3" }, () => [song.artists || "未知艺术家"]),
|
||||
]),
|
||||
]),
|
||||
// 移除
|
||||
h(
|
||||
NIcon,
|
||||
{
|
||||
class: "delete",
|
||||
size: "18",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
removeSong(index);
|
||||
},
|
||||
},
|
||||
() => [h(SvgIcon, { icon: "delete" })],
|
||||
),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 播放歌曲
|
||||
const playSong = async (song, index) => {
|
||||
const playSong = debounce(async (song, index) => {
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
// 更改播放索引
|
||||
@@ -153,28 +169,42 @@ const playSong = async (song, index) => {
|
||||
} else {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 渐出音乐
|
||||
if (checkPlayer()) await fadePlayOrPause("pause");
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 清空列表
|
||||
const cleanPlaylists = () => {
|
||||
soundStop();
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
playListShow.value = false;
|
||||
showFullPlayer.value = false;
|
||||
$message.success("已清空播放列表");
|
||||
};
|
||||
|
||||
// 移除歌曲
|
||||
const removeSong = async (index) => {
|
||||
if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
} else if (index === playIndex.value) {
|
||||
// 如果删除的是当前播放歌曲,则下一曲
|
||||
// 若删除时仅剩一首
|
||||
if (playList.value.length === 1) {
|
||||
cleanPlaylists();
|
||||
return false;
|
||||
}
|
||||
// 若为当前播放
|
||||
if (index === playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
changePlayIndex("next", true);
|
||||
}
|
||||
playList.value.splice(index, 1);
|
||||
// 检查当前播放歌曲的索引是否超出了列表范围
|
||||
if (playIndex.value >= playList.value.length) {
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
soundStop();
|
||||
// 若为当前播放之前
|
||||
else if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
// 若大于当前播放
|
||||
else if (index > playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -186,146 +216,132 @@ const removeSong = async (index) => {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
height: 48px;
|
||||
.list {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pl-list {
|
||||
:deep(.n-data-table-thead) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.n-data-table-tbody) {
|
||||
.songs-item {
|
||||
padding: 0;
|
||||
.songs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
background-color: var(--n-border-color-modal);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
.songs-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--n-close-color-hover);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
box-shadow:
|
||||
0 1px 2px -2px var(--main-boxshadow-color),
|
||||
0 3px 6px 0 var(--main-boxshadow-color),
|
||||
0 5px 12px 4px var(--main-boxshadow-hover-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-data-table-tr {
|
||||
&:last-child {
|
||||
.songs {
|
||||
margin-bottom: 0;
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
border-radius: 8px;
|
||||
.controls {
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
.n-button {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -342,8 +358,12 @@ const removeSong = async (index) => {
|
||||
}
|
||||
.n-scrollbar-content {
|
||||
padding: 16px !important;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.player {
|
||||
&.full-player {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
.n-drawer-header {
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
useNotification,
|
||||
} from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteSettings, siteData } from "@/stores";
|
||||
import { siteSettings, siteStatus } from "@/stores";
|
||||
import themeColorData from "@/assets/themeColor.json";
|
||||
|
||||
const data = siteData();
|
||||
const osThemeRef = useOsTheme();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const osThemeRef = useOsTheme();
|
||||
const { coverTheme } = storeToRefs(status);
|
||||
const { themeType, themeAuto, themeTypeName, themeTypeData, themeAutoCover } =
|
||||
storeToRefs(settings);
|
||||
|
||||
@@ -63,62 +64,70 @@ const changeTheme = () => {
|
||||
|
||||
// 配置主题色
|
||||
const changeThemeColor = (val, isCover = false) => {
|
||||
const color =
|
||||
isCover && Object.keys(val)?.length
|
||||
try {
|
||||
// 获取主色数据
|
||||
const mainColorData =
|
||||
isCover && Object.keys(val)?.length
|
||||
? val[themeType.value]
|
||||
: val !== "custom"
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
// 微调主题色
|
||||
const primaryColor = isCover ? `rgb(${mainColorData.bg})` : mainColorData.primaryColor;
|
||||
const primaryColorHover = isCover ? `rgba(${mainColorData.bg}, 0.29)` : primaryColor + "29";
|
||||
const primaryColorPressed = isCover ? `rgba(${mainColorData.bg}, 0.26)` : primaryColor + "26";
|
||||
const primaryColorSuppl = isCover ? `rgba(${mainColorData.bg}, 0.05)` : primaryColor + "05";
|
||||
// 自适应主题背景色
|
||||
const coverAutobgCover = isCover
|
||||
? themeType.value === "dark"
|
||||
? val.light.bg
|
||||
: val.dark.bg
|
||||
: val !== "custom"
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
// 微调主题色
|
||||
const primaryColor = isCover ? `rgb(${color})` : color.primaryColor;
|
||||
const primaryColorHover = isCover ? `rgba(${color}, 0.29)` : primaryColor + "29";
|
||||
const primaryColorPressed = isCover ? `rgba(${color}, 0.26)` : primaryColor + "26";
|
||||
const primaryColorSuppl = isCover ? `rgba(${color}, 0.05)` : primaryColor + "05";
|
||||
// 自适应主题背景色
|
||||
const coverAutobgCover = isCover
|
||||
? themeType.value === "dark"
|
||||
? val.dark.bg
|
||||
: "239, 239, 239"
|
||||
: null;
|
||||
// 更新主题覆盖
|
||||
themeOverrides.value = {
|
||||
common:
|
||||
isCover && Object.keys(val)?.length
|
||||
? {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
textColor1: `rgba(${color}, 0.9)`,
|
||||
textColor2: `rgba(${color}, 0.82)`,
|
||||
textColor3: `rgba(${color}, 0.52)`,
|
||||
bodyColor: `rgba(${val.dark.mainBg}, 0.52)`,
|
||||
cardColor: `rgb(${coverAutobgCover})`,
|
||||
tagColor: `rgb(${coverAutobgCover})`,
|
||||
modalColor: `rgb(${coverAutobgCover})`,
|
||||
popoverColor: `rgb(${coverAutobgCover})`,
|
||||
}
|
||||
: color,
|
||||
Icon: { color: isCover ? primaryColor : null },
|
||||
};
|
||||
if (!isCover) themeTypeData.value = color;
|
||||
// 更新全局颜色变量
|
||||
setCssVariable("--main-color", primaryColor);
|
||||
setCssVariable(
|
||||
"--main-color-bg",
|
||||
isCover ? `rgb(${val[themeType.value]?.bg})` : "rgb(16, 16, 20)",
|
||||
);
|
||||
setCssVariable("--main-second-color", primaryColorHover);
|
||||
setCssVariable("--main-boxshadow-color", primaryColorPressed);
|
||||
setCssVariable("--main-boxshadow-hover-color", primaryColorSuppl);
|
||||
: "250, 250, 252"
|
||||
: null;
|
||||
// 更新主题覆盖
|
||||
themeOverrides.value = {
|
||||
common:
|
||||
isCover && Object.keys(val)?.length
|
||||
? themeType.value === "dark"
|
||||
? {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
textColor1: `rgba(${mainColorData.bg}, 0.9)`,
|
||||
textColor2: `rgba(${mainColorData.bg}, 0.82)`,
|
||||
textColor3: `rgba(${mainColorData.bg}, 0.52)`,
|
||||
bodyColor: `rgb(${val.dark.mainBg})`,
|
||||
cardColor: `rgb(${coverAutobgCover})`,
|
||||
tagColor: `rgb(${coverAutobgCover})`,
|
||||
modalColor: `rgb(${coverAutobgCover})`,
|
||||
popoverColor: `rgb(${coverAutobgCover})`,
|
||||
}
|
||||
: {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
}
|
||||
: mainColorData,
|
||||
Icon: { color: isCover ? primaryColor : null },
|
||||
};
|
||||
if (!isCover) themeTypeData.value = mainColorData;
|
||||
// 更新全局颜色变量
|
||||
setCssVariable("--main-color", primaryColor);
|
||||
setCssVariable("--main-second-color", primaryColorHover);
|
||||
setCssVariable("--main-boxshadow-color", primaryColorPressed);
|
||||
setCssVariable("--main-boxshadow-hover-color", primaryColorSuppl);
|
||||
} catch (error) {
|
||||
themeOverrides.value = {};
|
||||
console.error("切换主题色出现错误:", error);
|
||||
$message.error("切换主题色出现错误,已使用默认配置");
|
||||
}
|
||||
};
|
||||
|
||||
// 修改全局颜色
|
||||
const setCssVariable = (name, value) => {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
// document.body.style.setProperty(name, value);
|
||||
// document.documentElement.style.setProperty(name, value);
|
||||
document.body.style.setProperty(name, value);
|
||||
};
|
||||
|
||||
// 挂载 naive 组件
|
||||
@@ -148,8 +157,10 @@ watch(
|
||||
() => {
|
||||
changeTheme();
|
||||
changeThemeColor(
|
||||
themeAutoCover.value ? data.coverTheme : themeTypeName.value,
|
||||
themeAutoCover.value,
|
||||
themeAutoCover.value && Object.keys(coverTheme.value)?.length
|
||||
? coverTheme.value
|
||||
: themeTypeName.value,
|
||||
themeAutoCover.value && Object.keys(coverTheme.value)?.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -170,9 +181,8 @@ watch(
|
||||
(val) => changeThemeColor(val.label),
|
||||
);
|
||||
watch(
|
||||
() => data.coverTheme,
|
||||
() => coverTheme.value,
|
||||
(val) => {
|
||||
console.log(val);
|
||||
if (themeAutoCover.value) changeThemeColor(val, themeAutoCover.value);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -293,7 +293,7 @@ const toLikeComment = throttle(
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--n-border-color);
|
||||
background-color: var(--main-second-color);
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
<div v-if="showTitle" class="song-list-header">
|
||||
<n-text class="num" depth="3"> # </n-text>
|
||||
<n-text :class="{ info: true, 'has-cover': data[0].cover && showCover }" depth="3">
|
||||
歌曲
|
||||
{{ type === "song" ? "歌曲" : "声音" }}
|
||||
</n-text>
|
||||
<n-text v-if="data[0].album && showAlbum" class="album" depth="3"> 专辑 </n-text>
|
||||
<n-text v-if="data[0].updateTime && type === 'dj'" class="update" depth="3">
|
||||
更新日期
|
||||
</n-text>
|
||||
<n-text v-if="data[0].playCount && type === 'dj'" class="count" depth="3"> 播放量 </n-text>
|
||||
<n-text v-if="data[0].duration" class="duration" depth="3"> 时长 </n-text>
|
||||
<n-text v-if="data[0].size" class="size" depth="3"> 大小 </n-text>
|
||||
</div>
|
||||
@@ -29,7 +33,7 @@
|
||||
hoverable
|
||||
@dblclick.stop="playSong(data, item, songsIndex + index)"
|
||||
@contextmenu="
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId)
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId, type)
|
||||
"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
@@ -122,6 +126,9 @@
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="type === 'dj'" class="artist">
|
||||
<n-text class="ar"> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
@@ -129,7 +136,7 @@
|
||||
<n-text v-if="item.alia" class="alia" depth="3">{{ item.alia }}</n-text>
|
||||
</div>
|
||||
<!-- 专辑 -->
|
||||
<template v-if="showAlbum">
|
||||
<template v-if="showAlbum && type !== 'dj'">
|
||||
<n-text
|
||||
v-if="item.album"
|
||||
class="album"
|
||||
@@ -139,6 +146,33 @@
|
||||
</n-text>
|
||||
<n-text v-else class="album">未知专辑</n-text>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<div v-if="type !== 'dj'" class="action">
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
:depth="dataStore.getSongIsLike(item?.id) ? 0 : 3"
|
||||
class="favorite"
|
||||
size="20"
|
||||
@click.stop="
|
||||
dataStore.changeLikeList(item?.id, !dataStore.getSongIsLike(item?.id), item?.path)
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
<SvgIcon
|
||||
:icon="
|
||||
dataStore.getSongIsLike(item?.id) ? 'favorite-rounded' : 'favorite-outline-rounded'
|
||||
"
|
||||
/>
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 更新日期 -->
|
||||
<n-text v-if="type === 'dj' && item.updateTime" class="update" depth="3">
|
||||
{{ getTimestampTime(item.updateTime, false) }}
|
||||
</n-text>
|
||||
<!-- 播放量 -->
|
||||
<n-text v-if="type === 'dj' && item.playCount" class="count" depth="3">
|
||||
{{ item.playCount }}次
|
||||
</n-text>
|
||||
<!-- 时长 -->
|
||||
<n-text v-if="item.duration" class="duration" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-else class="duration"> -- </n-text>
|
||||
@@ -193,18 +227,24 @@ import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings, musicData } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, addSongToNext } from "@/utils/Player";
|
||||
import { getTimestampTime } from "@/utils/timeTools";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const dataStore = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(dataStore);
|
||||
const { loadSize } = storeToRefs(settings);
|
||||
const { loadSize, playSearch } = storeToRefs(settings);
|
||||
const { playList, playIndex, playSongData, playSongSource, playHeartbeatMode, playMode } =
|
||||
storeToRefs(music);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const props = defineProps({
|
||||
// 列表类型
|
||||
type: {
|
||||
type: String,
|
||||
default: "song",
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
type: [Array, String],
|
||||
@@ -273,7 +313,7 @@ const checkHasPlaying = (isScoll = null) => {
|
||||
const playSong = async (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
playMode.value = props.type === "song" ? "normal" : "dj";
|
||||
// 检查当前页面
|
||||
const isPage = router.currentRoute.value.matched?.[0].path || null;
|
||||
// 是否关闭心动模式
|
||||
@@ -284,7 +324,12 @@ const playSong = async (data, song, index) => {
|
||||
fadePlayOrPause();
|
||||
} else {
|
||||
// 若为特殊状态
|
||||
if (isPage === "/search" || isPage === "/history" || playHeartbeatMode.value) {
|
||||
if (
|
||||
(isPage === "/search" && !playSearch.value) ||
|
||||
isPage === "/history" ||
|
||||
playHeartbeatMode.value
|
||||
) {
|
||||
console.log("仅播放当前歌曲");
|
||||
addSongToNext(song, true);
|
||||
} else {
|
||||
// 添加播放列表
|
||||
@@ -295,7 +340,7 @@ const playSong = async (data, song, index) => {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
// 附加来源
|
||||
playSongSource.value = Number(props.sourceId);
|
||||
@@ -352,6 +397,14 @@ onBeforeUnmount(() => {
|
||||
.has-cover {
|
||||
margin-right: 66px;
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
@@ -483,6 +536,31 @@ onBeforeUnmount(() => {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.action {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
.favorite {
|
||||
padding-top: 1px;
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
<!-- 下载歌曲 -->
|
||||
<DownloadSong ref="downloadSongRef" />
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<CloudSongMatch ref="cloudSongMatchRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -27,6 +29,7 @@ import { useRouter } from "vue-router";
|
||||
import { addSongToNext } from "@/utils/Player";
|
||||
import { setCloudDel } from "@/api/cloud";
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { copyData } from "@/utils/helper";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
|
||||
const emit = defineEmits(["playSong"]);
|
||||
@@ -45,6 +48,7 @@ const dropdownOptions = ref(null);
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
const downloadSongRef = ref(null);
|
||||
const cloudSongMatchRef = ref(null);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon, size, translate = 0) => {
|
||||
@@ -60,7 +64,7 @@ const renderIcon = (icon, size, translate = 0) => {
|
||||
};
|
||||
|
||||
// 歌曲信息
|
||||
const renderSong = (song) => {
|
||||
const renderSong = (song, isSong) => {
|
||||
return () =>
|
||||
h(
|
||||
"div",
|
||||
@@ -71,26 +75,28 @@ const renderSong = (song) => {
|
||||
h(NImage, { src: song?.coverSize?.s || song?.cover, class: "cover" }),
|
||||
h("div", { class: "song-detail" }, [
|
||||
h(NText, { class: "name" }, () => [song?.name || "未知曲目"]),
|
||||
song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
),
|
||||
isSong
|
||||
? song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
)
|
||||
: h(NText, { class: "ar", depth: 3 }, () => ["电台节目"]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 打开右键菜单
|
||||
const openDropdown = (e, data, song, index, sourceId) => {
|
||||
const openDropdown = (e, data, song, index, sourceId, type) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
dropdownShow.value = false;
|
||||
@@ -101,7 +107,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
(playlist) => playlist.userId === userId,
|
||||
);
|
||||
// 当前状态
|
||||
const isFm = playMode.value === "fm";
|
||||
const isSong = type === "song";
|
||||
const isLocalSong = song?.path ? true : false;
|
||||
const isHasMv = song.mv && song.mv !== 0 ? true : false;
|
||||
const isCloud = router.currentRoute.value.name === "cloud";
|
||||
const isUserPlaylist = sourceId !== 0 && userPlaylistsData.some((pl) => pl.id == sourceId);
|
||||
// 生成菜单
|
||||
@@ -112,7 +121,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
key: "song-data",
|
||||
type: "render",
|
||||
show: !isLocalSong,
|
||||
render: renderSong(song),
|
||||
render: renderSong(song, isSong),
|
||||
},
|
||||
{
|
||||
key: "line-song",
|
||||
@@ -132,9 +141,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "next-play",
|
||||
label: "下一首播放",
|
||||
show: playSongData.value?.id === song.id || playMode.value === "fm" ? false : true,
|
||||
show: isSong && playMode.value !== "dj" && playSongData.value?.id !== song.id && !isFm,
|
||||
props: {
|
||||
onClick: () => {
|
||||
playMode.value = "song";
|
||||
addSongToNext(song);
|
||||
},
|
||||
},
|
||||
@@ -143,7 +153,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "add-pl",
|
||||
label: "添加到歌单",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
addPlaylistRef.value?.openAddToPlaylist(song?.id);
|
||||
@@ -154,7 +164,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "comment",
|
||||
label: "查看评论",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -170,7 +180,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "mv",
|
||||
label: "观看 MV",
|
||||
show: song.mv && song.mv !== 0 ? true : false,
|
||||
show: isSong && isHasMv,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -183,6 +193,39 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
},
|
||||
icon: renderIcon("video"),
|
||||
},
|
||||
{
|
||||
label: "更多操作",
|
||||
key: "others",
|
||||
show: !isLocalSong,
|
||||
icon: renderIcon("more"),
|
||||
children: [
|
||||
{
|
||||
key: "copy",
|
||||
label: `复制${isSong ? "歌曲" : "节目"} ID`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const songId = song?.id?.toString();
|
||||
copyData(songId);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("content-copy"),
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
label: `分享${isSong ? "歌曲" : "节目"}链接`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const shareUrl = isSong
|
||||
? `https://music.163.com/song?id=${song?.id?.toString()}`
|
||||
: `https://music.163.com/#/dj?id=${song?.id?.toString()}`;
|
||||
copyData(shareUrl, `复制${isSong ? "歌曲" : "节目"}链接`);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("share"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
key: "line-cloud",
|
||||
type: "divider",
|
||||
@@ -199,6 +242,17 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "云盘歌曲纠正",
|
||||
show: isCloud,
|
||||
props: {
|
||||
onClick: () => {
|
||||
cloudSongMatchRef.value?.openCloudSongMatch(song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("edit"),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "从云盘中删除",
|
||||
@@ -254,7 +308,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "download",
|
||||
label: "下载歌曲",
|
||||
show: !isLocalSong,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
downloadSongRef.value?.openDownloadModal(song);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
>
|
||||
<template #prefix>
|
||||
<n-image
|
||||
:src="item.coverImgUrl.replace(/^http:/, 'https:') + '?param=100y100'"
|
||||
:src="item?.coverSize?.s || '/images/pic/album.jpg?assest'"
|
||||
class="cover"
|
||||
preview-disabled
|
||||
lazy
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
<n-thing :title="item.name">
|
||||
<template #description>
|
||||
<n-text depth="3" class="size">{{ item.trackCount }} 首音乐</n-text>
|
||||
<n-text depth="3" class="size">{{ item.count }} 首音乐</n-text>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
@@ -87,7 +87,7 @@ const addToPlayList = async (pid, tracks, index) => {
|
||||
$message.success("添加歌曲至歌单成功");
|
||||
if (index === 0) await data.setUserLikeSongs();
|
||||
} else {
|
||||
$message.error("添加失败,请重试");
|
||||
$message.error(result?.message || "添加失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
248
src/components/Modal/CloudSongMatch.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="cloudSongMatchShow"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeCloudSongMatch"
|
||||
title="歌曲纠正"
|
||||
preset="card"
|
||||
>
|
||||
<n-form class="cloud-match" :label-width="80" :model="cloudMatchValue">
|
||||
<n-form-item label="原歌曲 ID" path="asid">
|
||||
<n-input-number v-model:value="cloudMatchValue.sid" :show-button="false" disabled />
|
||||
</n-form-item>
|
||||
<n-form-item label="匹配的 ID" path="asid">
|
||||
<n-input-number
|
||||
v-model:value="cloudMatchValue.asid"
|
||||
:show-button="false"
|
||||
placeholder="请输入要匹配的歌曲 ID"
|
||||
/>
|
||||
<n-button
|
||||
:disabled="!cloudMatchValue.asid"
|
||||
style="margin-left: 12px"
|
||||
@click="checkMatchSong(cloudMatchValue.asid)"
|
||||
>
|
||||
检查
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<!-- 纠正歌曲数据 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-card
|
||||
v-if="cloudMatchSongData"
|
||||
:key="cloudMatchSongData"
|
||||
:content-style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
height: '100%',
|
||||
}"
|
||||
class="song-detail"
|
||||
>
|
||||
<n-image
|
||||
:src="cloudMatchSongData?.coverSize?.s || cloudMatchSongData?.cover"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="loading-img" />
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<div class="content">
|
||||
<div class="name">{{ cloudMatchSongData?.name || "未知曲目" }}</div>
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div
|
||||
v-if="cloudMatchSongData?.artists && Array.isArray(cloudMatchSongData?.artists)"
|
||||
class="all-ar"
|
||||
>
|
||||
<span v-for="ar in cloudMatchSongData.artists" :key="ar.id" class="ar">
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<span class="ar"> {{ cloudMatchSongData?.artists || "未知艺术家" }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="closeCloudSongMatch"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!cloudMatchValue.asid"
|
||||
type="primary"
|
||||
@click="setCloudSongMatchBtn(cloudMatchValue)"
|
||||
>
|
||||
纠正
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData, indexedDBData } from "@/stores";
|
||||
import { setCloudMatch } from "@/api/cloud";
|
||||
import { getSongDetail } from "@/api/song";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const data = siteData();
|
||||
const indexedDB = indexedDBData();
|
||||
const { userData } = storeToRefs(data);
|
||||
|
||||
// 歌曲信息纠正数据
|
||||
const cloudMatchId = ref(null);
|
||||
const cloudMatchIndex = ref(null);
|
||||
const cloudSongMatchShow = ref(false);
|
||||
const cloudMatchSongData = ref(null);
|
||||
const cloudMatchValue = ref({
|
||||
uid: userData.value?.userId,
|
||||
sid: null,
|
||||
asid: null,
|
||||
});
|
||||
|
||||
// 检查纠正歌曲id
|
||||
const checkMatchSong = async (id) => {
|
||||
try {
|
||||
if (!id) return false;
|
||||
const songId = id.toString();
|
||||
const result = await getSongDetail(songId);
|
||||
if (result.code === 200 && result.songs.length > 0) {
|
||||
$message.success("匹配的歌曲检查通过");
|
||||
cloudMatchSongData.value = formatData(result.songs[0], "song")[0];
|
||||
} else {
|
||||
$message.warning("请检查要匹配的歌曲 ID 是否正确");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查纠正歌曲失败:", error);
|
||||
$message.error("检查纠正歌曲失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 歌曲纠正
|
||||
const setCloudSongMatchBtn = async (data) => {
|
||||
if (Number(data.sid) === Number(data.asid)) {
|
||||
return $message.warning("与原歌曲 ID 一致,无需修改");
|
||||
}
|
||||
if (!cloudMatchSongData.value) {
|
||||
return $message.warning("未检测到正确的匹配检查结果");
|
||||
}
|
||||
const result = await setCloudMatch(data.uid, data.sid, data.asid);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 更改歌曲信息
|
||||
try {
|
||||
cloudMatchSongData.value.pc = undefined;
|
||||
const allCloudSongs = await indexedDB.getfilesDB("userCloudList");
|
||||
allCloudSongs[cloudMatchIndex.value] = JSON.parse(JSON.stringify(cloudMatchSongData.value));
|
||||
await indexedDB.setfilesDB("userCloudList", allCloudSongs.slice());
|
||||
// 刷新列表
|
||||
if (typeof $refreshCloudList !== "undefined") $refreshCloudList();
|
||||
} catch (error) {
|
||||
console.error("更改云盘列表时出错:", error);
|
||||
$message.error("更改云盘列表时出错,请刷新后重试");
|
||||
}
|
||||
closeCloudSongMatch();
|
||||
$message.success("歌曲信息纠正成功");
|
||||
} else {
|
||||
$message.error("歌曲信息纠正失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 开启歌曲纠正
|
||||
const openCloudSongMatch = (data, index) => {
|
||||
cloudMatchIndex.value = index;
|
||||
cloudMatchValue.value.sid = data.id;
|
||||
cloudSongMatchShow.value = true;
|
||||
};
|
||||
|
||||
// 关闭歌曲纠正
|
||||
const closeCloudSongMatch = () => {
|
||||
cloudSongMatchShow.value = false;
|
||||
cloudMatchId.value = null;
|
||||
cloudMatchValue.value.asid = null;
|
||||
cloudMatchSongData.value = null;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openCloudSongMatch,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cloud-match {
|
||||
:deep(.n-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.song-detail {
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
.cover-img {
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
margin-right: 16px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 10px 6px #00000008;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.artist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -59,7 +59,7 @@ import { isLogin } from "@/utils/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getSongDetail, getSongDownload } from "@/api/song";
|
||||
import { downloadFile } from "@/utils/helper";
|
||||
import { downloadFile, checkPlatform } from "@/utils/helper";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -97,7 +97,7 @@ const toSongDownload = async (song, br) => {
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value) {
|
||||
if (!downloadPath.value && checkPlatform.electron()) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
@@ -189,12 +189,12 @@ const openDownloadModal = (data) => {
|
||||
router.currentRoute.value.name === "cloud" ||
|
||||
data?.fee === 0 ||
|
||||
data?.pc ||
|
||||
userData.detail?.profile?.vipType !== 0
|
||||
userData.value.detail?.profile?.vipType !== 0
|
||||
) {
|
||||
return toDownload();
|
||||
}
|
||||
// 权限不足
|
||||
if (data?.fee !== 0 && userData.detail?.profile?.vipType !== 11 && !data?.pc) {
|
||||
if (data?.fee !== 0 && userData.value.detail?.profile?.vipType !== 11 && !data?.pc) {
|
||||
return $message.warning("账号会员等级不足,请提升权限");
|
||||
}
|
||||
$message.warning("账号会员等级不足,请提升权限");
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
>
|
||||
<div class="login-content">
|
||||
<div class="title">
|
||||
<img class="logo" src="/images/logo/favicon.png?asset" alt="logo" />
|
||||
<img class="logo" src="/images/icons/favicon.png?asset" alt="logo" />
|
||||
</div>
|
||||
<!-- 登录方式 -->
|
||||
<n-tabs class="login-tabs" default-value="login-qr" type="segment" animated>
|
||||
<n-tab-pane name="login-qr" tab="扫码登录">
|
||||
<loginQRCode @setLoginData="setLoginData" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="login-phone" tab="验证码登录"> 444 </n-tab-pane>
|
||||
<n-tab-pane name="login-phone" tab="验证码登录">
|
||||
<loginPhone @setLoginData="setLoginData" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<!-- 关闭登录弹窗 -->
|
||||
<n-button
|
||||
@@ -43,12 +45,15 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getLoginState, refreshLogin } from "@/api/login";
|
||||
import { setCookies, toLogout, isLogin } from "@/utils/auth";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { autoSignIn } = storeToRefs(settings);
|
||||
|
||||
// 登录数据
|
||||
const loginModalShow = ref(false);
|
||||
@@ -72,6 +77,7 @@ const openLoginModal = () => {
|
||||
|
||||
// 储存登录信息
|
||||
const setLoginData = async (loginData) => {
|
||||
console.log(loginData);
|
||||
if (!loginData) return false;
|
||||
if (loginData.code === 200) {
|
||||
// 关闭登录弹窗
|
||||
@@ -80,6 +86,9 @@ const setLoginData = async (loginData) => {
|
||||
setCookies(loginData.cookie);
|
||||
// 获取用户信息
|
||||
await data.setUserProfile();
|
||||
await data.setDailySongsData();
|
||||
// 签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 更改状态
|
||||
data.userLoginStatus = true;
|
||||
$message.success("登录成功");
|
||||
@@ -168,7 +177,7 @@ onBeforeMount(() => {
|
||||
.close {
|
||||
position: absolute;
|
||||
bottom: -58px;
|
||||
background-color: var(--n-color-embedded);
|
||||
background-color: var(--n-color-modal);
|
||||
&:hover {
|
||||
background-color: var(--n-color-embedded);
|
||||
}
|
||||
|
||||
146
src/components/Modal/LoginPhone.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<!-- 登录 - 手机号 -->
|
||||
<template>
|
||||
<div class="login-phone">
|
||||
<n-form
|
||||
ref="phoneFormRef"
|
||||
:model="phoneFormData"
|
||||
:rules="phoneFormRules"
|
||||
:show-label="false"
|
||||
class="phone-form"
|
||||
>
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="phoneFormData.phone" placeholder="请输入手机号">
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SvgIcon icon="phone" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
<n-form-item path="captcha">
|
||||
<n-input-number
|
||||
v-model:value="phoneFormData.captcha"
|
||||
:show-button="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入短信验证码"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SvgIcon icon="password" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input-number>
|
||||
<n-button
|
||||
:disabled="captchaDisabled"
|
||||
type="primary"
|
||||
style="margin-left: 12px"
|
||||
@click="getCaptcha(phoneFormData.phone)"
|
||||
>
|
||||
{{ captchaText }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button style="width: 100%" type="primary" @click="phoneLogin"> 登录 </n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { sentCaptcha, verifyCaptcha, toLogin } from "@/api/login";
|
||||
import { formRules } from "@/utils/formRules";
|
||||
|
||||
const emit = defineEmits(["setLoginData"]);
|
||||
const { numberRule, mobileRule } = formRules();
|
||||
|
||||
// 手机号数据
|
||||
const phoneFormRef = ref(null);
|
||||
const phoneFormData = ref({
|
||||
phone: null,
|
||||
captcha: null,
|
||||
});
|
||||
const phoneFormRules = {
|
||||
phone: mobileRule,
|
||||
captcha: numberRule,
|
||||
};
|
||||
const captchaTimeOut = ref(null);
|
||||
const captchaText = ref("获取验证码");
|
||||
const captchaDisabled = ref(false);
|
||||
|
||||
// 获取验证码
|
||||
const getCaptcha = (phone) => {
|
||||
clearInterval(captchaTimeOut.value);
|
||||
phoneFormRef.value?.validate(
|
||||
async (errors) => {
|
||||
if (!errors) {
|
||||
console.log(phone + "发送验证码");
|
||||
const result = await sentCaptcha(phone);
|
||||
console.log(result);
|
||||
if (result.code == 200) {
|
||||
$message.success("验证码发送成功");
|
||||
let countDown = 60;
|
||||
captchaDisabled.value = true;
|
||||
captchaTimeOut.value = setInterval(() => {
|
||||
countDown--;
|
||||
captchaText.value = countDown + "s";
|
||||
if (countDown === 0) {
|
||||
clearInterval(captchaTimeOut.value);
|
||||
captchaText.value = "重新获取";
|
||||
captchaDisabled.value = false;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
$message.error("验证码发送失败,请重试");
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
},
|
||||
(rule) => {
|
||||
return rule?.key === "phone";
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 手机号登录
|
||||
const phoneLogin = (e) => {
|
||||
e.preventDefault();
|
||||
phoneFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const verifyRes = await verifyCaptcha(
|
||||
phoneFormData._value.phone,
|
||||
phoneFormData._value.captcha,
|
||||
);
|
||||
console.log(verifyRes);
|
||||
if (verifyRes.code == 200) {
|
||||
const result = await toLogin(phoneFormData._value.phone, phoneFormData._value.captcha);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 去除 HTTPOnly
|
||||
result.cookie = result.cookie.replaceAll(" HTTPOnly", "");
|
||||
// 是否含有 MUSIC_U
|
||||
if (result.cookie && result.cookie.includes("MUSIC_U")) {
|
||||
// 储存登录信息
|
||||
emit("setLoginData", result);
|
||||
} else {
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
} else {
|
||||
phoneFormData.value.captcha = null;
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-phone {
|
||||
.phone-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,17 +2,18 @@
|
||||
<template>
|
||||
<div class="login-qr">
|
||||
<div class="qr-img">
|
||||
<n-skeleton v-if="!qrImg" class="qr" />
|
||||
<QrcodeVue
|
||||
v-else
|
||||
:class="['qr', qrStatusCode === 802 ? 'hidden' : null]"
|
||||
:value="qrImg"
|
||||
:size="180"
|
||||
:margin="4"
|
||||
level="L"
|
||||
foreground="#000"
|
||||
background="#fff"
|
||||
/>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-qr-code
|
||||
v-if="qrImg"
|
||||
:value="qrImg"
|
||||
:class="['qr', qrStatusCode === 802 ? 'hidden' : null]"
|
||||
:size="156"
|
||||
:icon-size="30"
|
||||
icon-src="/images/icons/favicon.png?asset"
|
||||
error-correction-level="H"
|
||||
/>
|
||||
<n-skeleton v-else class="qr" />
|
||||
</Transition>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="qrStatusCode === 802" class="refresh" @click="getQrData">
|
||||
<n-icon size="22">
|
||||
@@ -28,7 +29,6 @@
|
||||
|
||||
<script setup>
|
||||
import { getQrKey, checkQr } from "@/api/login";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
|
||||
const emit = defineEmits(["setLoginData"]);
|
||||
|
||||
@@ -90,7 +90,7 @@ const checkQrStatus = (key) => {
|
||||
// 是否含有 MUSIC_U
|
||||
if (res.cookie && res.cookie.includes("MUSIC_U")) {
|
||||
// 储存登录信息
|
||||
emit("setLoginData", res, "qrcode");
|
||||
emit("setLoginData", res);
|
||||
} else {
|
||||
$message.error("登录出错,请重试");
|
||||
getQrData();
|
||||
@@ -133,6 +133,7 @@ onBeforeUnmount(() => {
|
||||
min-width: 180px;
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.3s;
|
||||
&.hidden {
|
||||
opacity: 0.2;
|
||||
|
||||
@@ -1,47 +1,119 @@
|
||||
<!-- 主导航栏 -->
|
||||
<template>
|
||||
<nav class="main-nav">
|
||||
<div
|
||||
:class="['logo', status.asideMenuCollapsed ? 'collapsed' : null]"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
<n-avatar class="logo-img" src="/images/logo/favicon.png?asset" />
|
||||
<nav :class="{ 'main-nav': true, 'no-sider': !showSider }">
|
||||
<div class="left">
|
||||
<div
|
||||
:class="['logo', status.asideMenuCollapsed ? 'collapsed' : null]"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
<n-avatar class="logo-img" src="/images/icons/favicon.png?asset" />
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-text v-if="!status.asideMenuCollapsed && showSider" class="site-name">
|
||||
{{ siteName }}
|
||||
</n-text>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<n-button class="nav-icon" quaternary @click="router.go(-1)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="chevron-left" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button class="nav-icon" quaternary @click="router.go(1)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="chevron-right" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<SearchInp />
|
||||
<!-- GitHub -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-text v-if="!status.asideMenuCollapsed" class="site-name">{{ siteName }}</n-text>
|
||||
<n-button v-if="showGithub" class="github" circle quaternary @click="openGithub">
|
||||
<template #icon>
|
||||
<n-icon size="20">
|
||||
<SvgIcon icon="github" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<n-button class="nav-icon" quaternary @click="router.go(-1)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="chevron-left" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button class="nav-icon" quaternary @click="router.go(1)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="chevron-right" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="right">
|
||||
<!-- 全局菜单 -->
|
||||
<n-dropdown
|
||||
v-if="!showSider"
|
||||
:show="mainMenuShow"
|
||||
:show-arrow="true"
|
||||
:options="mainMenuOptions"
|
||||
placement="bottom-end"
|
||||
@clickoutside="mainMenuShow = false"
|
||||
>
|
||||
<n-button
|
||||
:style="{ pointerEvents: mainMenuShow ? 'none' : 'auto' }"
|
||||
class="main-menu"
|
||||
secondary
|
||||
strong
|
||||
round
|
||||
@click="mainMenuShow = !mainMenuShow"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="menu" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<!-- 用户信息 -->
|
||||
<userData />
|
||||
<!-- TitleBar -->
|
||||
<TitleBar v-if="checkPlatform.electron()" />
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<SearchInp />
|
||||
<!-- 用户信息 -->
|
||||
<userData />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { siteStatus } from "@/stores";
|
||||
import { NScrollbar } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteStatus, siteSettings } from "@/stores";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { useRouter } from "vue-router";
|
||||
import Menu from "@/components/Global/Menu";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
const router = useRouter();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { showGithub, showSider } = storeToRefs(settings);
|
||||
|
||||
// 站点信息
|
||||
const siteName = import.meta.env.RENDERER_VITE_SITE_TITLE;
|
||||
|
||||
// 打开 GitHub
|
||||
const openGithub = () => {
|
||||
console.log(packageJson.github);
|
||||
window.open(packageJson.github);
|
||||
};
|
||||
|
||||
// 主菜单渲染
|
||||
const mainMenuShow = ref(false);
|
||||
const mainMenuOptions = computed(() => [
|
||||
{
|
||||
key: "menu",
|
||||
type: "render",
|
||||
props: {
|
||||
onClick: () => (mainMenuShow.value = false),
|
||||
},
|
||||
render: () => {
|
||||
return h(NScrollbar, { style: { maxHeight: "calc(100vh - 200px)", minWidth: "280px" } }, () =>
|
||||
h(Menu),
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -50,7 +122,14 @@ const siteName = import.meta.env.RENDERER_VITE_SITE_TITLE;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.logo {
|
||||
width: 224px;
|
||||
display: flex;
|
||||
@@ -101,5 +180,26 @@ const siteName = import.meta.env.RENDERER_VITE_SITE_TITLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
.github {
|
||||
margin-left: 12px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.main-menu {
|
||||
-webkit-app-region: no-drag;
|
||||
margin-right: 12px;
|
||||
}
|
||||
&.no-sider {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 10vw;
|
||||
@media (max-width: 1200px) {
|
||||
padding: 0 5vw;
|
||||
}
|
||||
.logo {
|
||||
width: auto;
|
||||
padding-left: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,11 +48,13 @@ import { useRouter } from "vue-router";
|
||||
import { NIcon, NText, NNumberAnimation, NButton } from "naive-ui";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
|
||||
const data = siteData();
|
||||
const router = useRouter();
|
||||
const settings = siteSettings();
|
||||
const { userLoginStatus, userData, userLikeData } = storeToRefs(data);
|
||||
const { themeType } = storeToRefs(settings);
|
||||
|
||||
// 菜单数据
|
||||
const userMenuShow = ref(false);
|
||||
@@ -60,9 +62,6 @@ const userMenuShow = ref(false);
|
||||
// 登录弹窗
|
||||
const loginRef = ref(null);
|
||||
|
||||
// 是否签到
|
||||
const signInStatus = ref(false);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => h(NIcon, null, () => h(SvgIcon, { icon }));
|
||||
@@ -70,14 +69,28 @@ const renderIcon = (icon) => {
|
||||
|
||||
// 数量统计模块
|
||||
const createUserNumber = (num, text, duration = 1000) => {
|
||||
return h("div", { className: "user-pl" }, [
|
||||
h(NNumberAnimation, { from: 0, to: num, duration }),
|
||||
h(NText, { depth: 3, style: { fontSize: "12px" } }, () => [text]),
|
||||
]);
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "user-pl",
|
||||
onclick: () => {
|
||||
userMenuShow.value = false;
|
||||
router.push(
|
||||
`/like/${text === "歌单" ? "playlists?" : text === "专辑" ? "albums" : "artists"}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
[
|
||||
h(NNumberAnimation, { from: 0, to: num, duration }),
|
||||
h(NText, { depth: 3, style: { fontSize: "12px" } }, () => [text]),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 生成导航栏用户信息
|
||||
const createUserData = () => {
|
||||
// 是否签到
|
||||
const signInStatus = sessionStorage.getItem("lastSignInDate") ? true : false;
|
||||
return h(
|
||||
"div",
|
||||
{ className: "nav-user-data" },
|
||||
@@ -95,12 +108,14 @@ const createUserData = () => {
|
||||
NButton,
|
||||
{
|
||||
round: true,
|
||||
renderIcon: renderIcon(signInStatus.value ? "calendar-check" : "calendar-badge"),
|
||||
onclick: () => {
|
||||
$message.warning("施工中( 新建文件夹 )");
|
||||
renderIcon: renderIcon(signInStatus ? "calendar-check" : "calendar-badge"),
|
||||
disabled: signInStatus,
|
||||
onclick: async () => {
|
||||
userMenuShow.value = false;
|
||||
await userSignIn();
|
||||
},
|
||||
},
|
||||
() => [signInStatus.value ? "Lv." + userData.value.detail?.level || 1 : "立即签到"],
|
||||
() => [signInStatus ? "今日已签到" : "立即签到"],
|
||||
),
|
||||
]),
|
||||
]
|
||||
@@ -120,9 +135,9 @@ const userMenuOptions = computed(() => [
|
||||
key: "d1",
|
||||
},
|
||||
{
|
||||
label: settings.themeType === "dark" ? "浅色模式" : "深色模式",
|
||||
label: themeType.value === "dark" ? "浅色模式" : "深色模式",
|
||||
key: "darkOrlight",
|
||||
icon: renderIcon(settings.themeType === "dark" ? "round-wb-sunny" : "round-dark-mode"),
|
||||
icon: renderIcon(themeType.value === "dark" ? "round-wb-sunny" : "round-dark-mode"),
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
@@ -143,7 +158,7 @@ const userMenuSelect = (key) => {
|
||||
switch (key) {
|
||||
// 明暗切换
|
||||
case "darkOrlight":
|
||||
settings.setThemeType(settings.themeType === "light" ? "dark" : "light");
|
||||
settings.setThemeType(themeType.value === "light" ? "dark" : "light");
|
||||
break;
|
||||
// 登录登出
|
||||
case "logoutOrlogin":
|
||||
@@ -218,6 +233,7 @@ const userMenuSelect = (key) => {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.user-pl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -4,23 +4,20 @@
|
||||
<div
|
||||
v-if="showFullPlayer"
|
||||
:style="{
|
||||
'--cover-main-color': `rgb(${coverTheme?.dark?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.dark?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
'--cover-bg': coverBackground,
|
||||
cursor: playerControlShow ? 'auto' : 'none',
|
||||
}"
|
||||
class="full-player"
|
||||
@mousemove="controlShowChange"
|
||||
@mouseleave="playerControlShow = false"
|
||||
>
|
||||
<!-- 遮罩 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
:key="
|
||||
playerBackgroundType === 'gradient'
|
||||
? coverBackground
|
||||
: music.getPlaySongData?.coverSize?.s || music.getPlaySongData?.localCover
|
||||
"
|
||||
:key="`${playerBackgroundType}-${coverBackground}-${
|
||||
music.getPlaySongData?.coverSize?.s || music.getPlaySongData?.localCover
|
||||
}`"
|
||||
:class="['overlay', playerBackgroundType]"
|
||||
>
|
||||
<!-- 模糊背景 -->
|
||||
@@ -49,7 +46,12 @@
|
||||
<!-- 按钮 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-show="playerControlShow" class="menu">
|
||||
<div class="left"></div>
|
||||
<div class="left">
|
||||
<!-- 歌词模式 -->
|
||||
<div v-if="isHasLrc" class="n-icon" @click="pureLyricMode = !pureLyricMode">
|
||||
<n-text>词</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<!-- 全屏切换 -->
|
||||
<n-icon @click.stop="screenfullChange">
|
||||
@@ -64,116 +66,164 @@
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 主内容 -->
|
||||
<div
|
||||
:class="{
|
||||
content: true,
|
||||
'no-lrc': !playSongLyric.lrc?.[0] && playSongLyric.lrc?.length <= 4,
|
||||
}"
|
||||
>
|
||||
<!-- 封面 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="music.getPlaySongData.id" class="cover">
|
||||
<n-image
|
||||
:src="
|
||||
music.getPlaySongData?.coverSize?.l ||
|
||||
music.getPlaySongData?.cover ||
|
||||
music.getPlaySongData?.localCover
|
||||
"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="loading-img" />
|
||||
<!-- 主播放器内容 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
:key="`${pureLyricMode}-${playCoverType}-${isHasLrc}-${music.getPlaySongData?.id}`"
|
||||
class="main-player"
|
||||
>
|
||||
<div v-show="!(pureLyricMode && isHasLrc)" :class="['content', { 'no-lrc': !isHasLrc }]">
|
||||
<!-- 封面 -->
|
||||
<PlayerCover />
|
||||
<!-- 信息 -->
|
||||
<div v-if="playCoverType === 'cover' || !isHasLrc" :class="['data', playCoverType]">
|
||||
<div class="desc">
|
||||
<div class="title">
|
||||
<span class="name">{{ music.getPlaySongData.name || "未知曲目" }}</span>
|
||||
<n-popover :show-arrow="false" placement="right-start" trigger="hover" raw>
|
||||
<template #trigger>
|
||||
<n-tag
|
||||
v-show="playUseOtherSource"
|
||||
:style="{
|
||||
color: `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
backgroundColor:
|
||||
`rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
round
|
||||
>
|
||||
其他音源
|
||||
</n-tag>
|
||||
</template>
|
||||
<div
|
||||
:style="{
|
||||
color: `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
backgroundColor:
|
||||
`rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
class="title-tip"
|
||||
>
|
||||
<span>该歌曲暂时无法播放,为您采用其他音源,可能会与原曲存在差别</span>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 封面背板 -->
|
||||
<n-image
|
||||
class="cover-shadow"
|
||||
preview-disabled
|
||||
:src="
|
||||
music.getPlaySongData?.coverSize?.l ||
|
||||
music.getPlaySongData?.cover ||
|
||||
music.getPlaySongData?.localCover
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 信息 -->
|
||||
<div class="data">
|
||||
<div class="desc">
|
||||
<div class="title">
|
||||
<n-text>{{ music.getPlaySongData.name || "未知曲目" }}</n-text>
|
||||
<n-popover :show-arrow="false" placement="right-start" trigger="hover" raw>
|
||||
<template #trigger>
|
||||
<n-tag v-show="playUseOtherSource" round> 其他音源 </n-tag>
|
||||
</template>
|
||||
<div class="title-tip">
|
||||
<n-text>该歌曲暂时无法播放,为您采用其他音源,可能会与原曲存在差别</n-text>
|
||||
<span v-if="music.getPlaySongData.alia" class="alia">
|
||||
{{ music.getPlaySongData.alia }}
|
||||
</span>
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div v-if="Array.isArray(music.getPlaySongData.artists)" class="all-ar">
|
||||
<span
|
||||
v-for="ar in music.getPlaySongData.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="
|
||||
() => {
|
||||
showFullPlayer = false;
|
||||
router.push(`/artist?id=${ar.id}`);
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<span class="ar"> {{ music.getPlaySongData.artists || "未知艺术家" }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
<span v-if="music.getPlaySongData.alia" class="alia">
|
||||
{{ music.getPlaySongData.alia }}
|
||||
</span>
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div v-if="Array.isArray(music.getPlaySongData.artists)" class="all-ar">
|
||||
<span
|
||||
v-for="ar in music.getPlaySongData.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
<div
|
||||
class="album"
|
||||
@click.stop="
|
||||
() => {
|
||||
showFullPlayer = false;
|
||||
router.push(`/artist?id=${ar.id}`);
|
||||
if (typeof music.getPlaySongData.album !== 'string') {
|
||||
showFullPlayer = false;
|
||||
router.push(`/album?id=${music.getPlaySongData?.album.id}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="album" />
|
||||
</n-icon>
|
||||
<span v-if="music.getPlaySongData.album" class="al">
|
||||
{{
|
||||
typeof music.getPlaySongData.album === "string"
|
||||
? music.getPlaySongData.album
|
||||
: music.getPlaySongData.album.name
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="album">未知专辑</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<span class="ar"> {{ music.getPlaySongData.artists || "未知艺术家" }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="album"
|
||||
@click.stop="
|
||||
() => {
|
||||
if (typeof music.getPlaySongData.album !== 'string') {
|
||||
showFullPlayer = false;
|
||||
router.push(`/album?id=${music.getPlaySongData?.album.id}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="album" />
|
||||
</n-icon>
|
||||
<span v-if="music.getPlaySongData.album" class="al">
|
||||
{{
|
||||
typeof music.getPlaySongData.album === "string"
|
||||
? music.getPlaySongData.album
|
||||
: music.getPlaySongData.album.name
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="album">未知专辑</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['right', { pure: pureLyricMode && isHasLrc }]">
|
||||
<!-- 唱片模式下信息 -->
|
||||
<div
|
||||
v-show="(pureLyricMode && isHasLrc) || (playCoverType === 'record' && isHasLrc)"
|
||||
class="data"
|
||||
>
|
||||
<div class="name">
|
||||
<span class="name-text">{{ music.getPlaySongData.name || "未知曲目" }}</span>
|
||||
<span v-if="music.getPlaySongData.alia" class="name-alias">
|
||||
{{ music.getPlaySongData.alia }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="other">
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div v-if="Array.isArray(music.getPlaySongData.artists)" class="all-ar">
|
||||
<span
|
||||
v-for="ar in music.getPlaySongData.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="
|
||||
() => {
|
||||
showFullPlayer = false;
|
||||
router.push(`/artist?id=${ar.id}`);
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<span class="ar"> {{ music.getPlaySongData.artists || "未知艺术家" }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="album"
|
||||
@click.stop="
|
||||
() => {
|
||||
if (typeof music.getPlaySongData.album !== 'string') {
|
||||
showFullPlayer = false;
|
||||
router.push(`/album?id=${music.getPlaySongData?.album.id}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="album" />
|
||||
</n-icon>
|
||||
<span v-if="music.getPlaySongData.album" class="al">
|
||||
{{
|
||||
typeof music.getPlaySongData.album === "string"
|
||||
? music.getPlaySongData.album
|
||||
: music.getPlaySongData.album.name
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="album">未知专辑</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 歌词 -->
|
||||
<Lyric :cursorShow="playerControlShow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 歌词 -->
|
||||
<Lyric :cursorShow="playerControlShow" />
|
||||
</Transition>
|
||||
<!-- 控制中心 -->
|
||||
<PlayerControl v-show="playerControlShow" />
|
||||
</div>
|
||||
@@ -183,20 +233,30 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicData, siteStatus, siteSettings, siteData } from "@/stores";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import screenfull from "screenfull";
|
||||
import throttle from "@/utils/throttle";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { playList, playSongLyric } = storeToRefs(music);
|
||||
const { playerBackgroundType, showYrc } = storeToRefs(settings);
|
||||
const { coverTheme, coverBackground } = storeToRefs(data);
|
||||
const { playerControlShow, controlTimeOut, showFullPlayer, playUseOtherSource } =
|
||||
storeToRefs(status);
|
||||
const { playerBackgroundType, showYrc, playCoverType } = storeToRefs(settings);
|
||||
const {
|
||||
playerControlShow,
|
||||
controlTimeOut,
|
||||
showFullPlayer,
|
||||
playUseOtherSource,
|
||||
coverTheme,
|
||||
coverBackground,
|
||||
pureLyricMode,
|
||||
} = storeToRefs(status);
|
||||
|
||||
// 是否有歌词
|
||||
const isHasLrc = computed(() => {
|
||||
return playSongLyric.value.lrc?.[0] && playSongLyric.value.lrc?.length > 4;
|
||||
});
|
||||
|
||||
// 全屏状态
|
||||
const screenfullStatus = ref(false);
|
||||
@@ -327,6 +387,7 @@ onUnmounted(() => {
|
||||
justify-content: space-between;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
-webkit-app-region: no-drag;
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
@@ -335,6 +396,17 @@ onUnmounted(() => {
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
.n-icon {
|
||||
margin-left: 0;
|
||||
margin-right: 12px;
|
||||
.n-text {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-icon {
|
||||
margin-left: 12px;
|
||||
width: 40px;
|
||||
@@ -360,138 +432,214 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 内容
|
||||
.content {
|
||||
width: 50%;
|
||||
.main-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
transition: transform 0.3s;
|
||||
&.no-lrc {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
.cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// 内容
|
||||
.content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 70%;
|
||||
max-width: 55vh;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 10px 6px #00000008;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
.cover-shadow {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(20px) opacity(0.6);
|
||||
transform: scale(0.95);
|
||||
z-index: 0;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.data {
|
||||
width: 70%;
|
||||
max-width: 55vh;
|
||||
margin-top: 24px;
|
||||
padding: 0 2px;
|
||||
box-sizing: border-box;
|
||||
.desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.title {
|
||||
align-items: center;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
.data {
|
||||
width: 70%;
|
||||
max-width: 55vh;
|
||||
margin-top: 24px;
|
||||
padding: 0 2px;
|
||||
box-sizing: border-box;
|
||||
.desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
.n-text {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: var(--cover-main-color);
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.n-tag {
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.alia {
|
||||
margin: 6px 0 6px 2px;
|
||||
opacity: 0.6;
|
||||
font-size: 18px;
|
||||
}
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--cover-main-color);
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 16px;
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
transition: opacity 0.3s;
|
||||
flex-direction: column;
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
.name {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: var(--cover-main-color);
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.n-tag {
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
transition: none;
|
||||
}
|
||||
&:last-child {
|
||||
}
|
||||
}
|
||||
.alia {
|
||||
margin: 6px 0 6px 2px;
|
||||
opacity: 0.6;
|
||||
font-size: 18px;
|
||||
}
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--cover-main-color);
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 16px;
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
transition: opacity 0.3s;
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
display: none;
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
transition: none;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.album {
|
||||
margin-top: 2px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--cover-main-color);
|
||||
}
|
||||
.al {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
// -webkit-line-clamp: 2;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.album {
|
||||
margin-top: 2px;
|
||||
font-size: 16px;
|
||||
&.record {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
.desc {
|
||||
align-items: center;
|
||||
.title {
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.no-lrc {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
}
|
||||
.right {
|
||||
width: 50%;
|
||||
transition: width 0.3s;
|
||||
.data {
|
||||
padding: 0 80px 0 24px;
|
||||
margin-bottom: 26px;
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
.name-text {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.name-alias {
|
||||
margin-top: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
.other {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 8px;
|
||||
font-size: 16px;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--cover-main-color);
|
||||
}
|
||||
.al {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
// -webkit-line-clamp: 2;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
.artist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
transition: opacity 0.3s;
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
transition: none;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.album {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
.al {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.pure {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.data {
|
||||
padding: 0 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.other,
|
||||
.name {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,7 +657,6 @@ onUnmounted(() => {
|
||||
.title-tip {
|
||||
width: 200px;
|
||||
padding: 12px 20px;
|
||||
background-color: var(--main-second-color);
|
||||
border-radius: 12px;
|
||||
.n-text {
|
||||
display: initial;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="{
|
||||
cursor: cursorShow ? 'pointer' : 'none',
|
||||
}"
|
||||
:class="['lyric', `lyric-${lyricsPosition}`]"
|
||||
:class="['lyric', `lyric-${lyricsPosition}`, { pure: pureLyricMode }, playCoverType]"
|
||||
@mouseenter="lrcMouseStatus = lrcMousePause ? true : false"
|
||||
@mouseleave="lrcAllLeave"
|
||||
>
|
||||
@@ -13,6 +13,8 @@
|
||||
v-if="playSongLyric.lrc?.[0] && playSongLyric.lrc?.length > 4"
|
||||
:key="playSongLyric.lrc?.[0]"
|
||||
class="lyric-all"
|
||||
@after-enter="lyricsScroll(playSongLyricIndex)"
|
||||
@after-leave="lyricsScroll(playSongLyricIndex)"
|
||||
>
|
||||
<n-scrollbar ref="lyricScroll" style="width: 100%">
|
||||
<!-- 普通歌词 -->
|
||||
@@ -32,6 +34,11 @@
|
||||
:id="'lrc' + index"
|
||||
:key="index"
|
||||
:class="{ 'lrc-line': true, on: Number(playSongLyricIndex) === index, islrc: true }"
|
||||
:style="{
|
||||
filter: lyricsBlur
|
||||
? `blur(${Math.min(Math.abs(Number(playSongLyricIndex) - index) * 1.5, 10)}px)`
|
||||
: 'blur(0)',
|
||||
}"
|
||||
@click.stop="jumpSeek(item?.time)"
|
||||
>
|
||||
<!-- 歌词 -->
|
||||
@@ -141,7 +148,7 @@ const props = defineProps({
|
||||
const music = musicData();
|
||||
const settings = siteSettings();
|
||||
const status = siteStatus();
|
||||
const { playSeek } = storeToRefs(status);
|
||||
const { playSeek, pureLyricMode } = storeToRefs(status);
|
||||
const { playSongLyric, playSongLyricIndex } = storeToRefs(music);
|
||||
const {
|
||||
showYrc,
|
||||
@@ -154,6 +161,7 @@ const {
|
||||
lyricsBlur,
|
||||
showTransl,
|
||||
showRoma,
|
||||
playCoverType,
|
||||
} = storeToRefs(settings);
|
||||
|
||||
// 歌词滚动数据
|
||||
@@ -188,14 +196,14 @@ const getYrcStyle = (wordData, lyricIndex) => {
|
||||
// 如果当前歌词索引与播放歌曲的歌词索引不匹配
|
||||
if (playSongLyricIndex.value !== lyricIndex) {
|
||||
return {
|
||||
transitionDuration: `0ms, 0ms, 0.5s`,
|
||||
transitionDuration: `0ms, 0ms, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
};
|
||||
}
|
||||
// 如果播放状态不是加载中,且当前单词的时间加上持续时间减去播放进度大于 0
|
||||
if (status.playLoading === false && wordData.time + wordData.duration - playSeek.value > 0) {
|
||||
return {
|
||||
transitionDuration: `0s, 0s, 0.5s`,
|
||||
transitionDuration: `0s, 0s, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
WebkitMaskPositionX: `${
|
||||
100 - Math.max(((playSeek.value - wordData.time) / wordData.duration) * 100, 0)
|
||||
@@ -204,7 +212,7 @@ const getYrcStyle = (wordData, lyricIndex) => {
|
||||
}
|
||||
// 如果以上条件都不满足
|
||||
return {
|
||||
transitionDuration: `${wordData.duration}ms, ${wordData.duration * 0.8}ms, 0.5s`,
|
||||
transitionDuration: `${wordData.duration}ms, ${wordData.duration * 0.8}ms, 0.35s`,
|
||||
transitionDelay: `${wordData.time - playSeek.value}ms, ${
|
||||
wordData.time - playSeek.value + wordData.duration * 0.5
|
||||
}ms, 0ms`,
|
||||
@@ -229,11 +237,22 @@ const jumpSeek = (time) => {
|
||||
fadePlayOrPause();
|
||||
};
|
||||
|
||||
// 主进程调用歌词滚动
|
||||
if (typeof electron !== "undefined") {
|
||||
electron.ipcRenderer.on("lyricsScroll", () => {
|
||||
lyricsScroll(playSongLyricIndex.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 监听歌词滚动
|
||||
watch(
|
||||
() => playSongLyricIndex.value,
|
||||
(val) => lyricsScroll(val),
|
||||
);
|
||||
watch(
|
||||
() => pureLyricMode.value,
|
||||
() => lyricsScroll(playSongLyricIndex.value),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick().then(() => {
|
||||
@@ -244,7 +263,7 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lyric {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -272,11 +291,13 @@ onMounted(() => {
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
:deep(.n-scrollbar-rail) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding-left: 10px;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
@@ -305,9 +326,9 @@ onMounted(() => {
|
||||
transform: scale(0.86);
|
||||
transform-origin: left center;
|
||||
transition:
|
||||
filter 0.3s,
|
||||
opacity 0.3s,
|
||||
transform 0.3s ease-in-out;
|
||||
filter 0.35s,
|
||||
opacity 0.35s,
|
||||
transform 0.35s ease-in-out;
|
||||
.lrc-content {
|
||||
font-size: 46px;
|
||||
font-weight: bold;
|
||||
@@ -340,7 +361,7 @@ onMounted(() => {
|
||||
);
|
||||
-webkit-mask-size: 220% 100%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.35s;
|
||||
}
|
||||
&.end-with-space {
|
||||
margin-right: 12px;
|
||||
@@ -354,11 +375,13 @@ onMounted(() => {
|
||||
margin-top: 8px;
|
||||
font-size: 30px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.35s;
|
||||
}
|
||||
.lrc-roma {
|
||||
margin-top: 4px;
|
||||
font-size: 20px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.35s;
|
||||
}
|
||||
&.islrc {
|
||||
opacity: 0.3;
|
||||
@@ -367,10 +390,10 @@ onMounted(() => {
|
||||
.lrc-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.lrc-fy,
|
||||
.lrc-roma {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
.lrc-fy,
|
||||
.lrc-roma {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
&.on {
|
||||
@@ -413,8 +436,8 @@ onMounted(() => {
|
||||
z-index: 0;
|
||||
transform: scale(1.05);
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
transform 0.35s ease,
|
||||
opacity 0.35s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover {
|
||||
@@ -432,18 +455,19 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.lyric-center {
|
||||
&.lyric-center,
|
||||
&.pure {
|
||||
span {
|
||||
text-align: center;
|
||||
text-align: center !important;
|
||||
}
|
||||
.placeholder {
|
||||
justify-content: center;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.lrc-line {
|
||||
transform-origin: center;
|
||||
align-items: center;
|
||||
transform-origin: center !important;
|
||||
align-items: center !important;
|
||||
.lrc-content {
|
||||
justify-content: center;
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,5 +486,22 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.pure {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding: 0 80px;
|
||||
}
|
||||
}
|
||||
&.record,
|
||||
&.pure {
|
||||
height: calc(100vh - 340px);
|
||||
margin-bottom: 20px;
|
||||
.lrc-line {
|
||||
margin-bottom: -12px;
|
||||
transform: scale(0.76);
|
||||
&.on {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
:class="{
|
||||
'main-player': true,
|
||||
'show-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
'no-sider': !showSider,
|
||||
}"
|
||||
content-style="padding: 0"
|
||||
@dblclick.stop="showFullPlayer = true"
|
||||
@dblclick.stop="openFullPlayer"
|
||||
>
|
||||
<!-- 进度条 -->
|
||||
<vue-slider
|
||||
@@ -30,7 +31,7 @@
|
||||
<div class="player">
|
||||
<!-- 歌曲信息 -->
|
||||
<div class="info">
|
||||
<div class="cover" @click.stop="showFullPlayer = true">
|
||||
<div class="cover" @click.stop="openFullPlayer">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-image
|
||||
:key="music.getPlaySongData?.id"
|
||||
@@ -61,9 +62,12 @@
|
||||
</div>
|
||||
<div class="song-info">
|
||||
<div class="name">
|
||||
<n-text class="text">{{ music.getPlaySongData?.name || "未知曲目" }}</n-text>
|
||||
<n-text class="text">
|
||||
{{ music.getPlaySongData?.name || "未知曲目" }}
|
||||
</n-text>
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
v-if="playMode !== 'dj'"
|
||||
class="favorite"
|
||||
@click.stop="
|
||||
data.changeLikeList(
|
||||
@@ -84,7 +88,7 @@
|
||||
</n-icon>
|
||||
<!-- 更多操作 -->
|
||||
<n-dropdown
|
||||
v-if="!music.getPlaySongData?.path"
|
||||
v-if="playMode !== 'dj' && !music.getPlaySongData?.path"
|
||||
:options="songMoreOptions"
|
||||
:show-arrow="true"
|
||||
placement="top-start"
|
||||
@@ -99,7 +103,8 @@
|
||||
<!-- 歌手 -->
|
||||
<div
|
||||
v-if="
|
||||
((!playState || !bottomLyricShow) && playSongLyric.lrc?.length) ||
|
||||
((!playState || !bottomLyricShow || playMode === 'dj') &&
|
||||
playSongLyric.lrc?.length) ||
|
||||
playSongLyricIndex === -1
|
||||
"
|
||||
class="artist"
|
||||
@@ -118,6 +123,7 @@
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</template>
|
||||
<div v-else-if="playMode === 'dj'" class="ar">电台节目</div>
|
||||
<n-text v-else class="ar">
|
||||
{{ music.getPlaySongData?.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
@@ -171,6 +177,7 @@
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
tag="div"
|
||||
type="primary"
|
||||
class="play-control"
|
||||
strong
|
||||
@@ -192,110 +199,121 @@
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 功能区 -->
|
||||
<div class="menu">
|
||||
<!-- 时间进度 -->
|
||||
<div class="time">
|
||||
<n-text class="played" depth="3">{{ playTimeData.played }}</n-text>
|
||||
<n-text depth="3">{{ playTimeData.durationTime }}</n-text>
|
||||
</div>
|
||||
<!-- 播放模式 -->
|
||||
<n-dropdown
|
||||
v-if="playMode === 'normal'"
|
||||
:options="playModeOptions"
|
||||
:show-arrow="true"
|
||||
trigger="hover"
|
||||
@select="playModeChange"
|
||||
>
|
||||
<div class="mode" @click.stop @dblclick.stop>
|
||||
<n-icon size="22">
|
||||
<SvgIcon
|
||||
:icon="
|
||||
playHeartbeatMode
|
||||
? 'heartbit'
|
||||
: playSongMode === 'normal'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
"
|
||||
isSpecial
|
||||
/>
|
||||
</n-icon>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="playMode" class="menu">
|
||||
<!-- 时间进度 -->
|
||||
<div class="time">
|
||||
<n-text class="played" depth="3">{{ playTimeData.played }}</n-text>
|
||||
<n-text depth="3">{{ playTimeData.durationTime }}</n-text>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<!-- 倍速 -->
|
||||
<n-popover :show-arrow="false" trigger="hover" placement="top-end" raw>
|
||||
<template #trigger>
|
||||
<div class="speed" @click.stop="(playRate = 1), setRate(1)" @dblclick.stop>
|
||||
<n-icon v-if="playRate === 1" size="22">
|
||||
<SvgIcon icon="speed-rounded" />
|
||||
</n-icon>
|
||||
<n-text v-else class="speed-text">{{ playRate }}x</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 倍速调整 -->
|
||||
<div class="slider-content">
|
||||
<n-slider
|
||||
v-model:value="playRate"
|
||||
:tooltip="false"
|
||||
:min="0.1"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
:marks="{
|
||||
0.1: '减速',
|
||||
1: '正常',
|
||||
2: '加速',
|
||||
}"
|
||||
style="width: 220px"
|
||||
@update:value="setRate"
|
||||
/>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon class="volume" size="22" @click.stop="setVolumeMute" @wheel="changeVolume">
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon v-else-if="playVolume > 0 && playVolume < 0.4" icon="volume-mute-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
:style="{
|
||||
padding: '10px 0',
|
||||
width: '50px',
|
||||
}"
|
||||
class="slider-content"
|
||||
@wheel="changeVolume"
|
||||
<!-- 播放模式 -->
|
||||
<n-dropdown
|
||||
v-if="playMode !== 'fm'"
|
||||
:options="playModeOptions"
|
||||
:show-arrow="true"
|
||||
trigger="hover"
|
||||
@select="playModeChange"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<n-icon
|
||||
v-if="playMode === 'normal'"
|
||||
class="playlist"
|
||||
size="22"
|
||||
@click.stop="playListShow = !playListShow"
|
||||
>
|
||||
<SvgIcon icon="queue-music-rounded" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div class="mode" @click.stop @dblclick.stop>
|
||||
<n-icon size="22">
|
||||
<SvgIcon
|
||||
:icon="
|
||||
playHeartbeatMode
|
||||
? 'heartbit'
|
||||
: playSongMode === 'normal'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
"
|
||||
isSpecial
|
||||
/>
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<!-- 倍速 -->
|
||||
<n-popover :show-arrow="false" trigger="hover" placement="top-end" raw>
|
||||
<template #trigger>
|
||||
<div class="speed" @click.stop="(playRate = 1), setRate(1)" @dblclick.stop>
|
||||
<n-icon v-if="playRate === 1" size="22">
|
||||
<SvgIcon icon="speed-rounded" />
|
||||
</n-icon>
|
||||
<n-text v-else class="speed-text">{{ playRate }}x</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 倍速调整 -->
|
||||
<div class="slider-content">
|
||||
<n-slider
|
||||
v-model:value="playRate"
|
||||
:tooltip="false"
|
||||
:min="0.1"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
:marks="{
|
||||
0.1: '减速',
|
||||
1: '正常',
|
||||
2: '加速',
|
||||
}"
|
||||
style="width: 220px"
|
||||
@update:value="setRate"
|
||||
/>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon class="volume" size="22" @click.stop="setVolumeMute" @wheel="changeVolume">
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume > 0 && playVolume < 0.4"
|
||||
icon="volume-mute-rounded"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
:style="{
|
||||
padding: '10px 0',
|
||||
width: '50px',
|
||||
}"
|
||||
class="slider-content"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<n-badge
|
||||
v-if="playMode !== 'fm'"
|
||||
:value="playList?.length ?? 0"
|
||||
:show="showPlaylistCount"
|
||||
:max="999"
|
||||
:style="{
|
||||
marginRight: showPlaylistCount ? '12px' : null,
|
||||
}"
|
||||
class="playlist"
|
||||
>
|
||||
<n-icon size="22" @click.stop="playListShow = !playListShow">
|
||||
<SvgIcon icon="queue-music-rounded" />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- 添加到歌单 -->
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
@@ -343,7 +361,7 @@ const {
|
||||
playSongLyric,
|
||||
} = storeToRefs(music);
|
||||
const { playLoading, playState, playListShow, showPlayBar, showFullPlayer } = storeToRefs(status);
|
||||
const { showYrc, bottomLyricShow } = storeToRefs(settings);
|
||||
const { showYrc, bottomLyricShow, showSider, showPlaylistCount } = storeToRefs(settings);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
@@ -448,6 +466,15 @@ const songTimeSliderUpdate = (val) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 开启播放器
|
||||
const openFullPlayer = () => {
|
||||
if (playMode.value === "dj") {
|
||||
$message.warning("当前为电台模式,无法开启播放器");
|
||||
return false;
|
||||
}
|
||||
showFullPlayer.value = true;
|
||||
};
|
||||
|
||||
// 上下曲切换
|
||||
const changePlayIndexDebounce = debounce(async (type, id) => {
|
||||
// 垃圾桶
|
||||
@@ -507,9 +534,6 @@ watch(
|
||||
padding: 0 15px;
|
||||
z-index: 2;
|
||||
transition: bottom 0.3s;
|
||||
&.show-bar {
|
||||
bottom: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
@@ -534,12 +558,12 @@ watch(
|
||||
border-radius: 25px;
|
||||
.vue-slider-process {
|
||||
background-color: var(--main-color);
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot-handle {
|
||||
transition: box-shadow 0.3s;
|
||||
@@ -672,6 +696,7 @@ watch(
|
||||
.lrc-text {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
.space {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -721,18 +746,7 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
.played {
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
transition: opacity 0.1s;
|
||||
.n-icon {
|
||||
margin-left: 8px;
|
||||
padding: 8px;
|
||||
@@ -754,6 +768,18 @@ watch(
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
.played {
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.speed {
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
@@ -775,6 +801,34 @@ watch(
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.playlist {
|
||||
transition: margin 0.3s;
|
||||
&.count {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
:deep(.n-badge-sup) {
|
||||
background: var(--main-boxshadow-color);
|
||||
backdrop-filter: blur(20px);
|
||||
.n-base-slot-machine {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.show-bar {
|
||||
bottom: 0;
|
||||
}
|
||||
&.no-sider {
|
||||
padding: 0;
|
||||
.player {
|
||||
width: auto;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 10vw;
|
||||
@media (max-width: 1200px) {
|
||||
padding: 0 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
:keyboard="false"
|
||||
class="play-control"
|
||||
strong
|
||||
secondary
|
||||
@@ -345,12 +346,12 @@ const controlMove = (e) => {
|
||||
background-color: var(--cover-second-color);
|
||||
.vue-slider-process {
|
||||
background-color: var(--cover-main-color);
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
.vue-slider-dot-handle {
|
||||
background-color: var(--cover-main-color);
|
||||
}
|
||||
|
||||
210
src/components/Player/PlayerCover.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<!-- 播放器 - 专辑封面 -->
|
||||
<template>
|
||||
<div :class="['cover', playCoverType]">
|
||||
<!-- 指针 -->
|
||||
<img
|
||||
v-if="playCoverType === 'record'"
|
||||
:class="{ pointer: true, play: playState }"
|
||||
src="/images/pic/pointer.png?assest"
|
||||
alt="pointer"
|
||||
/>
|
||||
<!-- 专辑图片 -->
|
||||
<n-image
|
||||
:src="
|
||||
music.getPlaySongData?.coverSize?.l ||
|
||||
music.getPlaySongData?.cover ||
|
||||
music.getPlaySongData?.localCover
|
||||
"
|
||||
:style="{
|
||||
animationPlayState: playState ? 'running' : 'paused',
|
||||
}"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="loading-img" />
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 封面背板 -->
|
||||
<n-image
|
||||
class="cover-shadow"
|
||||
preview-disabled
|
||||
:src="
|
||||
music.getPlaySongData?.coverSize?.l ||
|
||||
music.getPlaySongData?.cover ||
|
||||
music.getPlaySongData?.localCover
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { playCoverType } = storeToRefs(settings);
|
||||
const { playState } = storeToRefs(status);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70%;
|
||||
max-width: 55vh;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 10px 6px #00000008;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
.cover-shadow {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: blur(20px) opacity(0.6);
|
||||
transform: scale(0.95);
|
||||
z-index: 0;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
&.record {
|
||||
position: relative;
|
||||
width: 55vh;
|
||||
.pointer {
|
||||
position: absolute;
|
||||
width: 14vh;
|
||||
left: calc(50% - 1.8vh);
|
||||
top: -11vh;
|
||||
transform: rotate(-20deg);
|
||||
transform-origin: 1.8vh 1.8vh;
|
||||
z-index: 2;
|
||||
transition: transform 0.3s;
|
||||
&.play {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
.cover-img {
|
||||
animation: playerCoverRotate 18s linear infinite;
|
||||
border-radius: 50%;
|
||||
border: 1vh solid #ffffff30;
|
||||
background: linear-gradient(black 0%, transparent, black 98%),
|
||||
radial-gradient(
|
||||
#000 52%,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555,
|
||||
#000,
|
||||
#555
|
||||
);
|
||||
background-clip: content-box;
|
||||
width: 46vh;
|
||||
height: 46vh;
|
||||
min-width: 46vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
:deep(img) {
|
||||
border: 1vh solid #ffffff40;
|
||||
border-radius: 50%;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.cover-loading {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
padding-bottom: 0;
|
||||
.loading-img {
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cover-shadow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,10 @@
|
||||
<div
|
||||
class="private-fm"
|
||||
:style="{
|
||||
color: playMode === 'fm' && settings.themeAutoCover ? 'var(--main-color)' : '#efefef',
|
||||
'--color':
|
||||
playMode === 'fm' && settings.themeAutoCover && Object.keys(coverTheme)?.length
|
||||
? `rgb(${coverTheme.dark.bg})`
|
||||
: '#efefef',
|
||||
}"
|
||||
>
|
||||
<!-- 背景 -->
|
||||
@@ -45,7 +48,12 @@
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div v-if="privateFmSong?.artists" class="all-ar">
|
||||
<span v-for="ar in privateFmSong.artists" :key="ar.id" class="ar">
|
||||
<span
|
||||
v-for="ar in privateFmSong.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -67,8 +75,8 @@
|
||||
<div class="control">
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
:class="{ 'play-control': true, isFm: playMode === 'fm' && settings.themeAutoCover }"
|
||||
:loading="playMode === 'fm' && playLoading"
|
||||
class="play-control"
|
||||
color="#efefef"
|
||||
type="primary"
|
||||
strong
|
||||
@@ -134,7 +142,7 @@ const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const router = useRouter();
|
||||
const { privateFmSong, playMode } = storeToRefs(music);
|
||||
const { playLoading, playState } = storeToRefs(status);
|
||||
const { playLoading, playState, coverTheme } = storeToRefs(status);
|
||||
|
||||
// 播放暂停
|
||||
const fmPlayOrPause = () => {
|
||||
@@ -240,6 +248,7 @@ onBeforeMount(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
color: var(--color);
|
||||
.info {
|
||||
.name {
|
||||
display: -webkit-box;
|
||||
@@ -256,6 +265,7 @@ onBeforeMount(async () => {
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--color);
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
@@ -292,6 +302,7 @@ onBeforeMount(async () => {
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--color);
|
||||
}
|
||||
.al {
|
||||
display: -webkit-box;
|
||||
@@ -315,9 +326,11 @@ onBeforeMount(async () => {
|
||||
align-items: center;
|
||||
height: 46px;
|
||||
margin-top: auto;
|
||||
color: var(--color);
|
||||
.play-control {
|
||||
--n-width: 46px;
|
||||
--n-height: 46px;
|
||||
color: var(--color);
|
||||
margin-right: 12px;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
@@ -331,9 +344,6 @@ onBeforeMount(async () => {
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
&.isFm {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.play-other {
|
||||
margin-right: 12px;
|
||||
@@ -355,6 +365,7 @@ onBeforeMount(async () => {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: var(--color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -180,6 +180,7 @@ onBeforeMount(() => {
|
||||
margin-top: 8px;
|
||||
.n-tag {
|
||||
font-size: 13px;
|
||||
background-color: var(--n-action-color);
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
transform 0.3s,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
v-model:value="searchInputValue"
|
||||
:class="status.searchInputFocus ? 'input focus' : 'input'"
|
||||
:input-props="{ autoComplete: false }"
|
||||
:allow-input="noSideSpace"
|
||||
placeholder="搜索音乐 / 视频"
|
||||
round
|
||||
clearable
|
||||
@@ -28,22 +29,31 @@
|
||||
/>
|
||||
</Transition>
|
||||
<!-- 热搜榜及历史 -->
|
||||
<SearchHot :searchValue="searchInputValue" @toSearch="toSearch" />
|
||||
<SearchHot :searchValue="searchInputValue?.trim()" @toSearch="toSearch" />
|
||||
<!-- 搜索建议 -->
|
||||
<SearchSuggestions :searchValue="searchInputValue" @toSearch="toSearch" />
|
||||
<SearchSuggestions :searchValue="searchInputValue?.trim()" @toSearch="toSearch" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { siteData, siteStatus } from "@/stores";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSongDetail } from "@/api/song";
|
||||
import { siteData, siteStatus, musicData } from "@/stores";
|
||||
import { addSongToNext, initPlayer } from "@/utils/Player";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const data = siteData();
|
||||
const { playSongData } = storeToRefs(music);
|
||||
|
||||
const searchInpRef = ref(null);
|
||||
const searchInputValue = ref(null);
|
||||
const searchInputValue = ref("");
|
||||
|
||||
// 搜索框输入限制
|
||||
const noSideSpace = (value) => !value.startsWith(" ");
|
||||
|
||||
// 搜索框 focus
|
||||
const searchInputFocus = () => {
|
||||
@@ -53,6 +63,7 @@ const searchInputFocus = () => {
|
||||
|
||||
// 添加搜索历史
|
||||
const setSearchHistory = (name) => {
|
||||
if (!name || !name?.trim()) return false;
|
||||
const index = data.searchHistory.indexOf(name);
|
||||
if (index !== -1) {
|
||||
data.searchHistory.splice(index, 1);
|
||||
@@ -63,6 +74,24 @@ const setSearchHistory = (name) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 直接播放单曲
|
||||
const toPlaySong = async (id) => {
|
||||
try {
|
||||
if (!id) return false;
|
||||
// 获取歌曲信息
|
||||
const result = await getSongDetail(id.toString());
|
||||
const songData = formatData(result?.songs?.[0], "song")?.[0];
|
||||
// 添加至下一曲
|
||||
addSongToNext(songData, true);
|
||||
playSongData.value = songData;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
} catch (error) {
|
||||
console.error("获取歌曲信息失败:", error);
|
||||
$message.error("获取歌曲信息失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 前往搜索
|
||||
const toSearch = (val, type = "song") => {
|
||||
if (!val) return false;
|
||||
@@ -94,12 +123,7 @@ const toSearch = (val, type = "song") => {
|
||||
break;
|
||||
// 单曲
|
||||
case "songs":
|
||||
router.push({
|
||||
path: "/song",
|
||||
query: {
|
||||
id: val,
|
||||
},
|
||||
});
|
||||
toPlaySong(val);
|
||||
break;
|
||||
// 专辑
|
||||
case "albums":
|
||||
|
||||
@@ -42,8 +42,13 @@
|
||||
</n-icon>
|
||||
<n-text class="type-name">{{ searchSuggestionsType[item].name }}</n-text>
|
||||
</div>
|
||||
<div v-for="suggs in suggestionsData[item]" :key="suggs.id" class="suggestions-item">
|
||||
<n-text class="item-name" @click="toSearch(suggs.id, item)">
|
||||
<div
|
||||
v-for="suggs in suggestionsData[item]"
|
||||
:key="suggs.id"
|
||||
class="suggestions-item"
|
||||
@click="toSearch(suggs.id, item)"
|
||||
>
|
||||
<n-text class="item-name">
|
||||
{{ suggs.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
@@ -2,21 +2,28 @@
|
||||
<template>
|
||||
<div class="title-bar">
|
||||
<n-divider vertical />
|
||||
<n-button class="bar-icon" quaternary circle @click="windowMin">
|
||||
<n-button
|
||||
class="bar-icon"
|
||||
tag="div"
|
||||
style="margin-left: 0"
|
||||
quaternary
|
||||
circle
|
||||
@click="windowMin"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon :depth="2">
|
||||
<SvgIcon icon="window-minimize" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button class="bar-icon" quaternary circle @click="maxOrRestore">
|
||||
<n-button class="bar-icon" tag="div" quaternary circle @click="maxOrRestore">
|
||||
<template #icon>
|
||||
<n-icon :depth="2">
|
||||
<SvgIcon :icon="defaultWindowState ? 'window-restore' : 'window-maximize'" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button class="bar-icon" quaternary circle @click="openCloseTip">
|
||||
<n-button class="bar-icon" tag="div" quaternary circle @click="openCloseTip">
|
||||
<template #icon>
|
||||
<n-icon :depth="2">
|
||||
<SvgIcon icon="window-close" />
|
||||
@@ -36,7 +43,7 @@
|
||||
title="关闭软件"
|
||||
transform-origin="center"
|
||||
>
|
||||
<n-text>确认关闭软件吗?</n-text>
|
||||
<n-text class="close-tip">确认关闭软件吗?</n-text>
|
||||
<n-checkbox v-model:checked="closeTipCheckbox"> 记住且不再询问 </n-checkbox>
|
||||
<template #footer>
|
||||
<n-space justify="space-between">
|
||||
@@ -127,7 +134,7 @@ const closeCloseTip = (type) => {
|
||||
};
|
||||
|
||||
// 窗口状态响应
|
||||
electron.ipcRenderer.on("window-state", (_, val) => {
|
||||
electron.ipcRenderer.on("windowState", (_, val) => {
|
||||
defaultWindowState.value = val;
|
||||
});
|
||||
|
||||
@@ -138,16 +145,19 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title-bar {
|
||||
padding-right: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
.n-divider {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.bar-icon {
|
||||
margin-left: 8px;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.close-tip {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
44
src/main.js
@@ -4,13 +4,16 @@ import { checkPlatform } from "@/utils/helper";
|
||||
import App from "@/App.vue";
|
||||
import router from "@/router";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
// 全局样式
|
||||
import "@/style/main.scss";
|
||||
import "@/style/animate.scss";
|
||||
|
||||
// 根据设备类型动态添加
|
||||
// 是否为 Electron
|
||||
const isElectron = checkPlatform.electron();
|
||||
|
||||
// 根据设备类型动态添加
|
||||
const linkElement = document.createElement("link");
|
||||
linkElement.rel = "stylesheet";
|
||||
linkElement.href = isElectron
|
||||
@@ -19,6 +22,45 @@ linkElement.href = isElectron
|
||||
document.head.appendChild(linkElement);
|
||||
document.body.classList.add(isElectron ? "electron" : null);
|
||||
|
||||
// 程序重置
|
||||
window.$cleanAll = (tip = true) => {
|
||||
if (tip) {
|
||||
const isConfirmed = window.confirm(`确认要重置${isElectron ? "应用程序" : "该站点"}吗?`);
|
||||
if (!isConfirmed) return false;
|
||||
}
|
||||
// 清除 localStorage
|
||||
localStorage.clear();
|
||||
// 清除 IndexedDB 数据库
|
||||
indexedDB.deleteDatabase("filesDB");
|
||||
// 清除所有 Cookie
|
||||
document.cookie.split(";").forEach((cookie) => {
|
||||
var eqPos = cookie.indexOf("=");
|
||||
var name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
});
|
||||
// 清除缓存
|
||||
if (caches) {
|
||||
caches.keys().then((names) => {
|
||||
for (let name of names) caches.delete(name);
|
||||
});
|
||||
}
|
||||
return "已重置应用,请" + (isElectron ? "重启应用" : "刷新页面");
|
||||
};
|
||||
|
||||
// 版权声明
|
||||
const logoText = import.meta.env.RENDERER_VITE_SITE_TITLE;
|
||||
const copyrightNotice = `\n\n版本: ${packageJson.version}\n作者: ${packageJson.author}\n作者主页: ${packageJson.home}\nGitHub: ${packageJson.github}`;
|
||||
console.info(
|
||||
`%c${logoText} %c ${copyrightNotice}`,
|
||||
"color:#f55e55;font-size:26px;font-weight:bold;",
|
||||
"font-size:16px",
|
||||
);
|
||||
console.info(
|
||||
"若站点出现异常,可尝试在下方输入 %c$cleanAll()%c 然后按回车来重置",
|
||||
"background: #eaeffd;color:#f55e55;padding: 4px 6px;border-radius:8px;",
|
||||
"background:unset;color:unset;",
|
||||
);
|
||||
|
||||
// 挂载
|
||||
const app = createApp(App);
|
||||
// pinia
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
|
||||
const routes = [
|
||||
// 首页
|
||||
@@ -9,7 +8,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "主页",
|
||||
},
|
||||
component: () => import("@/views/home.vue"),
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
// 搜索
|
||||
{
|
||||
@@ -46,6 +45,11 @@ const routes = [
|
||||
name: "sea-playlists",
|
||||
component: () => import("@/views/Search/playlists.vue"),
|
||||
},
|
||||
{
|
||||
path: "djs",
|
||||
name: "sea-djs",
|
||||
component: () => import("@/views/Search/djs.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 发现音乐
|
||||
@@ -80,23 +84,6 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 视频
|
||||
{
|
||||
path: "/videos",
|
||||
name: "videos",
|
||||
meta: {
|
||||
title: "视频",
|
||||
},
|
||||
component: () => import("@/views/Videos/index.vue"),
|
||||
redirect: "/videos/list",
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
name: "video-list",
|
||||
component: () => import("@/views/Videos/list.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 视频播放
|
||||
{
|
||||
path: "/videos-player",
|
||||
@@ -104,7 +91,17 @@ const routes = [
|
||||
meta: {
|
||||
title: "视频播放器",
|
||||
},
|
||||
component: () => import("@/views/Videos/player.vue"),
|
||||
component: () => import("@/views/Player.vue"),
|
||||
},
|
||||
// 每日推荐
|
||||
{
|
||||
path: "/daily-songs",
|
||||
name: "daily-songs",
|
||||
meta: {
|
||||
title: "每日推荐",
|
||||
needLogin: true,
|
||||
},
|
||||
component: () => import("@/views/DailySongs.vue"),
|
||||
},
|
||||
// 评论
|
||||
{
|
||||
@@ -113,7 +110,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "评论",
|
||||
},
|
||||
component: () => import("@/views/comment.vue"),
|
||||
component: () => import("@/views/Comment.vue"),
|
||||
},
|
||||
// 最近播放
|
||||
{
|
||||
@@ -122,7 +119,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "最近播放",
|
||||
},
|
||||
component: () => import("@/views/history.vue"),
|
||||
component: () => import("@/views/History.vue"),
|
||||
},
|
||||
// 我的云盘
|
||||
{
|
||||
@@ -130,17 +127,9 @@ const routes = [
|
||||
name: "cloud",
|
||||
meta: {
|
||||
title: "我的云盘",
|
||||
needLogin: true,
|
||||
},
|
||||
component: () => import("@/views/cloud.vue"),
|
||||
beforeEnter: (_, __, next) => {
|
||||
if (isLogin()) {
|
||||
next();
|
||||
} else {
|
||||
if (typeof $changeLogin !== "undefined") $changeLogin();
|
||||
$message.error("请登录后使用");
|
||||
$loadingBar.error();
|
||||
}
|
||||
},
|
||||
component: () => import("@/views/Cloud.vue"),
|
||||
},
|
||||
// 歌单
|
||||
{
|
||||
@@ -157,17 +146,9 @@ const routes = [
|
||||
name: "like-songs",
|
||||
meta: {
|
||||
title: "歌单",
|
||||
needLogin: true,
|
||||
},
|
||||
component: () => import("@/views/List/playlist.vue"),
|
||||
beforeEnter: (_, __, next) => {
|
||||
if (isLogin()) {
|
||||
next();
|
||||
} else {
|
||||
if (typeof $changeLogin !== "undefined") $changeLogin();
|
||||
$message.error("请登录后使用");
|
||||
$loadingBar.error();
|
||||
}
|
||||
},
|
||||
},
|
||||
// 专辑
|
||||
{
|
||||
@@ -178,6 +159,15 @@ const routes = [
|
||||
},
|
||||
component: () => import("@/views/List/album.vue"),
|
||||
},
|
||||
// 播客电台
|
||||
{
|
||||
path: "/dj",
|
||||
name: "dj",
|
||||
meta: {
|
||||
title: "播客电台",
|
||||
},
|
||||
component: () => import("@/views/List/dj.vue"),
|
||||
},
|
||||
// 歌手
|
||||
{
|
||||
path: "/artist",
|
||||
@@ -210,22 +200,54 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 我的收藏
|
||||
{
|
||||
path: "/like",
|
||||
name: "like",
|
||||
meta: {
|
||||
title: "我的收藏",
|
||||
needLogin: true,
|
||||
},
|
||||
component: () => import("@/views/Like/index.vue"),
|
||||
redirect: "/like/albums",
|
||||
children: [
|
||||
{
|
||||
path: "albums",
|
||||
name: "like-albums",
|
||||
component: () => import("@/views/Like/albums.vue"),
|
||||
},
|
||||
{
|
||||
path: "artists",
|
||||
name: "like-artists",
|
||||
component: () => import("@/views/Like/artists.vue"),
|
||||
},
|
||||
{
|
||||
path: "videos",
|
||||
name: "like-videos",
|
||||
component: () => import("@/views/Like/videos.vue"),
|
||||
},
|
||||
{
|
||||
path: "playlists",
|
||||
name: "like-playlists",
|
||||
component: () => import("@/views/Like/playlists.vue"),
|
||||
},
|
||||
{
|
||||
path: "djs",
|
||||
name: "like-djs",
|
||||
component: () => import("@/views/Like/djs.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 本地歌曲
|
||||
{
|
||||
path: "/local",
|
||||
name: "local",
|
||||
meta: {
|
||||
title: "本地歌曲",
|
||||
needLogin: true,
|
||||
show: checkPlatform.electron(),
|
||||
},
|
||||
component: () => import("@/views/Local/index.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (checkPlatform.electron()) {
|
||||
next();
|
||||
} else {
|
||||
next("/403");
|
||||
}
|
||||
},
|
||||
redirect: "/local/songs",
|
||||
children: [
|
||||
{
|
||||
@@ -245,6 +267,24 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 播客
|
||||
{
|
||||
path: "/dj-hot",
|
||||
name: "dj-hot",
|
||||
meta: {
|
||||
title: "热门播客",
|
||||
},
|
||||
component: () => import("@/views/Dj/index.vue"),
|
||||
},
|
||||
// 播客 -分类
|
||||
{
|
||||
path: "/dj-type",
|
||||
name: "dj-type",
|
||||
meta: {
|
||||
title: "播客分类",
|
||||
},
|
||||
component: () => import("@/views/Dj/type.vue"),
|
||||
},
|
||||
// 全局设置
|
||||
{
|
||||
path: "/setting",
|
||||
@@ -261,7 +301,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "全局设置",
|
||||
},
|
||||
component: () => import("@/views/song.vue"),
|
||||
component: () => import("@/views/Song.vue"),
|
||||
},
|
||||
// 测试页面
|
||||
{
|
||||
@@ -270,7 +310,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "测试页面",
|
||||
},
|
||||
component: () => import("@/views/test.vue"),
|
||||
component: () => import("@/views/Test.vue"),
|
||||
},
|
||||
// 状态页
|
||||
{
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { getPersonalFm, setFmToTrash } from "@/api/recommend";
|
||||
import { changePlayIndex } from "@/utils/Player";
|
||||
// import { isLogin } from "@/utils/auth";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const useMusicDataStore = defineStore("musicData", {
|
||||
state: () => {
|
||||
return {
|
||||
// 当前模式
|
||||
// normal 正常 / fm 私人 FM
|
||||
// normal 正常 / fm 私人 FM / dj 电台
|
||||
playMode: "normal",
|
||||
// normal 顺序播放 / random 随机播放 / repeat 单曲循环
|
||||
playSongMode: "normal",
|
||||
@@ -197,6 +197,7 @@ const useMusicDataStore = defineStore("musicData", {
|
||||
// 私人FM垃圾桶
|
||||
async setPersonalFmToTrash(id) {
|
||||
try {
|
||||
if (!isLogin()) return $message.warning("请登录后使用");
|
||||
const result = await setFmToTrash(id);
|
||||
if (result.code === 200) {
|
||||
$message.success("已移至垃圾桶");
|
||||
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
getUserArtist,
|
||||
getUserAlbum,
|
||||
getUserMv,
|
||||
getUserDj,
|
||||
} from "@/api/user";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
import throttle from "@/utils/throttle";
|
||||
|
||||
const useSiteDataStore = defineStore("siteData", {
|
||||
@@ -34,6 +36,7 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
artists: [],
|
||||
albums: [],
|
||||
mvs: [],
|
||||
djs: [],
|
||||
},
|
||||
// 每日推荐
|
||||
dailySongsData: {
|
||||
@@ -46,9 +49,6 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
catList: [], // 普通分类
|
||||
hqCatList: [], // 精品分类
|
||||
},
|
||||
// 封面主题
|
||||
coverTheme: {},
|
||||
coverBackground: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
@@ -59,38 +59,31 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
},
|
||||
actions: {
|
||||
// 获取每日推荐
|
||||
async setDailySongsData() {
|
||||
async setDailySongsData(refresh = false) {
|
||||
try {
|
||||
if (!isLogin()) {
|
||||
this.dailySongsData = { timestamp: null, data: [] };
|
||||
return false;
|
||||
}
|
||||
const data = this.dailySongsData.data;
|
||||
const songsData = this.dailySongsData.data;
|
||||
const timestamp = this.dailySongsData.timestamp;
|
||||
if (data[0] && timestamp) {
|
||||
console.log("触发日推缓存");
|
||||
const currentTime = new Date().getTime();
|
||||
const storedTime = parseInt(timestamp, 10);
|
||||
const nextDay6AM = new Date(storedTime);
|
||||
nextDay6AM.setHours(6, 0, 0, 0);
|
||||
if (currentTime <= nextDay6AM.getTime()) {
|
||||
return true;
|
||||
}
|
||||
// 下一天六点
|
||||
const nextDay6AM = new Date(timestamp);
|
||||
nextDay6AM.setDate(nextDay6AM.getDate() + 1);
|
||||
nextDay6AM.setHours(6, 0, 0, 0);
|
||||
// 是否小于今日 6:00
|
||||
const originalHour = new Date(timestamp).getHours();
|
||||
const isAfter6AM =
|
||||
new Date(timestamp).toDateString() === new Date().toDateString() && originalHour >= 6;
|
||||
if (!refresh && songsData?.[0] && isAfter6AM && timestamp <= nextDay6AM.getTime()) {
|
||||
console.log("日推缓存未过期,不更新");
|
||||
return true;
|
||||
} else {
|
||||
const res = await getDailyRec();
|
||||
const data = res.data.dailySongs;
|
||||
const currentTime = new Date().getTime();
|
||||
const formatData = data.map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
artist: v.ar,
|
||||
album: v.al,
|
||||
cover: v.al.picUrl.replace(/^http:/, "https:"),
|
||||
reason: v?.reason,
|
||||
};
|
||||
});
|
||||
this.dailySongsData = { timestamp: currentTime, data: formatData };
|
||||
const songsData = formatData(res.data.dailySongs, "song");
|
||||
console.log("日推缓存不存在或已过期", songsData);
|
||||
this.dailySongsData = { timestamp: new Date().getTime(), data: songsData };
|
||||
if (refresh) $message.success("日推更新成功");
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "每日推荐加载失败");
|
||||
@@ -134,11 +127,11 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
this.setUserLikeArtists(),
|
||||
this.setUserLikeAlbums(),
|
||||
this.setUserLikeMvs(),
|
||||
this.setUserLikeDjs(),
|
||||
];
|
||||
await Promise.all(allUserLikeResult);
|
||||
} catch (error) {
|
||||
console.error("用户信息加载失败:", error);
|
||||
$message.error("用户信息加载失败");
|
||||
showError(error, "用户信息加载失败");
|
||||
}
|
||||
},
|
||||
// 获取用户喜欢歌曲
|
||||
@@ -150,8 +143,7 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
this.userLikeData.songs = res.ids;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("用户喜欢歌曲加载失败:", error);
|
||||
$message.error("用户喜欢歌曲加载失败");
|
||||
showError(error, "用户喜欢歌曲加载失败");
|
||||
}
|
||||
},
|
||||
// 获取用户喜欢歌单
|
||||
@@ -163,11 +155,10 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
const number = createdPlaylistCount + subPlaylistCount ?? 50;
|
||||
// 获取数据
|
||||
getUserPlaylist(this.userData.userId, number).then((res) => {
|
||||
this.userLikeData.playlists = res.playlist;
|
||||
this.userLikeData.playlists = formatData(res.playlist);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("用户喜欢歌单加载失败:", error);
|
||||
$message.error("用户喜欢歌单加载失败");
|
||||
showError(error, "用户喜欢歌单加载失败");
|
||||
}
|
||||
},
|
||||
// 更改用户喜欢歌手
|
||||
@@ -176,11 +167,10 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
if (!isLogin()) return false;
|
||||
// 获取数据
|
||||
getUserArtist().then((res) => {
|
||||
this.userLikeData.artists = res.data;
|
||||
this.userLikeData.artists = formatData(res.data, "artist");
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("用户喜欢歌手加载失败:", error);
|
||||
$message.error("用户喜欢歌手加载失败");
|
||||
showError(error, "用户喜欢歌手加载失败");
|
||||
}
|
||||
},
|
||||
// 更改用户喜欢专辑
|
||||
@@ -194,28 +184,37 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
// 获取数据
|
||||
while (totalCount === null || offset < totalCount) {
|
||||
const res = await getUserAlbum(50, offset);
|
||||
res.data.forEach((v) => {
|
||||
this.userLikeData.albums.push(v);
|
||||
});
|
||||
const albumsData = formatData(res.data, "album");
|
||||
this.userLikeData.albums = this.userLikeData.albums.concat(albumsData);
|
||||
totalCount = res.count;
|
||||
offset += 50;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("用户喜欢专辑加载失败:", error);
|
||||
$message.error("用户喜欢专辑加载失败");
|
||||
showError(error, "用户喜欢专辑加载失败");
|
||||
}
|
||||
},
|
||||
// 更改用户喜欢歌手
|
||||
// 更改用户喜欢视频
|
||||
async setUserLikeMvs() {
|
||||
try {
|
||||
if (!isLogin()) return false;
|
||||
// 获取数据
|
||||
getUserMv().then((res) => {
|
||||
this.userLikeData.mvs = res.data;
|
||||
this.userLikeData.mvs = formatData(res.data, "mv");
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("用户喜欢歌手加载失败:", error);
|
||||
$message.error("用户喜欢歌手加载失败");
|
||||
showError(error, "用户喜欢歌手加载失败");
|
||||
}
|
||||
},
|
||||
// 更改用户喜欢电台
|
||||
async setUserLikeDjs() {
|
||||
try {
|
||||
if (!isLogin()) return false;
|
||||
// 获取数据
|
||||
getUserDj().then((res) => {
|
||||
this.userLikeData.djs = formatData(res.djRadios, "dj");
|
||||
});
|
||||
} catch (error) {
|
||||
showError(error, "用户喜欢电台加载失败");
|
||||
}
|
||||
},
|
||||
// 查找歌曲是否处于喜欢列表
|
||||
@@ -239,7 +238,7 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
// 输出错误
|
||||
const showError = (error, msg, show = true) => {
|
||||
console.error(msg, error);
|
||||
if (show) $message.error(msg);
|
||||
if (show && typeof $message !== "undefined") $message.error(msg);
|
||||
};
|
||||
|
||||
// 移入移除喜欢列表
|
||||
|
||||
@@ -10,13 +10,17 @@ const useSiteSettingsStore = defineStore("siteSettings", {
|
||||
showTaskbarProgress: false, // 显示歌曲任务栏进度
|
||||
searchHistory: true, // 搜索历史
|
||||
autoSignIn: true, // 自动签到
|
||||
showGithub: true,
|
||||
showSider: true, // 显示侧边栏
|
||||
// 主题部分
|
||||
themeType: "dark",
|
||||
themeAuto: false,
|
||||
themeTypeName: "red",
|
||||
themeTypeData: {},
|
||||
themeAutoCover: true, // 主题色跟随封面
|
||||
themeAutoCoverType: "secondary",
|
||||
// 播放部分
|
||||
playCoverType: "cover", // 播放器样式
|
||||
songLevel: "exhigh", // 歌曲音质
|
||||
autoPlay: false, // 程序启动时自动播放
|
||||
songVolumeFade: true, // 歌曲渐入渐出
|
||||
@@ -25,6 +29,8 @@ const useSiteSettingsStore = defineStore("siteSettings", {
|
||||
bottomLyricShow: true, // 底栏歌词显示
|
||||
playerBackgroundType: "blur", // 播放器背景类别 animation 流动 / blur 模糊
|
||||
memorySeek: true, // 记忆上次播放位置
|
||||
playSearch: false, // 是否播放全部搜索结果
|
||||
showPlaylistCount: true, // 是否显示播放列表数量
|
||||
// 数量部分
|
||||
loadSize: 50, // 每页加载数量
|
||||
// 歌词部分
|
||||
|
||||
@@ -25,6 +25,11 @@ const useSiteStatusStore = defineStore("siteStatus", {
|
||||
playSeek: 0,
|
||||
// 是否下一首
|
||||
hasNextSong: false,
|
||||
// 封面主题
|
||||
coverTheme: {},
|
||||
coverBackground: null,
|
||||
// 纯净歌词模式
|
||||
pureLyricMode: false,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
@@ -34,7 +39,7 @@ const useSiteStatusStore = defineStore("siteStatus", {
|
||||
{
|
||||
key: "siteStatus",
|
||||
storage: localStorage,
|
||||
paths: ["asideMenuCollapsed"],
|
||||
paths: ["asideMenuCollapsed", "pureLyricMode"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
1
src/style/animate.css
vendored
@@ -1 +0,0 @@
|
||||
.fade-enter-active,.fade-leave-active{transition:opacity .3s ease-in-out}.fade-enter-from,.fade-leave-to{opacity:0}.fadeDown-enter-active,.fadeDown-leave-active{transition:opacity .3s ease,transform .3s ease}.fadeDown-enter-from,.fadeDown-leave-to{opacity:0;transform:translateY(-10px)}.router-enter-active,.router-leave-active{transition:all .2s ease}.router-enter-from,.router-leave-to{opacity:0;transform:translateX(10px)}.shrink-enter-active,.shrink-leave-active{opacity:1;transform:scale(1);transition:transform .3s,opacity .3s}.shrink-enter-from,.shrink-leave-to{opacity:0;transform:scale(0.4)}.up-enter-active,.up-leave-active{transform:translateY(0);transition:transform .5s cubic-bezier(0.65, 0.05, 0.36, 1)}.up-enter-from,.up-leave-to{transform:translateY(100%)}@keyframes skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}@keyframes coverRotate{0%{transform:rotate(0deg) scale(1) translateX(0)}50%{transform:rotate(180deg) scale(2) translateX(40%)}100%{transform:rotate(360deg) scale(1) translateX(0)}}
|
||||
44
src/style/animate.scss
vendored
@@ -62,6 +62,20 @@
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
// left
|
||||
.left-enter-active,
|
||||
.left-leave-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
}
|
||||
.left-enter-from,
|
||||
.left-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
@@ -83,3 +97,33 @@
|
||||
transform: rotate(360deg) scale(1) translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes playerCoverRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-spacing {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
letter-spacing: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-down {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
*{margin:0;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}html,body,#app{width:100vw;height:100vh;font-family:"HarmonyOS_Regular",sans-serif !important;overflow:hidden;background-color:var(--main-color-bg) !important}.n-text{display:-webkit-box;overflow:hidden;word-break:break-all;-webkit-box-orient:vertical;-webkit-line-clamp:1}.n-image .cover-loading{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:0;padding-bottom:100%;background-color:var(--n-color);border-radius:8px}.n-image .cover-loading .n-spin-body{position:absolute;height:100%;top:0}.n-image .cover-loading.mv{padding-bottom:56%}.n-image .cover-loading .loading-img{position:absolute;top:0;width:100%;height:100%;opacity:1 !important}.n-scrollbar .n-scrollbar-rail{right:0 !important;z-index:20}.n-message-container{bottom:90px !important}.n-message-container .n-message-wrapper .n-message{border-radius:12px}.n-dropdown{--n-border-radius: 8px !important}.n-skeleton{background:linear-gradient(90deg, var(--n-color-end) 25%, var(--n-color-start) 37%, var(--n-color-end) 63%);background-size:400% 100%;animation:skeleton-loading 1.4s ease infinite}.n-tabs{--n-tab-border-radius: 6px !important}.n-tabs .n-tabs-rail .n-tabs-tab-wrapper .n-tabs-tab:hover{transition:color .3s,background-color .5s ease-in-out,box-shadow .3s}.n-modal{width:60vw;max-width:700px;min-width:min(24rem,100vw);border-radius:8px}.n-modal-mask{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.n-result{min-height:calc(100vh - 178px);display:flex;flex-direction:column;justify-content:center}.n-popover{border-radius:8px !important;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.n-back-top{transition:color .3s cubic-bezier(0.4, 0, 0.2, 1),box-shadow .3s cubic-bezier(0.4, 0, 0.2, 1),background-color .3s cubic-bezier(0.4, 0, 0.2, 1),transform .3s cubic-bezier(0.4, 0, 0.2, 1)}.n-back-top:active{transform:scale(0.9)}.n-data-table{--n-merged-th-color: transparent !important;--n-merged-td-color: transparent !important;--n-merged-border-color: transparent !important;--n-merged-th-color-hover: transparent !important;--n-merged-td-color-hover: transparent !important;--n-merged-td-color-striped: transparent !important}.n-data-table .n-data-table__pagination{margin-top:40px;margin-bottom:30px;justify-content:center}.n-image-preview-container .n-image-preview-overlay{-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.n-switch.n-switch--active .n-switch__rail{background-color:var(--main-second-color)}
|
||||
@@ -12,7 +12,6 @@ body,
|
||||
height: 100vh;
|
||||
font-family: "HarmonyOS_Regular", sans-serif !important;
|
||||
overflow: hidden;
|
||||
background-color: var(--main-color-bg) !important;
|
||||
}
|
||||
|
||||
// n-text
|
||||
@@ -77,6 +76,11 @@ body,
|
||||
--n-border-radius: 8px !important;
|
||||
}
|
||||
|
||||
// n-notification
|
||||
.n-notification {
|
||||
--n-border-radius: 8px !important;
|
||||
}
|
||||
|
||||
// n-skeleton
|
||||
.n-skeleton {
|
||||
background: linear-gradient(
|
||||
@@ -165,11 +169,15 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
// n-switch
|
||||
.n-switch {
|
||||
&.n-switch--active {
|
||||
.n-switch__rail {
|
||||
background-color: var(--main-second-color);
|
||||
}
|
||||
// layout-toggle-bar
|
||||
.n-layout-toggle-bar {
|
||||
height: 44px !important;
|
||||
top: calc(50% - 22px) !important;
|
||||
.n-layout-toggle-bar__top,
|
||||
.n-layout-toggle-bar__bottom {
|
||||
height: 24px !important;
|
||||
}
|
||||
.n-layout-toggle-bar__bottom {
|
||||
top: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Howl, Howler } from "howler";
|
||||
import { musicData, siteStatus, siteSettings, siteData } from "@/stores";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { getSongUrl, getSongLyric, songScrobble } from "@/api/song";
|
||||
import { checkPlatform, getLocalCoverData } from "@/utils/helper";
|
||||
import { decode as base642Buffer } from "@/utils/base64";
|
||||
@@ -30,9 +30,11 @@ export const initPlayer = async (playNow = false) => {
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { playList, playIndex } = music;
|
||||
const { playList, playIndex, playMode } = music;
|
||||
// 当前播放歌曲数据
|
||||
const playSongData = music.getPlaySongData;
|
||||
// 若为电台则更改 id
|
||||
playSongData.id = playMode === "dj" ? playSongData.mainTrackId : playSongData.id;
|
||||
// 是否为本地歌曲
|
||||
const isLocalSong = playSongData?.path ? true : false;
|
||||
// 获取封面
|
||||
@@ -42,6 +44,8 @@ export const initPlayer = async (playNow = false) => {
|
||||
const cover = isLocalSong ? music.playSongData?.localCover : playSongData?.coverSize;
|
||||
// 歌词归位
|
||||
music.playSongLyricIndex = -1;
|
||||
// 若为 fm 模式,则清除当前歌曲信息
|
||||
if (playMode === "fm") music.playSongData = {};
|
||||
// 在线歌曲
|
||||
if (!isLocalSong) {
|
||||
// 获取歌曲信息
|
||||
@@ -57,14 +61,22 @@ export const initPlayer = async (playNow = false) => {
|
||||
createPlayer(url);
|
||||
}
|
||||
// 无法正常获取播放地址
|
||||
else if (checkPlatform.electron() && settings.useUnmServer) {
|
||||
else if (checkPlatform.electron() && playMode !== "dj" && settings.useUnmServer) {
|
||||
const url = await getFromUnblockMusic(playSongData, status, playNow);
|
||||
if (url) {
|
||||
status.playUseOtherSource = true;
|
||||
createPlayer(url);
|
||||
} else {
|
||||
isPlayEnd = true;
|
||||
status.playUseOtherSource = false;
|
||||
changePlayIndex("next", true);
|
||||
// 是否为最后一首
|
||||
if (playIndex === playList.length - 1) {
|
||||
status.playState = false;
|
||||
$message.warning("当前列表歌曲无法播放,请更换歌曲");
|
||||
} else {
|
||||
$message.error("该歌曲暂无音源,跳至下一首");
|
||||
changePlayIndex("next", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 下一曲
|
||||
@@ -90,9 +102,9 @@ export const initPlayer = async (playNow = false) => {
|
||||
}
|
||||
}
|
||||
// 获取歌词
|
||||
getSongLyricData(isLocalSong, playSongData);
|
||||
if (playMode !== "dj") getSongLyricData(isLocalSong, playSongData);
|
||||
// 初始化媒体会话控制
|
||||
initMediaSession(playSongData, isLocalSong, cover);
|
||||
initMediaSession(playSongData, cover, isLocalSong, playMode === "dj");
|
||||
// 获取图片主色
|
||||
getColorMainColor(isLocalSong, cover);
|
||||
} catch (error) {
|
||||
@@ -153,7 +165,6 @@ const getFromUnblockMusic = async (data, status, playNow) => {
|
||||
// 调用解灰
|
||||
let musicUrl = await electron.ipcRenderer.invoke("getMusicNumUrl", JSON.stringify(data));
|
||||
if (!musicUrl) {
|
||||
$message.error("该歌曲暂无音源");
|
||||
status.playLoading = false;
|
||||
return null;
|
||||
}
|
||||
@@ -232,13 +243,14 @@ export const createPlayer = async (src, autoPlay = true) => {
|
||||
status.playLoading = false;
|
||||
// 发送歌曲名
|
||||
if (checkPlatform.electron()) {
|
||||
const songName =
|
||||
playSongData.name +
|
||||
" - " +
|
||||
(Array.isArray(playSongData.artists)
|
||||
const songName = playSongData.name || "未知曲目";
|
||||
const songArtist =
|
||||
music.playMode === "dj"
|
||||
? "电台节目"
|
||||
: Array.isArray(playSongData.artists)
|
||||
? playSongData.artists.map((ar) => ar.name).join(" / ")
|
||||
: playSongData.artists || "未知歌手");
|
||||
electron.ipcRenderer.send("songNameChange", songName);
|
||||
: playSongData.artists || "未知歌手";
|
||||
electron.ipcRenderer.send("songNameChange", songName + " - " + songArtist);
|
||||
}
|
||||
// 听歌打卡
|
||||
if (isLogin() && !playSongData?.path) {
|
||||
@@ -331,7 +343,7 @@ export const changePlayIndex = async (type = "next", play = false) => {
|
||||
if (playMode === "fm") {
|
||||
await music.setPersonalFm(true);
|
||||
// 渐出音乐
|
||||
if (!isPlayEnd) await fadePlayOrPause("pause");
|
||||
if (!isPlayEnd) fadePlayOrPause("pause");
|
||||
// 初始化播放器
|
||||
initPlayer(play);
|
||||
return true;
|
||||
@@ -366,9 +378,8 @@ export const changePlayIndex = async (type = "next", play = false) => {
|
||||
const songData = playList?.[music.playIndex];
|
||||
if (songData) {
|
||||
music.playSongData = songData;
|
||||
console.log(songData);
|
||||
// 渐出音乐
|
||||
if (!isPlayEnd) await fadePlayOrPause("pause");
|
||||
if (!isPlayEnd) fadePlayOrPause("pause");
|
||||
// 初始化播放器
|
||||
initPlayer(play);
|
||||
} else {
|
||||
@@ -424,34 +435,25 @@ export const addSongToNext = (data, play = false) => {
|
||||
export const fadePlayOrPause = (type = "play") => {
|
||||
const settings = siteSettings();
|
||||
const duration = settings.songVolumeFade ? 300 : 0;
|
||||
return new Promise((resolve) => {
|
||||
const music = musicData();
|
||||
// 渐入
|
||||
if (type === "play") {
|
||||
if (player?.playing()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
player?.play();
|
||||
// 更新播放进度
|
||||
setAllInterval();
|
||||
player?.once("play", () => {
|
||||
player?.fade(0, music.playVolume, duration);
|
||||
player?.once("fade", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
// 渐出
|
||||
else if (type === "pause") {
|
||||
player?.fade(music.playVolume, 0, duration);
|
||||
player?.once("fade", () => {
|
||||
player?.pause();
|
||||
cleanAllInterval();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
const music = musicData();
|
||||
// 渐入
|
||||
if (type === "play") {
|
||||
if (player?.playing()) return;
|
||||
player?.play();
|
||||
// 更新播放进度
|
||||
setAllInterval();
|
||||
player?.once("play", () => {
|
||||
player?.fade(0, music.playVolume, duration);
|
||||
});
|
||||
}
|
||||
// 渐出
|
||||
else if (type === "pause") {
|
||||
player?.fade(music.playVolume, 0, duration);
|
||||
player?.once("fade", () => {
|
||||
player?.pause();
|
||||
cleanAllInterval();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -459,7 +461,7 @@ export const fadePlayOrPause = (type = "play") => {
|
||||
*/
|
||||
export const playOrPause = async () => {
|
||||
const status = player?.playing();
|
||||
await fadePlayOrPause(status ? "pause" : "play");
|
||||
fadePlayOrPause(status ? "pause" : "play");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -489,7 +491,7 @@ export const checkPlayer = () => {
|
||||
* 停止播放器
|
||||
*/
|
||||
export const soundStop = () => {
|
||||
player?.stop();
|
||||
// player?.stop();
|
||||
// setSeek();
|
||||
Howler.unload();
|
||||
};
|
||||
@@ -614,13 +616,17 @@ const getSongLyricData = async (islocal, data) => {
|
||||
* @param {string} islocal - 是否为本地歌曲
|
||||
* @param {string} cover - 封面图像的URL或数据
|
||||
*/
|
||||
const initMediaSession = async (data, islocal, cover) => {
|
||||
const initMediaSession = async (data, cover, islocal, isDj) => {
|
||||
if ("mediaSession" in navigator) {
|
||||
// 歌曲信息
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: data.name,
|
||||
artist: islocal ? data.artists : data.artists?.map((a) => a.name)?.join(" & "),
|
||||
album: islocal ? data.album : data.album.name,
|
||||
artist: isDj
|
||||
? "电台节目"
|
||||
: islocal
|
||||
? data.artists
|
||||
: data.artists?.map((a) => a.name)?.join(" & "),
|
||||
album: isDj ? "电台节目" : islocal ? data.album : data.album.name,
|
||||
artwork: islocal
|
||||
? [
|
||||
{
|
||||
@@ -646,10 +652,10 @@ const initMediaSession = async (data, islocal, cover) => {
|
||||
});
|
||||
// 按键关联
|
||||
navigator.mediaSession.setActionHandler("play", async () => {
|
||||
await fadePlayOrPause("play");
|
||||
fadePlayOrPause("play");
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("pause", async () => {
|
||||
await fadePlayOrPause("pause");
|
||||
fadePlayOrPause("pause");
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () => {
|
||||
changePlayIndex("prev", true);
|
||||
@@ -667,17 +673,17 @@ const initMediaSession = async (data, islocal, cover) => {
|
||||
* @returns {string} - 主要颜色的RGB十六进制表示
|
||||
*/
|
||||
const getColorMainColor = async (islocal, cover) => {
|
||||
const data = siteData();
|
||||
const status = siteStatus();
|
||||
try {
|
||||
// 获取封面图像的URL
|
||||
if (!cover) return (data.coverColor = {});
|
||||
if (!cover) return (status.coverTheme = {});
|
||||
const colorUrl = islocal ? cover : cover.s;
|
||||
// 获取渐变色背景
|
||||
const gradientColor = await getCoverGradient(colorUrl);
|
||||
data.coverBackground = gradientColor;
|
||||
status.coverBackground = gradientColor;
|
||||
} catch (error) {
|
||||
console.error("封面颜色获取失败:", error);
|
||||
data.coverColor = {};
|
||||
status.coverTheme = {};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||