mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 11:29:26 +08:00
Compare commits
29 Commits
v2.0.0-bet
...
v2.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -27,3 +27,14 @@ 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
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
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
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
1
.gitignore
vendored
@@ -14,6 +14,7 @@ dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
out
|
||||
.env
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
149
LICENSE
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/>.
|
||||
|
||||
131
README.md
131
README.md
@@ -1,10 +1,12 @@
|
||||
> [!IMPORTANT]
|
||||
> ## 🎉 当前项目正在重构中 🎉
|
||||
>
|
||||
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
|
||||
> - 支持客户端与网页端
|
||||
> - 支持现有版本所有功能
|
||||
> - 新增支持播放与管理本地歌曲
|
||||
> ## 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
@@ -118,68 +120,76 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ 部署
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
|
||||
### API 服务(客户端无需理会,如果需要网页端,则必需部署)
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
|
||||
|
||||
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
```
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||
- 请在根目录下的 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 中填入 API 地址(必需)
|
||||

|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "your api url";
|
||||
```
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
### 安装依赖
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
```
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
### 开发
|
||||
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或者
|
||||
yarn dev
|
||||
# 或者
|
||||
npm dev
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/example/repository.git
|
||||
```
|
||||
|
||||
### 构建网页端
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
```
|
||||
|
||||
构建完成后可将生成的 `out/renderer` 文件夹内的文件上传至服务器
|
||||
4. 编译打包
|
||||
|
||||
若使用的为第三方部署平台,比如 `Vercel`,请将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
|
||||

|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
### 构建客户端
|
||||
## ⚙️ 本地部署
|
||||
|
||||
```bash
|
||||
# win
|
||||
pnpm build:win
|
||||
# linux
|
||||
pnpm build:linux
|
||||
# mac
|
||||
pnpm build:mac
|
||||
```
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
构建完成后可在 `dist` 文件夹中打开可执行文件来完成安装操作
|
||||
```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 +201,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 +210,14 @@ 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) 了解更多详情
|
||||
|
||||
7
components.d.ts
vendored
7
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,10 +16,12 @@ 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']
|
||||
@@ -48,7 +51,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,6 +63,7 @@ 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']
|
||||
@@ -79,6 +85,7 @@ declare module 'vue' {
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
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']
|
||||
|
||||
@@ -23,9 +23,7 @@ win:
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
# 构建类型
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
target: nsis
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
# NSIS 安装器配置
|
||||
@@ -75,7 +73,6 @@ linux:
|
||||
# 构建类型
|
||||
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,52 @@ 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/logo/favicon.png",
|
||||
sizes: "200x200",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/logo/favicon_512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
@@ -103,6 +150,9 @@ export default defineConfig(({ mode }) => {
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
linux: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,154 +1,251 @@
|
||||
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 } 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 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);
|
||||
// 设置日志文件的最大大小为 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({
|
||||
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: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 创建系统信息
|
||||
createSystemInfo(mainWindow);
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.once("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
// 创建系统信息
|
||||
createSystemInfo(this.mainWindow);
|
||||
});
|
||||
|
||||
// 在 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);
|
||||
// 注册快捷键
|
||||
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,4 +1,5 @@
|
||||
import { join } from "path";
|
||||
import { platform } from "@electron-toolkit/utils";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
@@ -14,8 +15,12 @@ const createSystemInfo = (win) => {
|
||||
app.setUserTasks([]);
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 默认托盘菜单
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
// 给托盘图标设置气球提示
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 自定义任务栏缩略图
|
||||
createThumbar(win);
|
||||
// 歌曲数据改变时
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
@@ -26,6 +31,11 @@ const createSystemInfo = (win) => {
|
||||
});
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
createThumbar(win);
|
||||
});
|
||||
// 监听系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
createThumbar(win);
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
@@ -36,22 +46,28 @@ const createSystemInfo = (win) => {
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
// linux 右键菜单
|
||||
if (platform.isLinux) {
|
||||
mainTray.setContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
}
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
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 });
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
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 [
|
||||
{
|
||||
@@ -69,6 +85,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
@@ -76,6 +93,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
accelerator: "CmdOrCtrl+Space",
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
@@ -83,6 +101,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
@@ -112,4 +131,32 @@ const createTrayMenu = (win) => {
|
||||
];
|
||||
};
|
||||
|
||||
// 自定义任务栏缩略图 - Win
|
||||
const createThumbar = (win) => {
|
||||
win.setThumbarButtons([]);
|
||||
win.setThumbarButtons([
|
||||
{
|
||||
tooltip: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
},
|
||||
{
|
||||
tooltip: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
click() {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
},
|
||||
{
|
||||
tooltip: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export default createSystemInfo;
|
||||
|
||||
@@ -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
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");
|
||||
17
package.json
17
package.json
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.2",
|
||||
"version": "2.0.0-beta.4",
|
||||
"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",
|
||||
@@ -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": "git+https://github.com/imsyy/NeteaseCloudMusicApi.git",
|
||||
"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",
|
||||
@@ -49,13 +52,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.35.0",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
@@ -63,6 +67,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"
|
||||
}
|
||||
}
|
||||
|
||||
2473
pnpm-lock.yaml
generated
2473
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/images/pic/pointer.png
Normal file
BIN
public/images/pic/pointer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
144
src/App.vue
144
src/App.vue
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<Provider>
|
||||
<!-- 主框架 -->
|
||||
<n-layout :class="['all-layout', status.showFullPlayer ? 'full-player' : null]">
|
||||
<n-layout class="all-layout">
|
||||
<!-- 导航栏 -->
|
||||
<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,82 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { darkTheme } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { darkTheme, NButton } from "naive-ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
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 } = 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,29 +163,38 @@ const canNotConnect = (error) => {
|
||||
title: "网络连接错误",
|
||||
content: "网络连接错误,请检查您当前的网络状态",
|
||||
positiveText: "重试",
|
||||
negativeText: "前往本地歌曲",
|
||||
negativeText: checkPlatform.electron() ? "前往本地歌曲" : "取消",
|
||||
onPositiveClick: () => {
|
||||
location.reload();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
router.push("/local");
|
||||
if (checkPlatform.electron()) router.push("/local");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 网页端键盘事件
|
||||
const handleKeyUp = (event) => {
|
||||
globalShortcut(event, router);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载方法
|
||||
window.$canNotConnect = canNotConnect;
|
||||
// 主播放器
|
||||
initPlayer(settings.autoPlay);
|
||||
initPlayer(autoPlay.value);
|
||||
// 全局事件
|
||||
globalEvents(router);
|
||||
// 键盘监听
|
||||
window.addEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) {
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
// 显示公告
|
||||
showAnnouncements();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) window.removeEventListener("keyup", handleKeyUp);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -142,9 +212,6 @@ onUnmounted(() => {
|
||||
.body-layout {
|
||||
top: 60px;
|
||||
transition: bottom 0.3s;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
.main-sider {
|
||||
:deep(.n-scrollbar-content) {
|
||||
height: 100%;
|
||||
@@ -156,12 +223,9 @@ onUnmounted(() => {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.main-layout {
|
||||
padding: 24px;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
&.full-player {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -151,80 +151,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(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,6 +22,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",
|
||||
@@ -80,5 +81,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"
|
||||
}
|
||||
|
||||
@@ -69,8 +69,13 @@
|
||||
</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="item.artists" class="artists">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" class="ar">
|
||||
{{ ar.name }}
|
||||
<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>
|
||||
<!-- 歌曲数量 -->
|
||||
@@ -297,8 +302,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 +318,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
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: "record",
|
||||
},
|
||||
},
|
||||
() => ["视频"],
|
||||
() => ["播客电台"],
|
||||
),
|
||||
key: "videos",
|
||||
icon: renderIcon("video"),
|
||||
key: "record",
|
||||
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="showFullPlayer ? 'main-playlist full-player' : 'main-playlist'"
|
||||
: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>
|
||||
@@ -22,7 +23,7 @@
|
||||
</template>
|
||||
<!-- 提示 -->
|
||||
<n-alert v-if="playList?.length >= 400" class="alert" :show-icon="false">
|
||||
因歌单数量过大,无法自动定位,请手动查找
|
||||
当前歌曲过多,无法自动定位,请手动查找
|
||||
</n-alert>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-data-table
|
||||
@@ -51,18 +52,13 @@
|
||||
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";
|
||||
|
||||
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 playlistOpen = () => {
|
||||
@@ -95,7 +91,7 @@ const createSongs = (song, index) => {
|
||||
class: {
|
||||
songs: true,
|
||||
play: playSongData.value?.id === song?.id,
|
||||
player: status.showFullPlayer,
|
||||
player: showFullPlayer.value,
|
||||
},
|
||||
onClick: () => {
|
||||
playSong(song, index);
|
||||
@@ -153,8 +149,6 @@ const playSong = async (song, index) => {
|
||||
} else {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 渐出音乐
|
||||
if (checkPlayer()) await fadePlayOrPause("pause");
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
}
|
||||
@@ -343,7 +337,7 @@ const removeSong = async (index) => {
|
||||
.n-scrollbar-content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
&.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;
|
||||
|
||||
@@ -139,6 +139,25 @@
|
||||
</n-text>
|
||||
<n-text v-else class="album">未知专辑</n-text>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<div 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="item.duration" class="duration" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-else class="duration"> -- </n-text>
|
||||
@@ -483,6 +502,23 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
.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) => {
|
||||
@@ -101,6 +105,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
(playlist) => playlist.userId === userId,
|
||||
);
|
||||
// 当前状态
|
||||
const isFm = playMode.value === "fm";
|
||||
const isLocalSong = song?.path ? true : false;
|
||||
const isCloud = router.currentRoute.value.name === "cloud";
|
||||
const isUserPlaylist = sourceId !== 0 && userPlaylistsData.some((pl) => pl.id == sourceId);
|
||||
@@ -132,7 +137,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "next-play",
|
||||
label: "下一首播放",
|
||||
show: playSongData.value?.id === song.id || playMode.value === "fm" ? false : true,
|
||||
show: playSongData.value?.id !== song.id && !isFm,
|
||||
props: {
|
||||
onClick: () => {
|
||||
addSongToNext(song);
|
||||
@@ -183,6 +188,37 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
},
|
||||
icon: renderIcon("video"),
|
||||
},
|
||||
{
|
||||
label: "更多操作",
|
||||
key: "others",
|
||||
show: !isLocalSong,
|
||||
icon: renderIcon("more"),
|
||||
children: [
|
||||
{
|
||||
key: "copy",
|
||||
label: "复制歌曲 ID",
|
||||
props: {
|
||||
onClick: () => {
|
||||
const songId = song?.id?.toString();
|
||||
copyData(songId);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("content-copy"),
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
label: "分享歌曲链接",
|
||||
props: {
|
||||
onClick: () => {
|
||||
const shareUrl = `https://music.163.com/song?id=${song?.id?.toString()}`;
|
||||
copyData(shareUrl, "复制歌曲链接");
|
||||
},
|
||||
},
|
||||
icon: renderIcon("share"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
key: "line-cloud",
|
||||
type: "divider",
|
||||
@@ -199,6 +235,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: "从云盘中删除",
|
||||
|
||||
248
src/components/Modal/CloudSongMatch.vue
Normal file
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>
|
||||
@@ -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("账号会员等级不足,请提升权限");
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
<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
|
||||
@@ -72,6 +74,7 @@ const openLoginModal = () => {
|
||||
|
||||
// 储存登录信息
|
||||
const setLoginData = async (loginData) => {
|
||||
console.log(loginData);
|
||||
if (!loginData) return false;
|
||||
if (loginData.code === 200) {
|
||||
// 关闭登录弹窗
|
||||
@@ -168,7 +171,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
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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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/logo/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>
|
||||
|
||||
@@ -53,6 +53,7 @@ const data = siteData();
|
||||
const router = useRouter();
|
||||
const settings = siteSettings();
|
||||
const { userLoginStatus, userData, userLikeData } = storeToRefs(data);
|
||||
const { themeType } = storeToRefs(settings);
|
||||
|
||||
// 菜单数据
|
||||
const userMenuShow = ref(false);
|
||||
@@ -120,9 +121,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 +144,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":
|
||||
|
||||
@@ -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,6 +4,7 @@
|
||||
:class="{
|
||||
'main-player': true,
|
||||
'show-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
'no-sider': !showSider,
|
||||
}"
|
||||
content-style="padding: 0"
|
||||
@dblclick.stop="showFullPlayer = true"
|
||||
@@ -171,6 +172,7 @@
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
tag="div"
|
||||
type="primary"
|
||||
class="play-control"
|
||||
strong
|
||||
@@ -343,7 +345,7 @@ const {
|
||||
playSongLyric,
|
||||
} = storeToRefs(music);
|
||||
const { playLoading, playState, playListShow, showPlayBar, showFullPlayer } = storeToRefs(status);
|
||||
const { showYrc, bottomLyricShow } = storeToRefs(settings);
|
||||
const { showYrc, bottomLyricShow, showSider } = storeToRefs(settings);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
@@ -507,9 +509,6 @@ watch(
|
||||
padding: 0 15px;
|
||||
z-index: 2;
|
||||
transition: bottom 0.3s;
|
||||
&.show-bar {
|
||||
bottom: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
@@ -777,6 +776,21 @@ watch(
|
||||
}
|
||||
}
|
||||
}
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 音量控制
|
||||
.slider-content {
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
:keyboard="false"
|
||||
class="play-control"
|
||||
strong
|
||||
secondary
|
||||
|
||||
210
src/components/Player/PlayerCover.vue
Normal file
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',
|
||||
}"
|
||||
>
|
||||
<!-- 背景 -->
|
||||
@@ -67,8 +70,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 +137,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 +243,7 @@ onBeforeMount(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
color: var(--color);
|
||||
.info {
|
||||
.name {
|
||||
display: -webkit-box;
|
||||
@@ -256,6 +260,7 @@ onBeforeMount(async () => {
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--color);
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
@@ -292,6 +297,7 @@ onBeforeMount(async () => {
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--color);
|
||||
}
|
||||
.al {
|
||||
display: -webkit-box;
|
||||
@@ -315,9 +321,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 +339,6 @@ onBeforeMount(async () => {
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
&.isFm {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.play-other {
|
||||
margin-right: 12px;
|
||||
@@ -355,6 +360,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(" ") && !value.endsWith(" ");
|
||||
|
||||
// 搜索框 focus
|
||||
const searchInputFocus = () => {
|
||||
@@ -63,6 +73,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 +122,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
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
|
||||
|
||||
@@ -80,23 +80,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 +87,7 @@ const routes = [
|
||||
meta: {
|
||||
title: "视频播放器",
|
||||
},
|
||||
component: () => import("@/views/Videos/player.vue"),
|
||||
component: () => import("@/views/player.vue"),
|
||||
},
|
||||
// 评论
|
||||
{
|
||||
@@ -210,6 +193,47 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 我的收藏
|
||||
{
|
||||
path: "/like",
|
||||
name: "like",
|
||||
meta: {
|
||||
title: "我的收藏",
|
||||
},
|
||||
component: () => import("@/views/Like/index.vue"),
|
||||
beforeEnter: (_, __, next) => {
|
||||
if (isLogin()) {
|
||||
next();
|
||||
} else {
|
||||
if (typeof $changeLogin !== "undefined") $changeLogin();
|
||||
$message.error("请登录后使用");
|
||||
$loadingBar.error();
|
||||
}
|
||||
},
|
||||
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: "/local",
|
||||
@@ -245,6 +269,23 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 播客
|
||||
{
|
||||
path: "/record",
|
||||
name: "record",
|
||||
meta: {
|
||||
title: "播客",
|
||||
},
|
||||
component: () => import("@/views/Record/index.vue"),
|
||||
redirect: "/record/hot",
|
||||
children: [
|
||||
{
|
||||
path: "hot",
|
||||
name: "record-hot",
|
||||
component: () => import("@/views/Record/hot.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 全局设置
|
||||
{
|
||||
path: "/setting",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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", {
|
||||
@@ -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("已移至垃圾桶");
|
||||
|
||||
@@ -46,9 +46,6 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
catList: [], // 普通分类
|
||||
hqCatList: [], // 精品分类
|
||||
},
|
||||
// 封面主题
|
||||
coverTheme: {},
|
||||
coverBackground: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
@@ -205,7 +202,7 @@ const useSiteDataStore = defineStore("siteData", {
|
||||
$message.error("用户喜欢专辑加载失败");
|
||||
}
|
||||
},
|
||||
// 更改用户喜欢歌手
|
||||
// 更改用户喜欢视频
|
||||
async setUserLikeMvs() {
|
||||
try {
|
||||
if (!isLogin()) return false;
|
||||
|
||||
@@ -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 +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
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)}}
|
||||
23
src/style/animate.scss
vendored
23
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,12 @@
|
||||
transform: rotate(360deg) scale(1) translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes playerCoverRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -164,12 +168,3 @@ body,
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
}
|
||||
|
||||
// n-switch
|
||||
.n-switch {
|
||||
&.n-switch--active {
|
||||
.n-switch__rail {
|
||||
background-color: var(--main-second-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -63,8 +63,16 @@ export const initPlayer = async (playNow = false) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 下一曲
|
||||
@@ -153,7 +161,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;
|
||||
}
|
||||
@@ -331,7 +338,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 +373,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 +430,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 +456,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 +486,7 @@ export const checkPlayer = () => {
|
||||
* 停止播放器
|
||||
*/
|
||||
export const soundStop = () => {
|
||||
player?.stop();
|
||||
// player?.stop();
|
||||
// setSeek();
|
||||
Howler.unload();
|
||||
};
|
||||
@@ -646,10 +643,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 +664,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 = {};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Hct,
|
||||
Score,
|
||||
} from "@material/material-color-utilities";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { siteSettings, siteStatus } from "@/stores";
|
||||
import { getGradientFromPalette, argb2Rgb, rgb2Argb } from "@/utils/color-utils";
|
||||
import { chunk } from "@/utils/helper";
|
||||
import ColorThief from "colorthief";
|
||||
@@ -45,7 +45,7 @@ export const getCoverGradient = (coverSrc) => {
|
||||
*/
|
||||
const calcAccentColor = (dom) => {
|
||||
// pinia
|
||||
const data = siteData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
// 创建一个用于提取颜色的 canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -77,11 +77,11 @@ const calcAccentColor = (dom) => {
|
||||
const top = ranked[0];
|
||||
const theme = themeFromSourceColor(top);
|
||||
// 错误 error, 中性 neutral, 中性的变体 neutralVariant, 主要的 primary, 二次 secondary, 三级 tertiary
|
||||
const variant = window.accentColorVariant ?? "secondary";
|
||||
const variant = settings.themeAutoCoverType;
|
||||
// 更新主题色
|
||||
data.coverTheme = {
|
||||
light: {
|
||||
light: getAccentColor(theme.schemes.dark[variant]),
|
||||
status.coverTheme = {
|
||||
dark: {
|
||||
dark: getAccentColor(theme.schemes.dark[variant]),
|
||||
primary: getAccentColor(
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 100).toInt(),
|
||||
),
|
||||
@@ -92,11 +92,14 @@ const calcAccentColor = (dom) => {
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 15).toInt(),
|
||||
),
|
||||
bg: getAccentColor(
|
||||
Hct.from(theme.palettes.secondary.hue, theme.palettes.secondary.chroma, 90).toInt(),
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 90).toInt(),
|
||||
),
|
||||
mainBg: getAccentColor(
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 10).toInt(),
|
||||
),
|
||||
},
|
||||
dark: {
|
||||
dark: getAccentColor(theme.schemes.dark[variant]),
|
||||
light: {
|
||||
light: getAccentColor(theme.schemes.light[variant]),
|
||||
primary: getAccentColor(
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 20).toInt(),
|
||||
),
|
||||
@@ -107,16 +110,16 @@ const calcAccentColor = (dom) => {
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 90).toInt(),
|
||||
),
|
||||
bg: getAccentColor(
|
||||
Hct.from(theme.palettes.secondary.hue, theme.palettes.secondary.chroma, 20).toInt(),
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 20).toInt(),
|
||||
),
|
||||
mainBg: getAccentColor(
|
||||
Hct.from(theme.palettes.secondary.hue, theme.palettes.secondary.chroma, 10).toInt(),
|
||||
Hct.from(theme.palettes[variant].hue, theme.palettes[variant].chroma, 100).toInt(),
|
||||
),
|
||||
},
|
||||
};
|
||||
// 尝试更新主题色
|
||||
if (typeof $changeThemeColor !== "undefined" && settings.themeAutoCover) {
|
||||
$changeThemeColor(data.coverTheme, settings.themeAutoCover);
|
||||
$changeThemeColor(status.coverTheme, settings.themeAutoCover);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,21 +128,22 @@ const calcAccentColor = (dom) => {
|
||||
*/
|
||||
const useGreyAccentColor = () => {
|
||||
// pinia
|
||||
const data = siteData();
|
||||
data.coverTheme = {
|
||||
light: {
|
||||
light: getAccentColor(rgb2Argb(120, 120, 120)),
|
||||
const status = siteStatus();
|
||||
status.coverTheme = {
|
||||
dark: {
|
||||
dark: getAccentColor(rgb2Argb(120, 120, 120)),
|
||||
primary: getAccentColor(rgb2Argb(250, 250, 250)),
|
||||
shade: getAccentColor(rgb2Argb(40, 40, 40)),
|
||||
shadeTwo: getAccentColor(rgb2Argb(20, 20, 20)),
|
||||
bg: getAccentColor(rgb2Argb(190, 190, 190)),
|
||||
mainBg: getAccentColor(rgb2Argb(16, 16, 20)),
|
||||
},
|
||||
dark: {
|
||||
dark: getAccentColor(rgb2Argb(150, 150, 150)),
|
||||
light: {
|
||||
light: getAccentColor(rgb2Argb(150, 150, 150)),
|
||||
primary: getAccentColor(rgb2Argb(10, 10, 10)),
|
||||
shade: getAccentColor(rgb2Argb(210, 210, 210)),
|
||||
shadeTwo: getAccentColor(rgb2Argb(255, 255, 255)),
|
||||
bg: getAccentColor(rgb2Argb(50, 50, 50)),
|
||||
bg: getAccentColor(rgb2Argb(24, 24, 28)),
|
||||
mainBg: getAccentColor(rgb2Argb(11, 11, 11)),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const formRules = () => {
|
||||
// 邮箱验证
|
||||
emailRule: {
|
||||
required: true,
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return new Error("请输入电子邮箱");
|
||||
} else if (
|
||||
@@ -35,14 +35,10 @@ const formRules = () => {
|
||||
mobileRule: {
|
||||
key: "phone",
|
||||
required: true,
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return new Error("请输入手机号码");
|
||||
} else if (
|
||||
!/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1589]))\d{8}$/.test(
|
||||
value,
|
||||
)
|
||||
) {
|
||||
} else if (!/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value)) {
|
||||
return new Error("请输入正确的手机号码");
|
||||
}
|
||||
return true;
|
||||
@@ -52,5 +48,4 @@ const formRules = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 导出所有规则
|
||||
export { formRules };
|
||||
|
||||
@@ -18,6 +18,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
const imgUrl =
|
||||
v &&
|
||||
(v.picUrl ||
|
||||
v.coverUrl ||
|
||||
v.coverImgUrl ||
|
||||
v.cover ||
|
||||
(v.album && v.album.picUrl) ||
|
||||
@@ -46,6 +47,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
updateTime: v.updateTime || v.trackNumberUpdateTime,
|
||||
description: v.description,
|
||||
tags: v.tags || v.algTags,
|
||||
userId: v.userId,
|
||||
};
|
||||
// 歌曲
|
||||
case "song":
|
||||
@@ -57,7 +59,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
cover,
|
||||
coverSize,
|
||||
mv: v.mv,
|
||||
alia: v.alias?.[0] || v.transNames?.[0],
|
||||
alia: v.alia?.[0] || v.alias?.[0] || v.transNames?.[0],
|
||||
fee: v.fee,
|
||||
pc: v.pc,
|
||||
size: v.size,
|
||||
@@ -97,14 +99,14 @@ const formatData = (data, type = "playlist", noTracks = false) => {
|
||||
// mv
|
||||
case "mv":
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
artists: v.artists,
|
||||
id: v.id || v.vid,
|
||||
name: v.name || v.title,
|
||||
artists: v.artists || v.creator,
|
||||
desc: v.copywriter,
|
||||
cover,
|
||||
coverSize: getCoverUrl(v.imgurl16v9 || v.picUrl || v.cover, "464y260"),
|
||||
duration: v.duration,
|
||||
playCount: v.playCount,
|
||||
coverSize: getCoverUrl(cover, "464y260"),
|
||||
duration: v.duration || v.durationms,
|
||||
playCount: v.playCount || v.playTime,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { playOrPause, changePlayIndex } from "@/utils/Player";
|
||||
import { siteStatus } from "@/stores";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
|
||||
/**
|
||||
* 全局事件
|
||||
* @param {import('vue-router').Router} router - router
|
||||
*/
|
||||
const globalEvents = (router) => {
|
||||
if (!checkPlatform.electron()) return false;
|
||||
// 显示播放器
|
||||
|
||||
@@ -4,18 +4,23 @@ import { musicData } from "@/stores";
|
||||
/**
|
||||
* 全局快捷键监听
|
||||
* @param {KeyboardEvent} e - 键盘事件对象
|
||||
* @param {import('vue-router').Router} router - router
|
||||
* @returns {boolean} - 如果事件对象不存在,则返回false
|
||||
*/
|
||||
const globalShortcut = (e) => {
|
||||
const globalShortcut = (e, router) => {
|
||||
if (!e) return false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 播放或暂停
|
||||
if (e.ctrlKey && e.code === "Space") playOrPause();
|
||||
if (e.code === "Space") {
|
||||
if (e.target.tagName === "INPUT") return false;
|
||||
if (router.currentRoute.value.name === "videos-player") return false;
|
||||
playOrPause();
|
||||
}
|
||||
|
||||
// 调整音量
|
||||
if (e.ctrlKey && (e.code === "ArrowUp" || e.code === "ArrowDown")) {
|
||||
if (e.code === "ArrowUp" || e.code === "ArrowDown") {
|
||||
const music = musicData();
|
||||
const volume = music.playVolume;
|
||||
const delta = e.code === "ArrowUp" ? 0.1 : -0.1;
|
||||
|
||||
@@ -94,20 +94,20 @@ export const getLocalCoverData = async (path, isAlbum = false) => {
|
||||
/**
|
||||
* 内容复制
|
||||
*/
|
||||
export const copyData = async (data) => {
|
||||
export const copyData = async (data, info) => {
|
||||
try {
|
||||
const isElectron = checkPlatform.electron();
|
||||
// electron
|
||||
if (isElectron) {
|
||||
const result = await electron.ipcRenderer.invoke("copyData", data);
|
||||
result ? $message.success("复制成功") : $message.error("复制失败");
|
||||
result ? $message.success(`${info || "复制"}成功`) : $message.error(`${info || "复制"}失败`);
|
||||
}
|
||||
// 浏览器端
|
||||
else {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(data);
|
||||
$message.success("复制成功");
|
||||
$message.success(`${info || "复制"}成功`);
|
||||
} catch (error) {
|
||||
console.error("复制出错:", error);
|
||||
$message.error("复制失败");
|
||||
@@ -120,7 +120,9 @@ export const copyData = async (data) => {
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
successful ? $message.success("复制成功") : $message.error("复制失败");
|
||||
successful
|
||||
? $message.success(`${info || "复制"}成功`)
|
||||
: $message.error(`${info || "复制"}失败`);
|
||||
} catch (err) {
|
||||
console.error("复制出错:", err);
|
||||
$message.error("复制失败");
|
||||
@@ -338,3 +340,18 @@ export const downloadFile = async (data, song, path = null) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将字节数格式化为可读的大小字符串。
|
||||
* @param {number} bytes - 要格式化的字节数
|
||||
* @param {number} [decimals=2] - 小数点位数
|
||||
* @returns {string} - 格式化后的大小字符串(例如,"10 KB")
|
||||
*/
|
||||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return "0 K";
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["K", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@ axios.interceptors.request.use(
|
||||
if (!request.noCookie && (isLogin() || getCookie("MUSIC_U") !== null)) {
|
||||
request.params.cookie = `MUSIC_U=${getCookie("MUSIC_U")};`;
|
||||
}
|
||||
// 去除 cookie
|
||||
if (request.noCookie) {
|
||||
request.params.noCookie = true;
|
||||
}
|
||||
// 附加 realIP
|
||||
if (!checkPlatform.electron()) request.params.realIP = "116.25.146.177";
|
||||
// 发送请求
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { getArtistSongs } from "@/api/artist";
|
||||
import { getSongDetail } from "@/api/song";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -41,7 +42,14 @@ const getArtistHotData = async (id) => {
|
||||
artistHotSongs.value = null;
|
||||
// 获取热门歌曲
|
||||
const result = await getArtistSongs(id);
|
||||
artistHotSongs.value = formatData(result.hotSongs, "song");
|
||||
// 处理数据
|
||||
if (result.hotSongs?.[0]?.al?.picUrl) {
|
||||
artistHotSongs.value = formatData(result.hotSongs, "song");
|
||||
return true;
|
||||
}
|
||||
const ids = result.hotSongs.map((song) => song.id).join(",");
|
||||
const songsDetail = await getSongDetail(ids);
|
||||
artistHotSongs.value = formatData(songsDetail.songs, "song");
|
||||
} catch (error) {
|
||||
console.error("获取歌手热门数据失败:", error);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
v-for="item in artistInitials"
|
||||
:key="item"
|
||||
:bordered="false"
|
||||
:type="item.key == artistInitialChoose ? 'primary' : 'default'"
|
||||
class="tag"
|
||||
:class="['tag', { choose: item.key == artistInitialChoose }]"
|
||||
round
|
||||
@click="artistInitialChange(item.key)"
|
||||
>
|
||||
@@ -21,10 +20,12 @@
|
||||
<n-tag
|
||||
v-for="(item, index) in artistTypeNames"
|
||||
:key="item"
|
||||
:class="['tag', item.length > 2 ? 'hidden' : 'show']"
|
||||
:class="[
|
||||
'tag',
|
||||
item.length > 2 ? 'hidden' : 'show',
|
||||
{ choose: index == artistTypeNamesChoose },
|
||||
]"
|
||||
:bordered="false"
|
||||
:type="index == artistTypeNamesChoose ? 'primary' : 'default'"
|
||||
class="tag"
|
||||
round
|
||||
@click="artistTypeChange(index)"
|
||||
>
|
||||
@@ -199,6 +200,10 @@ onMounted(() => {
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
&.choose {
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.category {
|
||||
margin-top: 18px;
|
||||
|
||||
@@ -51,21 +51,8 @@ watch(
|
||||
font-weight: bold;
|
||||
}
|
||||
.tabs {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
margin-bottom: 20px;
|
||||
z-index: 2;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -25px;
|
||||
width: calc(100% + 50px);
|
||||
height: calc(100% + 40px);
|
||||
background-color: var(--n-color);
|
||||
backdrop-filter: blur(40px);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -275,6 +275,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
background-color: var(--n-action-color);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
background-color 0.3s,
|
||||
|
||||
31
src/views/Like/albums.vue
Normal file
31
src/views/Like/albums.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="like-albums">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="userLikeData.albums?.length" class="list">
|
||||
<!-- 列表 -->
|
||||
<MainCover :data="likeAlbumsData" type="album" />
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前暂无收藏专辑"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const data = siteData();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
|
||||
// 处理专辑数据
|
||||
const likeAlbumsData = computed(() => {
|
||||
return formatData(userLikeData.value.albums, "album");
|
||||
});
|
||||
</script>
|
||||
31
src/views/Like/artists.vue
Normal file
31
src/views/Like/artists.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="like-artists">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="userLikeData.artists?.length" class="list">
|
||||
<!-- 列表 -->
|
||||
<MainCover :data="likeArtistsData" type="artist" columns="3 s:4 m:5 l:6" />
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前暂无收藏歌手"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const data = siteData();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
|
||||
// 处理歌手数据
|
||||
const likeArtistsData = computed(() => {
|
||||
return formatData(userLikeData.value.artists, "artist");
|
||||
});
|
||||
</script>
|
||||
50
src/views/Like/index.vue
Normal file
50
src/views/Like/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="like">
|
||||
<n-h1 class="title">我的收藏</n-h1>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="tabValue" class="tabs" type="segment" @update:value="tabChange">
|
||||
<n-tab name="like-albums"> 专辑 </n-tab>
|
||||
<n-tab name="like-playlists"> 歌单 </n-tab>
|
||||
<n-tab name="like-artists"> 歌手 </n-tab>
|
||||
<n-tab name="like-videos"> 视频 </n-tab>
|
||||
</n-tabs>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 默认选中
|
||||
const tabValue = ref(router.currentRoute.value?.name ?? "like-albums");
|
||||
|
||||
// 标签页切换
|
||||
const tabChange = (val) => {
|
||||
const routerPath = val.replace(/^like-/, "");
|
||||
router.push({
|
||||
path: `/like/${routerPath}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.like {
|
||||
.title {
|
||||
margin: 20px 0;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
src/views/Like/playlists.vue
Normal file
106
src/views/Like/playlists.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="like-playlists">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="userLikeData.playlists?.length" class="pl-list">
|
||||
<!-- 分类 -->
|
||||
<n-space class="type">
|
||||
<n-tag
|
||||
v-for="(item, index) in ['我创建的', '我收藏的']"
|
||||
:key="index"
|
||||
:bordered="false"
|
||||
:type="index === plTypeChoose ? 'primary' : 'default'"
|
||||
class="tag"
|
||||
round
|
||||
@click="plTypeChange(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<!-- 列表 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="plTypeChoose === 0" class="list">
|
||||
<MainCover
|
||||
:data="likePlaylistsData.filter((playlist) => playlist.userId === userData?.userId)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="list">
|
||||
<MainCover
|
||||
:data="likePlaylistsData.filter((playlist) => playlist.userId !== userData?.userId)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前暂无收藏歌单"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import { useRouter } from "vue-router";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
|
||||
// 默认分类
|
||||
const plTypeChoose = ref(Number(router.currentRoute.value.query?.type) || 0);
|
||||
|
||||
// 处理视频数据
|
||||
const likePlaylistsData = computed(() => {
|
||||
return formatData(userLikeData.value.playlists);
|
||||
});
|
||||
|
||||
// 默认分类变化
|
||||
const plTypeChange = (type) => {
|
||||
router.push({
|
||||
path: "/like/playlists",
|
||||
query: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由参数变化
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(val) => {
|
||||
if (val.name === "like-playlists") {
|
||||
plTypeChoose.value = Number(val.query?.type) || 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.like-playlists {
|
||||
.type {
|
||||
margin-bottom: 20px;
|
||||
.tag {
|
||||
font-size: 13px;
|
||||
padding: 0 16px;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
background-color 0.3s,
|
||||
color 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--main-second-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
src/views/Like/videos.vue
Normal file
31
src/views/Like/videos.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="like-videos">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="userLikeData.mvs?.length" class="list">
|
||||
<!-- 列表 -->
|
||||
<MainCover :data="likeVideosData" columns="1 s:2 m:3 l:4 xl:5" type="mv" />
|
||||
</div>
|
||||
<n-empty
|
||||
v-else
|
||||
description="当前暂无收藏视频"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const data = siteData();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
|
||||
// 处理视频数据
|
||||
const likeVideosData = computed(() => {
|
||||
return formatData(userLikeData.value.mvs, "mv");
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 专辑页面 -->
|
||||
<template>
|
||||
<div v-if="albumId" class="album">
|
||||
<div v-if="albumId && Number(albumId)" class="album">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="albumDetail && Object.keys(albumDetail)?.length" class="detail">
|
||||
<div class="cover">
|
||||
@@ -32,7 +32,12 @@
|
||||
</n-text>
|
||||
<n-text v-if="albumDetail.alia" class="alia" depth="3">{{ albumDetail.alia }}</n-text>
|
||||
<div v-if="albumDetail.artists" class="creator">
|
||||
<n-text v-for="(item, index) in albumDetail.artists" :key="index" class="ar">
|
||||
<n-text
|
||||
v-for="(item, index) in albumDetail.artists"
|
||||
:key="index"
|
||||
class="ar"
|
||||
@click="router.push(`/artist?id=${item.id}`)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
@@ -102,6 +107,7 @@
|
||||
:disabled="albumData === 'empty'"
|
||||
type="primary"
|
||||
class="play"
|
||||
tag="div"
|
||||
circle
|
||||
strong
|
||||
secondary
|
||||
@@ -113,7 +119,7 @@
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="large" round strong secondary @click="likeOrDislike(albumId)">
|
||||
<n-button size="large" tag="div" round strong secondary @click="likeOrDislike(albumId)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon
|
||||
@@ -124,7 +130,7 @@
|
||||
{{ isLikeOrDislike(albumId) ? "收藏专辑" : "取消收藏" }}
|
||||
</n-button>
|
||||
<n-dropdown :options="moreOptions" trigger="hover" placement="bottom-start">
|
||||
<n-button size="large" circle strong secondary>
|
||||
<n-button size="large" tag="div" circle strong secondary>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="format-list-bulleted" />
|
||||
@@ -255,26 +261,30 @@ const playAllSongs = async () => {
|
||||
if (!albumData.value) return false;
|
||||
// 关闭心动模式
|
||||
playHeartbeatMode.value = false;
|
||||
// 更改模式和歌单
|
||||
playMode.value = "normal";
|
||||
playList.value = albumData.value.slice();
|
||||
// 是否处于专辑内
|
||||
const songId = playSongData.value?.id;
|
||||
const isHas = albumData.value.some((song) => song.id === songId);
|
||||
const existingIndex = albumData.value.findIndex((song) => song.id === songId);
|
||||
// 若不处于
|
||||
if (!isHas) {
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
playList.value = albumData.value;
|
||||
if (existingIndex === -1 || !songId) {
|
||||
console.log("不在专辑内");
|
||||
playSongData.value = albumData.value[0];
|
||||
playIndex.value = 0;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
} else {
|
||||
console.log("处于专辑内");
|
||||
playSongData.value = albumData.value[existingIndex];
|
||||
playIndex.value = existingIndex;
|
||||
// 播放
|
||||
await fadePlayOrPause();
|
||||
fadePlayOrPause();
|
||||
}
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
};
|
||||
|
||||
// 本地歌曲模糊搜索
|
||||
// 歌曲模糊搜索
|
||||
const localSearch = debounce((val) => {
|
||||
const searchValue = val?.trim();
|
||||
// 是否为空
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
:disabled="playListData === 'empty'"
|
||||
type="primary"
|
||||
class="play"
|
||||
tag="div"
|
||||
circle
|
||||
strong
|
||||
secondary
|
||||
@@ -128,6 +129,7 @@
|
||||
<n-button
|
||||
v-if="isUserPLayList"
|
||||
size="large"
|
||||
tag="div"
|
||||
round
|
||||
strong
|
||||
secondary
|
||||
@@ -140,7 +142,15 @@
|
||||
</template>
|
||||
编辑歌单
|
||||
</n-button>
|
||||
<n-button v-else size="large" round strong secondary @click="likeOrDislike(playlistId)">
|
||||
<n-button
|
||||
v-else
|
||||
size="large"
|
||||
tag="div"
|
||||
round
|
||||
strong
|
||||
secondary
|
||||
@click="likeOrDislike(playlistId)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon
|
||||
@@ -153,7 +163,7 @@
|
||||
{{ isLikeOrDislike(playlistId) ? "收藏歌单" : "取消收藏" }}
|
||||
</n-button>
|
||||
<n-dropdown :options="moreOptions" trigger="hover" placement="bottom-start">
|
||||
<n-button size="large" circle strong secondary>
|
||||
<n-button size="large" tag="div" circle strong secondary>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="format-list-bulleted" />
|
||||
@@ -342,27 +352,30 @@ const playAllSongs = async () => {
|
||||
if (!playListData.value) return false;
|
||||
// 关闭心动模式
|
||||
playHeartbeatMode.value = false;
|
||||
// 更改模式和歌单
|
||||
playMode.value = "normal";
|
||||
playList.value = playListData.value.slice();
|
||||
// 是否处于歌单内
|
||||
const songId = music.getPlaySongData?.id;
|
||||
const isHas = playListData.value.some((song) => song.id === songId);
|
||||
const existingIndex = playListData.value.findIndex((song) => song.id === songId);
|
||||
// 若不处于
|
||||
if (!isHas || !songId) {
|
||||
if (existingIndex === -1 || !songId) {
|
||||
console.log("不在歌单内");
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
playList.value = playListData.value.slice();
|
||||
playSongData.value = playListData.value[0];
|
||||
playIndex.value = 0;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
} else {
|
||||
console.log("处于歌单内");
|
||||
playSongData.value = playListData.value[existingIndex];
|
||||
playIndex.value = existingIndex;
|
||||
// 播放
|
||||
await fadePlayOrPause();
|
||||
fadePlayOrPause();
|
||||
}
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
};
|
||||
|
||||
// 本地歌曲模糊搜索
|
||||
// 歌曲模糊搜索
|
||||
const localSearch = debounce((val) => {
|
||||
const searchValue = val?.trim();
|
||||
// 是否为空
|
||||
|
||||
@@ -2,6 +2,30 @@
|
||||
<template>
|
||||
<div class="local">
|
||||
<n-h1 class="title">本地歌曲</n-h1>
|
||||
<!-- 数据统计 -->
|
||||
<n-space class="num">
|
||||
<!-- 总数 -->
|
||||
<div class="num-item">
|
||||
<n-icon size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<n-number-animation :from="0" :to="localSongList ? localSongList.length : 0" />
|
||||
首歌曲
|
||||
</div>
|
||||
<!-- 空间 -->
|
||||
<div class="num-item">
|
||||
<n-icon size="18">
|
||||
<SvgIcon icon="storage" />
|
||||
</n-icon>
|
||||
占用
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="Number((allSongStorage / 1024).toFixed(2))"
|
||||
:precision="2"
|
||||
/>
|
||||
GB
|
||||
</div>
|
||||
</n-space>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="tabValue" class="tabs" type="segment" @update:value="tabChange">
|
||||
<n-tab name="local-songs"> 歌曲 </n-tab>
|
||||
@@ -157,6 +181,7 @@ const isLoading = ref(false);
|
||||
|
||||
// 本地歌曲
|
||||
const localSongList = ref([]);
|
||||
const allSongStorage = ref(0);
|
||||
|
||||
// 默认选中
|
||||
const tabValue = ref(router.currentRoute.value?.name ?? "local-songs");
|
||||
@@ -187,7 +212,7 @@ const getLocalSongList = async () => {
|
||||
|
||||
// 获取全部路径歌曲
|
||||
const getAllPathMusic = async () => {
|
||||
console.log("调用");
|
||||
console.log("获取全部路径歌曲");
|
||||
// 先获取
|
||||
getLocalSongList();
|
||||
// 全部路径
|
||||
@@ -225,6 +250,8 @@ const getAllPathMusic = async () => {
|
||||
// 更新本地歌曲
|
||||
await indexedDB.setfilesDB("localSongList", uniqueSongsArray.slice());
|
||||
getLocalSongList();
|
||||
// 统计歌曲总容量
|
||||
allSongStorage.value = uniqueSongsArray.reduce((acc, v) => (acc += Number(v.size)), 0);
|
||||
} catch (error) {
|
||||
console.error("获取歌曲内容时出错:", error);
|
||||
} finally {
|
||||
@@ -258,7 +285,7 @@ const changeLocalPath = async (isDel = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 本地歌曲模糊搜索
|
||||
// 歌曲模糊搜索
|
||||
const localSearch = debounce((val) => {
|
||||
const searchValue = val?.trim();
|
||||
// 是否为空
|
||||
@@ -293,10 +320,21 @@ onBeforeMount(async () => {
|
||||
<style lang="scss" scoped>
|
||||
.local {
|
||||
.title {
|
||||
margin: 20px 0;
|
||||
margin: 20px 0 12px 0;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.num {
|
||||
margin-bottom: 20px;
|
||||
.num-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
.search {
|
||||
|
||||
4
src/views/Record/hot.vue
Normal file
4
src/views/Record/hot.vue
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- 播客 - 热门 -->
|
||||
<template>
|
||||
<div class="record-hot">114514</div>
|
||||
</template>
|
||||
4
src/views/Record/index.vue
Normal file
4
src/views/Record/index.vue
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- 播客 -->
|
||||
<template>
|
||||
<div class="record">新建文件夹...</div>
|
||||
</template>
|
||||
@@ -2,21 +2,18 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<template v-if="searchKeywords">
|
||||
<!-- 固定 -->
|
||||
<div class="fixed">
|
||||
<div class="title">
|
||||
<n-text class="key">{{ searchKeywords }}</n-text>
|
||||
<n-text depth="3">的相关搜索</n-text>
|
||||
</div>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="tabValue" class="tabs" type="line" @update:value="tabChange">
|
||||
<n-tab name="sea-songs"> 单曲 </n-tab>
|
||||
<n-tab name="sea-artists"> 歌手 </n-tab>
|
||||
<n-tab name="sea-albums"> 专辑 </n-tab>
|
||||
<n-tab name="sea-playlists"> 歌单 </n-tab>
|
||||
<n-tab name="sea-videos"> 视频 </n-tab>
|
||||
</n-tabs>
|
||||
<div class="title">
|
||||
<n-text class="key">{{ searchKeywords }}</n-text>
|
||||
<n-text depth="3">的相关搜索</n-text>
|
||||
</div>
|
||||
<!-- 标签页 -->
|
||||
<n-tabs v-model:value="tabValue" class="tabs" type="line" @update:value="tabChange">
|
||||
<n-tab name="sea-songs"> 单曲 </n-tab>
|
||||
<n-tab name="sea-artists"> 歌手 </n-tab>
|
||||
<n-tab name="sea-albums"> 专辑 </n-tab>
|
||||
<n-tab name="sea-playlists"> 歌单 </n-tab>
|
||||
<n-tab name="sea-videos"> 视频 </n-tab>
|
||||
</n-tabs>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
@@ -69,38 +66,20 @@ watch(
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
.fixed {
|
||||
position: sticky;
|
||||
top: 34px;
|
||||
background-color: var(--n-color);
|
||||
z-index: 2;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: -25px;
|
||||
width: calc(100% + 50px);
|
||||
height: calc(100% + 54px);
|
||||
background-color: var(--n-color);
|
||||
backdrop-filter: blur(40px);
|
||||
z-index: -1;
|
||||
.title {
|
||||
margin: 10px 0;
|
||||
font-size: 22px;
|
||||
.key {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0;
|
||||
font-size: 22px;
|
||||
.key {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.n-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
.n-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<!-- 全局设置 -->
|
||||
<template>
|
||||
<div class="setting">
|
||||
<div :class="{ setting: true, 'use-cover': themeAutoCover }">
|
||||
<n-h1 class="title">
|
||||
<n-text>全局设置</n-text>
|
||||
<n-text class="version" depth="3">v {{ packageJson.version }}</n-text>
|
||||
<div class="copyright" @click="jump">
|
||||
<div class="author">
|
||||
<n-icon depth="3" size="18">
|
||||
<SvgIcon icon="github" />
|
||||
</n-icon>
|
||||
<n-text class="author-text" depth="3">{{ packageJson.author }}</n-text>
|
||||
</div>
|
||||
<n-text class="version" depth="3">v {{ packageJson.version }}</n-text>
|
||||
</div>
|
||||
</n-h1>
|
||||
<!-- 导航栏 -->
|
||||
<n-tabs
|
||||
@@ -24,7 +32,7 @@
|
||||
ref="setScrollRef"
|
||||
:style="{
|
||||
height: `calc(100vh - ${
|
||||
Object.keys(music.getPlaySongData)?.length && status.showPlayBar ? 328 : 248
|
||||
Object.keys(music.getPlaySongData)?.length && showPlayBar ? 328 : 248
|
||||
}px)`,
|
||||
}"
|
||||
class="all-set"
|
||||
@@ -63,6 +71,13 @@
|
||||
"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
开启侧边栏
|
||||
<n-text class="tip">将导航栏放于侧边显示,可展开或收起</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="showSider" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">显示搜索历史</div>
|
||||
<n-switch v-model:value="searchHistory" :round="false" />
|
||||
@@ -77,7 +92,7 @@
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
<div class="dev">
|
||||
主题色跟随歌曲封面
|
||||
全局动态取色
|
||||
<n-tag :bordered="false" round size="small" type="warning">
|
||||
开发中
|
||||
<template #icon>
|
||||
@@ -92,9 +107,53 @@
|
||||
<n-switch
|
||||
v-model:value="themeAutoCover"
|
||||
:round="false"
|
||||
:disabled="Object.keys(coverTheme)?.length === 0"
|
||||
@update:value="themeAutoCoverChange"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
<div class="dev">
|
||||
全局动态取色类别
|
||||
<n-tag :bordered="false" round size="small" type="warning">
|
||||
开发中
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="code" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-tag>
|
||||
</div>
|
||||
<n-text class="tip">将在下一首播放或刷新时生效,不建议更改</n-text>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="themeAutoCoverType"
|
||||
:disabled="!themeAutoCover"
|
||||
:options="[
|
||||
{
|
||||
label: '中性',
|
||||
value: 'neutral',
|
||||
},
|
||||
{
|
||||
label: '中性变体',
|
||||
value: 'neutralVariant',
|
||||
},
|
||||
{
|
||||
label: '主要',
|
||||
value: 'primary',
|
||||
},
|
||||
{
|
||||
label: '次要',
|
||||
value: 'secondary',
|
||||
},
|
||||
{
|
||||
label: '次次要',
|
||||
value: 'tertiary',
|
||||
},
|
||||
]"
|
||||
class="set"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
<!-- 系统 -->
|
||||
<div v-if="checkPlatform.electron()" class="set-type">
|
||||
@@ -140,9 +199,11 @@
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
启动时自动播放
|
||||
<n-text class="tip">程序启动时自动播放上次歌曲</n-text>
|
||||
<n-text class="tip">
|
||||
{{ checkPlatform.electron() ? "程序启动时自动播放上次歌曲" : "客户端独占功能" }}
|
||||
</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="autoPlay" :round="false" />
|
||||
<n-switch v-model:value="autoPlay" :disabled="!checkPlatform.electron()" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">记忆上次播放位置</div>
|
||||
@@ -168,6 +229,26 @@
|
||||
</div>
|
||||
<n-switch v-model:value="bottomLyricShow" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放器样式
|
||||
<n-text class="tip"> 播放器左侧区域样式 </n-text>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="playCoverType"
|
||||
:options="[
|
||||
{
|
||||
label: '封面模式',
|
||||
value: 'cover',
|
||||
},
|
||||
{
|
||||
label: '唱片模式',
|
||||
value: 'record',
|
||||
},
|
||||
]"
|
||||
class="set"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
播放背景样式
|
||||
@@ -331,7 +412,7 @@
|
||||
</template>
|
||||
</n-tag>
|
||||
</div>
|
||||
<n-text class="tip">开启后可能会造成卡顿等性能问题</n-text>
|
||||
<n-text class="tip">可能会造成卡顿等性能问题,请确保显卡为 GTX 2060 及以上</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="showYrcAnimation" :disabled="!showYrc" :round="false" />
|
||||
</n-card>
|
||||
@@ -386,6 +467,10 @@
|
||||
<!-- 其他 -->
|
||||
<div class="set-type">
|
||||
<n-h3 prefix="bar"> 其他 </n-h3>
|
||||
<n-card class="set-item">
|
||||
<div class="name">是否显示 GitHub 仓库按钮</div>
|
||||
<n-switch v-model:value="showGithub" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="name">
|
||||
默认加载数量
|
||||
@@ -426,20 +511,22 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useOsTheme } from "naive-ui";
|
||||
import { siteSettings, siteStatus, musicData, siteData } from "@/stores";
|
||||
import { siteSettings, siteStatus, musicData } from "@/stores";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import debounce from "@/utils/debounce";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { showPlayBar, coverTheme } = storeToRefs(status);
|
||||
const {
|
||||
themeType,
|
||||
themeTypeName,
|
||||
themeAuto,
|
||||
themeAutoCover,
|
||||
themeAutoCoverType,
|
||||
showSider,
|
||||
closeTip,
|
||||
closeType,
|
||||
loadSize,
|
||||
@@ -464,6 +551,8 @@ const {
|
||||
bottomLyricShow,
|
||||
downloadPath,
|
||||
memorySeek,
|
||||
showGithub,
|
||||
playCoverType,
|
||||
} = storeToRefs(settings);
|
||||
|
||||
// 标签页数据
|
||||
@@ -515,9 +604,9 @@ const songLevelData = {
|
||||
|
||||
// 封面自动跟随变化
|
||||
const themeAutoCoverChange = (val) => {
|
||||
typeof $changeThemeColor !== "undefined" && val
|
||||
? $changeThemeColor(data.coverTheme, val)
|
||||
: $changeThemeColor(themeTypeName.value, val);
|
||||
if ($changeThemeColor !== "undefined" && Object.keys(coverTheme.value)?.length) {
|
||||
$changeThemeColor(val ? coverTheme.value : themeTypeName.value, val);
|
||||
}
|
||||
};
|
||||
|
||||
// 标签页切换
|
||||
@@ -549,6 +638,11 @@ const choosePath = async () => {
|
||||
if (selectedDir) downloadPath.value = selectedDir;
|
||||
};
|
||||
|
||||
// 跳转
|
||||
const jump = () => {
|
||||
window.open(packageJson.github);
|
||||
};
|
||||
|
||||
// 程序重置
|
||||
const resetApp = () => {
|
||||
$dialog.warning({
|
||||
@@ -557,8 +651,11 @@ const resetApp = () => {
|
||||
positiveText: "重置",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
if (typeof $cleanAll === "undefined") {
|
||||
return $message.error("重置操作出现错误,请重试");
|
||||
}
|
||||
$cleanAll(false);
|
||||
$message.success("重置成功,正在重启");
|
||||
localStorage.clear();
|
||||
setTimeout(() => {
|
||||
if (checkPlatform.electron()) {
|
||||
electron.ipcRenderer.send("window-relaunch");
|
||||
@@ -583,11 +680,29 @@ const resetApp = () => {
|
||||
margin: 20px 0;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
.version {
|
||||
.copyright {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&::after {
|
||||
content: "/";
|
||||
transform: translateY(2px);
|
||||
font-size: 14px;
|
||||
margin: 0 6px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.author-text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-tabs {
|
||||
@@ -635,5 +750,14 @@ const resetApp = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.use-cover {
|
||||
.n-switch {
|
||||
&.n-switch--active {
|
||||
:deep(.n-switch__rail) {
|
||||
background-color: var(--main-second-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div class="videos">114514</div>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div class="videos-list">111</div>
|
||||
</template>
|
||||
@@ -138,7 +138,7 @@ const getUserCloudData = async (isOnce = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 本地歌曲模糊搜索
|
||||
// 歌曲模糊搜索
|
||||
const localSearch = debounce((val) => {
|
||||
const searchValue = val?.trim();
|
||||
// 是否为空
|
||||
@@ -167,7 +167,7 @@ const playAllSongs = async () => {
|
||||
initPlayer(true);
|
||||
} else {
|
||||
// 播放
|
||||
await fadePlayOrPause();
|
||||
fadePlayOrPause();
|
||||
}
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
};
|
||||
@@ -181,6 +181,10 @@ onBeforeMount(() => {
|
||||
getUserCloudDataList();
|
||||
getUserCloudData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
window.$refreshCloudList = getUserCloudDataList;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -48,7 +48,7 @@ const playAllSongs = async () => {
|
||||
console.log(historyPlaylist.value);
|
||||
playList.value = historyPlaylist.value;
|
||||
playIndex.value = 0;
|
||||
await fadePlayOrPause();
|
||||
fadePlayOrPause();
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
};
|
||||
|
||||
|
||||
@@ -47,12 +47,7 @@
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="videoData?.id" class="menu">
|
||||
<!-- 点赞 -->
|
||||
<n-button
|
||||
:type="videoData?.liked ? 'primary' : 'default'"
|
||||
:title="`点赞数:${videoData?.likedCount}`"
|
||||
quaternary
|
||||
@click="videoLike"
|
||||
>
|
||||
<n-button quaternary @click="videoLike">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon :icon="videoData?.liked ? 'thumb-up' : 'thumb-up-outline'" />
|
||||
@@ -65,13 +60,21 @@
|
||||
}}
|
||||
</n-button>
|
||||
<!-- 收藏 -->
|
||||
<n-button quaternary>
|
||||
<n-button quaternary @click="videoCollection">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="folder-plus-outline" />
|
||||
<SvgIcon
|
||||
:icon="
|
||||
isLikeOrDislike(videoData?.id) ? 'favorite-outline-rounded' : 'favorite-rounded'
|
||||
"
|
||||
/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ formatNumber(videoData?.subCount || 0) }}
|
||||
{{
|
||||
isLikeOrDislike(videoData?.id)
|
||||
? formatNumber(videoData?.subCount || 0)
|
||||
: videoData?.subCount || 0
|
||||
}}
|
||||
</n-button>
|
||||
<!-- 分享 -->
|
||||
<n-button quaternary>
|
||||
@@ -177,7 +180,13 @@
|
||||
</n-image>
|
||||
<div class="desc">
|
||||
<n-text class="name">{{ item.name }}</n-text>
|
||||
<n-button class="open" size="small" tertiary round>
|
||||
<n-button
|
||||
class="open"
|
||||
size="small"
|
||||
tertiary
|
||||
round
|
||||
@click="router.push(`/artist?id=${item.id}`)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="14">
|
||||
<SvgIcon icon="account-music" />
|
||||
@@ -205,24 +214,36 @@
|
||||
|
||||
<script setup>
|
||||
import { NIcon } from "naive-ui";
|
||||
import { getVideoDetail, getVideoInfo, getVideoUrl, getSimiVideo } from "@/api/video";
|
||||
import { storeToRefs } from "pinia";
|
||||
import {
|
||||
getVideoDetail,
|
||||
getVideoInfo,
|
||||
getVideoUrl,
|
||||
getSimiVideo,
|
||||
likeVideo,
|
||||
likeMv,
|
||||
} from "@/api/video";
|
||||
import { getComment, getHotComment } from "@/api/comment";
|
||||
import { resourceLike } from "@/api/other";
|
||||
import { fadePlayOrPause } from "@/utils/Player";
|
||||
import { siteStatus } from "@/stores";
|
||||
import { siteStatus, siteData } from "@/stores";
|
||||
import { useRouter } from "vue-router";
|
||||
import { formatNumber } from "@/utils/helper";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
import throttle from "@/utils/throttle";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import Plyr from "plyr";
|
||||
import "plyr/dist/plyr.css";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const status = siteStatus();
|
||||
const router = useRouter();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
|
||||
// 视频 id
|
||||
const videoId = ref(router.currentRoute.value.query.id);
|
||||
const isVideo = ref(router.currentRoute.value.query.is_video);
|
||||
|
||||
// 播放器数据
|
||||
const videoData = ref(null);
|
||||
@@ -303,7 +324,7 @@ const initPlayer = () => {
|
||||
// 播放器事件
|
||||
player.value?.on("playing", async () => {
|
||||
status.showPlayBar = false;
|
||||
await fadePlayOrPause("pause");
|
||||
fadePlayOrPause("pause");
|
||||
});
|
||||
player.value?.on("pause", async () => {
|
||||
status.showPlayBar = true;
|
||||
@@ -350,6 +371,15 @@ const getVideoData = async (id) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 判断收藏还是取消
|
||||
const isLikeOrDislike = (id) => {
|
||||
const mvs = userLikeData.value.mvs;
|
||||
if (mvs.length) {
|
||||
return !mvs.some((item) => Number(item.vid) === Number(id));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取相关视频
|
||||
const getSimiVideoData = async (id) => {
|
||||
simiVideo.value = null;
|
||||
@@ -362,6 +392,7 @@ const getSimiVideoData = async (id) => {
|
||||
const videoLike = throttle(
|
||||
async () => {
|
||||
try {
|
||||
if (!isLogin()) return $message.warning("请登录后使用");
|
||||
const isLike = videoData.value?.liked;
|
||||
if (isLike === undefined || !videoData.value?.id) return false;
|
||||
// 点赞或取消
|
||||
@@ -379,6 +410,30 @@ const videoLike = throttle(
|
||||
"请稍后再操作",
|
||||
);
|
||||
|
||||
// 视频收藏
|
||||
const videoCollection = throttle(
|
||||
async () => {
|
||||
try {
|
||||
if (!isLogin()) return $message.warning("请登录后使用");
|
||||
const id = videoData.value.id;
|
||||
const type = isLikeOrDislike(id) ? 1 : 2;
|
||||
const result = isVideo.value ? await likeVideo(type, id) : await likeMv(type, id);
|
||||
if (result.code === 200) {
|
||||
$message.success((type === 1 ? "收藏" : "取消收藏") + "成功");
|
||||
// 更新用户专辑
|
||||
if (!isVideo.value) await data.setUserLikeMvs();
|
||||
} else {
|
||||
$message.error((type === 1 ? "收藏" : "取消收藏") + "失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("收藏出错:", error);
|
||||
$message.error("收藏操作出现错误");
|
||||
}
|
||||
},
|
||||
3000,
|
||||
"请稍后再操作",
|
||||
);
|
||||
|
||||
// 获取评论数据
|
||||
const getCommentData = async (id, pageNo = 1, sortType = 3, pageSize = 20) => {
|
||||
try {
|
||||
@@ -430,6 +485,7 @@ watch(
|
||||
videoData.value = null;
|
||||
simiVideo.value = null;
|
||||
videoId.value = val.query.id;
|
||||
isVideo.value = val.query.is_video;
|
||||
commentData.value = null;
|
||||
hotCommentData.value = null;
|
||||
commentPage.value = 1;
|
||||
Reference in New Issue
Block a user