Compare commits
45 Commits
v2.0.0-bet
...
v2.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 | ||
|
|
c012f84064 | ||
|
|
a57a18b9f5 | ||
|
|
309c323a14 | ||
|
|
6a1e606d6d | ||
|
|
af3931847e | ||
|
|
41eadb5843 | ||
|
|
8963d719d9 | ||
|
|
0a7761ffff | ||
|
|
1a63771f2d | ||
|
|
1f9141ba33 | ||
|
|
a341a69d48 | ||
|
|
0cedfe0af3 | ||
|
|
59f492ed8f | ||
|
|
8f416ff841 | ||
|
|
99ab194e4b | ||
|
|
43fb9b48dc | ||
|
|
c61e54d6a3 | ||
|
|
c8d195053f | ||
|
|
8cfe5d0481 | ||
|
|
fcc2f5015f | ||
|
|
9b98a45264 | ||
|
|
3c4e836fb8 | ||
|
|
a8111b9d3f | ||
|
|
8eaeffeda3 | ||
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 |
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
@@ -1,9 +1,18 @@
|
||||
# 根配置文件
|
||||
## 编辑器在查找配置时会停止查找更高层次的配置文件
|
||||
root = true
|
||||
|
||||
# 通配符,匹配所有文件
|
||||
[*]
|
||||
# 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码
|
||||
charset = utf-8
|
||||
# 使用空格作为缩进风格
|
||||
indent_style = space
|
||||
# 设置每个缩进级别的空格数量为 2
|
||||
indent_size = 2
|
||||
# 设置行尾换行符为LF(Line Feed)
|
||||
end_of_line = lf
|
||||
# 在文件的末尾插入一个新行
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
# 删除每一行末尾的尾随空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -21,8 +21,6 @@ RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
RENDERER_VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
RENDERER_VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
|
||||
@@ -2,3 +2,5 @@ node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
@@ -49,7 +49,8 @@ module.exports = {
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
$refreshCloudList: true,
|
||||
$refreshCloudCatch: true,
|
||||
$cleanAll: true,
|
||||
$player: true,
|
||||
},
|
||||
};
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/add.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 添加功能
|
||||
description: 请填写希望添加的功能的具体信息
|
||||
title: "添加功能"
|
||||
labels: [add]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "希望添加什么功能?"
|
||||
placeholder: "请填写功能名称"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请详细描述希望添加的功能的具体信息"
|
||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
@@ -30,4 +31,5 @@ body:
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的"
|
||||
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 添加功能
|
||||
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
|
||||
about: 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
|
||||
46
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
imsyy/splayer
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
5
.npmrc
@@ -1,2 +1,5 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -4,3 +4,5 @@ pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# 是否使用单引号而不是双引号
|
||||
singleQuote: false
|
||||
# 是否在语句末尾使用分号
|
||||
semi: true
|
||||
# 每行的最大打印宽度
|
||||
printWidth: 100
|
||||
# 是否在对象和数组的末尾加上逗号
|
||||
trailingComma: all
|
||||
|
||||
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# build
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.25.3-alpine-slim as app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
319
README.md
@@ -1,6 +1,26 @@
|
||||
<!-- <div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/icons/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br /> -->
|
||||
|
||||
# SPlayer
|
||||
|
||||
> 一个简约的音乐播放器
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ## 严肃警告
|
||||
> 由于 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 项目已停止维护,由于接口状态的不可确定性,将无法保障功能的正常使用,本项目将会停止新功能的开发,进入无限期停更状态
|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ### 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
@@ -8,20 +28,13 @@
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
## 说明
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行构建
|
||||
- ~~仅对移动端做了基础适配,**不保证功能全部可用**~~
|
||||
- 欢迎各位大佬指点和 `Star` 哦 😍
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
|
||||
|
||||
- 欢迎各位大佬 `Star` 😍
|
||||
|
||||
## 👀 Demo
|
||||
|
||||
@@ -29,38 +42,31 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 支持扫码登录
|
||||
- 支持手机号登录
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 封面主题色自适应
|
||||
- 本地歌曲管理及分类 ~~以及音乐标签编辑~~
|
||||
- **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 支持逐字歌词
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 暂时去除,还待完善 )
|
||||
- 音乐渐入渐出
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- ~~移动端基础适配~~
|
||||
- ~~`i18n` 支持~~
|
||||
|
||||
#### 待办
|
||||
|
||||
- [ ] 完善音乐频谱
|
||||
- [ ] 添加桌面歌词
|
||||
- [ ] 多种布局方式
|
||||
- [ ] 发表评论
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 🎨 封面主题色自适应
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲(最高支持 Hi-Res)
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
- 📻 私人 FM
|
||||
- ☁️ 云盘音乐上传
|
||||
- 📂 云盘内歌曲播放
|
||||
- 🔄 云盘内歌曲纠正
|
||||
- 🗑️ 云盘歌曲删除
|
||||
- 📝 支持逐字歌词
|
||||
- 🔄 歌词滚动以及歌词翻译
|
||||
- 📹 MV 与视频播放
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区及评论点赞
|
||||
- 🌓 明暗模式自动 / 手动切换
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
## 🖼️ Screenshots
|
||||
|
||||
@@ -120,6 +126,38 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
### 本地构建
|
||||
|
||||
> 请尽量拉取最新分支后使用本地构建方式,在线部署的仓库可能更新不及时
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
# 从 Docker Hub 拉取
|
||||
docker pull imsyy/splayer:latest
|
||||
# 从 GitHub ghcr 拉取
|
||||
docker pull ghcr.io/imsyy/splayer:latest
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
```
|
||||
|
||||
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
@@ -132,6 +170,7 @@
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
```
|
||||
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||

|
||||
@@ -185,11 +224,11 @@
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| --- | --- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -198,7 +237,6 @@
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📢 免责声明
|
||||
@@ -221,3 +259,180 @@
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
<details>
|
||||
<summary>查看目录结构详情</summary>
|
||||
|
||||
> ChatGPT 写的,如有错误,请见谅
|
||||
|
||||
```dir
|
||||
├── auto-imports.d.ts # 自动导入
|
||||
├── components.d.ts # 自动导入
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker
|
||||
├── electron # Electron
|
||||
│ ├── main # Electron 主进程
|
||||
│ │ ├── index.js # 主进程入口
|
||||
│ │ ├── mainIpcMain.js # 主进程与渲染进程通信
|
||||
│ │ ├── startMainServer.js # 启动主进程服务器
|
||||
│ │ ├── startNcmServer.js # 启动网易云音乐服务
|
||||
│ │ └── utils # 主进程工具函数
|
||||
│ │ ├── checkUpdates.js # 检查更新
|
||||
│ │ ├── createGlobalShortcut.js # 创建全局快捷键
|
||||
│ │ ├── createSystemTray.js # 创建系统托盘
|
||||
│ │ ├── getNeteaseMusicUrl.js # 解灰
|
||||
│ │ ├── kwDES.js # DES加密算法
|
||||
│ │ └── readDirAsync.js # 异步读取目录
|
||||
│ └── preload # Electron 预加载脚本
|
||||
│ └── index.js # 预加载脚本入口文件
|
||||
├── electron-builder.yml # Electron Builder
|
||||
├── electron.vite.config.js # Electron Vite
|
||||
├── index.html # 主页面 HTML
|
||||
├── LICENSE # 项目许可证
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── src # 项目源代码
|
||||
│ ├── api # API 相关
|
||||
│ │ ├── ./..
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── assets # 静态资源
|
||||
│ │ ├── emoji.json # 表情数据
|
||||
│ │ ├── icon.json # 图标数据
|
||||
│ │ └── themeColor.json # 主题颜色数据
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── Cover # 封面相关组件目录
|
||||
│ │ │ ├── CoverDropdown.vue # 封面下拉组件
|
||||
│ │ │ ├── MainCover.vue # 主封面组件
|
||||
│ │ │ ├── SpecialCoverCard.vue # 特殊封面卡片组件
|
||||
│ │ │ └── SpecialCover.vue # 特殊封面组件
|
||||
│ │ ├── Global # 全局组件目录
|
||||
│ │ │ ├── MainLayout.vue # 主布局组件
|
||||
│ │ │ ├── Menu.vue # 菜单组件
|
||||
│ │ │ ├── Pagination.vue # 分页组件
|
||||
│ │ │ ├── Playlist.vue # 歌单组件
|
||||
│ │ │ ├── Provider.vue # 全局化配置组件
|
||||
│ │ │ └── SvgIcon.vue # SVG 图标组件
|
||||
│ │ ├── List # 列表组件目录
|
||||
│ │ │ ├── CommentList.vue # 评论列表组件
|
||||
│ │ │ ├── SongListDropdown.vue # 歌曲下拉组件
|
||||
│ │ │ └── SongList.vue # 歌曲列表组件
|
||||
│ │ ├── Modal # 弹窗相关组件目录
|
||||
│ │ │ ├── AddPlaylist.vue # 添加歌单组件
|
||||
│ │ │ ├── CloudSongMatch.vue # 云盘歌曲匹配组件
|
||||
│ │ │ ├── CreatePlaylist.vue # 创建歌单组件
|
||||
│ │ │ ├── DownloadSong.vue # 下载歌曲组件
|
||||
│ │ │ ├── LoginPhone.vue # 手机登录组件
|
||||
│ │ │ ├── LoginQRCode.vue # 二维码登录组件
|
||||
│ │ │ ├── Login.vue # 登录组件
|
||||
│ │ │ ├── PlaylistUpdate.vue # 歌单编辑组件
|
||||
│ │ │ └── UpCloudSong.vue # 上传云盘歌曲组件
|
||||
│ │ ├── Nav # 导航相关组件目录
|
||||
│ │ │ ├── MainNav.vue # 主导航组件
|
||||
│ │ │ └── UserData.vue # 用户数据组件
|
||||
│ │ ├── Player # 播放器相关组件目录
|
||||
│ │ │ ├── CountDown.vue # 倒计时组件
|
||||
│ │ │ ├── FullPlayer.vue # 全屏播放器组件
|
||||
│ │ │ ├── Lyric.vue # 歌词组件
|
||||
│ │ │ ├── MainControl.vue # 主控制组件
|
||||
│ │ │ ├── PlayerControl.vue # 播放器控制组件
|
||||
│ │ │ ├── PlayerCover.vue # 播放器封面组件
|
||||
│ │ │ └── PrivateFm.vue # 私人 FM 组件
|
||||
│ │ ├── Search # 搜索相关组件
|
||||
│ │ │ ├── SearchHot.vue # 热门搜索组件
|
||||
│ │ │ ├── SearchInp.vue # 搜索输入组件
|
||||
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
|
||||
│ │ └── WinDom # 窗口 DOM 相关组件
|
||||
│ │ └── TitleBar.vue # 标题栏组件
|
||||
│ ├── main.js # Vue 应用的入口文件
|
||||
│ ├── router # Vue Router 相关文件夹
|
||||
│ │ ├── index.js # Vue Router 入口文件
|
||||
│ │ └── routes.js # 路由配置文件
|
||||
│ ├── stores # Vuex Store 相关文件夹
|
||||
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
|
||||
│ │ ├── index.js # Vuex Store 入口文件
|
||||
│ │ ├── musicData.js # 音乐数据相关文件
|
||||
│ │ ├── siteData.js # 网站数据相关文件
|
||||
│ │ ├── siteSettings.js # 网站设置相关文件
|
||||
│ │ └── siteStatus.js # 网站状态相关文件
|
||||
│ ├── style # 样式相关文件夹
|
||||
│ │ ├── animate.scss # 动画样式文件
|
||||
│ │ └── main.scss # 主样式文件
|
||||
│ ├── utils # 工具函数文件夹
|
||||
│ │ ├── auth.js # 认证相关函数
|
||||
│ │ ├── base64.js # Base64编码解码相关函数
|
||||
│ │ ├── color-utils.js # 颜色工具函数
|
||||
│ │ ├── cover-color.js # 封面颜色相关函数
|
||||
│ │ ├── debounce.js # 防抖函数
|
||||
│ │ ├── formatData.js # 数据格式化函数
|
||||
│ │ ├── formRules.js # 表单验证规则
|
||||
│ │ ├── globalEvents.js # 全局事件处理函数
|
||||
│ │ ├── globalShortcut.js # 全局快捷键相关函数
|
||||
│ │ ├── helper.js # 辅助函数
|
||||
│ │ ├── parseLyric.js # 解析歌词函数
|
||||
│ │ ├── Player.js # 播放器控制相关函数
|
||||
│ │ ├── request.js # 网络请求相关函数
|
||||
│ │ ├── throttle.js # 节流函数
|
||||
│ │ ├── timeTools.js # 时间工具函数
|
||||
│ │ └── userSignIn.js # 用户登录相关函数
|
||||
│ └── views # Vue组件文件夹
|
||||
│ ├── Artist # 艺术家相关组件
|
||||
│ │ ├── albums.vue # 艺术家专辑组件
|
||||
│ │ ├── hot.vue # 艺术家热门组件
|
||||
│ │ ├── index.vue # 艺术家主组件
|
||||
│ │ ├── songs.vue # 艺术家歌曲组件
|
||||
│ │ └── videos.vue # 艺术家视频组件
|
||||
│ ├── Cloud.vue # 云盘组件
|
||||
│ ├── Comment.vue # 评论组件
|
||||
│ ├── DailySongs.vue # 每日推荐组件
|
||||
│ ├── Discover # 发现音乐相关组件
|
||||
│ │ ├── artists.vue # 发现音乐艺术家组件
|
||||
│ │ ├── index.vue # 发现音乐主组件
|
||||
│ │ ├── new.vue # 发现音乐新歌组件
|
||||
│ │ ├── playlists.vue # 发现音乐歌单组件
|
||||
│ │ └── toplists.vue # 发现音乐排行榜组件
|
||||
│ ├── History.vue # 历史记录组件
|
||||
│ ├── Home.vue # 主页组件
|
||||
│ ├── Like # 我喜欢的相关组件
|
||||
│ │ ├── albums.vue # 我喜欢的专辑组件
|
||||
│ │ ├── artists.vue # 我喜欢的艺术家组件
|
||||
│ │ ├── index.vue # 我喜欢的主组件
|
||||
│ │ ├── playlists.vue # 我喜欢的歌单组件
|
||||
│ │ └── videos.vue # 我喜欢的视频组件
|
||||
│ ├── List # 列表相关组件
|
||||
│ │ ├── album.vue # 专辑组件
|
||||
│ │ └── playlist.vue # 歌单组件
|
||||
│ │ └── dj.vue # 电台组件
|
||||
│ ├── Local # 本地音乐相关组件
|
||||
│ │ ├── albums.vue # 本地音乐专辑组件
|
||||
│ │ ├── artists.vue # 本地音乐艺术家组件
|
||||
│ │ ├── index.vue # 本地音乐主组件
|
||||
│ │ └── songs.vue # 本地音乐歌曲组件
|
||||
│ ├── Player.vue # 视频播放器组件
|
||||
│ ├── Dj # 电台相关组件
|
||||
│ │ └── index.vue # 电台主组件
|
||||
│ │ └── type.vue # 电台分类组件
|
||||
│ ├── Search # 搜索相关组件
|
||||
│ │ ├── albums.vue # 搜索专辑组件
|
||||
│ │ ├── artists.vue # 搜索艺术家组件
|
||||
│ │ ├── index.vue # 搜索主组件
|
||||
│ │ ├── playlists.vue # 搜索歌单组件
|
||||
│ │ ├── songs.vue # 搜索歌曲组件
|
||||
│ │ └── videos.vue # 搜索视频组件
|
||||
│ │ └── djs.vue # 搜索电台组件
|
||||
│ ├── Setting # 设置相关组件
|
||||
│ │ └── index.vue # 设置主组件
|
||||
│ ├── Song.vue
|
||||
│ ├── State
|
||||
│ │ ├── 403.vue
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ └── Test.vue
|
||||
└── vercel.json # Vercel 部署配置
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
3
auto-imports.d.ts
vendored
@@ -65,5 +65,6 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
9
components.d.ts
vendored
@@ -12,6 +12,7 @@ declare module 'vue' {
|
||||
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']
|
||||
CoverPlayBtn: typeof import('./src/components/Cover/CoverPlayBtn.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
@@ -27,11 +28,11 @@ declare module 'vue' {
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@@ -39,6 +40,7 @@ declare module 'vue' {
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
@@ -67,6 +69,7 @@ declare module 'vue' {
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
@@ -74,7 +77,6 @@ declare module 'vue' {
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
@@ -83,6 +85,7 @@ declare module 'vue' {
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
@@ -96,9 +99,11 @@ declare module 'vue' {
|
||||
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
|
||||
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
|
||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||
SongListDrawer: typeof import('./src/components/List/SongListDrawer.vue')['default']
|
||||
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
|
||||
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
|
||||
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
|
||||
Spectrum: typeof import('./src/components/Player/Spectrum.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
|
||||
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
|
||||
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
|
||||
|
||||
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 7899:7899
|
||||
restart: always
|
||||
@@ -19,13 +19,11 @@ asarUnpack:
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target: nsis
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
@@ -42,12 +40,16 @@ nsis:
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/images/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/images/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_512.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
@@ -67,9 +69,9 @@ dmg:
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
|
||||
@@ -42,7 +42,7 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.js"),
|
||||
index: resolve(__dirname, "electron/preload/index.mjs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -97,21 +97,31 @@ export default defineConfig(({ mode }) => {
|
||||
],
|
||||
},
|
||||
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,
|
||||
name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
short_name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
description: getEnv("RENDERER_VITE_SITE_DES"),
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
background_color: "#efefef",
|
||||
icons: [
|
||||
{
|
||||
src: "/images/logo/favicon.png",
|
||||
sizes: "200x200",
|
||||
src: "/images/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/logo/favicon_512.png",
|
||||
src: "/images/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
@@ -147,12 +157,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
linux: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { join } from "path";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
|
||||
import { platform, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createSystemTray from "@main/utils/createSystemTray";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import Store from "electron-store";
|
||||
@@ -15,7 +15,7 @@ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/splayer-log.txt");
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
// 设置日志文件的最大大小为 2 MB
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
||||
// 绑定 console 事件
|
||||
@@ -93,7 +93,7 @@ class MainProcess {
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("splayer");
|
||||
app.setAsDefaultProtocolClient("SPlayer");
|
||||
// 应用程序准备好之前注册
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
@@ -107,6 +107,7 @@ class MainProcess {
|
||||
createWindow() {
|
||||
// 创建浏览器窗口
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: app.getName() || "SPlayer",
|
||||
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
|
||||
height: this.store.get("windowSize.height") || 740, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
@@ -117,11 +118,11 @@ class MainProcess {
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
// devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
@@ -133,8 +134,6 @@ class MainProcess {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
// 创建系统信息
|
||||
createSystemInfo(this.mainWindow);
|
||||
});
|
||||
|
||||
// 主窗口事件
|
||||
@@ -166,6 +165,8 @@ class MainProcess {
|
||||
configureAutoUpdater();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow);
|
||||
// 系统托盘
|
||||
createSystemTray(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { write } from "node-id3";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
@@ -193,32 +193,39 @@ const mainIpcMain = (win) => {
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
||||
ipcMain.handle("downloadFile", async (_, songData, options) => {
|
||||
try {
|
||||
const { url, data, lyric, name, type } = JSON.parse(songData);
|
||||
const { path, downloadMeta, downloadCover, downloadLyrics } = JSON.parse(options);
|
||||
if (fs.access(path)) {
|
||||
const songData = JSON.parse(song);
|
||||
console.info("开始下载:", songData, data);
|
||||
console.info("开始下载:", name, url);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, data.url, {
|
||||
const songDownload = await download(win, url, {
|
||||
directory: path,
|
||||
filename: `${songName}.${songType}`,
|
||||
filename: `${name}.${type}`,
|
||||
});
|
||||
// 若关闭,则不进行元信息写入
|
||||
if (!downloadMeta) return true;
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, songData.cover, {
|
||||
const coverDownload = await download(win, data.cover, {
|
||||
directory: path,
|
||||
filename: `${songName}.jpg`,
|
||||
filename: `${name}.jpg`,
|
||||
});
|
||||
// 生成歌曲文件的元数据
|
||||
const songTag = {
|
||||
title: songData.name,
|
||||
artist: Array.isArray(songData.artists)
|
||||
? songData.artists.map((ar) => ar.name).join(" / ")
|
||||
: songData.artists || "未知歌手",
|
||||
album: songData.album?.name || songData.album,
|
||||
image: coverDownload.getSavePath(),
|
||||
};
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
write(songTag, songDownload.getSavePath());
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = data.name || "未知曲目";
|
||||
songFile.tag.album = data.album?.name || "未知专辑";
|
||||
songFile.tag.performers = data?.artists?.map((ar) => ar.name) || ["未知艺术家"];
|
||||
if (downloadLyrics) songFile.tag.lyrics = lyric;
|
||||
if (downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
import netEaseApi from "NeteaseCloudMusicApi";
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { dialog, shell } from "electron";
|
||||
import { dialog } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import pkg from "electron-updater";
|
||||
|
||||
const { autoUpdater } = pkg;
|
||||
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
@@ -8,23 +10,56 @@ const hasNewVersion = (info) => {
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否前往 GitHub 下载新版本安装包?",
|
||||
buttons: ["前往", "取消"],
|
||||
detail: "是否立即下载并安装新版本?",
|
||||
buttons: ["立即下载", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
shell.openExternal("https://github.com/imsyy/SPlayer/releases");
|
||||
// 触发手动下载
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
if (is.dev) return false;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
// 监听下载进度事件
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
console.log(`更新下载进度: ${progressObj.percent}%`);
|
||||
});
|
||||
|
||||
// 下载完成
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
// 显示安装弹窗
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "下载完成",
|
||||
message: "新版本已下载完成,是否现在安装?",
|
||||
buttons: ["是", "稍后"],
|
||||
type: "question",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 下载失败
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.error("下载更新失败:", err);
|
||||
dialog.showErrorBox("下载更新失败", "请检查网络连接并稍后重试!");
|
||||
});
|
||||
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
hasNewVersion(info);
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
};
|
||||
|
||||
@@ -1,55 +1,58 @@
|
||||
import { join } from "path";
|
||||
import { platform } from "@electron-toolkit/utils";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
// 当前歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统自定义信息
|
||||
* 创建系统托盘
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemInfo = (win) => {
|
||||
// 弹出列表
|
||||
app.setUserTasks([]);
|
||||
const createSystemTray = (win) => {
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 默认托盘菜单
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
// 给托盘图标设置气球提示
|
||||
const mainTray = new Tray(
|
||||
nativeImage
|
||||
.createFromPath(
|
||||
join(
|
||||
__dirname,
|
||||
process.platform === "win32"
|
||||
? "../../public/images/icons/favicon.ico"
|
||||
: "../../public/images/icons/favicon-32x32.png",
|
||||
),
|
||||
)
|
||||
.resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
}),
|
||||
);
|
||||
// 应用内菜单
|
||||
Menu.setApplicationMenu(createTrayMenu(win));
|
||||
// 默认名称
|
||||
win.setTitle(app.getName());
|
||||
mainTray.setTitle(app.getName());
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 自定义任务栏缩略图
|
||||
createThumbar(win);
|
||||
// 歌曲数据改变时
|
||||
// 左键事件
|
||||
mainTray.on("click", () => win.show());
|
||||
// 托盘菜单
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
// 系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放歌曲改变
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
// 托盘图标标题
|
||||
mainTray.setToolTip(val);
|
||||
// 更改应用标题
|
||||
win.setTitle(val);
|
||||
mainTray.setTitle(val);
|
||||
mainTray.setToolTip(val);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放状态改变
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
createThumbar(win);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 监听系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
createThumbar(win);
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
// 显示窗口
|
||||
win.show();
|
||||
});
|
||||
// 右键事件
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
// linux 右键菜单
|
||||
if (platform.isLinux) {
|
||||
mainTray.setContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
}
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
@@ -60,8 +63,8 @@ const createIcon = (name) => {
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icon/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icon/${name}-light.png`),
|
||||
? join(__dirname, `../../public/images/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icons/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
@@ -69,7 +72,7 @@ const createIcon = (name) => {
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 返回菜单
|
||||
return [
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
@@ -113,7 +116,9 @@ const createTrayMenu = (win) => {
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.webContents.send("setting");
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("open-setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -128,35 +133,7 @@ const createTrayMenu = (win) => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 自定义任务栏缩略图 - 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;
|
||||
export default createSystemTray;
|
||||
39
index.html
@@ -1,24 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="%RENDERER_VITE_SITE_LOGO%" />
|
||||
<link rel="apple-touch-icon" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="bookmark" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
nginx.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 7899;
|
||||
listen [::]:7899;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_buffers 16 32k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 128k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
77
package.json
@@ -1,21 +1,25 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.4",
|
||||
"version": "2.0.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",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
"node": ">=18.16.0",
|
||||
"npm": ">=9.6.7",
|
||||
"pnpm": ">=8.14.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.12.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "chcp 65001 && electron-vite dev --watch",
|
||||
"dev": "electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
@@ -23,51 +27,50 @@
|
||||
"build:linux": "npm run build && electron-builder --linux --config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "git+https://github.com/imsyy/NeteaseCloudMusicApi.git",
|
||||
"axios": "^1.4.0",
|
||||
"NeteaseCloudMusicApi": "^4.15.3",
|
||||
"axios": "^1.6.7",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-updater": "^6.1.8",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
"howler": "^2.2.3",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.13.4",
|
||||
"node-id3": "^0.2.6",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"music-metadata": "7.14.0",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.7.2",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.12.0",
|
||||
"electron": "^27.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.29",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"naive-ui": "^2.35.0",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"electron": "^28.2.4",
|
||||
"electron-builder": "^24.12.0",
|
||||
"electron-log": "^5.1.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"naive-ui": "^2.37.3",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.1",
|
||||
"terser": "^5.27.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vue": "^3.3.4"
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"vue": "3.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
3322
pnpm-lock.yaml
generated
724
public/font/font.min.css
vendored
BIN
public/images/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/images/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icons/favicon-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/images/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/icons/favicon-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/images/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
25
public/images/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2400 4444 c-14 -2 -63 -8 -110 -14 -407 -50 -737 -184 -1077 -436
|
||||
-124 -93 -348 -309 -406 -394 -100 -144 -190 -286 -223 -352 -101 -200 -199
|
||||
-506 -226 -707 -17 -127 -22 -436 -8 -546 40 -338 161 -687 327 -942 138 -213
|
||||
226 -319 372 -450 237 -211 471 -353 761 -460 147 -54 156 -57 330 -88 383
|
||||
-71 516 -67 965 25 76 16 288 91 370 132 28 13 66 31 85 38 39 16 158 88 270
|
||||
164 242 164 474 410 628 668 254 422 362 920 308 1408 -16 139 -23 172 -73
|
||||
344 -80 273 -188 496 -346 711 -104 143 -302 348 -404 421 l-48 34 -192 0
|
||||
c-184 0 -193 -1 -203 -21 -8 -14 -11 -271 -10 -882 1 -474 -1 -896 -5 -937
|
||||
-29 -306 -228 -596 -507 -737 -207 -104 -426 -126 -663 -66 -168 43 -300 125
|
||||
-453 283 -118 122 -203 310 -223 495 -10 86 -3 271 11 320 41 135 58 180 95
|
||||
248 103 193 330 376 545 441 157 47 364 49 513 5 38 -11 78 -19 90 -17 l22 3
|
||||
-3 635 c-2 349 -7 638 -10 642 -22 22 -413 47 -502 32z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
public/images/pic/artist.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
public/images/pic/record.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/pic/video.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
screenshots/SPlayer.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 524 KiB |
41
src/App.vue
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Provider>
|
||||
<!-- 主框架 -->
|
||||
<n-layout class="all-layout">
|
||||
<n-layout :class="['all-layout', { 'full-player': showFullPlayer }]">
|
||||
<!-- 导航栏 -->
|
||||
<n-layout-header bordered>
|
||||
<MainNav />
|
||||
@@ -11,7 +11,7 @@
|
||||
v-if="showSider"
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
'player-bar': music.getPlaySongData?.id && showPlayBar,
|
||||
}"
|
||||
position="absolute"
|
||||
has-sider
|
||||
@@ -43,7 +43,7 @@
|
||||
v-else
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
'player-bar': music.getPlaySongData?.id && showPlayBar,
|
||||
}"
|
||||
:native-scrollbar="false"
|
||||
position="absolute"
|
||||
@@ -79,11 +79,12 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { darkTheme, NButton } from "naive-ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { darkTheme, NButton } from "naive-ui";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
import globalShortcut from "@/utils/globalShortcut";
|
||||
import globalEvents from "@/utils/globalEvents";
|
||||
import packageJson from "@/../package.json";
|
||||
@@ -92,7 +93,7 @@ const router = useRouter();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { autoPlay, showSider } = storeToRefs(settings);
|
||||
const { autoPlay, showSider, autoSignIn } = storeToRefs(settings);
|
||||
const { showPlayBar, asideMenuCollapsed, showFullPlayer } = storeToRefs(status);
|
||||
|
||||
// 公告数据
|
||||
@@ -111,9 +112,9 @@ if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (checkPlatform.electron()) {
|
||||
$notification.create({
|
||||
title: "🎉 更新提醒",
|
||||
content: "检测到软件有更新,是否重新启动软件以应用更新?",
|
||||
meta: "v " + (packageJson.version || "1.0.0"),
|
||||
title: "🎉 有更新啦",
|
||||
content: "检测到软件内资源有更新,是否重新启动软件以应用更新?",
|
||||
meta: "当前版本 v " + (packageJson.version || "1.0.0"),
|
||||
action: () =>
|
||||
h(
|
||||
NButton,
|
||||
@@ -129,14 +130,14 @@ if ("serviceWorker" in navigator) {
|
||||
},
|
||||
),
|
||||
onAfterLeave: () => {
|
||||
$message.info("已取消本次更新,将在下次启动软件后生效", {
|
||||
$message.info("已取消本次更新,更新将在下次启动软件后生效", {
|
||||
duration: 6000,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.info("站点已更新,刷新后生效");
|
||||
$message.info("站点已更新,刷新后生效", {
|
||||
console.info("站点资源有更新,请刷新以应用更新");
|
||||
$message.info("站点资源有更新,请刷新以应用更新", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
});
|
||||
@@ -178,17 +179,19 @@ const handleKeyUp = (event) => {
|
||||
globalShortcut(event, router);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 挂载方法
|
||||
window.$canNotConnect = canNotConnect;
|
||||
// 主播放器
|
||||
initPlayer(autoPlay.value);
|
||||
await initPlayer(autoPlay.value);
|
||||
// 全局事件
|
||||
globalEvents(router);
|
||||
// 键盘监听
|
||||
if (!checkPlatform.electron()) {
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
// 自动签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 显示公告
|
||||
showAnnouncements();
|
||||
});
|
||||
@@ -201,7 +204,9 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.all-layout {
|
||||
height: 100%;
|
||||
transition: transform 0.3s;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
.n-layout-header {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
@@ -219,7 +224,7 @@ onUnmounted(() => {
|
||||
.sider-all {
|
||||
height: 100%;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -227,5 +232,9 @@ onUnmounted(() => {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
&.full-player {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
136
src/api/dj.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import axios from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 电台部分
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取电台 - 分类
|
||||
*/
|
||||
export const getDjCatelist = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/catelist",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐
|
||||
*/
|
||||
export const getDjRecommend = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台个性推荐
|
||||
*/
|
||||
export const getDjPersonalRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/personalize/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐类型
|
||||
*/
|
||||
export const getDjCategoryRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/category/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 私人 DJ
|
||||
*/
|
||||
export const getPrivateDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/aidj/content/rcmd",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 类别热门电台
|
||||
* @param {string} cateId - 类别 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getRadioHot = (cateId, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/radio/hot",
|
||||
params: {
|
||||
cateId,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 分类推荐
|
||||
* @param {string} type - 类别 id
|
||||
*/
|
||||
export const getRecType = (type) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend/type",
|
||||
params: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 详情
|
||||
* @param {string} rid - 电台 的 id
|
||||
*/
|
||||
export const getDjDetail = (rid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/detail",
|
||||
params: {
|
||||
rid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 节目
|
||||
* @param {string} rid - 电台 的 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getDjProgram = (rid, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/program",
|
||||
params: {
|
||||
rid,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 订阅
|
||||
* @param {number} rid - 电台 的 id
|
||||
* @param {number} t - 操作类型,1为收藏,0为取消收藏
|
||||
*/
|
||||
export const likeDj = (rid, t) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sub",
|
||||
params: {
|
||||
rid,
|
||||
t,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -67,10 +67,10 @@ export const getPlayListDetail = (id) => {
|
||||
/**
|
||||
* 获取歌单中所有歌曲信息
|
||||
* @param {number} id - 歌单id
|
||||
* @param {number} [limit=30] - 返回数量,默认30
|
||||
* @param {number} [limit=50] - 返回数量,默认50
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getAllPlayList = (id, limit = 30, offset = 0) => {
|
||||
export const getAllPlayList = (id, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/track/all",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "@/utils/request";
|
||||
import idMeta from "@/assets/idMeta.json";
|
||||
|
||||
/**
|
||||
* 推荐部分
|
||||
@@ -34,6 +35,21 @@ export const getPersonalized = (type, limit = 12) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 雷达歌单
|
||||
*/
|
||||
export const getRadarPlaylist = async () => {
|
||||
const allRadar = idMeta.radarPlaylist.map((playlist) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/detail",
|
||||
params: { id: playlist.id },
|
||||
});
|
||||
});
|
||||
const result = await Promise.allSettled(allRadar);
|
||||
return result.map((res) => res?.value.playlist);
|
||||
};
|
||||
|
||||
/**
|
||||
* 热门歌手列表
|
||||
* @param {number} [limit=6] - 要返回的歌手数量,默认为 6 个
|
||||
|
||||
@@ -33,6 +33,19 @@ export const getSearchSuggest = (keywords, mobile = false) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认搜索关键词
|
||||
*/
|
||||
export const getSearchDefault = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/search/default",
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索结果
|
||||
* @param {string} keywords - 搜索关键词
|
||||
|
||||
@@ -119,6 +119,19 @@ export const getUserMv = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户电台的订阅列表
|
||||
*/
|
||||
export const getUserDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sublist",
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户喜欢的音乐列表
|
||||
* @param {string} uid 用户的id
|
||||
|
||||
@@ -97,3 +97,25 @@ export const likeMv = (t, mvid) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 全部 mv
|
||||
* @param {string} area - 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部
|
||||
* @param {string} type - 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部
|
||||
* @param {string} order - 排序,可选值为上升最快,最热,最新,不填则为上升最快
|
||||
* @param {number} [limit=12] - 返回数量,默认12
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const allMv = (area, type, order, limit = 12, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/mv/all",
|
||||
params: {
|
||||
area,
|
||||
type,
|
||||
order,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"album": "M12 11a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m0 5.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5s4.5 2 4.5 4.5s-2 4.5-4.5 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"chevron-up": "M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z",
|
||||
"chevron-down": "M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z",
|
||||
"play": "M9.525 18.025q-.5.325-1.012.038T8 17.175V6.825q0-.6.513-.888t1.012.038l8.15 5.175q.45.3.45.85t-.45.85l-8.15 5.175Z",
|
||||
"play-circle": "M10 16.5v-9l6 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"pause-circle": "M15 16h-2V8h2m-4 8H9V8h2m1-6A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2",
|
||||
"play-next": "m10 16.5l6-4.5l-6-4.5M22 12c0-5.54-4.46-10-10-10c-1.17 0-2.3.19-3.38.56l.7 1.94c.85-.34 1.74-.53 2.68-.53c4.41 0 8.03 3.62 8.03 8.03c0 4.41-3.62 8.03-8.03 8.03c-4.41 0-8.03-3.62-8.03-8.03c0-.94.19-1.88.53-2.72l-1.94-.66C2.19 9.7 2 10.83 2 12c0 5.54 4.46 10 10 10s10-4.46 10-10M5.47 3.97c.85 0 1.53.71 1.53 1.5C7 6.32 6.32 7 5.47 7c-.79 0-1.5-.68-1.5-1.53c0-.79.71-1.5 1.5-1.5Z",
|
||||
"playlist-add": "M4 16q-.425 0-.713-.288T3 15q0-.425.288-.713T4 14h5q.425 0 .713.288T10 15q0 .425-.288.713T9 16H4Zm0-4q-.425 0-.713-.288T3 11q0-.425.288-.713T4 10h9q.425 0 .713.288T14 11q0 .425-.288.713T13 12H4Zm0-4q-.425 0-.713-.288T3 7q0-.425.288-.713T4 6h9q.425 0 .713.288T14 7q0 .425-.288.713T13 8H4Zm13 12q-.425 0-.713-.288T16 19v-3h-3q-.425 0-.713-.288T12 15q0-.425.288-.713T13 14h3v-3q0-.425.288-.713T17 10q.425 0 .713.288T18 11v3h3q.425 0 .713.288T22 15q0 .425-.288.713T21 16h-3v3q0 .425-.288.713T17 20Z",
|
||||
"account-music": "M11 14c1 0 2.05.16 3.2.44c-.81.87-1.2 1.89-1.2 3.06c0 .89.25 1.73.78 2.5H3v-2c0-1.19.91-2.15 2.74-2.88C7.57 14.38 9.33 14 11 14m0-2c-1.08 0-2-.39-2.82-1.17C7.38 10.05 7 9.11 7 8c0-1.08.38-2 1.18-2.82C9 4.38 9.92 4 11 4c1.11 0 2.05.38 2.83 1.18C14.61 6 15 6.92 15 8c0 1.11-.39 2.05-1.17 2.83c-.78.78-1.72 1.17-2.83 1.17m7.5-2H22v2h-2v5.5a2.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 2.5-2.5c.36 0 .69.07 1 .21V10Z",
|
||||
@@ -48,6 +50,7 @@
|
||||
"favorite-rounded": "M12 20.325q-.35 0-.713-.125t-.637-.4l-1.725-1.575q-2.65-2.425-4.788-4.813T2 8.15Q2 5.8 3.575 4.225T7.5 2.65q1.325 0 2.5.562t2 1.538q.825-.975 2-1.538t2.5-.562q2.35 0 3.925 1.575T22 8.15q0 2.875-2.125 5.275T15.05 18.25l-1.7 1.55q-.275.275-.637.4t-.713.125Z",
|
||||
"history": "M12 21q-3.15 0-5.575-1.913T3.275 14.2q-.1-.375.15-.687t.675-.363q.4-.05.725.15t.45.6q.6 2.25 2.475 3.675T12 19q2.925 0 4.963-2.038T19 12q0-2.925-2.038-4.963T12 5q-1.725 0-3.225.8T6.25 8H8q.425 0 .713.288T9 9q0 .425-.288.713T8 10H4q-.425 0-.713-.288T3 9V5q0-.425.288-.713T4 4q.425 0 .713.288T5 5v1.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924q1.212 1.213 1.925 2.85T21 12q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 21Zm1-9.4l2.5 2.5q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-2.8-2.8q-.15-.15-.225-.337T11 11.975V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v3.6Z",
|
||||
"delete": "M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12Z",
|
||||
"delete-sweep": "M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3zM14 5h-3l-1-1H6L5 5H2v2h12z",
|
||||
"fire": "M17.66 11.2c-.23-.3-.51-.56-.77-.82c-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32c-2.59 2.08-3.61 5.75-2.39 8.9c.04.1.08.2.08.33c0 .22-.15.42-.35.5c-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5c.14.6.41 1.2.71 1.73c1.08 1.73 2.95 2.97 4.96 3.22c2.14.27 4.43-.12 6.07-1.6c1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6c-1.12.4-2.24-.16-2.9-.82c1.19-.28 1.9-1.16 2.11-2.05c.17-.8-.15-1.46-.28-2.23c-.12-.74-.1-1.37.17-2.06c.19.38.39.76.63 1.06c.77 1 1.98 1.44 2.24 2.8c.04.14.06.28.06.43c.03.82-.33 1.72-.93 2.27Z",
|
||||
"search-rounded": "M9.5 16q-2.725 0-4.612-1.888T3 9.5q0-2.725 1.888-4.612T9.5 3q2.725 0 4.612 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l5.6 5.6q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-5.6-5.6q-.75.6-1.725.95T9.5 16Zm0-2q1.875 0 3.188-1.313T14 9.5q0-1.875-1.313-3.188T9.5 5Q7.625 5 6.312 6.313T5 9.5q0 1.875 1.313 3.188T9.5 14Z",
|
||||
"search-off": "m7 17.7l1.4 1.425q.15.15.35.15t.35-.15q.15-.15.15-.363T9.1 18.4L7.7 17l1.425-1.425q.15-.15.15-.35t-.15-.35q-.15-.15-.35-.15t-.35.15L7 16.3l-1.425-1.425q-.15-.15-.35-.15t-.35.15q-.15.15-.15.35t.15.35L6.3 17l-1.425 1.425q-.15.15-.15.35t.15.35q.15.15.35.15t.35-.15L7 17.7ZM7 22q-2.075 0-3.538-1.463T2 17q0-2.075 1.463-3.538T7 12q2.075 0 3.538 1.463T12 17q0 2.075-1.463 3.538T7 22Zm7.2-7.4q-.3-.325-.638-.663T12.9 13.3q.95-.6 1.525-1.6T15 9.5q0-1.875-1.313-3.188T10.5 5Q8.625 5 7.312 6.313T6 9.5q0 .15.013.288t.037.287q-.45.05-.987.2t-.963.35q-.05-.275-.075-.55T4 9.5q0-2.725 1.888-4.612T10.5 3q2.725 0 4.612 1.888T17 9.5q0 1.075-.338 2.038t-.937 1.762l5.575 5.6q.275.275.288.688t-.288.712q-.275.275-.7.275t-.7-.275l-5.7-5.7Z",
|
||||
@@ -87,5 +90,6 @@
|
||||
"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"
|
||||
"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",
|
||||
"lrc-text": "M13.8 22H5c-1.7 0-3-1.3-3-3v-1h11.1c-.1.3-.1.7-.1 1c0 1.1.3 2.1.8 3m0-6H5V5c0-1.7 1.3-3 3-3h11c1.7 0 3 1.3 3 3v1h-2V5c0-.6-.4-1-1-1s-1 .4-1 1v8.1c-1.8.3-3.3 1.4-4.2 2.9M8 8h7V6H8zm0 4h6v-2H8zm9 4v6l5-3z"
|
||||
}
|
||||
|
||||
28
src/assets/idMeta.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"radarPlaylist": [
|
||||
{
|
||||
"id": 3136952023,
|
||||
"name": "私人雷达"
|
||||
},
|
||||
{
|
||||
"id": 5320167908,
|
||||
"name": "时光雷达"
|
||||
},
|
||||
{
|
||||
"id": 5327906368,
|
||||
"name": "乐迷雷达"
|
||||
},
|
||||
{
|
||||
"id": 5362359247,
|
||||
"name": "宝藏雷达"
|
||||
},
|
||||
{
|
||||
"id": 5300458264,
|
||||
"name": "新歌雷达"
|
||||
},
|
||||
{
|
||||
"id": 5341776086,
|
||||
"name": "神秘雷达"
|
||||
}
|
||||
]
|
||||
}
|
||||
161
src/components/Cover/CoverPlayBtn.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<!-- 封面列表 - 播放按钮 -->
|
||||
<template>
|
||||
<div class="play-btn" @click.stop>
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
color="#efefef"
|
||||
tag="div"
|
||||
type="primary"
|
||||
class="play"
|
||||
size="large"
|
||||
strong
|
||||
secondary
|
||||
circle
|
||||
@click.stop="playAllSongs"
|
||||
>
|
||||
<template #icon>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-icon :key="`${isHasSongs}-${playState}`" size="50">
|
||||
<SvgIcon :icon="isHasSongs !== -1 && playState ? 'pause-circle' : 'play-circle'" />
|
||||
</n-icon>
|
||||
</Transition>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getAllPlayList } from "@/api/playlist";
|
||||
import { getAlbumDetail } from "@/api/album";
|
||||
import { getDjProgram } from "@/api/dj";
|
||||
import { musicData, siteStatus, siteData } from "@/stores";
|
||||
import { playOrPause, initPlayer } from "@/utils/Player";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
const { playList, playSongData } = storeToRefs(music);
|
||||
const { playIndex, playMode, playHeartbeatMode, playState } = storeToRefs(status);
|
||||
|
||||
const props = defineProps({
|
||||
// id
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
// 歌单类型
|
||||
type: {
|
||||
type: String,
|
||||
default: "playlist",
|
||||
},
|
||||
});
|
||||
// 播放按钮数据
|
||||
const playLoading = ref(false);
|
||||
const playListData = ref(null);
|
||||
|
||||
// 是否处于当前歌单
|
||||
const isHasSongs = computed(() => {
|
||||
if (!playListData.value || playListData.value === 400) return -1;
|
||||
const songId = music.getPlaySongData?.id;
|
||||
const existingIndex = playListData.value.findIndex((song) => song.id === songId);
|
||||
return existingIndex;
|
||||
});
|
||||
|
||||
// 获取歌单数据
|
||||
const getPlaylistData = async () => {
|
||||
// 为了播放速度,仅加载列表前 500 首
|
||||
console.log(props.type, props.id);
|
||||
// 按列表类别获取数据
|
||||
switch (props.type) {
|
||||
case "playlist": {
|
||||
if (props.id === 1024) {
|
||||
console.log("播放我喜欢的音乐");
|
||||
const id = userLikeData.value.playlists?.[0]?.id || null;
|
||||
if (!isLogin() || !id) return 400;
|
||||
const result = await getAllPlayList(id, 500);
|
||||
return formatData(result.songs, "song");
|
||||
} else {
|
||||
const result = await getAllPlayList(props.id, 500);
|
||||
return formatData(result.songs, "song");
|
||||
}
|
||||
}
|
||||
case "album": {
|
||||
const result = await getAlbumDetail(props.id);
|
||||
return formatData(result.songs, "song");
|
||||
}
|
||||
case "dj": {
|
||||
const result = await getDjProgram(props.id, 500);
|
||||
return formatData(result.programs, "dj");
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放歌单
|
||||
const playAllSongs = async () => {
|
||||
try {
|
||||
if (!props.id) return false;
|
||||
// 开启加载状态
|
||||
if (props.type !== "mv") {
|
||||
// 若不处于歌单内
|
||||
if (isHasSongs.value === -1) {
|
||||
playLoading.value = true;
|
||||
// 获取歌单数据
|
||||
playListData.value = await getPlaylistData();
|
||||
console.log(playListData.value);
|
||||
if (!playListData.value) {
|
||||
playLoading.value = false;
|
||||
return $message.error("获取播放列表时出现错误");
|
||||
}
|
||||
if (playListData.value === 400) {
|
||||
playLoading.value = false;
|
||||
return $message.error("请登录后使用");
|
||||
}
|
||||
console.log("不在歌单内");
|
||||
// 更改模式和歌单
|
||||
playHeartbeatMode.value = false;
|
||||
playMode.value = props.type === "dj" ? "dj" : "normal";
|
||||
playList.value = playListData.value.slice();
|
||||
playSongData.value = playListData.value[0];
|
||||
playIndex.value = 0;
|
||||
// 初始化播放器
|
||||
await initPlayer(true);
|
||||
playLoading.value = false;
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
}
|
||||
// 若处于歌单内
|
||||
else {
|
||||
console.log("处于歌单内");
|
||||
playSongData.value = playListData.value[isHasSongs.value];
|
||||
playIndex.value = isHasSongs.value;
|
||||
// 播放
|
||||
playOrPause();
|
||||
}
|
||||
} else {
|
||||
// 跳转播放器
|
||||
router.push({
|
||||
path: "/videos-player",
|
||||
query: {
|
||||
id: props.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取播放列表时出现错误:", error);
|
||||
$message.error("获取播放列表时出现错误");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.play-btn {
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
</style>
|
||||
@@ -36,7 +36,17 @@
|
||||
>
|
||||
<template #placeholder>
|
||||
<div :class="['cover-loading', type]">
|
||||
<img class="loading-img" src="/images/pic/album.jpg?assest" alt="song" />
|
||||
<img
|
||||
class="loading-img"
|
||||
:src="
|
||||
type === 'mv'
|
||||
? '/images/pic/video.png?assest'
|
||||
: type === 'artist'
|
||||
? '/images/pic/artist.jpg?assest'
|
||||
: '/images/pic/album.jpg?assest'
|
||||
"
|
||||
alt="song"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div :class="['cover-loading', type]">
|
||||
<n-spin size="small" />
|
||||
@@ -56,19 +66,24 @@
|
||||
<n-text class="add-desc">{{ item.desc }}</n-text>
|
||||
</div>
|
||||
<!-- 播放按钮 -->
|
||||
<n-icon class="play">
|
||||
<SvgIcon :icon="type !== 'artist' ? 'play-circle' : 'account-music'" />
|
||||
<CoverPlayBtn v-if="type !== 'artist'" :id="item.id" :type="type" />
|
||||
<n-icon v-else class="play-btn">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 信息 -->
|
||||
<div class="cover-content">
|
||||
<n-text class="name">{{ item.name }}</n-text>
|
||||
<!-- 创建者 -->
|
||||
<n-text v-if="item.creator" class="creator" depth="3">
|
||||
<n-text v-if="item?.creator" class="creator" depth="3">
|
||||
{{ item.creator?.nickname || item.creator || "未知" }}
|
||||
</n-text>
|
||||
<!-- 电台简介 -->
|
||||
<n-text v-if="item?.rcmdText" class="creator" depth="3">
|
||||
{{ item.rcmdText || "未知简介" }}
|
||||
</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="item.artists" class="artists">
|
||||
<div v-if="item.artists && type !== 'dj'" class="artists">
|
||||
<n-text
|
||||
v-for="ar in item.artists"
|
||||
:key="ar.id"
|
||||
@@ -198,6 +213,14 @@ const jumpLink = (data, type) => {
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "dj":
|
||||
router.push({
|
||||
path: "/dj",
|
||||
query: {
|
||||
id: data?.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -260,7 +283,9 @@ const jumpLink = (data, type) => {
|
||||
top: -80px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
box-sizing: border-box;
|
||||
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
opacity: 0;
|
||||
transition:
|
||||
@@ -272,14 +297,18 @@ const jumpLink = (data, type) => {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
.play {
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
opacity: 0;
|
||||
color: #efefefde;
|
||||
transform: translateY(6px);
|
||||
font-size: 50px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
z-index: 3;
|
||||
transition:
|
||||
opacity 0.3s,
|
||||
@@ -342,7 +371,7 @@ const jumpLink = (data, type) => {
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.play {
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
&:hover {
|
||||
@@ -371,8 +400,8 @@ const jumpLink = (data, type) => {
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.play {
|
||||
// display: none;
|
||||
.play-btn {
|
||||
font-size: 50px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
@@ -33,9 +33,7 @@
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 播放按钮 -->
|
||||
<n-icon v-if="showIcon" class="play" @click.stop>
|
||||
<SvgIcon icon="play-circle" />
|
||||
</n-icon>
|
||||
<CoverPlayBtn v-if="showIcon" :id="data.id" class="play" />
|
||||
<!-- 日期 -->
|
||||
<div v-if="showDate" class="cover-date">
|
||||
<n-icon class="date-icon">
|
||||
@@ -145,15 +143,24 @@ const props = defineProps({
|
||||
.play {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-size: 60px;
|
||||
color: #ffffffe6;
|
||||
transform: scale(0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
cursor: pointer;
|
||||
:deep(.play) {
|
||||
--n-width: 50px;
|
||||
--n-height: 50px;
|
||||
--n-font-size: 20px;
|
||||
.n-icon {
|
||||
font-size: 60px !important;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
@@ -212,7 +219,6 @@ const props = defineProps({
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
transform: translate3d(-2px, 0, 0);
|
||||
.cover-main-img {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<main id="main-layout" :class="['main-layout', { 'no-sider': !showSider }]">
|
||||
<!-- 回顶 -->
|
||||
<n-back-top
|
||||
:bottom="Object.keys(music.getPlaySongData)?.length && showPlayBar ? 110 : 50"
|
||||
:bottom="music.getPlaySongData?.id && showPlayBar ? 110 : 50"
|
||||
style="transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
>
|
||||
<n-icon size="26">
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
<n-menu
|
||||
ref="mainMenuRef"
|
||||
v-model:value="menuActiveKey"
|
||||
class="main-menu"
|
||||
:root-indent="showSider ? 36 : 28"
|
||||
:class="['main-menu', { cover: siderShowCover }]"
|
||||
:root-indent="showSider ? 36 : 26"
|
||||
:indent="0"
|
||||
:collapsed="asideMenuCollapsed.value"
|
||||
:defaultExpandedKeys="['user-playlists', 'favorite-playlists']"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
@contextmenu="openSideDropdown($event)"
|
||||
@contextmenu.stop
|
||||
@update:value="checkMenuItem"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteStatus, siteData, musicData } from "@/stores";
|
||||
import { NIcon, NText, NButton } from "naive-ui";
|
||||
import { siteStatus, siteData, musicData, siteSettings } from "@/stores";
|
||||
import { NIcon, NText, NButton, NAvatar } from "naive-ui";
|
||||
import { useRouter, RouterLink } from "vue-router";
|
||||
import { getHeartRateList } from "@/api/playlist";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
@@ -37,17 +37,12 @@ const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { asideMenuCollapsed, showSider, showFullPlayer } = storeToRefs(status);
|
||||
const settings = siteSettings();
|
||||
const { siderShowCover } = storeToRefs(settings);
|
||||
const { asideMenuCollapsed, showSider, showFullPlayer, playIndex, playMode, playHeartbeatMode } =
|
||||
storeToRefs(status);
|
||||
const { userData, userLikeData, userLoginStatus } = storeToRefs(data);
|
||||
const {
|
||||
playList,
|
||||
playListOld,
|
||||
playIndex,
|
||||
playSongData,
|
||||
playHeartbeatMode,
|
||||
playMode,
|
||||
privateFmSong,
|
||||
} = storeToRefs(music);
|
||||
const { playList, playListOld, playSongData, privateFmSong } = storeToRefs(music);
|
||||
|
||||
// 子组件
|
||||
const coverDropdownRef = ref(null);
|
||||
@@ -140,12 +135,12 @@ const menuOptions = computed(() => [
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: "record",
|
||||
name: "dj-hot",
|
||||
},
|
||||
},
|
||||
() => ["播客电台"],
|
||||
),
|
||||
key: "record",
|
||||
key: "dj-hot",
|
||||
icon: renderIcon("record"),
|
||||
},
|
||||
{
|
||||
@@ -168,7 +163,6 @@ const menuOptions = computed(() => [
|
||||
path: "/like-songs",
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuid: "like-songs",
|
||||
},
|
||||
() => [
|
||||
h(
|
||||
@@ -285,43 +279,79 @@ const changeUserPlaylists = (data) => {
|
||||
userPlaylists.value.children = userPlaylistsData.slice(1).map((v) => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
siderShowCover.value
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "user-pl-cover",
|
||||
onclick: () => {
|
||||
router.push({
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuId: v.id,
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
[
|
||||
h(NAvatar, { src: v?.coverSize?.s, fallbackSrc: "/images/pic/album.jpg?assest" }),
|
||||
h(NText, null, () => [v.name]),
|
||||
],
|
||||
)
|
||||
: h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
key: v.id,
|
||||
icon: renderIcon("queue-music-rounded"),
|
||||
icon: siderShowCover.value ? null : renderIcon("queue-music-rounded"),
|
||||
};
|
||||
});
|
||||
favoritePlaylists.value.children = favoritePlaylistsData.map((v) => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
siderShowCover.value
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "user-pl-cover",
|
||||
onclick: () => {
|
||||
router.push({
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuId: v.id,
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
[
|
||||
h(NAvatar, { src: v?.coverSize?.s, fallbackSrc: "/images/pic/album.jpg?assest" }),
|
||||
h(NText, null, () => [v.name]),
|
||||
],
|
||||
)
|
||||
: h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
key: v.id,
|
||||
icon: renderIcon("queue-music-rounded"),
|
||||
icon: siderShowCover.value ? null : renderIcon("queue-music-rounded"),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -379,15 +409,6 @@ const checkMenuItem = async (key) => {
|
||||
mainMenuRef.value?.showOption(key);
|
||||
};
|
||||
|
||||
// 开启右键菜单
|
||||
const openSideDropdown = (e) => {
|
||||
e.preventDefault();
|
||||
if (!e.target.classList.contains("user-playlist")) return false;
|
||||
// 获取 id
|
||||
const menuId = e.target.getAttribute("menuid");
|
||||
coverDropdownRef.value?.openDropdown(e, "playlist", menuId);
|
||||
};
|
||||
|
||||
// 开启心动模式
|
||||
const startHeartRate = debounce(async () => {
|
||||
try {
|
||||
@@ -441,13 +462,7 @@ watch(
|
||||
|
||||
// 监听用户歌单变化
|
||||
watch(
|
||||
() => userLikeData.value.playlists,
|
||||
(val) => {
|
||||
changeUserPlaylists(val);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => userLoginStatus.value,
|
||||
[() => userLikeData.value.playlists, () => userLoginStatus.value, () => siderShowCover.value],
|
||||
() => changeUserPlaylists(userLikeData.value.playlists),
|
||||
);
|
||||
|
||||
@@ -458,6 +473,7 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-menu {
|
||||
padding-bottom: 14px;
|
||||
:deep(.n-menu-item) {
|
||||
.n-menu-item-content {
|
||||
&.n-menu-item-content--selected {
|
||||
@@ -470,12 +486,25 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
// 我喜欢的音乐
|
||||
// 普通歌单
|
||||
.user-playlist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
// 带封面歌单
|
||||
.user-pl-cover {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.n-avatar {
|
||||
border-radius: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 折叠菜单
|
||||
@@ -508,6 +537,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.cover {
|
||||
:deep(.n-submenu-children) {
|
||||
--n-item-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="playListShow"
|
||||
:class="showFullPlayer ? 'main-playlist full-player' : 'main-playlist'"
|
||||
:class="['main-playlist', { 'full-player': showFullPlayer }]"
|
||||
:style="{
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
@@ -21,28 +21,100 @@
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 提示 -->
|
||||
<n-alert v-if="playList?.length >= 400" class="alert" :show-icon="false">
|
||||
当前歌曲过多,无法自动定位,请手动查找
|
||||
</n-alert>
|
||||
<!-- 歌曲列表 -->
|
||||
<div class="list">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-virtual-list
|
||||
v-if="playList?.length"
|
||||
ref="playListRef"
|
||||
:item-size="76"
|
||||
:items="playListData"
|
||||
:default-scroll-index="playIndex"
|
||||
style="max-height: calc(100vh - 158px)"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div
|
||||
:id="`songs-${index}`"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'songs-item',
|
||||
{ play: playSongData?.id === item?.id, player: showFullPlayer },
|
||||
]"
|
||||
@click.stop="playSong(item, index)"
|
||||
@dblclick.stop="playSong(item, index)"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<n-text v-if="playSongData?.id !== item?.id" class="num" depth="3">
|
||||
{{ index + 1 }}
|
||||
</n-text>
|
||||
<n-icon v-else class="play" size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<!-- 信息 -->
|
||||
<div class="info">
|
||||
<!-- 歌曲名 -->
|
||||
<n-text class="name" depth="2">{{ item?.name || "未知曲目" }}</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="Array.isArray(item?.artists)" class="artist">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" depth="3" class="ar">
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="playMode === 'dj'" class="artist">
|
||||
<n-text class="ar"> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item?.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除 -->
|
||||
<n-icon class="delete" size="18" @click.stop="removeSong(index)">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-data-table
|
||||
v-if="playList?.length"
|
||||
class="pl-list"
|
||||
:columns="columns"
|
||||
:data="playList"
|
||||
:bordered="false"
|
||||
:bottom-bordered="false"
|
||||
:max-height="playList?.length >= 400 ? 'calc(100vh - 162px)' : '100%'"
|
||||
virtual-scroll
|
||||
/>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<n-grid v-if="playList?.length" :cols="2" x-gap="16" class="controls">
|
||||
<n-gi>
|
||||
<!-- 定位歌曲 -->
|
||||
<n-button
|
||||
size="large"
|
||||
tag="div"
|
||||
strong
|
||||
secondary
|
||||
@click="playListRef?.scrollTo({ index: playIndex })"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="location" />
|
||||
</n-icon>
|
||||
</template>
|
||||
当前播放
|
||||
</n-button>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<!-- 清空列表 -->
|
||||
<n-button size="large" tag="div" strong secondary @click="cleanPlaylists">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="delete-sweep" />
|
||||
</n-icon>
|
||||
</template>
|
||||
清空列表
|
||||
</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</Transition>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
@@ -51,14 +123,30 @@
|
||||
<script setup>
|
||||
import { NText, NIcon } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus } from "@/stores";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, changePlayIndex, soundStop } from "@/utils/Player";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { playSongData, playList, playIndex, playMode } = storeToRefs(music);
|
||||
const { coverTheme, showFullPlayer, playListShow } = storeToRefs(status);
|
||||
const settings = siteSettings();
|
||||
const { useMusicCache } = storeToRefs(settings);
|
||||
const { playSongData, playList } = storeToRefs(music);
|
||||
const { coverTheme, showFullPlayer, playListShow, playIndex, playMode, playLoading } =
|
||||
storeToRefs(status);
|
||||
|
||||
const playListRef = ref(null);
|
||||
|
||||
// 播放列表数据
|
||||
const playListData = computed(() => {
|
||||
return playList.value?.[0]
|
||||
? playList.value.slice().map((v, i) => {
|
||||
v.key = `${i}`;
|
||||
return v;
|
||||
})
|
||||
: [];
|
||||
});
|
||||
|
||||
// 抽屉开启
|
||||
const playlistOpen = () => {
|
||||
@@ -71,75 +159,15 @@ const playlistOpen = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 表格数据
|
||||
const columns = computed(() => [
|
||||
{
|
||||
key: "songs",
|
||||
className: "songs-item",
|
||||
render(song, index) {
|
||||
return createSongs(song, index);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 列表歌曲模块
|
||||
const createSongs = (song, index) => {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
id: `pl-song-${index}`,
|
||||
class: {
|
||||
songs: true,
|
||||
play: playSongData.value?.id === song?.id,
|
||||
player: showFullPlayer.value,
|
||||
},
|
||||
onClick: () => {
|
||||
playSong(song, index);
|
||||
},
|
||||
},
|
||||
[
|
||||
// 序号
|
||||
playSongData.value?.id !== song?.id
|
||||
? h(NText, { class: "num", depth: "3" }, () => [index + 1])
|
||||
: h(NIcon, { class: "play", size: "18" }, () => [h(SvgIcon, { icon: "music-note" })]),
|
||||
// 信息
|
||||
h("div", { class: "info" }, [
|
||||
// 名称
|
||||
h(NText, { class: "name", depth: "2" }, () => [song?.name || "未知曲目"]),
|
||||
// 歌手
|
||||
Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "artist" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { class: "ar", depth: "3", key: ar.id }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h("div", { class: "artist" }, [
|
||||
h(NText, { class: "ar", depth: "3" }, () => [song.artists || "未知艺术家"]),
|
||||
]),
|
||||
]),
|
||||
// 移除
|
||||
h(
|
||||
NIcon,
|
||||
{
|
||||
class: "delete",
|
||||
size: "18",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
removeSong(index);
|
||||
},
|
||||
},
|
||||
() => [h(SvgIcon, { icon: "delete" })],
|
||||
),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 播放歌曲
|
||||
const playSong = async (song, index) => {
|
||||
const playSong = debounce(async (song, index) => {
|
||||
// 若开启了缓存且正在加载
|
||||
if (useMusicCache.value && playLoading.value) {
|
||||
$message.warning("歌曲正在缓冲中,请稍后");
|
||||
return false;
|
||||
}
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
if (playMode.value !== "dj") playMode.value = "normal";
|
||||
// 更改播放索引
|
||||
playIndex.value = index;
|
||||
// 是否为当前播放歌曲
|
||||
@@ -150,25 +178,41 @@ const playSong = async (song, index) => {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 清空列表
|
||||
const cleanPlaylists = () => {
|
||||
soundStop();
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
playListShow.value = false;
|
||||
showFullPlayer.value = false;
|
||||
$message.success("已清空播放列表");
|
||||
};
|
||||
|
||||
// 移除歌曲
|
||||
const removeSong = async (index) => {
|
||||
if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
} else if (index === playIndex.value) {
|
||||
// 如果删除的是当前播放歌曲,则下一曲
|
||||
// 若删除时仅剩一首
|
||||
if (playList.value.length === 1) {
|
||||
cleanPlaylists();
|
||||
return false;
|
||||
}
|
||||
// 若为当前播放
|
||||
if (index === playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
changePlayIndex("next", true);
|
||||
}
|
||||
playList.value.splice(index, 1);
|
||||
// 检查当前播放歌曲的索引是否超出了列表范围
|
||||
if (playIndex.value >= playList.value.length) {
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
soundStop();
|
||||
// 若为当前播放之前
|
||||
else if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
// 若大于当前播放
|
||||
else if (index > playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -180,146 +224,132 @@ const removeSong = async (index) => {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
height: 48px;
|
||||
.list {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pl-list {
|
||||
:deep(.n-data-table-thead) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.n-data-table-tbody) {
|
||||
.songs-item {
|
||||
padding: 0;
|
||||
.songs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
background-color: var(--n-border-color-modal);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
.songs-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--n-close-color-hover);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
box-shadow:
|
||||
0 1px 2px -2px var(--main-boxshadow-color),
|
||||
0 3px 6px 0 var(--main-boxshadow-color),
|
||||
0 5px 12px 4px var(--main-boxshadow-hover-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-data-table-tr {
|
||||
&:last-child {
|
||||
.songs {
|
||||
margin-bottom: 0;
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
border-radius: 8px;
|
||||
.controls {
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
.n-button {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -330,12 +360,17 @@ const removeSong = async (index) => {
|
||||
.main-playlist {
|
||||
width: 400px !important;
|
||||
border-radius: 12px 0 0 12px;
|
||||
transition: width 0.3s;
|
||||
.n-drawer-header {
|
||||
height: 70px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.n-scrollbar-content {
|
||||
padding: 16px !important;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.full-player {
|
||||
background-color: transparent;
|
||||
@@ -354,5 +389,9 @@ const removeSong = async (index) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
width: 100% !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,8 +70,8 @@ const changeThemeColor = (val, isCover = false) => {
|
||||
isCover && Object.keys(val)?.length
|
||||
? val[themeType.value]
|
||||
: val !== "custom"
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
// 微调主题色
|
||||
const primaryColor = isCover ? `rgb(${mainColorData.bg})` : mainColorData.primaryColor;
|
||||
const primaryColorHover = isCover ? `rgba(${mainColorData.bg}, 0.29)` : primaryColor + "29";
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<!-- 歌曲列表 -->
|
||||
<template>
|
||||
<Transition name="fade" mode="out-in" @after-enter="checkHasPlaying">
|
||||
<div v-if="data !== 'empty' && data?.length && data[0] !== 'empty'" class="song-list">
|
||||
<div v-if="data?.[0]?.id" class="song-list">
|
||||
<div v-if="showTitle" class="song-list-header">
|
||||
<n-text class="num" depth="3"> # </n-text>
|
||||
<n-text :class="{ info: true, 'has-cover': data[0].cover && showCover }" depth="3">
|
||||
歌曲
|
||||
<n-text :class="['info', { 'has-cover': data[0].cover && showCover }]" depth="3">
|
||||
{{ type === "song" ? "歌曲" : "声音" }}
|
||||
</n-text>
|
||||
<n-text v-if="data[0].album && showAlbum" class="album" depth="3"> 专辑 </n-text>
|
||||
<n-text v-if="data[0].duration" class="duration" depth="3"> 时长 </n-text>
|
||||
<n-text v-if="data[0].size" class="size" depth="3"> 大小 </n-text>
|
||||
<n-text v-if="data[0].album && showAlbum" class="album hidden" depth="3"> 专辑 </n-text>
|
||||
<n-text v-if="data[0].updateTime && type === 'dj'" class="update hidden" depth="3">
|
||||
更新日期
|
||||
</n-text>
|
||||
<n-text v-if="type !== 'dj'" class="control" />
|
||||
<n-text v-if="data[0].playCount && type === 'dj'" class="count hidden" depth="3">
|
||||
播放量
|
||||
</n-text>
|
||||
<n-text v-if="data[0].duration" class="duration hidden" depth="3"> 时长 </n-text>
|
||||
<n-text v-if="data[0].size" class="size hidden" depth="3"> 大小 </n-text>
|
||||
</div>
|
||||
<n-card
|
||||
v-for="(item, index) in data.slice(
|
||||
@@ -25,18 +32,19 @@
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}"
|
||||
:class="music.getPlaySongData?.id === item?.id ? 'songs play' : 'songs'"
|
||||
:class="Number(music.getPlaySongData?.id) === Number(item?.id) ? 'songs play' : 'songs'"
|
||||
hoverable
|
||||
@click="checkCanClick(data, item, songsIndex + index)"
|
||||
@dblclick.stop="playSong(data, item, songsIndex + index)"
|
||||
@contextmenu="
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId)
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId, type)
|
||||
"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<n-text v-if="music.getPlaySongData?.id !== item?.id" class="num" depth="3">
|
||||
{{ songsIndex + index + 1 }}
|
||||
</n-text>
|
||||
<n-icon v-else class="play" size="22">
|
||||
<n-icon v-else class="num" size="22">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<!-- 封面 -->
|
||||
@@ -63,7 +71,10 @@
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<!-- 名称 -->
|
||||
<n-text class="name" depth="2">{{ item?.name || "未知曲目" }}</n-text>
|
||||
<!-- @click.stop="type !== 'dj' && !item.path ? router.push(`/song?id=${item.id}`) : null" -->
|
||||
<n-text class="name" depth="2">
|
||||
{{ item?.name || "未知曲目" }}
|
||||
</n-text>
|
||||
<!-- 特权 -->
|
||||
<n-tag
|
||||
v-if="showPrivilege && item.fee === 1 && userData.detail?.profile?.vipType !== 11"
|
||||
@@ -118,29 +129,36 @@
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
@dblclick.stop
|
||||
>
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="type === 'dj'" class="artist">
|
||||
<n-text class="ar" @dblclick.stop> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item.artists || "未知艺术家" }} </n-text>
|
||||
<n-text class="ar" @dblclick.stop> {{ item.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
<!-- 别名 -->
|
||||
<n-text v-if="item.alia" class="alia" depth="3">{{ item.alia }}</n-text>
|
||||
</div>
|
||||
<!-- 专辑 -->
|
||||
<template v-if="showAlbum">
|
||||
<template v-if="showAlbum && type !== 'dj'">
|
||||
<n-text
|
||||
v-if="item.album"
|
||||
class="album"
|
||||
@click.stop="item.album !== 'string' ? router.push(`/album?id=${item.album.id}`) : null"
|
||||
class="album hidden"
|
||||
@click.stop="
|
||||
typeof item.album === 'object' ? router.push(`/album?id=${item.album.id}`) : null
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
{{ typeof item.album === "string" ? item.album : item.album.name }}
|
||||
{{ typeof item.album === "object" ? item.album?.name || "未知专辑" : item.album }}
|
||||
</n-text>
|
||||
<n-text v-else class="album">未知专辑</n-text>
|
||||
<n-text v-else class="album hidden">未知专辑</n-text>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<div class="action">
|
||||
<div v-if="type !== 'dj'" class="action">
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
:depth="dataStore.getSongIsLike(item?.id) ? 0 : 3"
|
||||
@@ -157,12 +175,32 @@
|
||||
"
|
||||
/>
|
||||
</n-icon>
|
||||
<!-- 更多操作 -->
|
||||
<n-icon
|
||||
class="more mobile"
|
||||
depth="3"
|
||||
size="20"
|
||||
@click.stop="
|
||||
songListDrawerRef?.drawerOpen(data, item, songsIndex + index, sourceId, type)
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
<SvgIcon icon="more" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 更新日期 -->
|
||||
<n-text v-if="type === 'dj' && item.updateTime" class="update hidden" depth="3">
|
||||
{{ djFormatDate(item.updateTime) }}
|
||||
</n-text>
|
||||
<!-- 播放量 -->
|
||||
<n-text v-if="type === 'dj' && item.playCount" class="count hidden" depth="3">
|
||||
{{ item.playCount }}次
|
||||
</n-text>
|
||||
<!-- 时长 -->
|
||||
<n-text v-if="item.duration" class="duration" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-if="item.duration" class="duration hidden" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-else class="duration"> -- </n-text>
|
||||
<!-- 大小 -->
|
||||
<n-text v-if="item.size" class="size" depth="3">{{ item.size }}M</n-text>
|
||||
<n-text v-if="item.size" class="size hidden" depth="3">{{ item.size }}M</n-text>
|
||||
</n-card>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
@@ -172,7 +210,21 @@
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<SongListDropdown ref="songListDropdownRef" @playSong="playSong" />
|
||||
<SongListDropdown
|
||||
ref="songListDropdownRef"
|
||||
@playSong="playSong"
|
||||
@delCloudSong="delCloudSong"
|
||||
@deletePlaylistSong="deletePlaylistSong"
|
||||
@delLocalSong="delLocalSong"
|
||||
/>
|
||||
<!-- 移动端菜单 -->
|
||||
<SongListDrawer
|
||||
ref="songListDrawerRef"
|
||||
@playSong="playSong"
|
||||
@delCloudSong="delCloudSong"
|
||||
@deletePlaylistSong="deletePlaylistSong"
|
||||
@delLocalSong="delLocalSong"
|
||||
/>
|
||||
<!-- 定位歌曲 -->
|
||||
<Transition name="shrink" mode="out-in">
|
||||
<n-card
|
||||
@@ -200,30 +252,46 @@
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<!-- 错误 -->
|
||||
<n-empty
|
||||
v-else-if="data === 'error' || data?.[0] === 'error'"
|
||||
description="列表获取出错,请重试"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<!-- 加载动画 -->
|
||||
<n-spin v-else class="loading" size="small">
|
||||
<template #description> 加载中 </template>
|
||||
</n-spin>
|
||||
<div v-else class="loading">
|
||||
<n-skeleton :repeat="10" text />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings, musicData } from "@/stores";
|
||||
import { setCloudDel } from "@/api/cloud";
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { siteData, siteSettings, musicData, siteStatus } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, addSongToNext } from "@/utils/Player";
|
||||
import { djFormatDate } from "@/utils/timeTools";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const dataStore = siteData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(dataStore);
|
||||
const { loadSize } = storeToRefs(settings);
|
||||
const { playList, playIndex, playSongData, playSongSource, playHeartbeatMode, playMode } =
|
||||
storeToRefs(music);
|
||||
const { loadSize, playSearch, useMusicCache } = storeToRefs(settings);
|
||||
const { playList, playSongData, playSongSource } = storeToRefs(music);
|
||||
const { playIndex, playMode, playHeartbeatMode, playLoading } = storeToRefs(status);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const props = defineProps({
|
||||
// 列表类型
|
||||
type: {
|
||||
type: String,
|
||||
default: "song",
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
type: [Array, String],
|
||||
@@ -264,7 +332,8 @@ const props = defineProps({
|
||||
// 分页数据
|
||||
const pageNumber = ref(1);
|
||||
|
||||
// 右键菜单
|
||||
// 子组件
|
||||
const songListDrawerRef = ref(null);
|
||||
const songListDropdownRef = ref(null);
|
||||
|
||||
// 当前索引
|
||||
@@ -291,8 +360,13 @@ const checkHasPlaying = (isScoll = null) => {
|
||||
// 播放歌曲
|
||||
const playSong = async (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
// 若开启了缓存且正在加载
|
||||
if (useMusicCache.value && playLoading.value) {
|
||||
$message.warning("歌曲正在缓冲中,请稍后");
|
||||
return false;
|
||||
}
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
playMode.value = props.type === "song" ? "normal" : "dj";
|
||||
// 检查当前页面
|
||||
const isPage = router.currentRoute.value.matched?.[0].path || null;
|
||||
// 是否关闭心动模式
|
||||
@@ -303,7 +377,12 @@ const playSong = async (data, song, index) => {
|
||||
fadePlayOrPause();
|
||||
} else {
|
||||
// 若为特殊状态
|
||||
if (isPage === "/search" || isPage === "/history" || playHeartbeatMode.value) {
|
||||
if (
|
||||
(isPage === "/search" && !playSearch.value) ||
|
||||
isPage === "/history" ||
|
||||
playHeartbeatMode.value
|
||||
) {
|
||||
console.log("仅播放当前歌曲");
|
||||
addSongToNext(song, true);
|
||||
} else {
|
||||
// 添加播放列表
|
||||
@@ -314,7 +393,7 @@ const playSong = async (data, song, index) => {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
// 附加来源
|
||||
playSongSource.value = Number(props.sourceId);
|
||||
@@ -332,6 +411,72 @@ const pageNumberChange = (page) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否可执行双击
|
||||
const checkCanClick = (data, item, index) => {
|
||||
if (window.innerWidth > 700) return false;
|
||||
playSong(data, item, index);
|
||||
};
|
||||
|
||||
// 云盘歌曲删除
|
||||
const delCloudSong = (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从云盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await setCloudDel(song.id);
|
||||
if (result.code == 200) {
|
||||
data.splice(index, 1);
|
||||
$message.success("删除成功");
|
||||
} else {
|
||||
$message.error("删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌单歌曲删除
|
||||
const deletePlaylistSong = (pid, song, data, index) => {
|
||||
if (!pid || !song) return $message.error("无法正确定位到歌单,请重试");
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await addSongToPlayList(pid, song?.id, "del");
|
||||
if (result.status === 200) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 本地歌曲删除
|
||||
const delLocalSong = (data, song, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从本地磁盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
console.log(data, song, index);
|
||||
const result = await electron.ipcRenderer.invoke("deleteFile", song?.path);
|
||||
if (result) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听歌曲变化
|
||||
watch(
|
||||
() => music.getPlaySongData?.id,
|
||||
@@ -371,8 +516,20 @@ onBeforeUnmount(() => {
|
||||
.has-cover {
|
||||
margin-right: 66px;
|
||||
}
|
||||
.duration {
|
||||
.control {
|
||||
width: 40px;
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-right: auto;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.size {
|
||||
@@ -413,8 +570,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.num,
|
||||
.play {
|
||||
.num {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
@@ -518,9 +674,20 @@ onBeforeUnmount(() => {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.more {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 40px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.size {
|
||||
@@ -532,7 +699,7 @@ onBeforeUnmount(() => {
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.play {
|
||||
.num {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
@@ -582,12 +749,49 @@ onBeforeUnmount(() => {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.song-list-header,
|
||||
.songs {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.songs {
|
||||
.num {
|
||||
font-size: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
}
|
||||
.info {
|
||||
.title {
|
||||
.name {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.artist {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
width: 60px;
|
||||
justify-content: flex-end;
|
||||
.more {
|
||||
display: inline-block;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
margin: 60px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
:deep(.n-skeleton) {
|
||||
&:nth-of-type(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
height: 80px;
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
323
src/components/List/SongListDrawer.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<!-- 歌曲列表 - 移动端菜单 -->
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="drawerShow"
|
||||
:auto-focus="false"
|
||||
height="calc(100vh - 200px)"
|
||||
placement="bottom"
|
||||
class="song-list-drawer"
|
||||
@after-leave="drawerShow = false"
|
||||
@mask-click="drawerShow = false"
|
||||
>
|
||||
<n-drawer-content :native-scrollbar="false" :body-content-style="{ padding: 0 }" closable>
|
||||
<template #header>
|
||||
<div v-if="!songData?.path" class="song-data">
|
||||
<n-image
|
||||
:src="songData?.coverSize?.s || songData?.cover"
|
||||
class="cover"
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="song-detail">
|
||||
<n-text class="name">{{ songData?.name || "未知曲目" }}</n-text>
|
||||
<template v-if="songType === 'song'">
|
||||
<div v-if="songData?.artists && Array.isArray(songData.artists)" class="all-ar">
|
||||
<n-text v-for="ar in songData.artists" :key="ar.id" class="ar" depth="3">
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<n-text class="ar" depth="3">
|
||||
{{ songData.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<n-text v-else class="ar">
|
||||
{{ songData?.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<n-text v-else>更多操作</n-text>
|
||||
</template>
|
||||
<div class="all-menu">
|
||||
<div
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('playSong', playlistData, songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="play" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 立即播放 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && playMode !== 'dj' && music.getPlaySongData?.id !== songData.id && !isFm"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
playMode = 'normal';
|
||||
addSongToNext(songData);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="play-next" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 下一首播放 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
addPlaylistRef?.openAddToPlaylist(songData?.id);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="playlist-add" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 添加到歌单 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
router.push({
|
||||
path: '/comment',
|
||||
query: {
|
||||
id: songData.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="20">
|
||||
<SvgIcon icon="comment-text" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 查看评论 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && isHasMv"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
router.push({
|
||||
path: '/videos-player',
|
||||
query: {
|
||||
id: songData.mv,
|
||||
},
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="video" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 观看 MV </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isCloud && isUserPlaylist"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('deletePlaylistSong', songSourceId, songData, playlistData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 从歌单中删除 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('delCloudSong', playlistData, songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 从云盘中删除 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
cloudSongMatchRef?.openCloudSongMatch(songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="edit" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 云盘歌曲纠正 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
downloadSongRef?.openDownloadModal(songData);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="download" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 下载歌曲 </n-text>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<!-- 添加到歌单 -->
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
<!-- 下载歌曲 -->
|
||||
<DownloadSong ref="downloadSongRef" />
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<CloudSongMatch ref="cloudSongMatchRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { addSongToNext } from "@/utils/Player";
|
||||
import { musicData, siteData, siteStatus } from "@/stores";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { playMode } = storeToRefs(status);
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
const emit = defineEmits(["playSong", "delCloudSong", "deletePlaylistSong", "delLocalSong"]);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
const downloadSongRef = ref(null);
|
||||
const cloudSongMatchRef = ref(null);
|
||||
|
||||
// 菜单数据
|
||||
const drawerShow = ref(false);
|
||||
const songType = ref("song");
|
||||
const songData = ref(null);
|
||||
const songIndex = ref(null);
|
||||
const songSourceId = ref(null);
|
||||
const playlistData = ref(null);
|
||||
|
||||
// 歌曲状态
|
||||
const isFm = computed(() => playMode.value === "fm");
|
||||
const isSong = computed(() => songType.value === "song");
|
||||
const isLocalSong = computed(() => !!songData.value?.path);
|
||||
const isHasMv = computed(() => !!songData.value?.mv && songData.value.mv !== 0);
|
||||
const isCloud = computed(() => router.currentRoute.value.name === "cloud");
|
||||
const isUserPlaylist = computed(() => {
|
||||
// 用户 id
|
||||
const userId = userData.value?.userId;
|
||||
// 用户歌单
|
||||
const userPlaylistsData = userLikeData.value.playlists?.filter(
|
||||
(playlist) => playlist.userId === userId,
|
||||
);
|
||||
return songSourceId.value !== 0 && userPlaylistsData.some((pl) => pl.id == songSourceId.value);
|
||||
});
|
||||
|
||||
// 开启菜单
|
||||
const drawerOpen = (data, song, index, sourceId, type) => {
|
||||
console.log(song, type);
|
||||
drawerShow.value = true;
|
||||
songData.value = song;
|
||||
songType.value = type;
|
||||
songIndex.value = index;
|
||||
songSourceId.value = sourceId;
|
||||
playlistData.value = data;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
drawerOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.song-data {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.cover {
|
||||
margin-right: 12px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.song-detail {
|
||||
.name {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.all-ar {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.all-menu {
|
||||
.menu-item {
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.n-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.name {
|
||||
transform: translateY(1px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.song-list-drawer {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -24,19 +24,18 @@
|
||||
<script setup>
|
||||
import { NIcon, NImage, NText } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteData } from "@/stores";
|
||||
import { musicData, siteData, siteStatus } from "@/stores";
|
||||
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"]);
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const emit = defineEmits(["playSong", "delCloudSong", "deletePlaylistSong", "delLocalSong"]);
|
||||
const data = siteData();
|
||||
const { playSongData, playMode } = storeToRefs(music);
|
||||
const music = musicData();
|
||||
const router = useRouter();
|
||||
const status = siteStatus();
|
||||
const { playMode } = storeToRefs(status);
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
|
||||
// 右键菜单数据
|
||||
@@ -64,7 +63,7 @@ const renderIcon = (icon, size, translate = 0) => {
|
||||
};
|
||||
|
||||
// 歌曲信息
|
||||
const renderSong = (song) => {
|
||||
const renderSong = (song, isSong) => {
|
||||
return () =>
|
||||
h(
|
||||
"div",
|
||||
@@ -72,29 +71,35 @@ const renderSong = (song) => {
|
||||
className: "song-data",
|
||||
},
|
||||
[
|
||||
h(NImage, { src: song?.coverSize?.s || song?.cover, class: "cover" }),
|
||||
h(NImage, {
|
||||
src: song?.coverSize?.s || song?.cover,
|
||||
class: "cover",
|
||||
previewDisabled: true,
|
||||
}),
|
||||
h("div", { class: "song-detail" }, [
|
||||
h(NText, { class: "name" }, () => [song?.name || "未知曲目"]),
|
||||
song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
),
|
||||
isSong
|
||||
? song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
)
|
||||
: h(NText, { class: "ar", depth: 3 }, () => ["电台节目"]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 打开右键菜单
|
||||
const openDropdown = (e, data, song, index, sourceId) => {
|
||||
const openDropdown = (e, data, song, index, sourceId, type) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
dropdownShow.value = false;
|
||||
@@ -106,7 +111,9 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
);
|
||||
// 当前状态
|
||||
const isFm = playMode.value === "fm";
|
||||
const isLocalSong = song?.path ? true : false;
|
||||
const isSong = type === "song";
|
||||
const isLocalSong = !!song?.path;
|
||||
const isHasMv = !!song?.mv && song.mv !== 0;
|
||||
const isCloud = router.currentRoute.value.name === "cloud";
|
||||
const isUserPlaylist = sourceId !== 0 && userPlaylistsData.some((pl) => pl.id == sourceId);
|
||||
// 生成菜单
|
||||
@@ -117,7 +124,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
key: "song-data",
|
||||
type: "render",
|
||||
show: !isLocalSong,
|
||||
render: renderSong(song),
|
||||
render: renderSong(song, isSong),
|
||||
},
|
||||
{
|
||||
key: "line-song",
|
||||
@@ -137,9 +144,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "next-play",
|
||||
label: "下一首播放",
|
||||
show: playSongData.value?.id !== song.id && !isFm,
|
||||
show: isSong && playMode.value !== "dj" && music.getPlaySongData?.id !== song.id && !isFm,
|
||||
props: {
|
||||
onClick: () => {
|
||||
playMode.value = "normal";
|
||||
addSongToNext(song);
|
||||
},
|
||||
},
|
||||
@@ -148,7 +156,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "add-pl",
|
||||
label: "添加到歌单",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
addPlaylistRef.value?.openAddToPlaylist(song?.id);
|
||||
@@ -159,7 +167,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "comment",
|
||||
label: "查看评论",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -175,7 +183,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "mv",
|
||||
label: "观看 MV",
|
||||
show: song.mv && song.mv !== 0 ? true : false,
|
||||
show: isSong && isHasMv,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -196,7 +204,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
children: [
|
||||
{
|
||||
key: "copy",
|
||||
label: "复制歌曲 ID",
|
||||
label: `复制${isSong ? "歌曲" : "节目"} ID`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const songId = song?.id?.toString();
|
||||
@@ -207,11 +215,13 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
label: "分享歌曲链接",
|
||||
label: `分享${isSong ? "歌曲" : "节目"}链接`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const shareUrl = `https://music.163.com/song?id=${song?.id?.toString()}`;
|
||||
copyData(shareUrl, "复制歌曲链接");
|
||||
const shareUrl = isSong
|
||||
? `https://music.163.com/song?id=${song?.id?.toString()}`
|
||||
: `https://music.163.com/#/dj?id=${song?.id?.toString()}`;
|
||||
copyData(shareUrl, `复制${isSong ? "歌曲" : "节目"}链接`);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("share"),
|
||||
@@ -230,7 +240,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
show: !isCloud && isUserPlaylist,
|
||||
props: {
|
||||
onClick: () => {
|
||||
deletePlaylistSong(sourceId, song, data, index);
|
||||
emit("deletePlaylistSong", sourceId, song, data, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
@@ -252,7 +262,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
show: isCloud,
|
||||
props: {
|
||||
onClick: () => {
|
||||
delCloudSong(data, song, index);
|
||||
emit("delCloudSong", data, song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
@@ -271,10 +281,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "delete",
|
||||
label: "从本地磁盘中删除",
|
||||
show: isLocalSong,
|
||||
show: isLocalSong && music.getPlaySongData?.id !== song.id,
|
||||
props: {
|
||||
onClick: () => {
|
||||
delLocalSong(data, song, index);
|
||||
emit("delLocalSong", data, song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
@@ -301,7 +311,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "download",
|
||||
label: "下载歌曲",
|
||||
show: !isLocalSong,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
downloadSongRef.value?.openDownloadModal(song);
|
||||
@@ -321,65 +331,6 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 云盘歌曲删除
|
||||
const delCloudSong = (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从云盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await setCloudDel(song.id);
|
||||
if (result.code == 200) {
|
||||
data.splice(index, 1);
|
||||
$message.success("删除成功");
|
||||
} else {
|
||||
$message.error("删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌单歌曲删除
|
||||
const deletePlaylistSong = (pid, song, data, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await addSongToPlayList(pid, song?.id, "del");
|
||||
if (result.status === 200) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 本地歌曲删除
|
||||
const delLocalSong = (data, song, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从本地磁盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
console.log(data, song, index);
|
||||
const result = await electron.ipcRenderer.invoke("deleteFile", song?.path);
|
||||
if (result) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openDropdown,
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
>
|
||||
<template #prefix>
|
||||
<n-image
|
||||
:src="item.coverImgUrl.replace(/^http:/, 'https:') + '?param=100y100'"
|
||||
:src="item?.coverSize?.s || '/images/pic/album.jpg?assest'"
|
||||
class="cover"
|
||||
preview-disabled
|
||||
lazy
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
<n-thing :title="item.name">
|
||||
<template #description>
|
||||
<n-text depth="3" class="size">{{ item.trackCount }} 首音乐</n-text>
|
||||
<n-text depth="3" class="size">{{ item.count }} 首音乐</n-text>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
@@ -87,7 +87,7 @@ const addToPlayList = async (pid, tracks, index) => {
|
||||
$message.success("添加歌曲至歌单成功");
|
||||
if (index === 0) await data.setUserLikeSongs();
|
||||
} else {
|
||||
$message.error("添加失败,请重试");
|
||||
$message.error(result?.message || "添加失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</n-card>
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeCloudSongMatch"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!cloudMatchValue.asid"
|
||||
@@ -87,7 +87,7 @@
|
||||
>
|
||||
纠正
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -150,7 +150,7 @@ const setCloudSongMatchBtn = async (data) => {
|
||||
allCloudSongs[cloudMatchIndex.value] = JSON.parse(JSON.stringify(cloudMatchSongData.value));
|
||||
await indexedDB.setfilesDB("userCloudList", allCloudSongs.slice());
|
||||
// 刷新列表
|
||||
if (typeof $refreshCloudList !== "undefined") $refreshCloudList();
|
||||
if (typeof $refreshCloudCatch !== "undefined") $refreshCloudCatch();
|
||||
} catch (error) {
|
||||
console.error("更改云盘列表时出错:", error);
|
||||
$message.error("更改云盘列表时出错,请刷新后重试");
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- 隐私歌单 -->
|
||||
<n-checkbox v-model:checked="createPrivacy"> 设为隐私歌单 </n-checkbox>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeCreatePlaylist"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!createName"
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
新建
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
当前为云盘歌曲,下载的文件均为最高音质
|
||||
</n-alert>
|
||||
<n-radio-group v-model:value="downloadChoose" class="download-group" name="downloadGroup">
|
||||
<n-space vertical>
|
||||
<n-flex vertical>
|
||||
<n-radio
|
||||
v-for="item in downloadLevel"
|
||||
:key="item"
|
||||
@@ -32,23 +32,24 @@
|
||||
</n-text>
|
||||
</div>
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
<n-text v-else>歌曲信息获取中</n-text>
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeDownloadModal"> 关闭 </n-button>
|
||||
<n-button
|
||||
:disabled="!downloadChoose"
|
||||
:loading="downloadStatus"
|
||||
:focusable="false"
|
||||
type="primary"
|
||||
@click="toSongDownload(songData, downloadChoose)"
|
||||
@click="toSongDownload(songData, lyricData, downloadChoose)"
|
||||
>
|
||||
下载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -58,19 +59,20 @@ import { storeToRefs } from "pinia";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getSongDetail, getSongDownload } from "@/api/song";
|
||||
import { downloadFile } from "@/utils/helper";
|
||||
import { getSongDetail, getSongDownload, getSongLyric } from "@/api/song";
|
||||
import { downloadFile, checkPlatform } from "@/utils/helper";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { downloadPath } = storeToRefs(settings);
|
||||
const { downloadPath, downloadMeta, downloadCover, downloadLyrics } = storeToRefs(settings);
|
||||
|
||||
// 歌曲下载数据
|
||||
const songId = ref(null);
|
||||
const songData = ref(null);
|
||||
const lyricData = ref(null);
|
||||
const downloadStatus = ref(false);
|
||||
const downloadSongShow = ref(false);
|
||||
const downloadChoose = ref(null);
|
||||
@@ -79,11 +81,13 @@ const downloadLevel = ref(null);
|
||||
// 获取歌曲详情
|
||||
const getMusicDetailData = async (id) => {
|
||||
try {
|
||||
const result = await getSongDetail(id);
|
||||
const songResult = await getSongDetail(id);
|
||||
const lyricResult = await getSongLyric(id);
|
||||
// 获取歌曲详情
|
||||
songData.value = formatData(result?.songs?.[0], "song")[0];
|
||||
songData.value = formatData(songResult?.songs?.[0], "song")[0];
|
||||
lyricData.value = lyricResult?.lrc?.lyric || null;
|
||||
// 生成音质列表
|
||||
generateLists(result);
|
||||
generateLists(songResult);
|
||||
} catch (error) {
|
||||
closeDownloadModal();
|
||||
console.error("歌曲信息获取失败:", error);
|
||||
@@ -91,26 +95,42 @@ const getMusicDetailData = async (id) => {
|
||||
};
|
||||
|
||||
// 歌曲下载
|
||||
const toSongDownload = async (song, br) => {
|
||||
console.log(song, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
const toSongDownload = async (song, lyric, br) => {
|
||||
try {
|
||||
console.log(song, lyric, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value && checkPlatform.electron()) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
if (!result.data?.url) {
|
||||
downloadStatus.value = false;
|
||||
return $message.error("下载失败,请重试");
|
||||
}
|
||||
// 获取下载结果
|
||||
const isDownloaded = await downloadFile(result.data, song, lyric, {
|
||||
path: downloadPath.value,
|
||||
downloadMeta: downloadMeta.value,
|
||||
downloadCover: downloadCover.value,
|
||||
downloadLyrics: downloadLyrics.value,
|
||||
});
|
||||
}
|
||||
const isDownloaded = await downloadFile(result.data, song, downloadPath.value);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
console.log(lyric);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("歌曲下载出错:", error);
|
||||
$message.error("歌曲下载失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="loginModalShow"
|
||||
style="width: 400px"
|
||||
class="login"
|
||||
:auto-focus="false"
|
||||
:mask-closable="false"
|
||||
:bordered="false"
|
||||
:close-on-esc="false"
|
||||
:closable="false"
|
||||
style="width: 400px"
|
||||
preset="card"
|
||||
transform-origin="center"
|
||||
>
|
||||
<div class="login-content">
|
||||
<div class="title">
|
||||
<img class="logo" src="/images/logo/favicon.png?asset" alt="logo" />
|
||||
<img class="logo" src="/images/icons/favicon.png?asset" alt="logo" />
|
||||
</div>
|
||||
<!-- 登录方式 -->
|
||||
<n-tabs class="login-tabs" default-value="login-qr" type="segment" animated>
|
||||
@@ -27,6 +26,7 @@
|
||||
</n-tabs>
|
||||
<!-- 关闭登录弹窗 -->
|
||||
<n-button
|
||||
:focusable="false"
|
||||
class="close"
|
||||
strong
|
||||
secondary
|
||||
@@ -45,12 +45,15 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getLoginState, refreshLogin } from "@/api/login";
|
||||
import { setCookies, toLogout, isLogin } from "@/utils/auth";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { autoSignIn } = storeToRefs(settings);
|
||||
|
||||
// 登录数据
|
||||
const loginModalShow = ref(false);
|
||||
@@ -83,6 +86,9 @@ const setLoginData = async (loginData) => {
|
||||
setCookies(loginData.cookie);
|
||||
// 获取用户信息
|
||||
await data.setUserProfile();
|
||||
await data.setDailySongsData();
|
||||
// 签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 更改状态
|
||||
data.userLoginStatus = true;
|
||||
$message.success("登录成功");
|
||||
|
||||
@@ -104,36 +104,39 @@ const getCaptcha = (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("登录出错,请重试");
|
||||
try {
|
||||
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("请检查你的输入");
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
phoneFormData.value.captcha = null;
|
||||
console.error("登录出错:", error);
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
<template>
|
||||
<div class="login-qr">
|
||||
<div class="qr-img">
|
||||
<n-skeleton v-if="!qrImg" class="qr" />
|
||||
<QrcodeVue
|
||||
v-else
|
||||
:class="['qr', qrStatusCode === 802 ? 'hidden' : null]"
|
||||
:value="qrImg"
|
||||
:size="180"
|
||||
:margin="4"
|
||||
level="L"
|
||||
foreground="#000"
|
||||
background="#fff"
|
||||
/>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-qr-code
|
||||
v-if="qrImg"
|
||||
:value="qrImg"
|
||||
:class="['qr', qrStatusCode === 802 ? 'hidden' : null]"
|
||||
:size="156"
|
||||
:icon-size="30"
|
||||
icon-src="/images/icons/favicon.png?asset"
|
||||
error-correction-level="H"
|
||||
/>
|
||||
<n-skeleton v-else class="qr" />
|
||||
</Transition>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="qrStatusCode === 802" class="refresh" @click="getQrData">
|
||||
<n-icon size="22">
|
||||
@@ -28,7 +29,6 @@
|
||||
|
||||
<script setup>
|
||||
import { getQrKey, checkQr } from "@/api/login";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
|
||||
const emit = defineEmits(["setLoginData"]);
|
||||
|
||||
@@ -133,6 +133,7 @@ onBeforeUnmount(() => {
|
||||
min-width: 180px;
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.3s;
|
||||
&.hidden {
|
||||
opacity: 0.2;
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeUpdateModal"> 取消 </n-button>
|
||||
<n-button type="primary" @click="toUpdatePlayList"> 编辑 </n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -2,38 +2,62 @@
|
||||
<template>
|
||||
<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" />
|
||||
<div :class="['logo', asideMenuCollapsed ? 'collapsed' : null]" @click="router.push('/')">
|
||||
<!-- <n-avatar class="logo-img" src="/images/icons/favicon.png?asset" /> -->
|
||||
<n-icon class="logo-img" size="30">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 1024 1024"
|
||||
>
|
||||
<path
|
||||
d="M511.764091 131.708086a446.145957 446.145957 0 1 0 446.145957 446.145957 446.145957 446.145957 0 0 0-446.145957-446.145957z m0 519.76004A71.829499 71.829499 0 1 1 583.59359 580.530919 72.275645 72.275645 0 0 1 511.764091 651.468126z"
|
||||
:fill="themeAutoCover ? 'var(--main-second-color)' : '#F55E55'"
|
||||
/>
|
||||
<path
|
||||
d="M802.205109 0.541175l-168.197026 37.030114a67.814185 67.814185 0 0 0-53.091369 66.029602V223.614153l3.569168 349.778431h114.213365V223.614153h108.859613a26.322611 26.322611 0 0 0 26.768758-26.322611V26.863786a26.768757 26.768757 0 0 0-32.122509-26.322611z"
|
||||
:fill="themeAutoCover ? 'var(--main-color)' : '#F9BBB8'"
|
||||
/>
|
||||
<path
|
||||
d="M511.764091 386.457428a186.935156 186.935156 0 1 0 186.935156 186.48901A186.935156 186.935156 0 0 0 511.764091 386.457428z m0 264.564552a71.383353 71.383353 0 1 1 71.383353-71.383353 71.383353 71.383353 0 0 1-71.383353 71.383353z"
|
||||
:fill="themeAutoCover ? 'var(--main-color)' : '#F9BBB8'"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-text v-if="!status.asideMenuCollapsed && showSider" class="site-name">
|
||||
<n-text v-if="!asideMenuCollapsed && showSider" class="site-name">
|
||||
{{ siteName }}
|
||||
</n-text>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<n-button class="nav-icon" quaternary @click="router.go(-1)">
|
||||
<n-flex :class="['navigation', { hidden: searchInputFocus }]" :size="6">
|
||||
<n-button :focusable="false" 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)">
|
||||
<n-button :focusable="false" class="nav-icon" quaternary @click="router.go(1)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="chevron-right" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</n-flex>
|
||||
<!-- 搜索框 -->
|
||||
<SearchInp />
|
||||
<!-- GitHub -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-button v-if="showGithub" class="github" circle quaternary @click="openGithub">
|
||||
<n-button
|
||||
v-if="showGithub"
|
||||
:focusable="false"
|
||||
class="github"
|
||||
circle
|
||||
quaternary
|
||||
@click="openGithub"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="20">
|
||||
<SvgIcon icon="github" />
|
||||
@@ -45,7 +69,6 @@
|
||||
<div class="right">
|
||||
<!-- 全局菜单 -->
|
||||
<n-dropdown
|
||||
v-if="!showSider"
|
||||
:show="mainMenuShow"
|
||||
:show-arrow="true"
|
||||
:options="mainMenuOptions"
|
||||
@@ -54,7 +77,7 @@
|
||||
>
|
||||
<n-button
|
||||
:style="{ pointerEvents: mainMenuShow ? 'none' : 'auto' }"
|
||||
class="main-menu"
|
||||
:class="['main-menu', { show: !showSider }]"
|
||||
secondary
|
||||
strong
|
||||
round
|
||||
@@ -70,7 +93,7 @@
|
||||
<!-- 用户信息 -->
|
||||
<userData />
|
||||
<!-- TitleBar -->
|
||||
<TitleBar v-if="checkPlatform.electron()" />
|
||||
<TitleBar v-if="isElectron" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -87,7 +110,8 @@ import packageJson from "@/../package.json";
|
||||
const router = useRouter();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { showGithub, showSider } = storeToRefs(settings);
|
||||
const { asideMenuCollapsed, searchInputFocus } = storeToRefs(status);
|
||||
const { showGithub, showSider, themeAutoCover } = storeToRefs(settings);
|
||||
|
||||
// 站点信息
|
||||
const siteName = import.meta.env.RENDERER_VITE_SITE_TITLE;
|
||||
@@ -98,6 +122,11 @@ const openGithub = () => {
|
||||
window.open(packageJson.github);
|
||||
};
|
||||
|
||||
// 是否为 Electron
|
||||
const isElectron = computed(() => {
|
||||
return checkPlatform.electron() || typeof electron !== "undefined";
|
||||
});
|
||||
|
||||
// 主菜单渲染
|
||||
const mainMenuShow = ref(false);
|
||||
const mainMenuOptions = computed(() => [
|
||||
@@ -142,13 +171,13 @@ const mainMenuOptions = computed(() => [
|
||||
width 0.3s,
|
||||
padding-left 0.3s;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: pointer;
|
||||
.logo-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
background-color: transparent;
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
@@ -167,18 +196,32 @@ const mainMenuOptions = computed(() => [
|
||||
}
|
||||
}
|
||||
.navigation {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
height: 34px;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
transition:
|
||||
width 0.3s,
|
||||
min-width 0.3s,
|
||||
opacity 0.3s;
|
||||
overflow: hidden;
|
||||
-webkit-app-region: no-drag;
|
||||
.nav-icon {
|
||||
border-radius: 8px;
|
||||
padding: 0 8px;
|
||||
&:first-child {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.n-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
width: 0px;
|
||||
min-width: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.github {
|
||||
margin-left: 12px;
|
||||
@@ -187,7 +230,15 @@ const mainMenuOptions = computed(() => [
|
||||
.main-menu {
|
||||
-webkit-app-region: no-drag;
|
||||
margin-right: 12px;
|
||||
display: none;
|
||||
&.show {
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-sider {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
@@ -201,5 +252,25 @@ const mainMenuOptions = computed(() => [
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.left {
|
||||
.logo {
|
||||
width: auto;
|
||||
padding-left: 0;
|
||||
margin-right: 12px;
|
||||
.site-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.left {
|
||||
width: 100%;
|
||||
}
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useRouter } from "vue-router";
|
||||
import { NIcon, NText, NNumberAnimation, NButton } from "naive-ui";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
|
||||
const data = siteData();
|
||||
const router = useRouter();
|
||||
@@ -61,9 +62,6 @@ const userMenuShow = ref(false);
|
||||
// 登录弹窗
|
||||
const loginRef = ref(null);
|
||||
|
||||
// 是否签到
|
||||
const signInStatus = ref(false);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon) => {
|
||||
return () => h(NIcon, null, () => h(SvgIcon, { icon }));
|
||||
@@ -71,14 +69,28 @@ const renderIcon = (icon) => {
|
||||
|
||||
// 数量统计模块
|
||||
const createUserNumber = (num, text, duration = 1000) => {
|
||||
return h("div", { className: "user-pl" }, [
|
||||
h(NNumberAnimation, { from: 0, to: num, duration }),
|
||||
h(NText, { depth: 3, style: { fontSize: "12px" } }, () => [text]),
|
||||
]);
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "user-pl",
|
||||
onclick: () => {
|
||||
userMenuShow.value = false;
|
||||
router.push(
|
||||
`/like/${text === "歌单" ? "playlists?" : text === "专辑" ? "albums" : "artists"}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
[
|
||||
h(NNumberAnimation, { from: 0, to: num, duration }),
|
||||
h(NText, { depth: 3, style: { fontSize: "12px" } }, () => [text]),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 生成导航栏用户信息
|
||||
const createUserData = () => {
|
||||
// 是否签到
|
||||
const signInStatus = sessionStorage.getItem("lastSignInDate") ? true : false;
|
||||
return h(
|
||||
"div",
|
||||
{ className: "nav-user-data" },
|
||||
@@ -96,12 +108,14 @@ const createUserData = () => {
|
||||
NButton,
|
||||
{
|
||||
round: true,
|
||||
renderIcon: renderIcon(signInStatus.value ? "calendar-check" : "calendar-badge"),
|
||||
onclick: () => {
|
||||
$message.warning("施工中( 新建文件夹 )");
|
||||
renderIcon: renderIcon(signInStatus ? "calendar-check" : "calendar-badge"),
|
||||
disabled: signInStatus,
|
||||
onclick: async () => {
|
||||
userMenuShow.value = false;
|
||||
await userSignIn();
|
||||
},
|
||||
},
|
||||
() => [signInStatus.value ? "Lv." + userData.value.detail?.level || 1 : "立即签到"],
|
||||
() => [signInStatus ? "今日已签到" : "立即签到"],
|
||||
),
|
||||
]),
|
||||
]
|
||||
@@ -205,6 +219,15 @@ const userMenuSelect = (key) => {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
padding: 0;
|
||||
.avatar {
|
||||
margin: 0;
|
||||
}
|
||||
.user-data {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -219,6 +242,7 @@ const userMenuSelect = (key) => {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.user-pl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -91,6 +91,14 @@ const pointOpacity = (index) => {
|
||||
margin-right: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--cover-main-color);
|
||||
@media (max-width: 900px) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes breathe {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
}"
|
||||
class="full-player"
|
||||
@mousemove="controlShowChange"
|
||||
@mouseleave="closePlayerControlShow"
|
||||
>
|
||||
<!-- 遮罩 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
@@ -48,13 +49,18 @@
|
||||
<div v-show="playerControlShow" class="menu">
|
||||
<div class="left">
|
||||
<!-- 歌词模式 -->
|
||||
<div v-if="isHasLrc" class="n-icon" @click="pureLyricMode = !pureLyricMode">
|
||||
<n-text>词</n-text>
|
||||
</div>
|
||||
<n-icon
|
||||
v-if="isHasLrc"
|
||||
:class="['lrc-open', { open: pureLyricMode }]"
|
||||
size="28"
|
||||
@click="pureLyricMode = !pureLyricMode"
|
||||
>
|
||||
<SvgIcon icon="lrc-text" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div class="right">
|
||||
<!-- 全屏切换 -->
|
||||
<n-icon @click.stop="screenfullChange">
|
||||
<n-icon class="hidden" @click.stop="screenfullChange">
|
||||
<SvgIcon
|
||||
:icon="screenfullStatus ? 'fullscreen-exit-rounded' : 'fullscreen-rounded'"
|
||||
/>
|
||||
@@ -76,7 +82,7 @@
|
||||
<!-- 封面 -->
|
||||
<PlayerCover />
|
||||
<!-- 信息 -->
|
||||
<div v-if="playCoverType === 'cover' || !isHasLrc" :class="['data', playCoverType]">
|
||||
<div v-show="playCoverType === 'cover' || !isHasLrc" :class="['data', playCoverType]">
|
||||
<div class="desc">
|
||||
<div class="title">
|
||||
<span class="name">{{ music.getPlaySongData.name || "未知曲目" }}</span>
|
||||
@@ -225,7 +231,9 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 控制中心 -->
|
||||
<PlayerControl v-show="playerControlShow" />
|
||||
<PlayerControl />
|
||||
<!-- 音乐频谱 -->
|
||||
<Spectrum v-if="showSpectrums" :show="!playerControlShow" :height="60" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -242,7 +250,7 @@ const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { playList, playSongLyric } = storeToRefs(music);
|
||||
const { playerBackgroundType, showYrc, playCoverType } = storeToRefs(settings);
|
||||
const { playerBackgroundType, showYrc, playCoverType, showSpectrums } = storeToRefs(settings);
|
||||
const {
|
||||
playerControlShow,
|
||||
controlTimeOut,
|
||||
@@ -269,9 +277,16 @@ const screenfullChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭控制中心
|
||||
const closePlayerControlShow = () => {
|
||||
if (window.innerWidth <= 700) return false;
|
||||
playerControlShow.value = false;
|
||||
};
|
||||
|
||||
// 控制中心显隐
|
||||
const controlShowChange = throttle(() => {
|
||||
playerControlShow.value = true;
|
||||
if (window.innerWidth <= 700) return false;
|
||||
if (controlTimeOut.value) {
|
||||
clearTimeout(controlTimeOut.value);
|
||||
}
|
||||
@@ -396,17 +411,6 @@ 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;
|
||||
@@ -431,6 +435,17 @@ onUnmounted(() => {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
.n-icon {
|
||||
margin-left: 0;
|
||||
&.lrc-open {
|
||||
&.open {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-player {
|
||||
display: flex;
|
||||
@@ -652,6 +667,41 @@ onUnmounted(() => {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.menu {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.main-player {
|
||||
.content {
|
||||
width: 100%;
|
||||
.data {
|
||||
display: block !important;
|
||||
&.record {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
&.no-lrc {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: none;
|
||||
.data {
|
||||
.name {
|
||||
font-size: 24px;
|
||||
.name-alias {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.other {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 局外样式
|
||||
.title-tip {
|
||||
|
||||
@@ -148,8 +148,8 @@ const props = defineProps({
|
||||
const music = musicData();
|
||||
const settings = siteSettings();
|
||||
const status = siteStatus();
|
||||
const { playSeek, pureLyricMode } = storeToRefs(status);
|
||||
const { playSongLyric, playSongLyricIndex } = storeToRefs(music);
|
||||
const { playSeek, pureLyricMode, playSongLyricIndex } = storeToRefs(status);
|
||||
const { playSongLyric } = storeToRefs(music);
|
||||
const {
|
||||
showYrc,
|
||||
showYrcAnimation,
|
||||
@@ -493,8 +493,8 @@ onMounted(() => {
|
||||
}
|
||||
&.record,
|
||||
&.pure {
|
||||
height: calc(100vh - 340px);
|
||||
margin-bottom: 20px;
|
||||
height: calc(100vh - 300px);
|
||||
margin-bottom: 40px;
|
||||
.lrc-line {
|
||||
margin-bottom: -12px;
|
||||
transform: scale(0.76);
|
||||
@@ -503,5 +503,21 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding: 0 20px !important;
|
||||
}
|
||||
.lrc-line {
|
||||
.lrc-content {
|
||||
font-size: 6.5vw !important;
|
||||
}
|
||||
.lrc-fy {
|
||||
font-size: 4.5vw !important;
|
||||
}
|
||||
.lrc-roma {
|
||||
font-size: 4vw !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<n-card
|
||||
:class="{
|
||||
'main-player': true,
|
||||
'show-bar': Object.keys(music.getPlaySongData)?.length && showPlayBar,
|
||||
'show-bar': music.getPlaySongData?.id && showPlayBar,
|
||||
'no-sider': !showSider,
|
||||
}"
|
||||
content-style="padding: 0"
|
||||
@dblclick.stop="showFullPlayer = true"
|
||||
@dblclick.stop="openFullPlayer"
|
||||
>
|
||||
<!-- 进度条 -->
|
||||
<vue-slider
|
||||
@@ -31,15 +31,21 @@
|
||||
<div class="player">
|
||||
<!-- 歌曲信息 -->
|
||||
<div class="info">
|
||||
<div class="cover" @click.stop="showFullPlayer = true">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
:key="`${music.getPlaySongData?.id}-${playCoverType}`"
|
||||
:class="['cover', playCoverType]"
|
||||
@click.stop="openFullPlayer"
|
||||
>
|
||||
<n-image
|
||||
:key="music.getPlaySongData?.id"
|
||||
:src="
|
||||
music.getPlaySongData?.coverSize?.s ||
|
||||
music.getPlaySongData?.cover ||
|
||||
music.getPlaySongData?.localCover
|
||||
"
|
||||
:style="{
|
||||
animationPlayState: playState ? 'running' : 'paused',
|
||||
}"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
@load="
|
||||
@@ -54,17 +60,20 @@
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
</Transition>
|
||||
<!-- 打开播放器 -->
|
||||
<n-icon class="open" size="30">
|
||||
<SvgIcon icon="pan-zoom-rounded" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 打开播放器 -->
|
||||
<n-icon class="open" size="30">
|
||||
<SvgIcon icon="pan-zoom-rounded" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="song-info">
|
||||
<div class="name">
|
||||
<n-text class="text">{{ music.getPlaySongData?.name || "未知曲目" }}</n-text>
|
||||
<n-text class="text">
|
||||
{{ music.getPlaySongData?.name || "未知曲目" }}
|
||||
</n-text>
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
v-if="playMode !== 'dj'"
|
||||
class="favorite"
|
||||
@click.stop="
|
||||
data.changeLikeList(
|
||||
@@ -85,7 +94,7 @@
|
||||
</n-icon>
|
||||
<!-- 更多操作 -->
|
||||
<n-dropdown
|
||||
v-if="!music.getPlaySongData?.path"
|
||||
v-if="playMode !== 'dj' && !music.getPlaySongData?.path"
|
||||
:options="songMoreOptions"
|
||||
:show-arrow="true"
|
||||
placement="top-start"
|
||||
@@ -100,7 +109,8 @@
|
||||
<!-- 歌手 -->
|
||||
<div
|
||||
v-if="
|
||||
((!playState || !bottomLyricShow) && playSongLyric.lrc?.length) ||
|
||||
((!playState || !bottomLyricShow || playMode === 'dj') &&
|
||||
playSongLyric.lrc?.length) ||
|
||||
playSongLyricIndex === -1
|
||||
"
|
||||
class="artist"
|
||||
@@ -119,6 +129,7 @@
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</template>
|
||||
<div v-else-if="playMode === 'dj'" class="ar">电台节目</div>
|
||||
<n-text v-else class="ar">
|
||||
{{ music.getPlaySongData?.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
@@ -172,6 +183,7 @@
|
||||
<!-- 播放暂停 -->
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
:focusable="false"
|
||||
tag="div"
|
||||
type="primary"
|
||||
class="play-control"
|
||||
@@ -194,110 +206,129 @@
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 功能区 -->
|
||||
<div class="menu">
|
||||
<!-- 时间进度 -->
|
||||
<div class="time">
|
||||
<n-text class="played" depth="3">{{ playTimeData.played }}</n-text>
|
||||
<n-text depth="3">{{ playTimeData.durationTime }}</n-text>
|
||||
</div>
|
||||
<!-- 播放模式 -->
|
||||
<n-dropdown
|
||||
v-if="playMode === 'normal'"
|
||||
:options="playModeOptions"
|
||||
:show-arrow="true"
|
||||
trigger="hover"
|
||||
@select="playModeChange"
|
||||
>
|
||||
<div class="mode" @click.stop @dblclick.stop>
|
||||
<n-icon size="22">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="playMode" class="menu">
|
||||
<!-- 时间进度 -->
|
||||
<div class="time hidden">
|
||||
<n-text class="played" depth="3">{{ playTimeData.played }}</n-text>
|
||||
<n-text depth="3">{{ playTimeData.durationTime }}</n-text>
|
||||
</div>
|
||||
<!-- 播放模式 -->
|
||||
<n-dropdown
|
||||
v-if="playMode !== 'fm'"
|
||||
:options="playModeOptions"
|
||||
:show-arrow="true"
|
||||
trigger="hover"
|
||||
@select="playModeChange"
|
||||
>
|
||||
<n-icon
|
||||
class="mode hidden"
|
||||
size="22"
|
||||
@click.stop="playModeChange(false)"
|
||||
@dblclick.stop
|
||||
>
|
||||
<SvgIcon
|
||||
:icon="
|
||||
playHeartbeatMode
|
||||
? 'heartbit'
|
||||
: playSongMode === 'normal'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
"
|
||||
isSpecial
|
||||
/>
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<!-- 倍速 -->
|
||||
<n-popover :show-arrow="false" trigger="hover" placement="top-end" raw>
|
||||
<template #trigger>
|
||||
<div class="speed" @click.stop="(playRate = 1), setRate(1)" @dblclick.stop>
|
||||
<n-icon v-if="playRate === 1" size="22">
|
||||
<SvgIcon icon="speed-rounded" />
|
||||
</n-icon>
|
||||
<n-text v-else class="speed-text">{{ playRate }}x</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 倍速调整 -->
|
||||
<div class="slider-content">
|
||||
<n-slider
|
||||
v-model:value="playRate"
|
||||
:tooltip="false"
|
||||
:min="0.1"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
:marks="{
|
||||
0.1: '减速',
|
||||
1: '正常',
|
||||
2: '加速',
|
||||
}"
|
||||
style="width: 220px"
|
||||
@update:value="setRate"
|
||||
/>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon class="volume" size="22" @click.stop="setVolumeMute" @wheel="changeVolume">
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon v-else-if="playVolume > 0 && playVolume < 0.4" icon="volume-mute-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
</n-dropdown>
|
||||
<!-- 倍速 -->
|
||||
<n-popover :show-arrow="false" trigger="hover" placement="top-end" raw>
|
||||
<template #trigger>
|
||||
<div class="speed hidden" @click.stop="(playRate = 1), setRate(1)" @dblclick.stop>
|
||||
<n-icon v-if="playRate === 1" size="22">
|
||||
<SvgIcon icon="speed-rounded" />
|
||||
</n-icon>
|
||||
<n-text v-else class="speed-text">{{ playRate }}x</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 倍速调整 -->
|
||||
<div class="slider-content">
|
||||
<n-slider
|
||||
v-model:value="playRate"
|
||||
:tooltip="false"
|
||||
:min="0.1"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
:marks="{
|
||||
0.1: '减速',
|
||||
1: '正常',
|
||||
2: '加速',
|
||||
}"
|
||||
style="width: 220px"
|
||||
@update:value="setRate"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
class="volume hidden"
|
||||
size="22"
|
||||
@click.stop="setVolumeMute"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume > 0 && playVolume < 0.4"
|
||||
icon="volume-mute-rounded"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
:style="{
|
||||
padding: '10px 0',
|
||||
width: '50px',
|
||||
}"
|
||||
class="slider-content hidden"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<n-badge
|
||||
v-if="playMode !== 'fm'"
|
||||
:value="playList?.length ?? 0"
|
||||
:show="showPlaylistCount"
|
||||
:max="999"
|
||||
:style="{
|
||||
padding: '10px 0',
|
||||
width: '50px',
|
||||
marginRight: showPlaylistCount ? '12px' : null,
|
||||
}"
|
||||
class="slider-content"
|
||||
@wheel="changeVolume"
|
||||
class="playlist"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<n-icon
|
||||
v-if="playMode === 'normal'"
|
||||
class="playlist"
|
||||
size="22"
|
||||
@click.stop="playListShow = !playListShow"
|
||||
>
|
||||
<SvgIcon icon="queue-music-rounded" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<n-icon size="22" @click.stop="playListShow = !playListShow">
|
||||
<SvgIcon icon="queue-music-rounded" />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- 添加到歌单 -->
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
@@ -319,6 +350,7 @@ import {
|
||||
setVolume,
|
||||
setVolumeMute,
|
||||
setRate,
|
||||
processSpectrum,
|
||||
} from "@/utils/Player";
|
||||
import { getSongPlayTime } from "@/utils/timeTools";
|
||||
import debounce from "@/utils/debounce";
|
||||
@@ -331,21 +363,24 @@ const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { playList, playListOld, playSongLyric } = storeToRefs(music);
|
||||
const {
|
||||
playMode,
|
||||
playIndex,
|
||||
playList,
|
||||
playListOld,
|
||||
playLoading,
|
||||
playState,
|
||||
playListShow,
|
||||
showPlayBar,
|
||||
showFullPlayer,
|
||||
playSongLyricIndex,
|
||||
playTimeData,
|
||||
playVolume,
|
||||
playRate,
|
||||
playVolume,
|
||||
playIndex,
|
||||
playMode,
|
||||
playSongMode,
|
||||
playHeartbeatMode,
|
||||
playSongLyricIndex,
|
||||
playSongLyric,
|
||||
} = storeToRefs(music);
|
||||
const { playLoading, playState, playListShow, showPlayBar, showFullPlayer } = storeToRefs(status);
|
||||
const { showYrc, bottomLyricShow, showSider } = storeToRefs(settings);
|
||||
} = storeToRefs(status);
|
||||
const { showYrc, bottomLyricShow, showSider, showPlaylistCount, showSpectrums, playCoverType } =
|
||||
storeToRefs(settings);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
@@ -358,16 +393,16 @@ const renderIcon = (icon, isSpecial = false) => {
|
||||
|
||||
// 播放模式数据
|
||||
const playModeOptions = ref([
|
||||
{
|
||||
label: "列表循环",
|
||||
key: "normal",
|
||||
icon: renderIcon("repeat-list", true),
|
||||
},
|
||||
{
|
||||
label: "单曲循环",
|
||||
key: "repeat",
|
||||
icon: renderIcon("repeat-song", true),
|
||||
},
|
||||
{
|
||||
label: "列表循环",
|
||||
key: "normal",
|
||||
icon: renderIcon("repeat-list", true),
|
||||
},
|
||||
{
|
||||
label: "随机播放",
|
||||
key: "random",
|
||||
@@ -450,6 +485,16 @@ const songTimeSliderUpdate = (val) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 开启播放器
|
||||
const openFullPlayer = () => {
|
||||
if (playMode.value === "dj") {
|
||||
$message.warning("当前为电台模式,无法开启播放器");
|
||||
return false;
|
||||
}
|
||||
if (showSpectrums.value && typeof $player !== "undefined") processSpectrum($player);
|
||||
showFullPlayer.value = true;
|
||||
};
|
||||
|
||||
// 上下曲切换
|
||||
const changePlayIndexDebounce = debounce(async (type, id) => {
|
||||
// 垃圾桶
|
||||
@@ -462,6 +507,11 @@ const changePlayIndexDebounce = debounce(async (type, id) => {
|
||||
|
||||
// 播放模式切换
|
||||
const playModeChange = (mode) => {
|
||||
const modeMap = {
|
||||
normal: "random",
|
||||
random: "shuffle",
|
||||
shuffle: "normal",
|
||||
};
|
||||
// 关闭心动模式
|
||||
if (playHeartbeatMode.value) {
|
||||
playHeartbeatMode.value = false;
|
||||
@@ -471,7 +521,11 @@ const playModeChange = (mode) => {
|
||||
playListOld.value = [];
|
||||
}
|
||||
// 切换模式
|
||||
playSongMode.value = mode;
|
||||
if (mode) {
|
||||
playSongMode.value = mode;
|
||||
} else {
|
||||
playSongMode.value = modeMap[playSongMode.value] || "normal";
|
||||
}
|
||||
};
|
||||
|
||||
// 音量条鼠标滚动
|
||||
@@ -533,12 +587,12 @@ watch(
|
||||
border-radius: 25px;
|
||||
.vue-slider-process {
|
||||
background-color: var(--main-color);
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot-handle {
|
||||
transition: box-shadow 0.3s;
|
||||
@@ -605,6 +659,33 @@ watch(
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
&.record {
|
||||
.cover-img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
animation: playerCoverRotate 18s linear infinite;
|
||||
background: no-repeat url("/images/pic/record.png?assest") center;
|
||||
:deep(img) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 1px 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
:deep(img) {
|
||||
transform: none;
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
.open {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
display: flex;
|
||||
@@ -671,6 +752,7 @@ watch(
|
||||
.lrc-text {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
.space {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -720,18 +802,7 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
.played {
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
transition: opacity 0.1s;
|
||||
.n-icon {
|
||||
margin-left: 8px;
|
||||
padding: 8px;
|
||||
@@ -753,6 +824,18 @@ watch(
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
.played {
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.speed {
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
@@ -774,6 +857,44 @@ watch(
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.playlist {
|
||||
transition: margin 0.3s;
|
||||
&.count {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
:deep(.n-badge-sup) {
|
||||
background: var(--main-boxshadow-color);
|
||||
backdrop-filter: blur(20px);
|
||||
.n-base-slot-machine {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.menu {
|
||||
.time {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.control {
|
||||
margin-left: auto;
|
||||
.play-prev,
|
||||
.play-next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.menu {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.show-bar {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<!-- 播放器 - 控制面板 -->
|
||||
<template>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div class="control" @mousemove="controlMove" @mouseenter="controlEnter">
|
||||
<div
|
||||
v-show="playerControlShow"
|
||||
class="control"
|
||||
@mousemove="controlMove"
|
||||
@mouseenter="controlEnter"
|
||||
>
|
||||
<div class="left">
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
v-if="!music.getPlaySongData.path"
|
||||
class="favorite"
|
||||
size="24"
|
||||
@click.stop="
|
||||
data.changeLikeList(
|
||||
@@ -27,7 +31,7 @@
|
||||
<!-- 添加到歌单 -->
|
||||
<n-icon
|
||||
v-if="!music.getPlaySongData.path"
|
||||
class="favorite"
|
||||
class="hidden"
|
||||
size="24"
|
||||
@click.stop="addPlaylistRef?.openAddToPlaylist(music.getPlaySongData?.id)"
|
||||
>
|
||||
@@ -36,7 +40,7 @@
|
||||
<!-- 下载 -->
|
||||
<n-icon
|
||||
v-if="!music.getPlaySongData.path"
|
||||
class="favorite"
|
||||
class="hidden"
|
||||
size="24"
|
||||
@click.stop="downloadSongRef?.openDownloadModal(music.getPlaySongData)"
|
||||
>
|
||||
@@ -69,6 +73,7 @@
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
:keyboard="false"
|
||||
:focusable="false"
|
||||
class="play-control"
|
||||
strong
|
||||
secondary
|
||||
@@ -107,7 +112,7 @@
|
||||
<!-- MV -->
|
||||
<n-icon
|
||||
v-if="music.getPlaySongData.mv"
|
||||
class="favorite"
|
||||
class="hidden"
|
||||
size="22"
|
||||
@click.stop="
|
||||
(showFullPlayer = false), router.push(`/videos-player?id=${music.getPlaySongData.mv}`)
|
||||
@@ -118,6 +123,7 @@
|
||||
<!-- 评论 -->
|
||||
<n-icon
|
||||
v-if="!music.getPlaySongData?.path"
|
||||
class="hidden"
|
||||
size="22"
|
||||
@click.stop="
|
||||
(showFullPlayer = false), router.push(`/comment?id=${music.getPlaySongData?.id}`)
|
||||
@@ -125,17 +131,57 @@
|
||||
>
|
||||
<SvgIcon icon="comment-text" />
|
||||
</n-icon>
|
||||
<!-- 音量 -->
|
||||
<n-popover trigger="hover" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
class="volume hidden"
|
||||
size="22"
|
||||
@click.stop="setVolumeMute"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<SvgIcon v-if="playVolume === 0" icon="no-sound-rounded" />
|
||||
<SvgIcon v-else-if="playVolume > 0 && playVolume < 0.4" icon="volume-mute-rounded" />
|
||||
<SvgIcon
|
||||
v-else-if="playVolume >= 0.4 && playVolume < 0.7"
|
||||
icon="volume-down-rounded"
|
||||
/>
|
||||
<SvgIcon v-else icon="volume-up-rounded" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<!-- 音量调整 -->
|
||||
<div
|
||||
:style="{
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
class="slider-content hidden"
|
||||
@wheel="changeVolume"
|
||||
>
|
||||
<n-slider
|
||||
v-model:value="playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
style="height: 120px"
|
||||
@update:value="setVolume"
|
||||
/>
|
||||
<n-text class="slider-num" depth="3">{{ (playVolume * 100).toFixed(0) }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放模式 -->
|
||||
<n-icon v-if="playMode === 'normal'" size="22" @click.stop="togglePlayMode">
|
||||
<n-icon v-if="playMode === 'normal'" class="hidden" size="22" @click.stop="togglePlayMode">
|
||||
<SvgIcon
|
||||
:icon="
|
||||
playHeartbeatMode
|
||||
? 'heartbit'
|
||||
: playSongMode === 'normal'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
? 'repeat-list'
|
||||
: playSongMode === 'random'
|
||||
? 'shuffle'
|
||||
: 'repeat-song'
|
||||
"
|
||||
isSpecial
|
||||
/>
|
||||
@@ -157,7 +203,14 @@
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus, siteData } from "@/stores";
|
||||
import { useRouter } from "vue-router";
|
||||
import { playOrPause, fadePlayOrPause, setSeek, changePlayIndex } from "@/utils/Player";
|
||||
import {
|
||||
playOrPause,
|
||||
fadePlayOrPause,
|
||||
setSeek,
|
||||
changePlayIndex,
|
||||
setVolume,
|
||||
setVolumeMute,
|
||||
} from "@/utils/Player";
|
||||
import debounce from "@/utils/debounce";
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
@@ -166,17 +219,22 @@ const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { playList, playListOld } = storeToRefs(music);
|
||||
const {
|
||||
playIndex,
|
||||
playList,
|
||||
playListOld,
|
||||
playMode,
|
||||
playerControlShow,
|
||||
controlTimeOut,
|
||||
playLoading,
|
||||
playState,
|
||||
showFullPlayer,
|
||||
playListShow,
|
||||
playTimeData,
|
||||
playMode,
|
||||
playSongMode,
|
||||
playHeartbeatMode,
|
||||
} = storeToRefs(music);
|
||||
const { playerControlShow, controlTimeOut, playLoading, playState, showFullPlayer, playListShow } =
|
||||
storeToRefs(status);
|
||||
playVolume,
|
||||
coverTheme,
|
||||
} = storeToRefs(status);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
@@ -225,6 +283,24 @@ const togglePlayMode = () => {
|
||||
playSongMode.value = modeMap[playSongMode.value] || "normal";
|
||||
};
|
||||
|
||||
// 音量条鼠标滚动
|
||||
const changeVolume = (e) => {
|
||||
const deltaY = e.deltaY;
|
||||
if (deltaY > 0) {
|
||||
// 向下滚动
|
||||
if (playVolume.value >= 0) {
|
||||
playVolume.value = Math.max(playVolume.value - 0.05, 0);
|
||||
}
|
||||
} else if (deltaY < 0) {
|
||||
// 向上滚动
|
||||
if (playVolume.value < 1) {
|
||||
playVolume.value = Math.min(playVolume.value + 0.05, 1);
|
||||
}
|
||||
}
|
||||
// 更新音量
|
||||
setVolume(playVolume.value);
|
||||
};
|
||||
|
||||
// 控制面板移入
|
||||
const controlEnter = () => {
|
||||
if (controlTimeOut.value) clearTimeout(controlTimeOut.value);
|
||||
@@ -346,12 +422,12 @@ const controlMove = (e) => {
|
||||
background-color: var(--cover-second-color);
|
||||
.vue-slider-process {
|
||||
background-color: var(--cover-main-color);
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
}
|
||||
.vue-slider-dot {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
transition: none !important;
|
||||
// transition: none !important;
|
||||
.vue-slider-dot-handle {
|
||||
background-color: var(--cover-main-color);
|
||||
}
|
||||
@@ -372,5 +448,40 @@ const controlMove = (e) => {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
.left,
|
||||
.right {
|
||||
opacity: 1;
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.center {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 音量控制
|
||||
.slider-content {
|
||||
padding: 10px 0px;
|
||||
width: 50px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--cover-second-color);
|
||||
.n-slider {
|
||||
--n-fill-color: var(--cover-main-color);
|
||||
--n-fill-color-hover: var(--cover-main-color);
|
||||
--n-handle-color: var(--cover-main-color);
|
||||
}
|
||||
.slider-num {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||