Compare commits

..

25 Commits
1.4.0 ... 1.5.0

Author SHA1 Message Date
alger
9fcf455c08 🌈 style: 更新版本 1.5.0 2024-05-31 08:56:16 +08:00
alger
9b14906a46 🌈 style: add LICENSE. 2024-05-27 18:18:02 +08:00
alger
14ce428951 🌈 style: 修改README 2024-05-27 11:52:10 +08:00
alger
8c93124311 🌈 style: 修改README 2024-05-27 11:51:15 +08:00
alger
c09707867b 🦄 refactor: 适配 web移动端 改造 2024-05-23 17:12:35 +08:00
alger
a2af0f3904 feat: 优化搜索功能 2024-05-22 19:20:57 +08:00
alger
73982f0e84 feat: 添加manifest.json 2024-05-22 15:38:43 +08:00
alger
449a6fd335 feat: 登录问题修复 2024-05-22 15:14:26 +08:00
alger
32b39c7927 feat: 添加每日推荐 样式, 请求等大量优化 2024-05-22 12:07:48 +08:00
alger
c6f1e0b233 🌈 style: 修改issue模板 2024-05-21 11:36:06 +08:00
alger
7c1a3ae4bc 🌈 style: 添加issue模板 2024-05-21 11:33:57 +08:00
alger
6bd6622484 🌈 style: 添加gzip压缩配置 2024-05-21 11:24:33 +08:00
alger
433aff385d 🌈 style: 更换ico文件 2024-05-21 11:06:20 +08:00
alger
c37ad07f93 🌈 style: 优化类型 2024-05-21 11:01:23 +08:00
alger
e4c1f855fb 🐞 fix: 修复关闭报错 2024-05-21 10:27:46 +08:00
alger
6978656061 🌈 style: 更新logo.png 2024-05-21 10:24:32 +08:00
alger
973d60c98f 🌈 style: 完善gitgnore 2024-05-21 10:21:51 +08:00
alger
5a43ba2576 🌈 style: 删除无用配置 2024-05-21 10:18:40 +08:00
alger
e52a02cf3c 🌈 style: 去除无用代码 2024-05-21 10:16:30 +08:00
alger
da8216e2ca 🦄 refactor: 重构打包方式 2024-05-21 08:52:34 +08:00
alger
bd0e2ec35c feat: 历史纪录去除按钮 2024-05-20 19:55:52 +08:00
alger
7c8598ffa5 feat: 优化歌词体验 2024-05-20 19:54:00 +08:00
alger
50e594b91d 🐞 fix: 修复歌词界面无法打开问题 2024-05-16 19:57:20 +08:00
alger
a9e5bb33e4 feat: 添加eslint 和 桌面歌词(未完成) 2024-05-16 18:54:30 +08:00
alger
5e8676a039 📃 docs: 完善文档 2024-05-14 18:11:54 +08:00
97 changed files with 4085 additions and 2481 deletions

View File

@@ -1,4 +1,3 @@
VITE_API = /api
VITE_API_MT = /mt
VITE_API_MUSIC = /music
VITE_API_PROXY = http://110.42.251.190:9856

View File

@@ -1,4 +1,3 @@
VITE_API = http://110.42.251.190:9898
VITE_API_MT = http://mt.myalger.top
VITE_API_MUSIC = http://110.42.251.190:4100
VITE_API_PROXY = http://110.42.251.190:9856

13
.eslintignore Normal file
View File

@@ -0,0 +1,13 @@
snapshot*
dist
lib
es
esm
node_modules
static
cypress
script/test/cypress
_site
temp*
static/
!.prettierrc.js

133
.eslintrc Normal file
View File

@@ -0,0 +1,133 @@
{
"extends": [
"plugin:@typescript-eslint/recommended",
"eslint-config-airbnb-base",
"@vue/typescript/recommended",
"plugin:vue/vue3-recommended",
"plugin:vue-scoped-css/base",
"plugin:prettier/recommended"
],
"env": {
"browser": true,
"node": true,
"jest": true,
"es6": true
},
"globals": {
"defineProps": "readonly",
"defineEmits": "readonly"
},
"plugins": [
"vue",
"@typescript-eslint",
"simple-import-sort"
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module",
"allowImportExportEverywhere": true,
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"import/extensions": [
".js",
".jsx",
".ts",
".tsx"
]
},
"rules": {
"no-console": "off",
"no-continue": "off",
"no-restricted-syntax": "off",
"no-plusplus": "off",
"no-param-reassign": "off",
"no-shadow": "off",
"guard-for-in": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": "off",
"import/prefer-default-export": "off",
"import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"vue/first-attribute-linebreak": 0,
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-types": "off",
"class-methods-use-this": "off", // 因为AxiosCancel必须实例化而能静态化所以加的规则如果有办法解决可以取消
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
},
"overrides": [
{
"files": [
"*.vue"
],
"rules": {
"vue/component-name-in-template-casing": [
2,
"kebab-case"
],
"vue/require-default-prop": 0,
"vue/multi-word-component-names": 0,
"vue/no-reserved-props": 0,
"vue/no-v-html": 0,
"vue-scoped-css/enforce-style-type": [
"error",
{
"allows": [
"scoped"
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
], // https://github.com/typescript-eslint eslint-recommended
"rules": {
"constructor-super": "off", // ts(2335) & ts(2377)
"getter-return": "off", // ts(2378)
"no-const-assign": "off", // ts(2588)
"no-dupe-args": "off", // ts(2300)
"no-dupe-class-members": "off", // ts(2393) & ts(2300)
"no-dupe-keys": "off", // ts(1117)
"no-func-assign": "off", // ts(2539)
"no-import-assign": "off", // ts(2539) & ts(2540)
"no-new-symbol": "off", // ts(2588)
"no-obj-calls": "off", // ts(2349)
"no-redeclare": "off", // ts(2451)
"no-setter-return": "off", // ts(2408)
"no-this-before-super": "off", // ts(2376)
"no-undef": "off", // ts(2304)
"no-unreachable": "off", // ts(7027)
"no-unsafe-negation": "off", // ts(2365) & ts(2360) & ts(2358)
"no-var": "error", // ts transpiles let/const to var, so no need for vars any more
"prefer-const": "error", // ts provides better types with const
"prefer-rest-params": "error", // ts provides better types with rest args over arguments
"prefer-spread": "error", // ts transpiles spread to apply, so no need for manual apply
"valid-typeof": "off" // ts(2367)
}
}
]
}

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @algerkong

View File

@@ -0,0 +1,70 @@
name: 反馈 Bug
description: 通过 github 模板进行 Bug 反馈。
title: "描述问题的标题"
body:
- type: markdown
attributes:
value: |
# 欢迎你的参与
Issue 列表接受 bug 报告或是新功能请求。
在发布一个 Issue 前,请确保:
- 在Issue中搜索过你的问题。你的问题可能已有人提出也可能已在最新版本中被修正
- 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
- type: input
id: reproduce
attributes:
label: 重现链接
description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接,否则你的 Issue 将被关闭。
placeholder: 请填写
- type: textarea
id: reproduceSteps
attributes:
label: 重现步骤
description: 请清晰的描述重现该 Issue 的步骤,这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复,标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤,将被关闭。
placeholder: 请填写
- type: textarea
id: expect
attributes:
label: 期望结果
placeholder: 请填写
- type: textarea
id: actual
attributes:
label: 实际结果
placeholder: 请填写
- type: input
id: frameworkVersion
attributes:
label: 框架版本
placeholder: Vue(3.3.0)
- type: input
id: browsersVersion
attributes:
label: 浏览器版本
placeholder: Chrome(8.213.231.123)
- type: input
id: systemVersion
attributes:
label: 系统版本
placeholder: MacOS(11.2.3)
- type: input
id: nodeVersion
attributes:
label: Node版本
placeholder: 请填写
- type: textarea
id: remarks
attributes:
label: 补充说明
description: 可以是遇到这个 bug 的业务场景、上下文等信息。
placeholder: 请填写

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name:
url:
about:

View File

@@ -0,0 +1,29 @@
name: 反馈新功能
description: 通过 github 模板进行新功能反馈。
title: "描述问题的标题"
body:
- type: markdown
attributes:
value: |
# 欢迎你的参与
在发布一个 Issue 前,请确保:
- 在 Issue 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正)
- 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
- type: textarea
id: functionContent
attributes:
label: 这个功能解决了什么问题
description: 请详尽说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。
placeholder: 请填写
validations:
required: true
- type: textarea
id: functionalExpectations
attributes:
label: 你建议的方案是什么
placeholder: 请填写
validations:
required: true

51
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,51 @@
<!--
首先,感谢你的贡献!😄
PR 在维护者审核通过后会合并,谢谢!
-->
### 🤔 这个 PR 的性质是?
- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他
### 🔗 相关 Issue
<!--
1. 描述相关需求的来源,如相关的 issue 讨论链接。
-->
### 💡 需求背景和解决方案
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
### 📝 更新日志
<!--
从用户角度描述具体变化,以及可能的 breaking change 和其他风险。
-->
- fix(组件名称): 处理问题或特性描述 ...
- [ ] 本条 PR 不需要纳入 Changelog
### ☑️ 请求合并前的自查清单
⚠️ 请自检并全部**勾选全部选项**。⚠️
- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# Basic dependabot.yml file with
# minimum configuration for two package managers
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
# Look for `package.json` and `lock` files in the `root` directory
directory: '/'
# Check the npm registry for updates every day (weekdays)
schedule:
interval: 'monthly'
# Enable version updates for Docker
- package-ecosystem: 'docker'
# Look for a `Dockerfile` in the `root` directory
directory: '/'
# Check for updates once a week
schedule:
interval: 'monthly'

8
.github/issue-shoot.md vendored Normal file
View File

@@ -0,0 +1,8 @@
## IssueShoot
- 预估时长: {{ .duration }}
- 期望完成时间: {{ .deadline }}
- 开发难度: {{ .level }}
- 参与人数: 1
- 需求对接人: ivringpeng
- 验收标准: 实现期望改造效果,提 PR 并通过验收无误
- 备注: 最终激励以实际提交 `pull request` 并合并为准

7
.gitignore vendored
View File

@@ -1,11 +1,16 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
dist_electron
.idea
# lock
yarn.lock
pnpm-lock.yaml
package-lock.json
dist.zip
dist.zip
.vscode

39
.prettierrc.js Normal file
View File

@@ -0,0 +1,39 @@
module.exports = {
// 一行最多 120 字符..
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾需要有逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: 'lf',
};

View File

@@ -1,3 +0,0 @@
{
"compile-hero.disable-compile-files-on-did-save-code": true
}

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,5 +1,43 @@
# Vue 3 + Typescript + Vite
# 一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
主要功能如下
vue3 + TypeScript + NaiveUI + animateCss + Vuex + VueRouter + Axios等实现音乐桌面web端
实现各项功能
网站地址http://mc.myalger.top/
- 音乐推荐
- 音乐播放
- 网易云登录
- 播放历史
- 桌面歌词
- 歌单 mv 搜索 专辑等功能
## 项目运行
```bash
# 安装依赖
npm install
# 运行项目 web
npm run dev
# 运行项目 electron
npm run start
# 打包项目 web
npm run build
# 打包项目 electron
npm run win ...
# 具体看 package.json
```
## 软件截图
![首页](./docs/img/image.png)
![歌单](./docs/img/image-1.png)
![搜索](./docs/img/image-2.png)
![mv](./docs/img/image-3.png)
![历史](./docs/img/image-4.png)
![我的](./docs/img/image-5.png)
## 欢迎提Issues
## 免责声明
本软件仅用于学习交流,禁止用于商业用途,否则后果自负。

124
app.js
View File

@@ -1,9 +1,10 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron')
const path = require('path')
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron');
const path = require('path');
const Store = require('electron-store');
const setJson = require('./electron/set.json')
const setJson = require('./electron/set.json');
const { loadLyricWindow } = require('./electron/lyric');
let mainWin = null
let mainWin = null;
function createWindow() {
mainWin = new BrowserWindow({
width: 1200,
@@ -11,129 +12,134 @@ function createWindow() {
frame: false,
webPreferences: {
nodeIntegration: true,
// contextIsolation: false,
preload: path.join(__dirname, '/electron/preload.js'),
},
})
const win = mainWin
win.setMinimumSize(1200, 780)
});
const win = mainWin;
win.setMinimumSize(1200, 780);
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' })
win.loadURL('http://localhost:4678/')
win.webContents.openDevTools({ mode: 'detach' });
win.loadURL('http://localhost:4678/');
} else {
win.loadURL(`file://${__dirname}/dist/index.html`)
win.loadURL(`file://${__dirname}/dist/index.html`);
}
const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'))
const tray = new Tray(image)
const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'));
const tray = new Tray(image);
// 创建一个上下文菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示',
click: () => {
win.show()
win.show();
},
},
{
label: '退出',
click: () => {
win.destroy()
win.destroy();
app.quit();
},
},
])
]);
// 设置系统托盘图标的上下文菜单
tray.setContextMenu(contextMenu)
tray.setContextMenu(contextMenu);
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
tray.on('click', () => {
if (win.isVisible()) {
win.hide()
win.hide();
} else {
win.show()
win.show();
}
})
});
const set = store.get('set')
const set = store.get('set');
// store.set('set', setJson)
if (!set) {
store.set('set', setJson)
store.set('set', setJson);
}
loadLyricWindow(ipcMain);
}
// 限制只能启动一个应用
const gotTheLock = app.requestSingleInstanceLock()
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit()
app.quit();
}
app.whenReady().then(createWindow)
app.whenReady().then(createWindow);
app.on('ready',()=>{
app.on('ready', () => {
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
if (mainWin.isVisible()) {
mainWin.hide()
mainWin.hide();
} else {
mainWin.show()
mainWin.show();
}
})
})
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
app.quit();
}
})
});
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
globalShortcut.unregisterAll();
});
ipcMain.on('minimize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.minimize()
})
const win = BrowserWindow.fromWebContents(event.sender);
win.minimize();
});
ipcMain.on('maximize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.unmaximize()
win.unmaximize();
} else {
win.maximize()
win.maximize();
}
})
});
ipcMain.on('close-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.destroy()
})
const win = BrowserWindow.fromWebContents(event.sender);
win.destroy();
app.quit();
});
ipcMain.on('drag-start', (event, data) => {
const win = BrowserWindow.fromWebContents(event.sender)
ipcMain.on('drag-start', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.webContents.beginFrameSubscription((frameBuffer) => {
event.reply('frame-buffer', frameBuffer)
})
})
event.reply('frame-buffer', frameBuffer);
});
});
ipcMain.on('mini-tray', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.hide()
})
const win = BrowserWindow.fromWebContents(event.sender);
win.hide();
});
// 重启
ipcMain.on('restart', () => {
app.relaunch()
app.exit(0)
})
app.relaunch();
app.exit(0);
});
const store = new Store();
// 定义ipcRenderer监听事件
ipcMain.on('setStore', (_, key, value) => {
store.set(key, value)
})
store.set(key, value);
});
ipcMain.on('getStore', (_, key) => {
let value = store.get(key)
_.returnValue = value || ""
})
const value = store.get(key);
_.returnValue = value || '';
});

31
build/win32.json Normal file
View File

@@ -0,0 +1,31 @@
{
"appId": "com.alger.music",
"productName": "AlgerMusic",
"artifactName": "${productName}_${version}_Setup_x86.${ext}",
"directories": {
"output": "dist_electron/win-x86"
},
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
"win": {
"icon": "public/icon.png",
"target": [
{
"target": "nsis",
"arch": ["ia32"]
}
],
"extraFiles": [
{
"from": "installer/installer.nsh",
"to": "$INSTDIR"
}
]
},
"nsis": {
"oneClick": false,
"language": "2052",
"allowToChangeInstallationDirectory": true,
"differentialPackage": true,
"shortcutName": "Alger Music"
}
}

31
build/win64.json Normal file
View File

@@ -0,0 +1,31 @@
{
"appId": "com.alger.music",
"productName": "AlgerMusic",
"artifactName": "${productName}_${version}_Setup_x64.${ext}",
"directories": {
"output": "dist_electron/win-x64"
},
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
"win": {
"icon": "public/icon.png",
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"extraFiles": [
{
"from": "installer/installer.nsh",
"to": "$INSTDIR"
}
]
},
"nsis": {
"oneClick": false,
"language": "2052",
"allowToChangeInstallationDirectory": true,
"differentialPackage": true,
"shortcutName": "Alger Music"
}
}

31
build/winarm64.json Normal file
View File

@@ -0,0 +1,31 @@
{
"appId": "com.alger.music",
"productName": "AlgerMusic",
"artifactName": "${productName}_${version}_Setup_arm64.${ext}",
"directories": {
"output": "dist_electron/win-arm64"
},
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*", "!node_modules/**/*"],
"win": {
"icon": "public/icon.png",
"target": [
{
"target": "nsis",
"arch": ["arm64"]
}
],
"extraFiles": [
{
"from": "installer/installer.nsh",
"to": "$INSTDIR"
}
]
},
"nsis": {
"oneClick": false,
"language": "2052",
"allowToChangeInstallationDirectory": true,
"differentialPackage": true,
"shortcutName": "Alger Music"
}
}

2
components.d.ts vendored
View File

@@ -23,11 +23,13 @@ declare module 'vue' {
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTooltip: typeof import('naive-ui')['NTooltip']
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
PlaylistType: typeof import('./src/components/PlaylistType.vue')['default']
PlayVideo: typeof import('./src/components/common/PlayVideo.vue')['default']
RecommendAlbum: typeof import('./src/components/RecommendAlbum.vue')['default']
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']

BIN
docs/img/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

BIN
docs/img/image-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
docs/img/image-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
docs/img/image-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
docs/img/image-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

BIN
docs/img/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

View File

@@ -1,18 +0,0 @@
{
"appId": "com.alger.music",
"productName": "AlgerMusic",
"directories": {
"output": "dist_electron"
},
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
"win": {
"icon": "public/icon.png",
"target": "nsis",
"extraFiles": [
{
"from": "installer/installer.nsh",
"to": "$INSTDIR"
}
]
}
}

71
electron/lyric.js Normal file
View File

@@ -0,0 +1,71 @@
const { BrowserWindow } = require('electron');
const path = require('path');
let lyricWindow = null;
const createWin = () => {
lyricWindow = new BrowserWindow({
width: 800,
height: 300,
frame: false,
show: false,
transparent: true,
webPreferences: {
nodeIntegration: true,
preload: `${__dirname}/preload.js`,
contextIsolation: false,
},
});
};
const loadLyricWindow = (ipcMain) => {
ipcMain.on('open-lyric', () => {
if (lyricWindow) {
if (lyricWindow.isMinimized()) lyricWindow.restore();
lyricWindow.focus();
lyricWindow.show();
return;
}
createWin();
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
lyricWindow.loadURL('http://localhost:4678/#/lyric');
} else {
const distPath = path.resolve(__dirname, '../dist');
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
}
lyricWindow.setMinimumSize(600, 200);
// 隐藏任务栏
lyricWindow.setSkipTaskbar(true);
lyricWindow.show();
});
ipcMain.on('send-lyric', (e, data) => {
if (lyricWindow) {
lyricWindow.webContents.send('receive-lyric', data);
}
});
ipcMain.on('top-lyric', (e, data) => {
lyricWindow.setAlwaysOnTop(data);
});
ipcMain.on('close-lyric', () => {
lyricWindow.close();
lyricWindow = null;
});
ipcMain.on('mouseenter-lyric', () => {
lyricWindow.setIgnoreMouseEvents(true);
});
ipcMain.on('mouseleave-lyric', () => {
lyricWindow.setIgnoreMouseEvents(false);
});
};
module.exports = {
loadLyricWindow,
};

View File

@@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron')
const { contextBridge, ipcRenderer, app } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
minimize: () => ipcRenderer.send('minimize-window'),
@@ -7,19 +7,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
dragStart: (data) => ipcRenderer.send('drag-start', data),
miniTray: () => ipcRenderer.send('mini-tray'),
restart: () => ipcRenderer.send('restart'),
})
openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
});
const electronHandler = {
ipcRenderer: {
setStoreValue: (key, value) => {
ipcRenderer.send("setStore", key, value)
ipcRenderer.send('setStore', key, value);
},
getStoreValue(key) {
const resp = ipcRenderer.sendSync("getStore", key)
return resp
const resp = ipcRenderer.sendSync('getStore', key);
return resp;
},
}
}
},
app,
};
contextBridge.exposeInMainWorld('electron', electronHandler)
contextBridge.exposeInMainWorld('electron', electronHandler);

View File

@@ -1,5 +1,5 @@
{
"version": "1.3.0",
"version": "1.5.0",
"isProxy": false,
"author": "alger"
}

View File

@@ -1,25 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>网抑云 | algerkong</title>
<link
rel="stylesheet"
href="./public/icon/iconfont.css"
/>
<link rel="stylesheet" href="./public/css/animate.css" />
<link rel="stylesheet" href="./public/css/base.css" />
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>网抑云 | algerkong</title>
<link rel="manifest" href="./public/manifest.json" />
<link rel="stylesheet" href="./public/icon/iconfont.css" />
<link rel="stylesheet" href="./public/css/animate.css" />
<link rel="stylesheet" href="./public/css/base.css" />
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "alger-music",
"version": "1.3.0",
"version": "1.5.0",
"description": "这是一个用于音乐播放的应用程序。",
"author": "Alger <algerkc@qq.com>",
"main": "app.js",
@@ -8,40 +8,54 @@
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"es": "vite && electron .",
"start": "set NODE_ENV=development&&electron .",
"e:b": "electron-builder --config ./electron.config.json",
"eb": "vite build && e:b"
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
"b:win:x64": "electron-builder --config ./build/win64.json",
"b:win:x86": "electron-builder --config ./build/win32.json",
"b:win:arm": "electron-builder --config ./build/winarm64.json"
},
"dependencies": {
"@tailwindcss/postcss7-compat": "^2.2.4",
"@vue/runtime-core": "^3.3.4",
"@vueuse/core": "^10.7.1",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",
"electron-store": "^8.1.0",
"lodash": "^4.17.21",
"naive-ui": "^2.34.4",
"postcss": "^7.0.36",
"sass": "^1.35.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuex": "^4.1.0"
"electron-store": "^8.1.0"
},
"devDependencies": {
"@sicons/antd": "^0.10.0",
"@vicons/antd": "^0.10.0",
"@tailwindcss/postcss7-compat": "^2.2.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/runtime-core": "^3.3.4",
"@vueuse/core": "^10.7.1",
"@vueuse/electron": "^10.9.0",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",
"electron": "^30.0.0",
"electron-builder": "^24.13.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-vue": "^9.21.1",
"eslint-plugin-vue-scoped-css": "^2.7.2",
"lodash": "^4.17.21",
"naive-ui": "^2.38.2",
"postcss": "^7.0.36",
"prettier": "^3.2.5",
"remixicon": "^4.2.0",
"sass": "^1.35.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
"typescript": "^4.3.2",
"unplugin-auto-import": "^0.17.2",
"unplugin-vue-components": "^0.26.0",
"vfonts": "^0.1.0",
"vite": "^4.4.7",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "1.0.0-beta.5",
"vue-tsc": "^0.0.24"
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-tsc": "^0.0.24",
"vuex": "^4.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 178 KiB

10
public/manifest.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "Alger Music PWA",
"icons": [
{
"src": "./icon.png",
"type": "image/png",
"sizes": "256x256"
}
]
}

View File

@@ -1,37 +1,50 @@
<template>
<div class="app">
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
<n-config-provider :theme="darkTheme">
<n-dialog-provider>
<router-view></router-view>
</n-dialog-provider>
</n-config-provider>
</div>
</template>
<script lang="ts" setup>
import { darkTheme } from 'naive-ui'
import store from '@/store'
const audio = ref<HTMLAudioElement | null>(null)
const playMusicUrl = computed(() => store.state.playMusicUrl as string)
// 是否播放
const play = computed(() => store.state.play as boolean)
const windowData = window as any
onMounted(()=>{
if(windowData.electron){
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
store.commit('setSetData', setData)
}
})
</script>
<style lang="scss" scoped >
div {
box-sizing: border-box;
}
.app {
user-select: none;
}
</style>
<template>
<div class="app" :class="isMobile ? 'mobile' : ''">
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
<n-config-provider :theme="darkTheme">
<n-dialog-provider>
<keep-alive>
<router-view></router-view>
</keep-alive>
</n-dialog-provider>
</n-config-provider>
</div>
</template>
<script lang="ts" setup>
import { darkTheme } from 'naive-ui';
import store from '@/store';
import { isMobile } from './utils';
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
// 是否播放
const play = computed(() => store.state.play as boolean);
const windowData = window as any;
onMounted(() => {
if (windowData.electron) {
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
store.commit('setSetData', setData);
}
});
</script>
<style lang="scss" scoped>
div {
box-sizing: border-box;
}
.app {
user-select: none;
}
.mobile {
.text-base {
font-size: 14px !important;
}
}
.html:has(.mobile) {
font-size: 14px;
}
</style>

View File

@@ -1,9 +1,11 @@
import request from "@/utils/request";
import { IHotSinger } from "@/type/singer";
import { ISearchKeyword, IHotSearch } from "@/type/search";
import { IPlayListSort } from "@/type/playlist";
import { IRecommendMusic } from "@/type/music";
import { IAlbumNew } from "@/type/album";
import { IData } from '@/type';
import { IAlbumNew } from '@/type/album';
import { IDayRecommend } from '@/type/day_recommend';
import { IRecommendMusic } from '@/type/music';
import { IPlayListSort } from '@/type/playlist';
import { IHotSearch, ISearchKeyword } from '@/type/search';
import { IHotSinger } from '@/type/singer';
import request from '@/utils/request';
interface IHotSingerParams {
offset: number;
@@ -16,30 +18,35 @@ interface IRecommendMusicParams {
// 获取热门歌手
export const getHotSinger = (params: IHotSingerParams) => {
return request.get<IHotSinger>("/top/artists", { params });
return request.get<IHotSinger>('/top/artists', { params });
};
// 获取搜索推荐词
export const getSearchKeyword = () => {
return request.get<ISearchKeyword>("/search/default");
return request.get<ISearchKeyword>('/search/default');
};
// 获取热门搜索
export const getHotSearch = () => {
return request.get<IHotSearch>("/search/hot/detail");
return request.get<IHotSearch>('/search/hot/detail');
};
// 获取歌单分类
export const getPlaylistCategory = () => {
return request.get<IPlayListSort>("/playlist/catlist");
return request.get<IPlayListSort>('/playlist/catlist');
};
// 获取推荐音乐
export const getRecommendMusic = (params: IRecommendMusicParams) => {
return request.get<IRecommendMusic>("/personalized/newsong", { params });
return request.get<IRecommendMusic>('/personalized/newsong', { params });
};
// 获取每日推荐
export const getDayRecommend = () => {
return request.get<IData<IDayRecommend>>('/recommend/songs');
};
// 获取最新专辑推荐
export const getNewAlbum = () => {
return request.get<IAlbumNew>("/album/newest");
return request.get<IAlbumNew>('/album/newest');
};

View File

@@ -1,42 +1,42 @@
import request from "@/utils/request";
import { IList } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
interface IListByTagParams {
tag: string;
before: number;
limit: number;
}
interface IListByCatParams {
cat: string;
offset: number;
limit: number;
}
// 根据tag 获取歌单列表
export function getListByTag(params: IListByTagParams) {
return request.get<IList>("/top/playlist/highquality", { params: params });
}
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get("/top/playlist", {
params: params,
});
}
// 获取推荐歌单
export function getRecommendList(limit: number = 30) {
return request.get("/personalized", { params: { limit } });
}
// 获取歌单详情
export function getListDetail(id: number | string) {
return request.get<IListDetail>("/playlist/detail", { params: { id } });
}
// 获取专辑内容
export function getAlbum(id: number | string) {
return request.get("/album", { params: { id } });
}
import { IList } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import request from '@/utils/request';
interface IListByTagParams {
tag: string;
before: number;
limit: number;
}
interface IListByCatParams {
cat: string;
offset: number;
limit: number;
}
// 根据tag 获取歌单列表
export function getListByTag(params: IListByTagParams) {
return request.get<IList>('/top/playlist/highquality', { params });
}
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get('/top/playlist', {
params,
});
}
// 获取推荐歌单
export function getRecommendList(limit: number = 30) {
return request.get('/personalized', { params: { limit } });
}
// 获取歌单详情
export function getListDetail(id: number | string) {
return request.get<IListDetail>('/playlist/detail', { params: { id } });
}
// 获取专辑内容
export function getAlbum(id: number | string) {
return request.get('/album', { params: { id } });
}

View File

@@ -1,46 +1,46 @@
import request from "@/utils/request";
import request from '@/utils/request';
// 创建二维码key
// /login/qr/key
export function getQrKey() {
return request.get("/login/qr/key");
return request.get('/login/qr/key');
}
// 创建二维码
// /login/qr/create
export function createQr(key: any) {
return request.get("/login/qr/create", { params: { key: key, qrimg: true } });
return request.get('/login/qr/create', { params: { key, qrimg: true } });
}
// 获取二维码状态
// /login/qr/check
export function checkQr(key: any) {
return request.get("/login/qr/check", { params: { key: key } });
return request.get('/login/qr/check', { params: { key } });
}
// 获取登录状态
// /login/status
export function getLoginStatus() {
return request.get("/login/status");
return request.get('/login/status');
}
// 获取用户信息
// /user/account
export function getUserDetail() {
return request.get("/user/account");
return request.get('/user/account');
}
// 退出登录
// /logout
export function logout() {
return request.get("/logout");
return request.get('/logout');
}
// 手机号登录
// /login/cellphone
export function loginByCellphone(phone: any, password: any) {
return request.post("/login/cellphone", {
phone: phone,
password: password,
export function loginByCellphone(phone: string, password: string) {
return request.post('/login/cellphone', {
phone,
password,
});
}

View File

@@ -1,22 +1,22 @@
import { IPlayMusicUrl } from "@/type/music"
import { ILyric } from "@/type/lyric"
import request from "@/utils/request"
import requestMusic from "@/utils/request_music"
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = (id: number) => {
return request.get<IPlayMusicUrl>("/song/url", { params: { id: id } })
}
// 获取歌曲详情
export const getMusicDetail = (ids: Array<number>) => {
return request.get("/song/detail", { params: { ids: ids.join(",")}})
}
// 根据音乐Id获取音乐歌词
export const getMusicLrc = (id: number) => {
return request.get<ILyric>("/lyric", { params: { id: id } })
}
export const getParsingMusicUrl = (id: number) => {
return requestMusic.get<any>("/music", { params: { id: id } })
}
import { ILyric } from '@/type/lyric';
import { IPlayMusicUrl } from '@/type/music';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = (id: number) => {
return request.get<IPlayMusicUrl>('/song/url', { params: { id } });
};
// 获取歌曲详情
export const getMusicDetail = (ids: Array<number>) => {
return request.get('/song/detail', { params: { ids: ids.join(',') } });
};
// 根据音乐Id获取音乐歌词
export const getMusicLrc = (id: number) => {
return request.get<ILyric>('/lyric', { params: { id } });
};
export const getParsingMusicUrl = (id: number) => {
return requestMusic.get<any>('/music', { params: { id } });
};

View File

@@ -1,6 +1,6 @@
import { IData } from '@/type'
import { IMvItem, IMvUrlData } from '@/type/mv'
import request from '@/utils/request'
import { IData } from '@/type';
import { IMvItem, IMvUrlData } from '@/type/mv';
import request from '@/utils/request';
// 获取 mv 排行
export const getTopMv = (limit: number) => {
@@ -8,8 +8,8 @@ export const getTopMv = (limit: number) => {
params: {
limit,
},
})
}
});
};
// 获取 mv 数据
export const getMvDetail = (mvid: string) => {
@@ -17,8 +17,8 @@ export const getMvDetail = (mvid: string) => {
params: {
mvid,
},
})
}
});
};
// 获取 mv 地址
export const getMvUrl = (id: Number) => {
@@ -26,5 +26,5 @@ export const getMvUrl = (id: Number) => {
params: {
id,
},
})
}
});
};

View File

@@ -1,13 +1,12 @@
import request from "@/utils/request"
import { ISearchDetail } from "@/type/search"
import request from '@/utils/request';
interface IParams {
keywords: string
type: number
keywords: string;
type: number;
}
// 搜索内容
export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', {
params,
})
}
});
};

View File

@@ -1,17 +1,17 @@
import request from "@/utils/request";
import request from '@/utils/request';
// /user/detail
export function getUserDetail(uid: number) {
return request.get("/user/detail", { params: { uid } });
return request.get('/user/detail', { params: { uid } });
}
// /user/playlist
export function getUserPlaylist(uid: number) {
return request.get("/user/playlist", { params: { uid } });
return request.get('/user/playlist', { params: { uid } });
}
// 播放历史
// /user/record?uid=32953014&type=1
export function getUserRecord(uid: number, type: number = 0) {
return request.get("/user/record", { params: { uid, type } });
return request.get('/user/record', { params: { uid, type } });
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,16 +1,25 @@
<template>
<n-drawer :show="show" height="70vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
<n-drawer
:show="show"
:height="isMobile ? '100vh' : '70vh'"
placement="bottom"
:drawer-style="{ backgroundColor: 'transparent' }"
>
<div class="music-page">
<i class="iconfont icon-icon_error music-close" @click="close"></i>
<div class="music-title">{{ name }}</div>
<div class="music-title text-el">{{ name }}</div>
<!-- 歌单歌曲列表 -->
<div class="music-list">
<n-scrollbar >
<div v-for="(item, index) in songList" :key="item.id" :class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)">
<SongItem :item="formatDetail(item)" @play="handlePlay" />
<n-scrollbar>
<div
v-for="(item, index) in songList"
:key="item.id"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)"
>
<song-item :item="formatDetail(item)" @play="handlePlay" />
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
</div>
</div>
@@ -18,40 +27,42 @@
</template>
<script setup lang="ts">
import { useStore } from 'vuex'
import { setAnimationClass, setAnimationDelay } from "@/utils";
import SongItem from "@/components/common/SongItem.vue";
import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue';
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
import PlayBottom from './common/PlayBottom.vue';
const store = useStore()
const store = useStore();
const props = defineProps<{
show: boolean;
name: string;
songList: any[]
}>()
const emit = defineEmits(['update:show'])
songList: any[];
}>();
const emit = defineEmits(['update:show']);
const formatDetail = computed(() => (detail: any) => {
let song = {
const song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
}
};
detail.song = song
detail.picUrl = detail.al.picUrl
return detail
})
detail.song = song;
detail.picUrl = detail.al.picUrl;
return detail;
});
const handlePlay = (item: any) => {
const tracks = props.songList || []
store.commit('setPlayList', tracks)
}
const handlePlay = () => {
const tracks = props.songList || [];
store.commit('setPlayList', tracks);
};
const close = () => {
emit('update:show', false)
}
emit('update:show', false);
};
</script>
<style scoped lang="scss">
@@ -72,4 +83,10 @@ const close = () => {
height: calc(100% - 60px);
}
}
</style>
.mobile {
.music-page {
@apply px-4;
}
}
</style>

View File

@@ -1,39 +1,37 @@
<template>
<!-- 歌单分类列表 -->
<div class="play-list-type">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
<div>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span
class="play-list-type-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
v-show="isShowAllPlaylistCategory || index <= 19"
@click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span>
</template>
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="
setAnimationDelay(
!isShowAllPlaylistCategory
? 25
: playlistCategory?.sub.length || 100 + 30
)
"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
</div>
<!-- 歌单分类列表 -->
<div class="play-list-type">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
<div>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span
v-show="isShowAllPlaylistCategory || index <= 19"
class="play-list-type-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
@click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span
>
</template>
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
>
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { getPlaylistCategory } from "@/api/home";
import type { IPlayListSort } from "@/type/playlist";
import { setAnimationDelay, setAnimationClass } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { getPlaylistCategory } from '@/api/home';
import type { IPlayListSort } from '@/type/playlist';
import { setAnimationClass, setAnimationDelay } from '@/utils';
// 歌单分类
const playlistCategory = ref<IPlayListSort>();
// 是否显示全部歌单分类
@@ -41,39 +39,45 @@ const isShowAllPlaylistCategory = ref<boolean>(false);
// 加载歌单分类
const loadPlaylistCategory = async () => {
const { data } = await getPlaylistCategory();
playlistCategory.value = data;
const { data } = await getPlaylistCategory();
playlistCategory.value = data;
};
const router = useRouter();
const handleClickPlaylistType = (type: any) => {
router.push({
path: "/list",
query: {
type: type,
}
});
const handleClickPlaylistType = (type: string) => {
router.push({
path: '/list',
query: {
type,
},
});
};
// 页面初始化
onMounted(() => {
loadPlaylistCategory();
loadPlaylistCategory();
});
</script>
<style lang="scss" scoped>
.title {
@apply text-lg font-bold mb-4;
@apply text-lg font-bold mb-4;
}
.play-list-type {
width: 250px;
@apply mx-6;
&-item,
&-showall {
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
background-color: #1a1a1a;
}
&-showall {
@apply block text-center;
}
width: 250px;
@apply mx-6;
&-item,
&-showall {
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
background-color: #1a1a1a;
}
&-showall {
@apply block text-center;
}
}
.mobile {
.play-list-type {
@apply mx-0 w-full;
}
}
</style>

View File

@@ -1,86 +1,86 @@
<template>
<div class="recommend-album">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
<div class="recommend-album-list">
<template v-for="(item,index) in albumData?.albums" :key="item.id">
<div
v-if="index < 6"
class="recommend-album-list-item"
:class="setAnimationClass('animate__backInUp')"
:style="setAnimationDelay(index, 100)"
@click="handleClick(item)"
>
<n-image
class="recommend-album-list-item-img"
:src="getImgUrl( item.blurPicUrl, '200y200')"
lazy
preview-disabled
/>
<div class="recommend-album-list-item-content">{{ item.name }}</div>
</div>
</template>
<div class="recommend-album">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
<div class="recommend-album-list">
<template v-for="(item, index) in albumData?.albums" :key="item.id">
<div
v-if="index < 6"
class="recommend-album-list-item"
:class="setAnimationClass('animate__backInUp')"
:style="setAnimationDelay(index, 100)"
@click="handleClick(item)"
>
<n-image
class="recommend-album-list-item-img"
:src="getImgUrl(item.blurPicUrl, '200y200')"
lazy
preview-disabled
/>
<div class="recommend-album-list-item-content">{{ item.name }}</div>
</div>
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
</template>
</div>
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
</div>
</template>
<script lang="ts" setup>
import { getNewAlbum } from "@/api/home"
import { ref, onMounted } from "vue";
import type { IAlbumNew } from "@/type/album"
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import { getAlbum } from "@/api/list";
import { onMounted, ref } from 'vue';
import { getNewAlbum } from '@/api/home';
import { getAlbum } from '@/api/list';
import type { IAlbumNew } from '@/type/album';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const albumData = ref<IAlbumNew>()
const albumData = ref<IAlbumNew>();
const loadAlbumList = async () => {
const { data } = await getNewAlbum();
albumData.value = data
}
const { data } = await getNewAlbum();
albumData.value = data;
};
const showMusic = ref(false)
const songList = ref([])
const albumName = ref('')
const showMusic = ref(false);
const songList = ref([]);
const albumName = ref('');
const handleClick = async (item:any) => {
albumName.value = item.name
showMusic.value = true
const res = await getAlbum(item.id)
songList.value = res.data.songs.map((song:any)=>{
song.al.picUrl = song.al.picUrl || item.picUrl
return song
})
}
const handleClick = async (item: any) => {
albumName.value = item.name;
showMusic.value = true;
const res = await getAlbum(item.id);
songList.value = res.data.songs.map((song: any) => {
song.al.picUrl = song.al.picUrl || item.picUrl;
return song;
});
};
onMounted(() => {
loadAlbumList()
})
loadAlbumList();
});
</script>
<style lang="scss" scoped>
.recommend-album {
@apply flex-1 mx-5;
.title {
@apply text-lg font-bold mb-4;
}
@apply flex-1 mx-5;
.title {
@apply text-lg font-bold mb-4;
}
.recommend-album-list {
@apply grid grid-cols-2 grid-rows-3 gap-2;
&-item {
@apply rounded-xl overflow-hidden relative;
&-img {
@apply rounded-xl transition w-full h-full;
}
&:hover img {
filter: brightness(50%);
}
&-content {
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
}
&-content:hover {
opacity: 1;
}
}
.recommend-album-list {
@apply grid grid-cols-2 grid-rows-3 gap-2;
&-item {
@apply rounded-xl overflow-hidden relative;
&-img {
@apply rounded-xl transition w-full h-full;
}
&:hover img {
filter: brightness(50%);
}
&-content {
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
}
&-content:hover {
opacity: 1;
}
}
}
}
</style>

View File

@@ -1,85 +1,141 @@
<template>
<!-- 推荐歌手 -->
<!-- 推荐歌手 -->
<n-scrollbar :size="100" :x-scrollable="true">
<div class="recommend-singer">
<div class="recommend-singer-list">
<div
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
v-for="(item, index) in hotSingerData?.artists"
:style="setAnimationDelay(index, 100)"
:key="item.id"
>
<div
:style="setBackgroundImg(getImgUrl(item.picUrl,'300y300'))"
class="recommend-singer-item-bg"
></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10"
>{{ item.musicSize }}</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
</div>
</div>
<div class="recommend-singer-list">
<div
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(0, 100)"
>
<div
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '300y300'))"
class="recommend-singer-item-bg"
></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
@click="showMusic = true"
>
<div class="font-bold text-xl">每日推荐</div>
<div class="mt-2">
<p v-for="item in dayRecommendData?.dailySongs.slice(0, 5)" :key="item.id" class="text-el">
{{ item.name }}
<br />
</p>
</div>
</div>
</div>
<div
v-for="(item, index) in hotSingerData?.artists.slice(0, 4)"
:key="item.id"
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(index + 1, 100)"
>
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '300y300'))" class="recommend-singer-item-bg"></div>
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">{{ item.musicSize }}</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
<div class="recommend-singer-item-info-name text-el">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<music-list
v-if="dayRecommendData?.dailySongs.length"
v-model:show="showMusic"
name="每日推荐列表"
:song-list="dayRecommendData?.dailySongs"
/>
</div>
</n-scrollbar>
</template>
<script lang="ts" setup>
import { setBackgroundImg, setAnimationDelay, setAnimationClass,getImgUrl } from "@/utils";
import { onMounted, ref } from "vue";
import { getHotSinger } from "@/api/home";
import type { IHotSinger } from "@/type/singer";
import router from "@/router";
import { onMounted, ref } from 'vue';
import { getDayRecommend, getHotSinger } from '@/api/home';
import router from '@/router';
import { IDayRecommend } from '@/type/day_recommend';
import type { IHotSinger } from '@/type/singer';
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
// 歌手信息
const hotSingerData = ref<IHotSinger>();
const dayRecommendData = ref<IDayRecommend>();
const showMusic = ref(false);
// // 加载推荐歌手
// const loadSingerList = async () => {
// const { data } = await getHotSinger({ offset: 0, limit: 5 });
// hotSingerData.value = data;
// };
//加载推荐歌手
const loadSingerList = async () => {
const { data } = await getHotSinger({ offset: 0, limit: 5 });
hotSingerData.value = data;
};
// const loadDayRecommend = async () => {
// const { data } = await getDayRecommend();
// dayRecommendData.value = data.data;
// };
// 页面初始化
onMounted(() => {
loadSingerList();
onMounted(async () => {
try {
const [{ data: singerData }, { data: dayRecommend }] = await Promise.all([
getHotSinger({ offset: 0, limit: 5 }),
getDayRecommend(),
]);
hotSingerData.value = singerData;
dayRecommendData.value = dayRecommend.data;
} catch (error) {
console.error('error', error);
}
});
const toSearchSinger = (keyword: string) => {
router.push({
path: "/search",
query: {
keyword: keyword,
},
});
router.push({
path: '/search',
query: {
keyword,
},
});
};
</script>
<style lang="scss" scoped>
.recommend-singer {
&-list {
@apply flex;
height: 280px;
&-list {
@apply flex;
height: 280px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between overflow-hidden;
&-bg {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(60%);
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
&-bg {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(80%);
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
}
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
}
}
}
}
.mobile .recommend-singer {
&-list {
height: 180px;
@apply ml-4;
}
&-item {
@apply p-4 rounded-xl;
&-bg {
@apply rounded-xl;
}
}
}
</style>

View File

@@ -1,19 +1,10 @@
<template>
<div class="recommend-music">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">
本周最热音乐
</div>
<div
class="recommend-music-list"
:class="setAnimationClass('animate__bounceInUp')"
v-show="recommendMusic?.result"
>
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">本周最热音乐</div>
<div v-show="recommendMusic?.result" class="recommend-music-list" :class="setAnimationClass('animate__bounceInUp')">
<!-- 推荐音乐列表 -->
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
<div
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)"
>
<div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
<song-item :item="item" @play="handlePlay" />
</div>
</template>
@@ -22,30 +13,32 @@
</template>
<script lang="ts" setup>
import { getRecommendMusic } from '@/api/home'
import type { IRecommendMusic } from '@/type/music'
import { setAnimationClass, setAnimationDelay } from '@/utils'
import SongItem from './common/SongItem.vue'
import { useStore } from 'vuex'
import { useStore } from 'vuex';
import { getRecommendMusic } from '@/api/home';
import type { IRecommendMusic } from '@/type/music';
import { setAnimationClass, setAnimationDelay } from '@/utils';
import SongItem from './common/SongItem.vue';
const store = useStore();
// 推荐歌曲
const recommendMusic = ref<IRecommendMusic>()
const recommendMusic = ref<IRecommendMusic>();
// 加载推荐歌曲
const loadRecommendMusic = async () => {
const { data } = await getRecommendMusic({ limit: 10 })
recommendMusic.value = data
}
const { data } = await getRecommendMusic({ limit: 10 });
recommendMusic.value = data;
};
// 页面初始化
onMounted(() => {
loadRecommendMusic()
})
loadRecommendMusic();
});
const handlePlay = (item: any) => {
store.commit('setPlayList', recommendMusic.value?.result)
}
const handlePlay = () => {
store.commit('setPlayList', recommendMusic.value?.result);
};
</script>
<style lang="scss" scoped>

View File

@@ -1,42 +1,40 @@
<script lang="ts" setup>
import { setAnimationClass, setAnimationDelay } from "@/utils";
const props = defineProps({
showPop: {
type: Boolean,
default: false
},
showClose: {
type: Boolean,
default: true
},
})
const musicFullClass = computed(() => {
if (props.showPop) {
return setAnimationClass('animate__fadeInUp')
} else {
return setAnimationClass('animate__fadeOutDown')
}
})
</script>
<template>
<div class="pop-page" v-show="props.showPop" :class="musicFullClass">
<i class="iconfont icon-icon_error close" v-if="props.showClose"></i>
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.pop-page {
height: 800px;
@apply absolute top-4 left-0 w-full;
background-color: #000000f0;
.close {
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
}
}
</style>
<script lang="ts" setup>
import { setAnimationClass } from '@/utils';
const props = defineProps({
showPop: {
type: Boolean,
default: false,
},
showClose: {
type: Boolean,
default: true,
},
});
const musicFullClass = computed(() => {
if (props.showPop) {
return setAnimationClass('animate__fadeInUp');
}
return setAnimationClass('animate__fadeOutDown');
});
</script>
<template>
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
<i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.pop-page {
height: 800px;
@apply absolute top-4 left-0 w-full;
background-color: #000000f0;
.close {
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
}
}
</style>

View File

@@ -1,15 +1,22 @@
<template>
<div class="bottom" v-if="isPlay"></div>
<div v-if="isPlay" class="bottom" :style="{ height }"></div>
</template>
<script setup lang="ts">
import { useStore } from 'vuex';
const store = useStore()
const isPlay = computed(() => store.state.isPlay as boolean)
const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean);
defineProps({
height: {
type: String,
default: undefined,
},
});
</script>
<style lang="scss" scoped>
.bottom{
.bottom {
@apply h-28;
}
</style>
</style>

View File

@@ -0,0 +1,70 @@
<template>
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
<div class="mv-detail">
<video :src="url" controls autoplay></video>
<div class="mv-detail-title">
<div class="title">{{ title }}</div>
<button @click="close">
<i class="iconfont icon-xiasanjiaoxing"></i>
</button>
</div>
</div>
</n-drawer>
</template>
<script setup lang="ts">
import { useStore } from 'vuex';
const props = defineProps<{
show: boolean;
title: string;
url: string;
}>();
const store = useStore();
watch(
() => props.show,
(val) => {
if (val) {
store.commit('setIsPlay', false);
store.commit('setPlayMusic', false);
}
},
);
const emit = defineEmits(['update:show']);
const close = () => {
emit('update:show', false);
};
</script>
<style scoped lang="scss">
.mv-detail {
@apply w-full h-full bg-black relative;
&-title {
@apply absolute w-full left-0 flex justify-between h-16 px-6 py-2 text-xl font-bold items-center z-50 transition-all duration-300 ease-in-out -top-24;
background: linear-gradient(0, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
button .icon-xiasanjiaoxing {
@apply text-3xl;
}
button:hover {
@apply text-green-400;
}
}
video {
@apply w-full h-full;
}
video:hover + .mv-detail-title {
@apply top-0;
}
.mv-detail-title:hover {
@apply top-0;
}
}
</style>

View File

@@ -1,70 +1,83 @@
<template>
<div class="search-item" @click="handleClick">
<div class="search-item-img">
<n-image
:src="getImgUrl(item.picUrl, 'album')"
lazy
preview-disabled
/>
<n-image :src="getImgUrl(item.picUrl, '200y200')" lazy preview-disabled />
</div>
<div class="search-item-info">
<div class="search-item-name">{{ item.name }}</div>
<div class="search-item-artist">{{ item.desc}}</div>
<div class="search-item-artist">{{ item.desc }}</div>
</div>
<MusicList v-model:show="showMusic" :name="item.name" :song-list="songList" />
<MusicList
v-if="['专辑', 'playlist'].includes(item.type)"
v-model:show="showPop"
:name="item.name"
:song-list="songList"
/>
<PlayVideo v-if="item.type === 'mv'" v-model:show="showPop" :title="item.name" :url="url" />
</div>
</template>
<script setup lang="ts">
import { getImgUrl } from '@/utils'
import type {Album} from '@/type/album'
import { getAlbum } from '@/api/list';
import { getAlbum, getListDetail } from '@/api/list';
import { getMvUrl } from '@/api/mv';
import { getImgUrl } from '@/utils';
const props = defineProps<{
item: {
picUrl: string
name: string
desc: string
type: string
[key: string]: any
}
}>()
picUrl: string;
name: string;
desc: string;
type: string;
[key: string]: any;
};
}>();
const songList = ref([])
const url = ref('');
const showMusic = ref(false)
const songList = ref<any[]>([]);
const showPop = ref(false);
const handleClick = async () => {
showMusic.value = true
if(props.item.type === '专辑'){
const res = await getAlbum(props.item.id)
songList.value = res.data.songs.map((song:any)=>{
song.al.picUrl = song.al.picUrl || props.item.picUrl
return song
})
if (props.item.type === '专辑') {
showPop.value = true;
const res = await getAlbum(props.item.id);
songList.value = res.data.songs.map((song: any) => {
song.al.picUrl = song.al.picUrl || props.item.picUrl;
return song;
});
}
}
if (props.item.type === 'playlist') {
showPop.value = true;
const res = await getListDetail(props.item.id);
songList.value = res.data.playlist.tracks;
}
if (props.item.type === 'mv') {
const res = await getMvUrl(props.item.id);
url.value = res.data.data.url;
showPop.value = true;
}
};
</script>
<style scoped lang="scss">
.search-item{
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
.search-item {
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition cursor-pointer;
margin: 0 10px;
.search-item-img{
.search-item-img {
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
}
.search-item-info{
&-name{
.search-item-info {
&-name {
@apply text-white text-sm text-center;
}
&-artist{
&-artist {
@apply text-gray-400 text-xs text-center;
}
}
}
</style>
</style>

View File

@@ -1,27 +1,14 @@
<template>
<div class="song-item" :class="{'song-mini': mini}">
<n-image
v-if="item.picUrl "
:src="getImgUrl( item.picUrl, '40y40')"
class="song-item-img"
lazy
preview-disabled
/>
<div class="song-item" :class="{ 'song-mini': mini }">
<n-image v-if="item.picUrl" :src="getImgUrl(item.picUrl, '40y40')" class="song-item-img" lazy preview-disabled />
<div class="song-item-content">
<div class="song-item-content-title">
<n-ellipsis class="text-ellipsis" line-clamp="1">{{
item.name
}}</n-ellipsis>
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
</div>
<div class="song-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span
v-for="(artists, artistsindex) in item.song.artists"
:key="artistsindex"
>{{ artists.name
}}{{
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
}}</span
<span v-for="(artists, artistsindex) in item.song.artists" :key="artistsindex"
>{{ artists.name }}{{ artistsindex < item.song.artists.length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
@@ -31,8 +18,8 @@
<i class="iconfont icon-likefill"></i>
</div>
<div
class="song-item-operating-play bg-black"
:class="isPlaying ? 'bg-green-600' : ''"
class="song-item-operating-play bg-black animate__animated"
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
@click="playMusicEvent(item)"
>
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
@@ -43,40 +30,48 @@
</template>
<script lang="ts" setup>
import { useStore } from 'vuex'
import type { SongResult } from '@/type/music'
import { getImgUrl } from '@/utils'
import { useStore } from 'vuex';
const props = withDefaults(defineProps<{
item: SongResult
mini?: boolean
}>(), {
mini: false
})
import type { SongResult } from '@/type/music';
import { getImgUrl } from '@/utils';
const store = useStore()
const props = withDefaults(
defineProps<{
item: SongResult;
mini?: boolean;
}>(),
{
mini: false,
},
);
const play = computed(() => store.state.play as boolean)
const store = useStore();
const playMusic = computed(() => store.state.playMusic)
const play = computed(() => store.state.play as boolean);
const playMusic = computed(() => store.state.playMusic);
const playLoading = computed(() => playMusic.value.id === props.item.id && playMusic.value.playLoading);
// 判断是否为正在播放的音乐
const isPlaying = computed(() => {
return playMusic.value.id == props.item.id
})
return playMusic.value.id === props.item.id;
});
const emits = defineEmits(['play'])
const emits = defineEmits(['play']);
// 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = (item: any) => {
store.commit('setPlay', item)
store.commit('setIsPlay', true)
emits('play', item)
}
const playMusicEvent = async (item: SongResult) => {
if (playMusic.value.id === item.id) {
return;
}
await store.commit('setPlay', item);
store.commit('setIsPlay', true);
emits('play', item);
};
</script>
<style lang="scss" scoped>
// 配置文字不可选中
.text-ellipsis {
width: 100%;
@@ -115,35 +110,36 @@ const playMusicEvent = (item: any) => {
}
&-play {
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
animation-iteration-count: infinite;
}
}
}
.song-mini{
.song-mini {
@apply p-2 rounded-2xl;
.song-item{
.song-item {
@apply p-0;
&-img{
&-img {
@apply w-10 h-10 mr-2;
}
&-content{
&-content {
@apply flex-1;
&-title{
&-title {
@apply text-sm;
}
&-name{
&-name {
@apply text-xs;
}
}
&-operating{
&-operating {
@apply pl-2;
.iconfont{
.iconfont {
@apply text-base;
}
&-like{
&-like {
@apply mr-1;
}
&-play{
&-play {
@apply w-8 h-8;
}
}

View File

@@ -19,7 +19,7 @@ export const USER_SET_OPTIONS = [
label: '设置',
key: 'set',
},
]
];
export const SEARCH_TYPES = [
{
@@ -30,36 +30,36 @@ export const SEARCH_TYPES = [
label: '专辑',
key: 10,
},
{
label: '歌手',
key: 100,
},
// {
// label: '歌手',
// key: 100,
// },
{
label: '歌单',
key: 1000,
},
{
label: '用户',
key: 1002,
},
// {
// label: '用户',
// key: 1002,
// },
{
label: 'MV',
key: 1004,
},
{
label: '歌词',
key: 1006,
},
{
label: '电台',
key: 1009,
},
{
label: '视频',
key: 1014,
},
{
label: '综合',
key: 1018,
},
]
// {
// label: '歌词',
// key: 1006,
// },
// {
// label: '电台',
// key: 1009,
// },
// {
// label: '视频',
// key: 1014,
// },
// {
// label: '综合',
// key: 1018,
// },
];

10
src/electron.d.ts vendored
View File

@@ -1,10 +1,10 @@
declare global {
interface Window {
electronAPI: {
minimize: () => void
maximize: () => void
close: () => void
dragStart: () => void
}
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: () => void;
};
}
}

View File

@@ -1,39 +1,39 @@
// musicHistoryHooks
import { RemovableRef, useLocalStorage } from '@vueuse/core'
import type { SongResult } from '@/type/music'
import { useLocalStorage } from '@vueuse/core';
import type { SongResult } from '@/type/music';
export const useMusicHistory = () => {
const musicHistory = useLocalStorage<SongResult[]>('musicHistory', [])
const musicHistory = useLocalStorage<SongResult[]>('musicHistory', []);
const addMusic = (music: SongResult) => {
const index = musicHistory.value.findIndex((item) => item.id === music.id)
const index = musicHistory.value.findIndex((item) => item.id === music.id);
if (index !== -1) {
musicHistory.value[index].count =
(musicHistory.value[index].count || 0) + 1
musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0])
musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1;
musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]);
} else {
musicHistory.value.unshift({ ...music, count: 1 })
musicHistory.value.unshift({ ...music, count: 1 });
}
}
};
const delMusic = (music: any) => {
const index = musicHistory.value.findIndex((item) => item.id === music.id)
const delMusic = (music: SongResult) => {
const index = musicHistory.value.findIndex((item) => item.id === music.id);
if (index !== -1) {
musicHistory.value.splice(index, 1)
musicHistory.value.splice(index, 1);
}
}
const musicList = ref(musicHistory.value)
};
const musicList = ref(musicHistory.value);
watch(
() => musicHistory.value,
() => {
musicList.value = musicHistory.value
}
)
musicList.value = musicHistory.value;
},
);
return {
musicHistory,
musicList,
addMusic,
delMusic,
}
}
};
};

View File

@@ -1,104 +1,170 @@
import { getMusicLrc } from '@/api/music'
import { ILyric } from '@/type/lyric'
import { getIsMc } from '@/utils'
import { getMusicLrc } from '@/api/music';
import { ILyric } from '@/type/lyric';
interface ILrcData {
text: string
trText: string
text: string;
trText: string;
}
const lrcData = ref<ILyric>()
const newLrcIndex = ref<number>(0)
const lrcArray = ref<Array<ILrcData>>([])
const lrcTimeArray = ref<Array<Number>>([])
export const lrcData = ref<ILyric>();
export const newLrcIndex = ref<number>(0);
export const lrcArray = ref<Array<ILrcData>>([]);
export const lrcTimeArray = ref<Array<Number>>([]);
const parseTime = (timeString: string) => {
const [minutes, seconds] = timeString.split(':')
return parseInt(minutes) * 60 + parseFloat(seconds)
}
export const parseTime = (timeString: string) => {
const [minutes, seconds] = timeString.split(':');
return Number(minutes) * 60 + Number(seconds);
};
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
function parseLyricLine(lyricLine: string) {
// [00:00.00] 作词 : 长友美知惠/
const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''
const time = parseTime(timeText)
const text = lyricLine.replace(LRC_REGEX, '').trim()
return { time, text }
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
}
interface ILyricText {
text: string
trText: string
text: string;
trText: string;
}
function parseLyrics(lyricsString: string) {
const lines = lyricsString.split('\n')
const lyrics: Array<ILyricText> = []
const times: number[] = []
const lines = lyricsString.split('\n');
const lyrics: Array<ILyricText> = [];
const times: number[] = [];
lines.forEach((line) => {
const { time, text } = parseLyricLine(line)
times.push(time)
lyrics.push({ text, trText: '' })
})
return { lyrics, times }
const { time, text } = parseLyricLine(line);
times.push(time);
lyrics.push({ text, trText: '' });
});
return { lyrics, times };
}
const loadLrc = async (playMusicId: number): Promise<void> => {
export const loadLrc = async (playMusicId: number): Promise<void> => {
try {
const { data } = await getMusicLrc(playMusicId)
const { lyrics, times } = parseLyrics(data.lrc.lyric)
lrcTimeArray.value = times
lrcArray.value = lyrics
const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric);
let tlyric: {
[key: string]: string;
} = {};
if (data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tlyric = tLyrics.reduce((acc: any, cur, index) => {
acc[tTimes[index]] = cur.text;
return acc;
}, {});
}
if (Object.keys(tlyric).length) {
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] : '';
});
}
lrcTimeArray.value = times;
lrcArray.value = lyrics;
} catch (err) {
console.error('err', err)
console.error('err', err);
}
}
};
// 歌词矫正时间Correction time
const correctionTime = ref(0.4)
const correctionTime = ref(0.4);
// 增加矫正时间
const addCorrectionTime = (time: number) => {
correctionTime.value += time
}
export const addCorrectionTime = (time: number) => {
correctionTime.value += time;
};
// 减少矫正时间
const reduceCorrectionTime = (time: number) => {
correctionTime.value -= time
}
export const reduceCorrectionTime = (time: number) => {
correctionTime.value -= time;
};
const isCurrentLrc = (index: any, time: number) => {
const currentTime = Number(lrcTimeArray.value[index])
const nextTime = Number(lrcTimeArray.value[index + 1])
const nowTime = time + correctionTime.value
const isTrue = nowTime > currentTime && nowTime < nextTime
export const isCurrentLrc = (index: number, time: number) => {
const currentTime = Number(lrcTimeArray.value[index]);
const nextTime = Number(lrcTimeArray.value[index + 1]);
const nowTime = time + correctionTime.value;
const isTrue = nowTime > currentTime && nowTime < nextTime;
if (isTrue) {
newLrcIndex.value = index
newLrcIndex.value = index;
}
return isTrue
}
return isTrue;
};
const nowTime = ref(0)
const allTime = ref(0)
export const nowTime = ref(0);
export const allTime = ref(0);
export const nowIndex = ref(0);
export const getLrcIndex = (time: number) => {
for (let i = 0; i < lrcTimeArray.value.length; i++) {
if (isCurrentLrc(i, time)) {
nowIndex.value = i || nowIndex.value;
return i;
}
}
return nowIndex.value;
};
// 设置当前播放时间
const setAudioTime = (index: any, audio: HTMLAudioElement) => {
audio.currentTime = lrcTimeArray.value[index] as number
audio.play()
}
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
audio.currentTime = lrcTimeArray.value[index] as number;
audio.play();
};
export {
lrcData,
lrcArray,
lrcTimeArray,
newLrcIndex,
loadLrc,
isCurrentLrc,
addCorrectionTime,
reduceCorrectionTime,
setAudioTime,
nowTime,
allTime,
}
// 计算这个歌词的播放时间
const getLrcTime = (index: number) => {
return Number(lrcTimeArray.value[index]);
};
// 获取当前播放的歌词
export const getCurrentLrc = () => {
const index = getLrcIndex(nowTime.value);
const currentLrc = lrcArray.value[index];
const nextLrc = lrcArray.value[index + 1];
return { currentLrc, nextLrc };
};
// 获取一句歌词播放时间是 几秒到几秒
export const getLrcTimeRange = (index: number) => {
const currentTime = Number(lrcTimeArray.value[index]);
const nextTime = Number(lrcTimeArray.value[index + 1]);
return { currentTime, nextTime };
};
export const sendLyricToWin = (isPlay: boolean = true) => {
try {
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
let lyricWinData = null;
if (lrcArray.value.length > 0) {
const nowIndex = getLrcIndex(nowTime.value);
const { currentLrc, nextLrc } = getCurrentLrc();
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
lyricWinData = {
currentLrc,
nextLrc,
currentTime,
nextTime,
nowIndex,
lrcTimeArray: lrcTimeArray.value,
lrcArray: lrcArray.value,
nowTime: nowTime.value,
allTime: allTime.value,
startCurrentTime: getLrcTime(nowIndex),
isPlay,
};
const windowData = window as any;
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
}
} catch (error) {
console.error('error', error);
}
};
export const openLyric = () => {
const windowData = window as any;
windowData.electronAPI.openLyric();
sendLyricToWin();
};

View File

@@ -8,4 +8,8 @@
.n-image img {
background-color: #111111;
width: 100%;
}
}
.text-el {
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
}

View File

@@ -1,144 +1,163 @@
<template>
<div class="layout-page">
<div class="layout-main">
<title-bar v-if="isElectron" />
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
<!-- 侧边菜单栏 -->
<app-menu class="menu" :menus="menus" />
<div class="main">
<!-- 搜索栏 -->
<search-bar />
<!-- 主页面路由 -->
<div class="main-content bg-black pb-" :native-scrollbar="false" :class="isPlay ? 'pb-20' : ''">
<n-message-provider>
<router-view class="main-page" v-slot="{ Component }" :class="route.meta.noScroll? 'pr-3' : ''">
<template v-if="route.meta.noScroll">
<keep-alive v-if="!route.meta.noKeepAlive">
<component :is="Component" />
</keep-alive>
<component v-else :is="Component"/>
</template>
<template v-else>
<n-scrollbar>
<keep-alive v-if="!route.meta.noKeepAlive">
<component :is="Component" />
</keep-alive>
<component v-else :is="Component"/>
</n-scrollbar>
</template>
</router-view>
</n-message-provider>
</div>
</div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-if="isPlay" />
<div class="layout-page">
<div class="layout-main">
<title-bar v-if="isElectron" />
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
<!-- 侧边菜单栏 -->
<app-menu v-if="!isMobile" class="menu" :menus="menus" />
<div class="main">
<!-- 搜索栏 -->
<search-bar />
<!-- 主页面路由 -->
<div class="main-content bg-black" :native-scrollbar="false">
<n-message-provider>
<router-view
v-slot="{ Component }"
class="main-page"
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
>
<keep-alive :include="keepAliveInclude">
<component :is="Component" />
</keep-alive>
</router-view>
</n-message-provider>
</div>
<play-bottom height="5rem" />
<app-menu v-if="isMobile" class="menu" :menus="menus" />
</div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-if="isPlay" />
</div>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import PlayBottom from '@/components/common/PlayBottom.vue';
import homeRouter from '@/router/home';
import { isMobile } from '@/utils';
const keepAliveInclude = computed(() =>
homeRouter
.filter((item) => {
return item.meta.keepAlive;
})
.map((item) => {
// return item.name;
// 首字母大写
return item.name.charAt(0).toUpperCase() + item.name.slice(1);
}),
);
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean)
const menus = store.state.menus;
const play = computed(() => store.state.play as boolean)
const isPlay = computed(() => store.state.isPlay as boolean);
const { menus } = store.state;
const play = computed(() => store.state.play as boolean);
const route = useRoute()
const route = useRoute();
const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement
}
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
};
const windowData = window as any
const windowData = window as any;
const isElectron = computed(() => {
return !!windowData.electronAPI
})
return !!windowData.electronAPI;
});
onMounted(() => {
// 监听音乐是否播放
watch(
() => play.value,
value => {
(value) => {
if (value && audio.value) {
audioPlay()
audioPlay();
} else {
audioPause()
audioPause();
}
}
)
},
);
document.onkeyup = (e) => {
switch (e.code) {
case 'Space':
playMusicEvent()
playMusicEvent();
break;
default:
}
}
};
// 按下键盘按钮监听
document.onkeydown = (e) => {
switch (e.code) {
case 'Space':
return false
return false;
default:
}
}
})
};
});
const audioPlay = () => {
if (audio.value) {
audio.value.play()
audio.value.play();
}
}
};
const audioPause = () => {
if (audio.value) {
audio.value.pause()
audio.value.pause();
}
}
};
const playMusicEvent = async () => {
if (play.value) {
store.commit('setPlayMusic', false)
store.commit('setPlayMusic', false);
} else {
store.commit('setPlayMusic', true)
store.commit('setPlayMusic', true);
}
}
};
</script>
<style lang="scss" scoped>
.layout-page {
width: 100vw;
height: 100vh;
@apply flex justify-center items-center overflow-hidden;
width: 100vw;
height: 100vh;
@apply flex justify-center items-center overflow-hidden;
}
.layout-main {
@apply bg-black text-white shadow-xl flex flex-col relative;
@apply bg-black text-white shadow-xl flex flex-col relative;
height: 100%;
width: 100%;
overflow: hidden;
&-page {
@apply flex flex-1 overflow-hidden;
}
.main {
@apply flex-1 box-border flex flex-col overflow-hidden;
height: 100%;
width: 100%;
overflow: hidden;
&-page{
@apply flex flex-1 overflow-hidden;
}
.menu {
width: 90px;
}
.main {
@apply flex-1 box-border flex flex-col;
height: 100%;
&-content {
@apply box-border flex-1 overflow-hidden;
}
}
:deep(.n-scrollbar-content){
@apply pr-3;
&-content {
@apply box-border flex-1 overflow-hidden;
}
}
// :deep(.n-scrollbar-content) {
// @apply pr-3;
// }
}
</style>
.mobile {
.layout-main {
&-page {
@apply pt-4;
}
}
}
</style>

View File

@@ -8,13 +8,9 @@
</div>
</div>
<div class="app-menu-list">
<div class="app-menu-item" v-for="(item,index) in menus">
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
<router-link class="app-menu-item-link" :to="item.path">
<i
class="iconfont app-menu-item-icon"
:style="iconStyle(index)"
:class="item.meta.icon"
></i>
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i>
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
</router-link>
</div>
@@ -24,44 +20,47 @@
</template>
<script lang="ts" setup>
import { useRoute } from "vue-router";
import { useRoute } from 'vue-router';
const props = defineProps({
isText: {
type: Boolean,
default: false
default: false,
},
size: {
type: String,
default: '26px'
default: '26px',
},
color: {
type: String,
default: '#aaa'
default: '#aaa',
},
selectColor: {
type: String,
default: '#10B981'
default: '#10B981',
},
menus: {
type: Array as any,
default: []
}
})
default: () => [],
},
});
const route = useRoute();
const path = ref(route.path);
watch(() => route.path, async newParams => {
path.value = newParams
})
watch(
() => route.path,
async (newParams) => {
path.value = newParams;
},
);
const iconStyle = (index: any) => {
let style = {
const iconStyle = (index: number) => {
const style = {
fontSize: props.size,
color: path.value === props.menus[index].path ? props.selectColor : props.color
}
return style
}
color: path.value === props.menus[index].path ? props.selectColor : props.color,
};
return style;
};
</script>
<style lang="scss" scoped>
@@ -83,4 +82,25 @@ const iconStyle = (index: any) => {
transform: scale(1.05);
transition: 0.2s ease-in-out;
}
</style>
.mobile {
.app-menu {
max-width: 100%;
width: 100vw;
position: relative;
z-index: 999999;
background-color: #000;
&-header {
display: none;
}
&-list {
@apply flex justify-between;
}
&-item {
&-link {
@apply my-4;
}
}
}
}
</style>

View File

@@ -1,33 +1,25 @@
<template>
<n-drawer
:show="musicFull"
height="100vh"
placement="bottom"
:drawer-style="{ backgroundColor: 'transparent' }"
>
<n-drawer :show="musicFull" height="100vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
<div id="drawer-target">
<div class="drawer-back" :class="{'paused': !isPlaying}" :style="{backgroundImage:`url(${getImgUrl(playMusic?.picUrl, '300y300')})`}"></div>
<div
class="drawer-back"
:class="{ paused: !isPlaying }"
:style="{ backgroundImage: `url(${getImgUrl(playMusic?.picUrl, '300y300')})` }"
></div>
<div class="music-img">
<n-image
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '300y300')"
class="img"
lazy
preview-disabled
/>
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
</div>
<div class="music-content">
<div class="music-content-name">{{ playMusic.song.name }}</div>
<div class="music-content-singer">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</div>
<n-layout
ref="lrcSider"
class="music-lrc"
style="height: 55vh"
ref="lrcSider"
:native-scrollbar="false"
@mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout"
@@ -38,7 +30,8 @@
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
@click="setAudioTime(index, audio)"
>
{{ item.text }}
<div>{{ item.text }}</div>
<div class="music-lrc-text-tr">{{ item.trText }}</div>
</div>
</template>
</n-layout>
@@ -53,20 +46,21 @@
</template>
<script setup lang="ts">
import type { SongResult } from '@/type/music'
import { getImgUrl } from '@/utils'
import { useStore } from 'vuex'
import { useStore } from 'vuex';
import {
addCorrectionTime,
isCurrentLrc,
lrcArray,
newLrcIndex,
isCurrentLrc,
addCorrectionTime,
nowTime,
reduceCorrectionTime,
setAudioTime,
nowTime,
} from '@/hooks/MusicHook'
} from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl } from '@/utils';
const store = useStore()
const store = useStore();
const props = defineProps({
musicFull: {
@@ -77,39 +71,36 @@ const props = defineProps({
type: HTMLAudioElement,
default: null,
},
})
const emit = defineEmits(['update:musicFull'])
});
// 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult)
const isPlaying = computed(() => store.state.play as boolean)
const playMusic = computed(() => store.state.playMusic as SongResult);
const isPlaying = computed(() => store.state.play as boolean);
// 获取歌词滚动dom
const lrcSider = ref<any>(null)
const isMouse = ref(false)
const lrcSider = ref<any>(null);
const isMouse = ref(false);
// 歌词滚动方法
const lrcScroll = () => {
if (props.musicFull && !isMouse.value) {
let top = newLrcIndex.value * 50 - 225
lrcSider.value.scrollTo({ top: top, behavior: 'smooth' })
const top = newLrcIndex.value * 60 - 225;
lrcSider.value.scrollTo({ top, behavior: 'smooth' });
}
}
};
const mouseOverLayout = () => {
isMouse.value = true
}
isMouse.value = true;
};
const mouseLeaveLayout = () => {
setTimeout(() => {
isMouse.value = false
}, 3000)
}
isMouse.value = false;
}, 3000);
};
defineExpose({
lrcScroll,
})
});
</script>
<style scoped lang="scss">
@keyframes round {
0% {
transform: rotate(0deg);
@@ -118,7 +109,7 @@ defineExpose({
transform: rotate(360deg);
}
}
.drawer-back{
.drawer-back {
@apply absolute bg-cover bg-center opacity-70;
filter: blur(80px) brightness(80%);
z-index: -1;
@@ -161,28 +152,42 @@ defineExpose({
}
}
.music-content-time{
.music-content-time {
display: none;
@apply flex justify-center items-center;
@apply flex justify-center items-center;
}
.music-lrc {
background-color: inherit;
width: 500px;
height: 550px;
.now-text {
@apply text-red-500;
}
&-text {
@apply text-white text-lg flex justify-center items-center cursor-pointer;
height: 50px;
@apply text-white text-lg flex flex-col justify-center items-center cursor-pointer font-bold;
height: 60px;
transition: all 0.2s ease-out;
&:hover {
@apply font-bold text-xl text-red-500;
@apply font-bold text-red-500;
}
&-tr {
@apply text-sm font-normal;
}
}
}
}
.now-text {
@apply font-bold text-xl text-red-500;
.mobile {
#drawer-target {
@apply flex-col p-4 pt-8;
.music-img {
display: none;
}
.music-lrc {
height: calc(100vh - 260px) !important;
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<!-- 展开全屏 -->
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio.value as HTMLAudioElement)" />
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="audio.value as HTMLAudioElement" />
<!-- 底部播放栏 -->
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
<n-image
@@ -19,41 +19,32 @@
<div class="music-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</n-ellipsis>
</div>
</div>
<div class="music-buttons">
<div @click="handlePrev">
<div class="music-buttons-prev" @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<div class="music-buttons-next" @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div>
<div class="music-time">
<div class="time">{{ getNowTime }}</div>
<n-slider
v-model:value="timeSlider"
:step="0.05"
:tooltip="false"
></n-slider>
<n-slider v-model:value="timeSlider" :step="0.05" :tooltip="false"></n-slider>
<div class="time">{{ getAllTime }}</div>
</div>
<div class="audio-volume">
<div>
<i class="iconfont icon-notificationfill"></i>
</div>
<n-slider
v-model:value="volumeSlider"
:step="0.01"
:tooltip="false"
></n-slider>
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false"></n-slider>
</div>
<div class="audio-button">
<!-- <n-tooltip trigger="hover" :z-index="9999999">
@@ -68,12 +59,12 @@
</template>
解析播放
</n-tooltip> -->
<!-- <n-tooltip trigger="hover" :z-index="9999999">
<n-tooltip class="music-lyric" trigger="hover" :z-index="9999999">
<template #trigger>
<i class="iconfont icon-full" @click="setMusicFull"></i>
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
</template>
歌词
</n-tooltip> -->
</n-tooltip>
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
<template #trigger>
<n-tooltip trigger="manual" :z-index="9999999">
@@ -87,154 +78,143 @@
<div class="music-play-list-back"></div>
<n-scrollbar>
<div class="music-play-list-content">
<song-item v-for="(item, index) in playList" :key="item.id" :item="item" mini></song-item>
<song-item v-for="item in playList" :key="item.id" :item="item" mini></song-item>
</div>
</n-scrollbar>
</div>
</n-popover>
</div>
<!-- 播放音乐 -->
</div>
</template>
<script lang="ts" setup>
import type { SongResult } from '@/type/music'
import { secondToMinute, getImgUrl } from '@/utils'
import { useStore } from 'vuex'
import { setAnimationClass } from '@/utils'
import {
loadLrc,
nowTime,
allTime
} from '@/hooks/MusicHook'
import MusicFull from './MusicFull.vue'
import SongItem from '@/components/common/SongItem.vue'
import { useStore } from 'vuex';
const store = useStore()
import SongItem from '@/components/common/SongItem.vue';
import { allTime, loadLrc, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
import MusicFull from './MusicFull.vue';
const store = useStore();
// 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult)
const playMusic = computed(() => store.state.playMusic as SongResult);
// 是否播放
const play = computed(() => store.state.play as boolean)
const play = computed(() => store.state.play as boolean);
const playList = computed(() => store.state.playList as SongResult[])
const playList = computed(() => store.state.playList as SongResult[]);
const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement
}
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
};
watch(
() => store.state.playMusicUrl,
() => {
loadLrc(playMusic.value.id)
loadLrc(playMusic.value.id);
},
{ immediate: true }
)
{ immediate: true },
);
const audioPlay = () => {
if (audio.value) {
audio.value.play()
audio.value.play();
}
}
const audioPause = () => {
if (audio.value) {
audio.value.pause()
}
}
};
// 计算属性 获取当前播放时间的进度
const timeSlider = computed({
get: () => (nowTime.value / allTime.value) * 100,
set: (value) => {
if (!audio.value) return
audio.value.currentTime = (value * allTime.value) / 100
audioPlay()
store.commit('setPlayMusic', true)
if (!audio.value) return;
audio.value.currentTime = (value * allTime.value) / 100;
audioPlay();
store.commit('setPlayMusic', true);
},
})
});
// 音量条
const audioVolume = ref(1)
const audioVolume = ref(1);
const volumeSlider = computed({
get: () => audioVolume.value * 100,
set: (value) => {
if(!audio.value) return
audio.value.volume = value / 100
if (!audio.value) return;
audio.value.volume = value / 100;
},
})
});
// 获取当前播放时间
const getNowTime = computed(() => {
return secondToMinute(nowTime.value)
})
return secondToMinute(nowTime.value);
});
// 获取总时间
const getAllTime = computed(() => {
return secondToMinute(allTime.value)
})
return secondToMinute(allTime.value);
});
// 监听音乐播放 获取时间
const onAudio = () => {
if(audio.value){
audio.value.removeEventListener('timeupdate', handleGetAudioTime)
audio.value.removeEventListener('ended', handleEnded)
audio.value.addEventListener('timeupdate', handleGetAudioTime)
audio.value.addEventListener('ended', handleEnded)
if (audio.value) {
audio.value.removeEventListener('timeupdate', handleGetAudioTime);
audio.value.removeEventListener('ended', handleEnded);
audio.value.addEventListener('timeupdate', handleGetAudioTime);
audio.value.addEventListener('ended', handleEnded);
// 监听音乐播放暂停
audio.value.addEventListener('pause', () => {
store.commit('setPlayMusic', false)
})
store.commit('setPlayMusic', false);
});
audio.value.addEventListener('play', () => {
store.commit('setPlayMusic', true)
})
store.commit('setPlayMusic', true);
});
}
}
};
onAudio()
onAudio();
function handleEnded() {
store.commit('nextPlay')
store.commit('nextPlay');
}
function handlePrev() {
store.commit('prevPlay')
store.commit('prevPlay');
}
const MusicFullRef = ref<any>(null)
const MusicFullRef = ref<any>(null);
function handleGetAudioTime(this: any) {
function handleGetAudioTime(this: HTMLAudioElement) {
// 监听音频播放的实时时间事件
const audio = this as HTMLAudioElement
const audio = this as HTMLAudioElement;
// 获取当前播放时间
nowTime.value = Math.floor(audio.currentTime)
nowTime.value = Math.floor(audio.currentTime);
// 获取总时间
allTime.value = audio.duration
allTime.value = audio.duration;
// 获取音量
audioVolume.value = audio.volume
MusicFullRef.value?.lrcScroll()
audioVolume.value = audio.volume;
sendLyricToWin(store.state.isPlay);
MusicFullRef.value?.lrcScroll();
}
// 播放暂停按钮事件
const playMusicEvent = async () => {
if (play.value) {
store.commit('setPlayMusic', false)
store.commit('setPlayMusic', false);
} else {
store.commit('setPlayMusic', true)
store.commit('setPlayMusic', true);
}
}
};
const musicFull = ref(false)
const musicFull = ref(false);
// 设置musicFull
const setMusicFull = () => {
musicFull.value = !musicFull.value
}
musicFull.value = !musicFull.value;
};
</script>
<style lang="scss" scoped>
.text-ellipsis {
width: 100%;
}
@@ -243,7 +223,9 @@ const setMusicFull = () => {
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
z-index: 9999;
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
background-color: rgba(0, 0, 0, 0.747); .music-content {
background-color: rgba(0, 0, 0, 0.747);
animation-duration: 0.5s !important;
.music-content {
width: 140px;
@apply ml-4;
@@ -310,18 +292,49 @@ const setMusicFull = () => {
}
}
.music-play{
&-list{
.music-play {
&-list {
height: 50vh;
@apply relative rounded-3xl overflow-hidden;
&-back{
&-back {
backdrop-filter: blur(20px);
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
}
&-content{
&-content {
padding: 10px;
}
}
}
.mobile {
.music-play-bar {
@apply px-4;
bottom: 70px;
}
.music-time {
display: none;
}
.ri-netease-cloud-music-line {
display: none;
}
.audio-volume {
display: none;
}
.audio-button {
@apply mx-0;
}
.music-buttons {
@apply m-0;
&-prev,
&-next {
display: none;
}
&-play {
@apply m-0;
}
}
.music-content {
flex: 1;
}
}
</style>

View File

@@ -1,168 +1,154 @@
<template>
<div class="search-box flex">
<div class="search-box-input flex-1">
<n-input
size="medium"
round
v-model:value="searchValue"
:placeholder="hotSearchKeyword"
class="border border-gray-600"
@keydown.enter="search"
>
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
<template #suffix>
<div class="w-20 px-3 flex justify-between items-center">
<div>{{ searchTypeOptions.find(item => item.key === searchType)?.label }}</div>
<n-dropdown trigger="hover" @select="selectSearchType" :options="searchTypeOptions">
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown>
</div>
</template>
</n-input>
</div>
<div class="user-box">
<n-dropdown trigger="hover" @select="selectItem" :options="userSetOptions">
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown>
<n-avatar
class="ml-2 cursor-pointer"
circle
size="medium"
:src="getImgUrl(store.state.user.avatarUrl)"
v-if="store.state.user"
/>
<div class="mx-2 rounded-full cursor-pointer text-sm" v-else @click="toLogin">登录</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getSearchKeyword } from '@/api/home';
import { getUserDetail, logout } from '@/api/login';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import request from '@/utils/request_mt'
import { getImgUrl } from '@/utils';
import {USER_SET_OPTIONS, SEARCH_TYPES} from '@/const/bar-const'
const router = useRouter()
const store = useStore();
const userSetOptions = ref(USER_SET_OPTIONS)
// 推荐热搜词
const hotSearchKeyword = ref("搜索点什么吧...")
const hotSearchValue = ref("")
const loadHotSearchKeyword = async () => {
const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword
hotSearchValue.value = data.data.realkeyword
}
const loadPage = async () => {
const token = localStorage.getItem("token")
if (!token) return
const { data } = await getUserDetail()
store.state.user = data.profile
localStorage.setItem('user', JSON.stringify(data.profile))
}
watchEffect(() => {
loadPage()
if (store.state.user) {
userSetOptions.value = USER_SET_OPTIONS
} else {
userSetOptions.value = USER_SET_OPTIONS.filter(item => item.key !== 'logout')
}
})
const toLogin = () => {
router.push('/login')
}
// 页面初始化
onMounted(() => {
loadHotSearchKeyword()
loadPage()
})
// 搜索词
const searchValue = ref("")
const searchType = ref(1)
const search = () => {
let value = searchValue.value
if (value == "") {
searchValue.value = hotSearchValue.value
} else {
router.push({
path: "/search",
query: {
keyword: value,
type: searchType.value
}
})
}
}
const selectSearchType = (key: any) => {
searchType.value = key
}
const searchTypeOptions = ref(SEARCH_TYPES)
const selectItem = async (key: any) => {
// switch 判断
switch (key) {
case 'card':
await request.get('/?do=sign')
.then(res => {
console.log(res)
})
break;
case 'card_music':
await request.get('/?do=daka')
.then(res => {
console.log(res)
})
break;
case 'listen':
await request.get('/?do=listen&id=1885175990&time=300')
.then(res => {
console.log(res)
})
break;
case 'logout':
logout().then(() => {
store.state.user = null
localStorage.clear()
})
break;
case 'login':
router.push("/login")
break;
case 'set':
router.push("/set")
break;
}
}
</script>
<style lang="scss" scoped>
.user-box {
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
background: #1a1a1a;
}
.search-box{
@apply pb-4 pr-4;
}
.search-box-input {
@apply relative;
}
</style>
<template>
<div class="search-box flex">
<div class="search-box-input flex-1">
<n-input
v-model:value="searchValue"
size="medium"
round
:placeholder="hotSearchKeyword"
class="border border-gray-600"
@keydown.enter="search"
>
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
<template #suffix>
<div class="w-20 px-3 flex justify-between items-center">
<div>{{ searchTypeOptions.find((item) => item.key === searchType)?.label }}</div>
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown>
</div>
</template>
</n-input>
</div>
<div class="user-box">
<n-dropdown trigger="hover" :options="userSetOptions" @select="selectItem">
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown>
<n-avatar
v-if="store.state.user"
class="ml-2 cursor-pointer"
circle
size="medium"
:src="getImgUrl(store.state.user.avatarUrl)"
/>
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getSearchKeyword } from '@/api/home';
import { getUserDetail, logout } from '@/api/login';
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
import { getImgUrl } from '@/utils';
const router = useRouter();
const store = useStore();
const userSetOptions = ref(USER_SET_OPTIONS);
// 推荐热搜词
const hotSearchKeyword = ref('搜索点什么吧...');
const hotSearchValue = ref('');
const loadHotSearchKeyword = async () => {
const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword;
hotSearchValue.value = data.data.realkeyword;
};
const loadPage = async () => {
const token = localStorage.getItem('token');
if (!token) return;
const { data } = await getUserDetail();
store.state.user = data.profile;
localStorage.setItem('user', JSON.stringify(data.profile));
};
loadPage();
watchEffect(() => {
if (store.state.user) {
userSetOptions.value = USER_SET_OPTIONS;
} else {
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
}
});
const toLogin = () => {
router.push('/login');
};
// 页面初始化
onMounted(() => {
loadHotSearchKeyword();
loadPage();
});
// 搜索词
const searchValue = ref('');
const searchType = ref(1);
const search = () => {
const { value } = searchValue;
if (value === '') {
searchValue.value = hotSearchValue.value;
return;
}
router.push({
path: '/search',
query: {
keyword: value,
type: searchType.value,
},
});
};
const selectSearchType = (key: number) => {
searchType.value = key;
};
const searchTypeOptions = ref(SEARCH_TYPES);
const selectItem = async (key: string) => {
// switch 判断
switch (key) {
case 'logout':
logout().then(() => {
store.state.user = null;
localStorage.clear();
router.push('/login');
});
break;
case 'login':
router.push('/login');
break;
case 'set':
router.push('/set');
break;
default:
}
};
</script>
<style lang="scss" scoped>
.user-box {
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
background: #1a1a1a;
}
.search-box {
@apply pb-4 pr-4;
}
.search-box-input {
@apply relative;
}
.mobile {
.search-box {
@apply pl-4;
}
}
</style>

View File

@@ -5,9 +5,6 @@
<button @click="minimize">
<i class="iconfont icon-minisize"></i>
</button>
<!-- <button @click="maximize">
<i class="iconfont icon-maxsize"></i>
</button> -->
<button @click="close">
<i class="iconfont icon-close"></i>
</button>
@@ -16,18 +13,14 @@
</template>
<script setup lang="ts">
import { useDialog } from 'naive-ui'
import { useDialog } from 'naive-ui';
const dialog = useDialog()
const windowData = window as any
const dialog = useDialog();
const windowData = window as any;
const minimize = () => {
windowData.electronAPI.minimize()
}
const maximize = () => {
windowData.electronAPI.maximize()
}
windowData.electronAPI.minimize();
};
const close = () => {
dialog.warning({
@@ -36,17 +29,17 @@ const close = () => {
positiveText: '最小化',
negativeText: '关闭',
onPositiveClick: () => {
windowData.electronAPI.miniTray()
windowData.electronAPI.miniTray();
},
onNegativeClick: () => {
windowData.electronAPI.close()
}
})
}
windowData.electronAPI.close();
},
});
};
const drag = (event: MouseEvent) => {
windowData.electronAPI.dragStart(event)
}
windowData.electronAPI.dragStart(event);
};
</script>
<style scoped lang="scss">

View File

@@ -1,5 +1,5 @@
import AppMenu from "./AppMenu.vue";
import PlayBar from "./PlayBar.vue";
import SearchBar from "./SearchBar.vue";
import AppMenu from './AppMenu.vue';
import PlayBar from './PlayBar.vue';
import SearchBar from './SearchBar.vue';
export { AppMenu, PlayBar, SearchBar };

View File

@@ -5,20 +5,20 @@
</template>
<script setup lang="ts">
const props = defineProps({
defineProps({
lrcList: {
type: Array,
default: () => []
default: () => [],
},
lrcIndex: {
type: Number,
default: 0
default: 0,
},
lrcTime: {
type: Number,
default: 0
default: 0,
},
})
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>

View File

@@ -1,19 +1,17 @@
import { createApp } from "vue";
import App from "./App.vue";
import naive from "naive-ui";
import "vfonts/Lato.css";
import "vfonts/FiraCode.css";
// tailwind css
import "./index.css";
import router from "@/router";
import store from "@/store";
const app = createApp(App);
app.use(router);
app.use(store);
// app.use(naive);
app.mount("#app");
import 'vfonts/Lato.css';
import 'vfonts/FiraCode.css';
// tailwind css
import './index.css';
import 'remixicon/fonts/remixicon.css';
import { createApp } from 'vue';
import router from '@/router';
import store from '@/store';
import App from './App.vue';
const app = createApp(App);
app.use(router);
app.use(store);
app.mount('#app');

View File

@@ -5,6 +5,7 @@ const layoutRouter = [
meta: {
title: '首页',
icon: 'icon-Home',
keepAlive: true,
},
component: () => import('@/views/home/index.vue'),
},
@@ -14,8 +15,8 @@ const layoutRouter = [
meta: {
title: '搜索',
noScroll: true,
noKeepAlive: true,
icon: 'icon-Search',
keepAlive: true,
},
component: () => import('@/views/search/index.vue'),
},
@@ -25,6 +26,7 @@ const layoutRouter = [
meta: {
title: '歌单',
icon: 'icon-Paper',
keepAlive: true,
},
component: () => import('@/views/list/index.vue'),
},
@@ -34,6 +36,7 @@ const layoutRouter = [
meta: {
title: 'MV',
icon: 'icon-recordfill',
keepAlive: true,
},
component: () => import('@/views/mv/index.vue'),
},
@@ -43,6 +46,7 @@ const layoutRouter = [
meta: {
title: '历史',
icon: 'icon-a-TicketStar',
keepAlive: true,
},
component: () => import('@/views/history/index.vue'),
},
@@ -51,11 +55,11 @@ const layoutRouter = [
name: 'user',
meta: {
title: '用户',
noKeepAlive: true,
icon: 'icon-Profile',
keepAlive: true,
noScroll: true,
},
component: () => import('@/views/user/index.vue'),
},
]
];
export default layoutRouter;

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import AppLayout from '@/layout/AppLayout.vue'
import homeRouter from '@/router/home'
import { createRouter, createWebHashHistory } from 'vue-router';
import AppLayout from '@/layout/AppLayout.vue';
import homeRouter from '@/router/home';
const loginRouter = {
path: '/login',
@@ -11,7 +12,7 @@ const loginRouter = {
icon: 'icon-Home',
},
component: () => import('@/views/login/index.vue'),
}
};
const setRouter = {
path: '/set',
@@ -22,7 +23,7 @@ const setRouter = {
icon: 'icon-Home',
},
component: () => import('@/views/set/index.vue'),
}
};
const routes = [
{
@@ -30,9 +31,13 @@ const routes = [
component: AppLayout,
children: [...homeRouter, loginRouter, setRouter],
},
]
{
path: '/lyric',
component: () => import('@/views/lyric/index.vue'),
},
];
export default createRouter({
routes: routes,
routes,
history: createWebHashHistory(),
})
});

11
src/shims-vue.d.ts vendored
View File

@@ -1,5 +1,6 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,20 +1,23 @@
import { createStore } from 'vuex'
import { SongResult } from '@/type/music'
import { getMusicUrl, getParsingMusicUrl } from '@/api/music'
import homeRouter from '@/router/home'
import { getMusicProxyUrl } from '@/utils'
import { useMusicHistory } from '@/hooks/MusicHistoryHook'
import { createStore } from 'vuex';
import { getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import homeRouter from '@/router/home';
import { SongResult } from '@/type/music';
import { getMusicProxyUrl } from '@/utils';
interface State {
menus: any[]
play: boolean
isPlay: boolean
playMusic: SongResult
playMusicUrl: string
user: any
playList: SongResult[]
playListIndex: number
setData: any
menus: any[];
play: boolean;
isPlay: boolean;
playMusic: SongResult;
playMusicUrl: string;
user: any;
playList: SongResult[];
playListIndex: number;
setData: any;
lyric: any;
isMobile: boolean;
}
const state: State = {
@@ -23,89 +26,86 @@ const state: State = {
isPlay: false,
playMusic: {} as SongResult,
playMusicUrl: '',
user: null,
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') as string) : null,
playList: [],
playListIndex: 0,
setData: null,
}
lyric: {},
isMobile: false,
};
const windowData = window as any
const windowData = window as any;
const musicHistory = useMusicHistory()
const musicHistory = useMusicHistory();
const mutations = {
setMenus(state: State, menus: any[]) {
state.menus = menus
state.menus = menus;
},
async setPlay(state: State, playMusic: SongResult) {
state.playMusic = playMusic
state.playMusicUrl = await getSongUrl(playMusic.id)
state.play = true
musicHistory.addMusic(playMusic)
state.playMusic = { ...playMusic, playLoading: true };
state.playMusicUrl = await getSongUrl(playMusic.id);
state.play = true;
musicHistory.addMusic(playMusic);
state.playMusic.playLoading = false;
},
setIsPlay(state: State, isPlay: boolean) {
state.isPlay = isPlay
state.isPlay = isPlay;
},
setPlayMusic(state: State, play: boolean) {
state.play = play
state.play = play;
},
setPlayList(state: State, playList: SongResult[]) {
state.playListIndex = playList.findIndex(
(item) => item.id === state.playMusic.id
)
state.playList = playList
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
state.playList = playList;
},
async nextPlay(state: State) {
if (state.playList.length === 0) {
state.play = true
return
state.play = true;
return;
}
state.playListIndex = (state.playListIndex + 1) % state.playList.length
await updatePlayMusic(state)
state.playListIndex = (state.playListIndex + 1) % state.playList.length;
await updatePlayMusic(state);
},
async prevPlay(state: State) {
if (state.playList.length === 0) {
state.play = true
return
state.play = true;
return;
}
state.playListIndex =
(state.playListIndex - 1 + state.playList.length) % state.playList.length
await updatePlayMusic(state)
state.playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
await updatePlayMusic(state);
},
async setSetData(state: State, setData: any) {
state.setData = setData
windowData.electron.ipcRenderer.setStoreValue(
'set',
JSON.parse(JSON.stringify(setData))
)
state.setData = setData;
windowData.electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
},
}
};
const getSongUrl = async (id: number) => {
const { data } = await getMusicUrl(id)
let url = ''
const { data } = await getMusicUrl(id);
let url = '';
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id)
url = res.data.data.url
const res = await getParsingMusicUrl(id);
url = res.data.data.url;
}
} catch (error) {
console.error('error', error)
console.error('error', error);
}
url = url ? url : data.data[0].url
return getMusicProxyUrl(url)
}
url = url || data.data[0].url;
return getMusicProxyUrl(url);
};
const updatePlayMusic = async (state: State) => {
state.playMusic = state.playList[state.playListIndex]
state.playMusicUrl = await getSongUrl(state.playMusic.id)
state.play = true
musicHistory.addMusic(state.playMusic)
}
state.playMusic = state.playList[state.playListIndex];
state.playMusicUrl = await getSongUrl(state.playMusic.id);
state.play = true;
musicHistory.addMusic(state.playMusic);
};
const store = createStore({
state: state,
mutations: mutations,
})
state,
mutations,
});
export default store
export default store;

View File

@@ -4,30 +4,30 @@ export interface IAlbumNew {
}
export interface Album {
name: string
id: number
type: string
size: number
picId: number
blurPicUrl: string
companyId: number
pic: number
picUrl: string
publishTime: number
description: string
tags: string
company: string
briefDesc: string
artist: Artist
songs?: any
alias: string[]
status: number
copyrightId: number
commentThreadId: string
artists: Artist2[]
paid: boolean
onSale: boolean
picId_str: string
name: string;
id: number;
type: string;
size: number;
picId: number;
blurPicUrl: string;
companyId: number;
pic: number;
picUrl: string;
publishTime: number;
description: string;
tags: string;
company: string;
briefDesc: string;
artist: Artist;
songs?: any;
alias: string[];
status: number;
copyrightId: number;
commentThreadId: string;
artists: Artist2[];
paid: boolean;
onSale: boolean;
picId_str: string;
}
interface Artist2 {

168
src/type/day_recommend.ts Normal file
View File

@@ -0,0 +1,168 @@
export interface IDayRecommend {
dailySongs: DailySong[];
orderSongs: any[];
recommendReasons: RecommendReason[];
mvResourceInfos: null;
}
interface RecommendReason {
songId: number;
reason: string;
reasonId: string;
targetUrl: null;
}
interface DailySong {
name: string;
id: number;
pst: number;
t: number;
ar: Ar[];
alia: string[];
pop: number;
st: number;
rt: null | string;
fee: number;
v: number;
crbt: null;
cf: string;
al: Al;
dt: number;
h: H;
m: H;
l: H;
sq: H | null;
hr: H | null;
a: null;
cd: string;
no: number;
rtUrl: null;
ftype: number;
rtUrls: any[];
djId: number;
copyright: number;
s_id: number;
mark: number;
originCoverType: number;
originSongSimpleData: OriginSongSimpleDatum | null;
tagPicList: null;
resourceState: boolean;
version: number;
songJumpInfo: null;
entertainmentTags: null;
single: number;
noCopyrightRcmd: null;
rtype: number;
rurl: null;
mst: number;
cp: number;
mv: number;
publishTime: number;
reason: null | string;
videoInfo: VideoInfo;
recommendReason: null | string;
privilege: Privilege;
alg: string;
tns?: string[];
s_ctrp?: string;
}
interface Privilege {
id: number;
fee: number;
payed: number;
realPayed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
pc: null;
toast: boolean;
flag: number;
paidBigBang: boolean;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
maxBrLevel: string;
playMaxBrLevel: string;
downloadMaxBrLevel: string;
plLevel: string;
dlLevel: string;
flLevel: string;
rscl: null;
freeTrialPrivilege: FreeTrialPrivilege;
rightSource: number;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl: null;
chargeMessage: null;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
listenType: number;
cannotListenReason: number;
playReason: null;
}
interface VideoInfo {
moreThanOne: boolean;
video: Video | null;
}
interface Video {
vid: string;
type: number;
title: string;
playTime: number;
coverUrl: string;
publishTime: number;
artists: null;
alias: null;
}
interface OriginSongSimpleDatum {
songId: number;
name: string;
artists: Artist[];
albumMeta: Artist;
}
interface Artist {
id: number;
name: string;
}
interface H {
br: number;
fid: number;
size: number;
vd: number;
sr: number;
}
interface Al {
id: number;
name: string;
picUrl: string;
tns: string[];
pic_str?: string;
pic: number;
}
interface Ar {
id: number;
name: string;
tns: any[];
alias: any[];
}

View File

@@ -1,4 +1,5 @@
export interface IData<T> {
code: number
data: T
code: number;
data: T;
result: T;
}

View File

@@ -7,42 +7,42 @@ export interface IList {
}
export interface Playlist {
name: string
id: number
trackNumberUpdateTime: number
status: number
userId: number
createTime: number
updateTime: number
subscribedCount: number
trackCount: number
cloudTrackCount: number
coverImgUrl: string
coverImgId: number
description: string
tags: string[]
playCount: number
trackUpdateTime: number
specialType: number
totalDuration: number
creator: Creator
tracks?: any
subscribers: Subscriber[]
subscribed: boolean
commentThreadId: string
newImported: boolean
adType: number
highQuality: boolean
privacy: number
ordered: boolean
anonimous: boolean
coverStatus: number
recommendInfo?: any
shareCount: number
coverImgId_str?: string
commentCount: number
copywriter: string
tag: string
name: string;
id: number;
trackNumberUpdateTime: number;
status: number;
userId: number;
createTime: number;
updateTime: number;
subscribedCount: number;
trackCount: number;
cloudTrackCount: number;
coverImgUrl: string;
coverImgId: number;
description: string;
tags: string[];
playCount: number;
trackUpdateTime: number;
specialType: number;
totalDuration: number;
creator: Creator;
tracks?: any;
subscribers: Subscriber[];
subscribed: boolean;
commentThreadId: string;
newImported: boolean;
adType: number;
highQuality: boolean;
privacy: number;
ordered: boolean;
anonimous: boolean;
coverStatus: number;
recommendInfo?: any;
shareCount: number;
coverImgId_str?: string;
commentCount: number;
copywriter: string;
tag: string;
}
interface Subscriber {
@@ -120,8 +120,8 @@ interface AvatarDetail {
}
interface Expert {
"2": string;
"1"?: string;
'2': string;
'1'?: string;
}
// 推荐歌单

View File

@@ -1,203 +1,203 @@
export interface IListDetail {
code: number;
relatedVideos?: any;
playlist: Playlist;
urls?: any;
privileges: Privilege[];
sharedPrivilege?: any;
resEntrance?: any;
}
interface Privilege {
id: number;
fee: number;
payed: number;
realPayed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
pc?: any;
toast: boolean;
flag: number;
paidBigBang: boolean;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
export interface Playlist {
id: number
name: string
coverImgId: number
coverImgUrl: string
coverImgId_str: string
adType: number
userId: number
createTime: number
status: number
opRecommend: boolean
highQuality: boolean
newImported: boolean
updateTime: number
trackCount: number
specialType: number
privacy: number
trackUpdateTime: number
commentThreadId: string
playCount: number
trackNumberUpdateTime: number
subscribedCount: number
cloudTrackCount: number
ordered: boolean
description: string
tags: string[]
updateFrequency?: any
backgroundCoverId: number
backgroundCoverUrl?: any
titleImage: number
titleImageUrl?: any
englishTitle?: any
officialPlaylistType?: any
subscribers: Subscriber[]
subscribed: boolean
creator: Subscriber
tracks: Track[]
videoIds?: any
videos?: any
trackIds: TrackId[]
shareCount: number
commentCount: number
remixVideo?: any
sharedUsers?: any
historySharedUsers?: any
}
interface TrackId {
id: number;
v: number;
t: number;
at: number;
alg?: any;
uid: number;
rcmdReason: string;
}
interface Track {
name: string;
id: number;
pst: number;
t: number;
ar: Ar[];
alia: string[];
pop: number;
st: number;
rt?: string;
fee: number;
v: number;
crbt?: any;
cf: string;
al: Al;
dt: number;
h: H;
m: H;
l?: H;
a?: any;
cd: string;
no: number;
rtUrl?: any;
ftype: number;
rtUrls: any[];
djId: number;
copyright: number;
s_id: number;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
mst: number;
cp: number;
mv: number;
rtype: number;
rurl?: any;
publishTime: number;
tns?: string[];
}
interface H {
br: number;
fid: number;
size: number;
vd: number;
}
interface Al {
id: number;
name: string;
picUrl: string;
tns: any[];
pic_str?: string;
pic: number;
}
interface Ar {
id: number;
name: string;
tns: any[];
alias: any[];
}
interface Subscriber {
defaultAvatar: boolean;
province: number;
authStatus: number;
followed: boolean;
avatarUrl: string;
accountStatus: number;
gender: number;
city: number;
birthday: number;
userId: number;
userType: number;
nickname: string;
signature: string;
description: string;
detailDescription: string;
avatarImgId: number;
backgroundImgId: number;
backgroundUrl: string;
authority: number;
mutual: boolean;
expertTags?: any;
experts?: any;
djStatus: number;
vipType: number;
remarkName?: any;
authenticationTypes: number;
avatarDetail?: any;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgIdStr: string;
avatarImgId_str: string;
}
export interface IListDetail {
code: number;
relatedVideos?: any;
playlist: Playlist;
urls?: any;
privileges: Privilege[];
sharedPrivilege?: any;
resEntrance?: any;
}
interface Privilege {
id: number;
fee: number;
payed: number;
realPayed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
pc?: any;
toast: boolean;
flag: number;
paidBigBang: boolean;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
export interface Playlist {
id: number;
name: string;
coverImgId: number;
coverImgUrl: string;
coverImgId_str: string;
adType: number;
userId: number;
createTime: number;
status: number;
opRecommend: boolean;
highQuality: boolean;
newImported: boolean;
updateTime: number;
trackCount: number;
specialType: number;
privacy: number;
trackUpdateTime: number;
commentThreadId: string;
playCount: number;
trackNumberUpdateTime: number;
subscribedCount: number;
cloudTrackCount: number;
ordered: boolean;
description: string;
tags: string[];
updateFrequency?: any;
backgroundCoverId: number;
backgroundCoverUrl?: any;
titleImage: number;
titleImageUrl?: any;
englishTitle?: any;
officialPlaylistType?: any;
subscribers: Subscriber[];
subscribed: boolean;
creator: Subscriber;
tracks: Track[];
videoIds?: any;
videos?: any;
trackIds: TrackId[];
shareCount: number;
commentCount: number;
remixVideo?: any;
sharedUsers?: any;
historySharedUsers?: any;
}
interface TrackId {
id: number;
v: number;
t: number;
at: number;
alg?: any;
uid: number;
rcmdReason: string;
}
interface Track {
name: string;
id: number;
pst: number;
t: number;
ar: Ar[];
alia: string[];
pop: number;
st: number;
rt?: string;
fee: number;
v: number;
crbt?: any;
cf: string;
al: Al;
dt: number;
h: H;
m: H;
l?: H;
a?: any;
cd: string;
no: number;
rtUrl?: any;
ftype: number;
rtUrls: any[];
djId: number;
copyright: number;
s_id: number;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
mst: number;
cp: number;
mv: number;
rtype: number;
rurl?: any;
publishTime: number;
tns?: string[];
}
interface H {
br: number;
fid: number;
size: number;
vd: number;
}
interface Al {
id: number;
name: string;
picUrl: string;
tns: any[];
pic_str?: string;
pic: number;
}
interface Ar {
id: number;
name: string;
tns: any[];
alias: any[];
}
interface Subscriber {
defaultAvatar: boolean;
province: number;
authStatus: number;
followed: boolean;
avatarUrl: string;
accountStatus: number;
gender: number;
city: number;
birthday: number;
userId: number;
userType: number;
nickname: string;
signature: string;
description: string;
detailDescription: string;
avatarImgId: number;
backgroundImgId: number;
backgroundUrl: string;
authority: number;
mutual: boolean;
expertTags?: any;
experts?: any;
djStatus: number;
vipType: number;
remarkName?: any;
authenticationTypes: number;
avatarDetail?: any;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgIdStr: string;
avatarImgId_str: string;
}

View File

@@ -1,14 +1,14 @@
export interface ILyric {
sgc: boolean;
sfy: boolean;
qfy: boolean;
lrc: Lrc;
klyric: Lrc;
tlyric: Lrc;
code: number;
}
interface Lrc {
version: number;
lyric: string;
}
export interface ILyric {
sgc: boolean;
sfy: boolean;
qfy: boolean;
lrc: Lrc;
klyric: Lrc;
tlyric: Lrc;
code: number;
}
interface Lrc {
version: number;
lyric: string;
}

View File

@@ -1,197 +1,198 @@
export interface IRecommendMusic {
code: number;
category: number;
result: SongResult[];
}
export interface SongResult {
id: number
type: number
name: string
copywriter?: any
picUrl: string
canDislike: boolean
trackNumberUpdateTime?: any
song: Song
alg: string
count?: number
}
interface Song {
name: string;
id: number;
position: number;
alias: string[];
status: number;
fee: number;
copyrightId: number;
disc: string;
no: number;
artists: Artist[];
album: Album;
starred: boolean;
popularity: number;
score: number;
starredNum: number;
duration: number;
playedNum: number;
dayPlays: number;
hearTime: number;
ringtone: string;
crbt?: any;
audition?: any;
copyFrom: string;
commentThreadId: string;
rtUrl?: any;
ftype: number;
rtUrls: any[];
copyright: number;
transName?: any;
sign?: any;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
rtype: number;
rurl?: any;
mvid: number;
bMusic: BMusic;
mp3Url?: any;
hMusic: BMusic;
mMusic: BMusic;
lMusic: BMusic;
exclusive: boolean;
privilege: Privilege;
}
interface Privilege {
id: number;
fee: number;
payed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
toast: boolean;
flag: number;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
interface BMusic {
name?: any;
id: number;
size: number;
extension: string;
sr: number;
dfsId: number;
bitrate: number;
playTime: number;
volumeDelta: number;
}
interface Album {
name: string;
id: number;
type: string;
size: number;
picId: number;
blurPicUrl: string;
companyId: number;
pic: number;
picUrl: string;
publishTime: number;
description: string;
tags: string;
company: string;
briefDesc: string;
artist: Artist;
songs: any[];
alias: string[];
status: number;
copyrightId: number;
commentThreadId: string;
artists: Artist[];
subType: string;
transName?: any;
onSale: boolean;
mark: number;
picId_str: string;
}
interface Artist {
name: string;
id: number;
picId: number;
img1v1Id: number;
briefDesc: string;
picUrl: string;
img1v1Url: string;
albumSize: number;
alias: any[];
trans: string;
musicSize: number;
topicPerson: number;
}
export interface IPlayMusicUrl {
data: Datum[];
code: number;
}
interface Datum {
id: number;
url: string;
br: number;
size: number;
md5: string;
code: number;
expi: number;
type: string;
gain: number;
fee: number;
uf?: any;
payed: number;
flag: number;
canExtend: boolean;
freeTrialInfo?: any;
level: string;
encodeType: string;
freeTrialPrivilege: FreeTrialPrivilege;
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
urlSource: number;
}
interface FreeTimeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
type: number;
remainTime: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
export interface IRecommendMusic {
code: number;
category: number;
result: SongResult[];
}
export interface SongResult {
id: number;
type: number;
name: string;
copywriter?: any;
picUrl: string;
canDislike: boolean;
trackNumberUpdateTime?: any;
song: Song;
alg: string;
count?: number;
playLoading?: boolean;
}
interface Song {
name: string;
id: number;
position: number;
alias: string[];
status: number;
fee: number;
copyrightId: number;
disc: string;
no: number;
artists: Artist[];
album: Album;
starred: boolean;
popularity: number;
score: number;
starredNum: number;
duration: number;
playedNum: number;
dayPlays: number;
hearTime: number;
ringtone: string;
crbt?: any;
audition?: any;
copyFrom: string;
commentThreadId: string;
rtUrl?: any;
ftype: number;
rtUrls: any[];
copyright: number;
transName?: any;
sign?: any;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
rtype: number;
rurl?: any;
mvid: number;
bMusic: BMusic;
mp3Url?: any;
hMusic: BMusic;
mMusic: BMusic;
lMusic: BMusic;
exclusive: boolean;
privilege: Privilege;
}
interface Privilege {
id: number;
fee: number;
payed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
toast: boolean;
flag: number;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
interface BMusic {
name?: any;
id: number;
size: number;
extension: string;
sr: number;
dfsId: number;
bitrate: number;
playTime: number;
volumeDelta: number;
}
interface Album {
name: string;
id: number;
type: string;
size: number;
picId: number;
blurPicUrl: string;
companyId: number;
pic: number;
picUrl: string;
publishTime: number;
description: string;
tags: string;
company: string;
briefDesc: string;
artist: Artist;
songs: any[];
alias: string[];
status: number;
copyrightId: number;
commentThreadId: string;
artists: Artist[];
subType: string;
transName?: any;
onSale: boolean;
mark: number;
picId_str: string;
}
interface Artist {
name: string;
id: number;
picId: number;
img1v1Id: number;
briefDesc: string;
picUrl: string;
img1v1Url: string;
albumSize: number;
alias: any[];
trans: string;
musicSize: number;
topicPerson: number;
}
export interface IPlayMusicUrl {
data: Datum[];
code: number;
}
interface Datum {
id: number;
url: string;
br: number;
size: number;
md5: string;
code: number;
expi: number;
type: string;
gain: number;
fee: number;
uf?: any;
payed: number;
flag: number;
canExtend: boolean;
freeTrialInfo?: any;
level: string;
encodeType: string;
freeTrialPrivilege: FreeTrialPrivilege;
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
urlSource: number;
}
interface FreeTimeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
type: number;
remainTime: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}

View File

@@ -1,84 +1,84 @@
export interface IMvItem {
id: number
cover: string
name: string
playCount: number
briefDesc?: any
desc?: any
artistName: string
artistId: number
duration: number
mark: number
mv: IMvData
lastRank: number
score: number
subed: boolean
artists: Artist[]
transNames?: string[]
alias?: string[]
id: number;
cover: string;
name: string;
playCount: number;
briefDesc?: any;
desc?: any;
artistName: string;
artistId: number;
duration: number;
mark: number;
mv: IMvData;
lastRank: number;
score: number;
subed: boolean;
artists: Artist[];
transNames?: string[];
alias?: string[];
}
export interface IMvData {
authId: number
status: number
id: number
title: string
subTitle: string
appTitle: string
aliaName: string
transName: string
pic4v3: number
pic16v9: number
caption: number
captionLanguage: string
style?: any
mottos: string
oneword?: any
appword: string
stars?: any
desc: string
area: string
type: string
subType: string
neteaseonly: number
upban: number
topWeeks: string
publishTime: string
online: number
score: number
plays: number
monthplays: number
weekplays: number
dayplays: number
fee: number
artists: Artist[]
videos: Video[]
authId: number;
status: number;
id: number;
title: string;
subTitle: string;
appTitle: string;
aliaName: string;
transName: string;
pic4v3: number;
pic16v9: number;
caption: number;
captionLanguage: string;
style?: any;
mottos: string;
oneword?: any;
appword: string;
stars?: any;
desc: string;
area: string;
type: string;
subType: string;
neteaseonly: number;
upban: number;
topWeeks: string;
publishTime: string;
online: number;
score: number;
plays: number;
monthplays: number;
weekplays: number;
dayplays: number;
fee: number;
artists: Artist[];
videos: Video[];
}
interface Video {
tagSign: TagSign
tag: string
url: string
duration: number
size: number
width: number
height: number
container: string
md5: string
check: boolean
tagSign: TagSign;
tag: string;
url: string;
duration: number;
size: number;
width: number;
height: number;
container: string;
md5: string;
check: boolean;
}
interface TagSign {
br: number
type: string
tagSign: string
resolution: number
mvtype: string
br: number;
type: string;
tagSign: string;
resolution: number;
mvtype: string;
}
interface Artist {
id: number
name: string
id: number;
name: string;
}
// {
@@ -97,16 +97,16 @@ interface Artist {
// }
export interface IMvUrlData {
id: number
url: string
r: number
size: number
md5: string
code: number
expi: number
fee: number
mvFee: number
st: number
promotionVo: null | any
msg: string
id: number;
url: string;
r: number;
size: number;
md5: string;
code: number;
expi: number;
fee: number;
mvFee: number;
st: number;
promotionVo: null | any;
msg: string;
}

View File

@@ -6,11 +6,11 @@ export interface IPlayListSort {
}
interface SortCategories {
"0": string;
"1": string;
"2": string;
"3": string;
"4": string;
'0': string;
'1': string;
'2': string;
'3': string;
'4': string;
}
interface SortAll {

View File

@@ -540,7 +540,7 @@ interface Song2 {
}
interface KsongInfos {
"347230": _347230;
'347230': _347230;
}
interface _347230 {

View File

@@ -1,64 +1,77 @@
import { computed } from 'vue';
import store from '@/store';
// 设置歌手背景图片
export const setBackgroundImg = (url: String) => {
return 'background-image:' + 'url(' + url + ')'
}
return `background-image:url(${url})`;
};
// 设置动画类型
export const setAnimationClass = (type: String) => {
return 'animate__animated ' + type
}
return `animate__animated ${type}`;
};
// 设置动画延时
export const setAnimationDelay = (index: number = 6, time: number = 50) => {
return 'animation-delay:' + index * time + 'ms'
}
return `animation-delay:${index * time}ms`;
};
//将秒转换为分钟和秒
// 将秒转换为分钟和秒
export const secondToMinute = (s: number) => {
if (!s) {
return '00:00'
return '00:00';
}
let minute: number = Math.floor(s / 60)
let second: number = Math.floor(s % 60)
let minuteStr: string =
minute > 9 ? minute.toString() : '0' + minute.toString()
let secondStr: string =
second > 9 ? second.toString() : '0' + second.toString()
return minuteStr + ':' + secondStr
}
const minute: number = Math.floor(s / 60);
const second: number = Math.floor(s % 60);
const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`;
const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`;
return `${minuteStr}:${secondStr}`;
};
// 格式化数字 千,万, 百万, 千万,亿
export const formatNumber = (num: any) => {
num = num * 1
export const formatNumber = (num: string | number) => {
num = Number(num);
if (num < 10000) {
return num
return num;
}
if (num < 100000000) {
return (num / 10000).toFixed(1) + '万'
return `${(num / 10000).toFixed(1)}`;
}
return (num / 100000000).toFixed(1) + '亿'
}
const windowData = window as any
return `${(num / 100000000).toFixed(1)}亿`;
};
const windowData = window as any;
export const getIsMc = () => {
if (!windowData.electron) {
return false
return false;
}
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
return true
return true;
}
return false
}
const ProxyUrl =
import.meta.env.VITE_API_PROXY + '' || 'http://110.42.251.190:9856'
return false;
};
const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856';
export const getMusicProxyUrl = (url: string) => {
if (!getIsMc()) {
return url
return url;
}
const PUrl = url.split('').join('+')
return `${ProxyUrl}/mc?url=${PUrl}`
}
const PUrl = url.split('').join('+');
return `${ProxyUrl}/mc?url=${PUrl}`;
};
export const getImgUrl = computed(() => (url: string, size: string = '') => {
const bdUrl = 'https://image.baidu.com/search/down?url='
const imgUrl = encodeURIComponent(`${url}?param=${size}`)
return `${bdUrl}${imgUrl}`
})
export const getImgUrl = computed(() => (url: string | undefined, size: string = '') => {
const bdUrl = 'https://image.baidu.com/search/down?url=';
const imgUrl = encodeURIComponent(`${url}?param=${size}`);
return `${bdUrl}${imgUrl}`;
});
export const isMobile = computed(() => {
const flag = navigator.userAgent.match(
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i,
);
store.state.isMobile = !!flag;
// 给html标签 添加mobile
if (flag) document.documentElement.classList.add('mobile');
return !!flag;
});

View File

@@ -1,8 +1,9 @@
import axios from "axios";
let baseURL = import.meta.env.VITE_API + "";
import axios from 'axios';
const baseURL = `${import.meta.env.VITE_API}`;
const request = axios.create({
baseURL: baseURL,
baseURL,
timeout: 10000,
});
@@ -11,12 +12,12 @@ request.interceptors.request.use(
(config) => {
// 在请求发送之前做一些处理
// 在get请求params中添加timestamp
if (config.method === "get") {
if (config.method === 'get') {
config.params = {
...config.params,
timestamp: Date.now(),
};
let token = localStorage.getItem("token");
const token = localStorage.getItem('token');
if (token) {
config.params.cookie = token;
}
@@ -27,7 +28,7 @@ request.interceptors.request.use(
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error);
}
},
);
export default request;

View File

@@ -1,19 +0,0 @@
import axios from "axios";
let baseURL = import.meta.env.VITE_API_MT + "";
const request = axios.create({
baseURL: baseURL,
timeout: 10000,
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
return config;
},
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error);
}
);
export default request;

View File

@@ -1,19 +1,20 @@
import axios from "axios"
let baseURL = import.meta.env.VITE_API_MUSIC + ""
import axios from 'axios';
const baseURL = `${import.meta.env.VITE_API_MUSIC}`;
const request = axios.create({
baseURL: baseURL,
baseURL,
timeout: 10000,
})
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
return config
return config;
},
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error)
}
)
return Promise.reject(error);
},
);
export default request
export default request;

View File

@@ -3,8 +3,13 @@
<div class="title">播放历史</div>
<n-scrollbar :size="100">
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="history-item" v-for="(item, index) in musicList" :key="item.id"
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
<div
v-for="(item, index) in musicList"
:key="item.id"
class="history-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<song-item class="history-item-content" :item="item" />
<div class="history-item-count">
{{ item.count }}
@@ -19,33 +24,37 @@
</template>
<script setup lang="ts">
import { useMusicHistory } from '@/hooks/MusicHistoryHook'
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { setAnimationClass, setAnimationDelay } from '@/utils';
const {delMusic, musicList} = useMusicHistory();
defineOptions({
name: 'History',
});
const { delMusic, musicList } = useMusicHistory();
</script>
<style scoped lang="scss">
.history-page{
.history-page {
@apply h-full w-full pt-2;
.title{
@apply text-xl font-bold;
.title {
@apply pl-4 text-xl font-bold;
}
.history-list-content{
@apply px-4 mt-2;
.history-item{
.history-list-content {
@apply px-4 mt-2 pb-28;
.history-item {
@apply flex items-center justify-between;
&-content{
&-content {
@apply flex-1;
}
&-count{
&-count {
@apply px-4 text-lg;
}
&-delete{
&-delete {
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
}
}
}
}
</style>
</style>

View File

@@ -1,30 +1,41 @@
<template>
<div class="main-page">
<n-scrollbar :size="100" :x-scrollable="false">
<div class="main-page">
<!-- 推荐歌手 -->
<recommend-singer />
<div class="main-content">
<!-- 歌单分类列表 -->
<playlist-type />
<playlist-type v-if="!isMobile" />
<!-- 本周最热音乐 -->
<recommend-songlist />
<!-- 推荐最新专辑 -->
<recommend-album />
</div>
</div>
</div>
</n-scrollbar>
</template>
<script lang="ts" setup>
const RecommendSinger = defineAsyncComponent(() => import("@/components/RecommendSinger.vue"));
const PlaylistType = defineAsyncComponent(() => import("@/components/PlaylistType.vue"));
const RecommendSonglist = defineAsyncComponent(() => import("@/components/RecommendSonglist.vue"));
const RecommendAlbum = defineAsyncComponent(() => import("@/components/RecommendAlbum.vue"));
import { isMobile } from '@/utils';
const RecommendSinger = defineAsyncComponent(() => import('@/components/RecommendSinger.vue'));
const PlaylistType = defineAsyncComponent(() => import('@/components/PlaylistType.vue'));
const RecommendSonglist = defineAsyncComponent(() => import('@/components/RecommendSonglist.vue'));
const RecommendAlbum = defineAsyncComponent(() => import('@/components/RecommendAlbum.vue'));
defineOptions({
name: 'Home',
});
</script>
<style lang="scss" scoped>
.main-page{
@apply h-full w-full;
.main-page {
@apply h-full w-full overflow-hidden;
}
.main-content {
@apply mt-6 flex pb-28;
@apply mt-6 flex mb-28;
}
.mobile .main-content {
@apply flex-col mx-4;
}
</style>

View File

@@ -1,75 +1,72 @@
<script lang="ts" setup>
import { getRecommendList, getListDetail, getListByCat } from '@/api/list'
import type { IRecommendItem } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useRoute } from 'vue-router';
import MusicList from "@/components/MusicList.vue";
import PlayBottom from '@/components/common/PlayBottom.vue';
const recommendList = ref()
const showMusic = ref(false)
import { getListByCat, getListDetail, getRecommendList } from '@/api/list';
import MusicList from '@/components/MusicList.vue';
import type { IRecommendItem } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const recommendItem = ref<IRecommendItem>()
const listDetail = ref<IListDetail>()
defineOptions({
name: 'List',
});
const recommendList = ref();
const showMusic = ref(false);
const recommendItem = ref<IRecommendItem | null>();
const listDetail = ref<IListDetail | null>();
const selectRecommendItem = async (item: IRecommendItem) => {
showMusic.value = true
const { data } = await getListDetail(item.id)
recommendItem.value = item
listDetail.value = data
}
recommendItem.value = null;
listDetail.value = null;
showMusic.value = true;
const { data } = await getListDetail(item.id);
recommendItem.value = item;
listDetail.value = data;
};
const route = useRoute();
const listTitle = ref(route.query.type || "歌单列表");
const listTitle = ref(route.query.type || '歌单列表');
const loadList = async (type: any) => {
const loadList = async (type: string) => {
const params = {
cat: type || '',
limit: 30,
offset: 0
}
offset: 0,
};
const { data } = await getListByCat(params);
recommendList.value = data.playlists
}
recommendList.value = data.playlists;
};
if (route.query.type) {
loadList(route.query.type)
loadList(route.query.type as string);
} else {
getRecommendList().then((res: { data: { result: any; }; }) => {
recommendList.value = res.data.result
})
getRecommendList().then((res: { data: { result: any } }) => {
recommendList.value = res.data.result;
});
}
watch(
() => route.query,
async newParams => {
if(newParams.type){
const params = {
tag: newParams.type || '',
limit: 30,
before: 0
}
loadList(newParams.type);
async (newParams) => {
if (newParams.type) {
recommendList.value = null;
loadList(newParams.type as string);
}
}
)
},
);
</script>
<template>
<div class="list-page">
<div
class="recommend-title"
:class="setAnimationClass('animate__bounceInLeft')"
>{{ listTitle }}</div>
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
<!-- 歌单列表 -->
<n-scrollbar class="recommend" @click="showMusic = false" :size="100">
<div class="recommend-list" v-if="recommendList">
<n-scrollbar class="recommend" :size="100" @click="showMusic = false">
<div v-if="recommendList" class="recommend-list">
<div
v-for="(item, index) in recommendList"
:key="item.id"
class="recommend-item"
v-for="(item,index) in recommendList"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
@click.stop="selectRecommendItem(item)"
@@ -77,7 +74,7 @@ watch(
<div class="recommend-item-img">
<n-image
class="recommend-item-img-img"
:src="getImgUrl( (item.picUrl || item.coverImgUrl), '200y200')"
:src="getImgUrl(item.picUrl || item.coverImgUrl, '200y200')"
width="200"
height="200"
lazy
@@ -91,15 +88,19 @@ watch(
<div class="recommend-item-title">{{ item.name }}</div>
</div>
</div>
<PlayBottom/>
</n-scrollbar>
<MusicList v-if="listDetail?.playlist" v-model:show="showMusic" :name="listDetail?.playlist.name" :song-list="listDetail?.playlist.tracks" />
<music-list
v-if="listDetail?.playlist"
v-model:show="showMusic"
:name="listDetail?.playlist.name"
:song-list="listDetail?.playlist.tracks"
/>
</div>
</template>
<style lang="scss" scoped>
.list-page {
@apply relative h-full w-full;
@apply relative h-full w-full px-4;
}
.recommend {
@@ -151,4 +152,9 @@ watch(
}
}
</style>
.mobile {
.recommend-list {
grid-template-columns: repeat(auto-fill, minmax(25%, 1fr));
}
}
</style>

View File

@@ -1,170 +1,186 @@
<script lang="ts" setup>
import { getQrKey, createQr, checkQr, getLoginStatus } from '@/api/login'
import { onMounted } from '@vue/runtime-core';
import { getUserDetail, loginByCellphone } from '@/api/login';
import { useStore } from 'vuex';
import { useMessage } from 'naive-ui'
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { useRouter } from 'vue-router';
const message = useMessage()
const store = useStore();
const router = useRouter()
const qrUrl = ref<string>()
onMounted(() => {
loadLogin()
})
const loadLogin = async () => {
const qrKey = await getQrKey()
const key = qrKey.data.data.unikey
const { data } = await createQr(key)
qrUrl.value = data.data.qrimg
timerIsQr(key)
}
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
const { data } = await checkQr(key)
if (data.code === 800) {
clearInterval(timer)
}
if (data.code === 803) {
// 将token存入localStorage
localStorage.setItem('token', data.cookie)
const user = await getUserDetail()
store.state.user = user.data.profile
message.success('登录成功')
await getLoginStatus().then(res => {
console.log(res);
})
clearInterval(timer)
setTimeout(() => {
router.push('/user')
}, 1000);
}
}, 5000);
}
// 是否扫码登陆
const isQr = ref(true)
const chooseQr = () => {
isQr.value = !isQr.value
}
// 手机号登录
const phone = ref('')
const password = ref('')
const loginPhone = async () => {
const { data } = await loginByCellphone(phone.value, password.value)
if (data.code === 200) {
message.success('登录成功')
store.state.user = data.profile
localStorage.setItem('token', data.cookie)
setTimeout(() => {
router.push('/user')
}, 1000);
}
}
</script>
<template>
<div class="login-page">
<div class="phone-login">
<div class="bg"></div>
<div class="content">
<div class="phone" v-if="isQr" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">扫码登陆</div>
<img class="qr-img" :src="qrUrl" />
<div class="text">使用网易云APP扫码登录</div>
</div>
<div class="phone" v-else :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">手机号登录</div>
<div class="phone-page">
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
</div>
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
</div>
</div>
<div class="bottom">
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
@apply flex flex-col items-center justify-center p-20 pt-20;
}
.login-title {
@apply text-2xl font-bold mb-6;
}
.text {
@apply mt-4 text-green-500 text-xs;
}
.phone-login {
width: 350px;
height: 550px;
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
background-color: #383838;
box-shadow: inset 0px 0px 20px 5px #0000005e;
.bg {
@apply absolute w-full h-full bg-black opacity-30;
}
.bottom {
width: 200%;
height: 250px;
bottom: -180px;
border-radius: 50%;
left: 50%;
padding: 10px;
transform: translateX(-50%);
color: #ffffff99;
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
box-shadow: 10px 0px 20px #000000a9;
}
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
.qr-img {
@apply opacity-80 rounded-2xl cursor-pointer;
}
.phone {
animation-duration: 0.5s;
&-page {
background-color: #ffffffdd;
width: 250px;
@apply rounded-2xl overflow-hidden;
}
&-input {
height: 40px;
border-bottom: 1px solid #e5e5e5;
@apply w-full text-black px-4 outline-none;
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl bg-black opacity-60;
}
}
}
</style>
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
import { isMobile, setAnimationClass } from '@/utils';
defineOptions({
name: 'Login',
});
const message = useMessage();
const store = useStore();
const router = useRouter();
const qrUrl = ref<string>();
onMounted(() => {
loadLogin();
});
const timerRef = ref(null);
const loadLogin = async () => {
try {
const qrKey = await getQrKey();
const key = qrKey.data.data.unikey;
const { data } = await createQr(key);
qrUrl.value = data.data.qrimg;
const timer = timerIsQr(key);
// 添加对定时器的引用,以便在出现错误时可以清除
timerRef.value = timer as any;
} catch (error) {
console.error('加载登录信息时出错:', error);
}
};
// 使用 ref 来保存定时器,便于在任何地方清除它
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
try {
const { data } = await checkQr(key);
if (data.code === 800) {
clearInterval(timer);
timerRef.value = null;
}
if (data.code === 803) {
localStorage.setItem('token', data.cookie);
const user = await getUserDetail();
store.state.user = user.data.profile;
localStorage.setItem('user', JSON.stringify(store.state.user));
message.success('登录成功');
clearInterval(timer);
timerRef.value = null;
router.push('/user');
}
} catch (error) {
console.error('检查二维码状态时出错:', error);
// 在出现错误时清除定时器
clearInterval(timer);
timerRef.value = null;
}
}, 2000);
return timer;
};
// 是否扫码登陆
const isQr = ref(!isMobile.value);
const chooseQr = () => {
isQr.value = !isQr.value;
};
// 手机号登录
const phone = ref('');
const password = ref('');
const loginPhone = async () => {
const { data } = await loginByCellphone(phone.value, password.value);
if (data.code === 200) {
message.success('登录成功');
store.state.user = data.profile;
localStorage.setItem('token', data.cookie);
setTimeout(() => {
router.push('/user');
}, 1000);
}
};
</script>
<template>
<div class="login-page">
<div class="phone-login">
<div class="bg"></div>
<div class="content">
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">扫码登陆</div>
<img class="qr-img" :src="qrUrl" />
<div class="text">使用网易云APP扫码登录</div>
</div>
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">手机号登录</div>
<div class="phone-page">
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
</div>
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
</div>
</div>
<div class="bottom">
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
@apply flex flex-col items-center justify-center p-20 pt-20;
}
.login-title {
@apply text-2xl font-bold mb-6;
}
.text {
@apply mt-4 text-green-500 text-xs;
}
.phone-login {
width: 350px;
height: 550px;
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
background-color: #383838;
box-shadow: inset 0px 0px 20px 5px #0000005e;
.bg {
@apply absolute w-full h-full bg-black opacity-30;
}
.bottom {
width: 200%;
height: 250px;
bottom: -180px;
border-radius: 50%;
left: 50%;
padding: 10px;
transform: translateX(-50%);
color: #ffffff99;
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
box-shadow: 10px 0px 20px #000000a9;
}
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
.qr-img {
@apply opacity-80 rounded-2xl cursor-pointer;
}
.phone {
animation-duration: 0.5s;
&-page {
background-color: #ffffffdd;
width: 250px;
@apply rounded-2xl overflow-hidden;
}
&-input {
height: 40px;
border-bottom: 1px solid #e5e5e5;
@apply w-full text-black px-4 outline-none;
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl bg-black opacity-60;
}
}
}
</style>

274
src/views/lyric/index.vue Normal file
View File

@@ -0,0 +1,274 @@
<template>
<div class="lyric-window" :class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]">
<div class="drag-bar"></div>
<div class="lyric-bar" :class="{ 'lyric-bar-hover': isDrag }">
<div class="buttons">
<!-- <div class="music-buttons">
<div @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div> -->
<div class="button check-theme" @click="checkTheme">
<i v-if="lyricSetting.theme === 'light'" class="icon ri-sun-line"></i>
<i v-else class="icon ri-moon-line"></i>
</div>
<div class="button">
<i class="icon ri-share-2-line" :class="{ checked: lyricSetting.isTop }" @click="handleTop"></i>
</div>
<div class="button button-lock" @click="handleLock">
<i v-if="lyricSetting.isLock" class="icon ri-lock-line"></i>
<i v-else class="icon ri-lock-unlock-line"></i>
</div>
<div class="button">
<i class="icon ri-close-circle-line" @click="handleClose"></i>
</div>
</div>
</div>
<div id="clickThroughElement" class="lyric-box">
<template v-if="lyricData.lrcArray[lyricData.nowIndex]">
<h2 class="lyric lyric-current">{{ lyricData.lrcArray[lyricData.nowIndex].text }}</h2>
<p class="lyric-current">{{ lyricData.currentLrc.trText }}</p>
<template v-if="lyricData.lrcArray[lyricData.nowIndex + 1]">
<h2 class="lyric lyric-next">
{{ lyricData.lrcArray[lyricData.nowIndex + 1].text }}
</h2>
<p class="lyric-next">{{ lyricData.nextLrc.trText }}</p>
</template>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { useIpcRenderer } from '@vueuse/electron';
defineOptions({
name: 'Lyric',
});
const ipcRenderer = useIpcRenderer();
const lyricData = ref({
currentLrc: {
text: '',
trText: '',
},
nextLrc: {
text: '',
trText: '',
},
currentTime: 0,
nextTime: 0,
nowTime: 0,
allTime: 0,
startCurrentTime: 0,
lrcArray: [] as any,
lrcTimeArray: [] as any,
nowIndex: 0,
});
const lyricSetting = ref({
...(localStorage.getItem('lyricData')
? JSON.parse(localStorage.getItem('lyricData') || '')
: {
isTop: false,
theme: 'dark',
isLock: false,
}),
});
onMounted(() => {
ipcRenderer.on('receive-lyric', (event, data) => {
try {
lyricData.value = JSON.parse(data);
} catch (error) {
console.error('error', error);
}
});
});
const checkTheme = () => {
if (lyricSetting.value.theme === 'light') {
lyricSetting.value.theme = 'dark';
} else {
lyricSetting.value.theme = 'light';
}
};
const handleTop = () => {
lyricSetting.value.isTop = !lyricSetting.value.isTop;
ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
};
const handleLock = () => {
lyricSetting.value.isLock = !lyricSetting.value.isLock;
};
const handleClose = () => {
ipcRenderer.send('close-lyric');
};
watch(
() => lyricSetting.value,
(newValue) => {
localStorage.setItem('lyricData', JSON.stringify(newValue));
},
{ deep: true },
);
// onMounted(() => {
// const el = document.getElementById('clickThroughElement') as HTMLElement;
// el.addEventListener('mouseenter', () => {
// if (lyricSetting.value.isLock) ipcRenderer.send('mouseenter-lyric');
// });
// el.addEventListener('mouseleave', () => {
// if (lyricSetting.value.isLock) ipcRenderer.send('mouseleave-lyric');
// });
// });
</script>
<style>
body {
background-color: transparent !important;
}
</style>
<style lang="scss" scoped>
.lyric-window {
width: 100vw;
height: 100vh;
@apply overflow-hidden text-gray-600 rounded-xl box-border;
// border: 4px solid transparent;
&:hover .lyric-bar {
opacity: 1;
}
&:hover .drag-bar {
opacity: 1;
}
&:hover {
box-shadow: inset 0 0 10px 0 rgba(255, 255, 255, 0.5);
}
}
.lyric_lock {
&:hover {
box-shadow: none;
}
&:hover .lyric-bar {
background-color: transparent;
.button {
opacity: 0;
}
.button-lock {
opacity: 1;
color: #d6d6d6;
}
}
&:hover .drag-bar {
opacity: 0;
}
}
.icon {
@apply text-xl hover:text-white;
}
.lyric-bar {
background-color: #b1b1b1;
@apply flex flex-col justify-center items-center;
width: 100vw;
height: 40px;
opacity: 0;
&:hover {
opacity: 1;
}
}
.lyric-bar-hover {
opacity: 1;
}
.drag-bar {
-webkit-app-region: drag;
height: 20px;
cursor: move;
background-color: #383838;
opacity: 0;
}
.buttons {
width: 100vw;
height: 100px;
@apply flex justify-center items-center gap-4;
}
.button {
@apply cursor-pointer text-center;
}
.checked {
color: #fff !important;
}
.button-move {
-webkit-app-region: drag;
cursor: move;
}
.music-buttons {
@apply mx-6;
-webkit-app-region: no-drag;
.iconfont {
@apply text-2xl hover:text-green-500 transition;
}
@apply flex items-center;
> div {
@apply cursor-pointer;
}
&-play {
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
background: #383838;
}
}
.check-theme {
font-size: 26px;
cursor: pointer;
opacity: 1;
}
.lyric {
text-shadow: 0 0 1vw #2c2c2c;
font-size: 4vw;
@apply font-bold m-0 p-0 select-none pointer-events-none;
}
.lyric-current {
color: #333;
}
.lyric-next {
color: #999;
margin: 10px;
}
.lyric-window.dark {
.lyric {
text-shadow: none;
text-shadow: 0 0 1vw #000000;
}
.lyric-current {
color: #fff;
}
.lyric-next {
color: #cecece;
}
}
.lyric-box {
// writing-mode: vertical-rl;
padding: 10px;
}
</style>

View File

@@ -5,10 +5,22 @@
</div>
<n-scrollbar :size="100">
<div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="mv-item" v-for="(item, index) in mvList" :key="item.id"
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
<div
v-for="(item, index) in mvList"
:key="item.id"
class="mv-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<div class="mv-item-img" @click="handleShowMv(item)">
<n-image class="mv-item-img-img" :src="getImgUrl((item.cover), '200y112')" lazy preview-disabled width="200" height="112" />
<n-image
class="mv-item-img-img"
:src="getImgUrl(item.cover, '200y112')"
lazy
preview-disabled
width="200"
height="112"
/>
<div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i>
@@ -35,51 +47,54 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { getTopMv, getMvUrl } from '@/api/mv';
import { IMvItem } from '@/type/mv';
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useStore } from 'vuex';
const showMv = ref(false)
const mvList = ref<Array<IMvItem>>([])
const playMvItem = ref<IMvItem>()
const playMvUrl = ref<string>()
const store = useStore()
import { getMvUrl, getTopMv } from '@/api/mv';
import { IMvItem } from '@/type/mv';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'Mv',
});
const showMv = ref(false);
const mvList = ref<Array<IMvItem>>([]);
const playMvItem = ref<IMvItem>();
const playMvUrl = ref<string>();
const store = useStore();
onMounted(async () => {
const res = await getTopMv(30)
mvList.value = res.data.data
console.log('mvList.value', mvList.value)
})
const res = await getTopMv(30);
mvList.value = res.data.data;
});
const handleShowMv = async (item: IMvItem) => {
store.commit('setIsPlay', false)
store.commit('setPlayMusic', false)
showMv.value = true
const res = await getMvUrl(item.id)
store.commit('setIsPlay', false);
store.commit('setPlayMusic', false);
showMv.value = true;
const res = await getMvUrl(item.id);
playMvItem.value = item;
playMvUrl.value = res.data.data.url
}
playMvUrl.value = res.data.data.url;
};
const close = () => {
showMv.value = false
showMv.value = false;
if (store.state.playMusicUrl) {
store.commit('setIsPlay', true)
store.commit('setIsPlay', true);
}
}
};
</script>
<style scoped lang="scss">
.mv-list {
@apply relative h-full w-full;
@apply relative h-full w-full px-4;
&-title {
@apply text-xl font-bold;
}
&-content {
@apply grid gap-6 pb-4 mt-2;
@apply grid gap-6 pb-28 mt-2;
grid-template-columns: repeat(auto-fill, minmax(14%, 1fr));
}
@@ -157,4 +172,11 @@ const close = () => {
.mv-detail-title:hover {
@apply top-0;
}
}</style>
}
.mobile {
.mv-list-content {
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
}
}
</style>

View File

@@ -1,182 +1,214 @@
<template>
<div class="search-page">
<n-layout
class="hot-search"
:class="setAnimationClass('animate__fadeInDown')"
:native-scrollbar="false"
>
<div class="title">热搜列表</div>
<div class="hot-search-list">
<template v-for="(item, index) in hotSearchData?.data">
<div
:class="setAnimationClass('animate__bounceInLeft')"
:style="setAnimationDelay(index, 10)"
class="hot-search-item"
@click.stop="clickHotKeyword(item.searchWord)"
>
<span
class="hot-search-item-count"
:class="{ 'hot-search-item-count-3': index < 3 }"
>{{ index + 1 }}</span>
{{ item.searchWord }}
</div>
</template>
</div>
</n-layout>
<!-- 搜索到的歌曲列表 -->
<n-layout
class="search-list"
:class="setAnimationClass('animate__fadeInUp')"
:native-scrollbar="false"
>
<div class="title">{{ hotKeyword }}</div>
<div class="search-list-box">
<template v-if="searchDetail">
<div
v-for="(item, index) in searchDetail?.songs"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SongItem :item="item" @play="handlePlay"/>
</div>
<template v-for="(list, key) in searchDetail">
<template v-if="key.toString() !== 'songs'">
<div
v-for="(item, index) in list"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SearchItem :item="item"/>
</div>
</template>
</template>
</template>
<div class="search-page">
<n-layout
v-if="isMobile ? !searchDetail : true"
class="hot-search"
:class="setAnimationClass('animate__fadeInDown')"
:native-scrollbar="false"
>
<div class="title">热搜列表</div>
<div class="hot-search-list">
<template v-for="(item, index) in hotSearchData?.data" :key="index">
<div
:class="setAnimationClass('animate__bounceInLeft')"
:style="setAnimationDelay(index, 10)"
class="hot-search-item"
@click.stop="clickHotKeyword(item.searchWord)"
>
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
{{ item.searchWord }}
</div>
</template>
</div>
</n-layout>
<!-- 搜索到的歌曲列表 -->
<n-layout
v-if="isMobile ? searchDetail : true"
class="search-list"
:class="setAnimationClass('animate__fadeInUp')"
:native-scrollbar="false"
>
<div class="title">{{ hotKeyword }}</div>
<n-spin :show="searchDetailLoading">
<div class="search-list-box">
<template v-if="searchDetail">
<div
v-for="(item, index) in searchDetail?.songs"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<song-item :item="item" @play="handlePlay" />
</div>
</n-layout>
</div>
<template v-for="(list, key) in searchDetail">
<template v-if="key.toString() !== 'songs'">
<div
v-for="(item, index) in list"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SearchItem :item="item" />
</div>
</template>
</template>
</template>
</div>
</n-spin>
</n-layout>
</div>
</template>
<script lang="ts" setup>
import { getSearch } from "@/api/search";
import type { IHotSearch } from "@/type/search";
import { getHotSearch } from "@/api/home";
import { useRoute, useRouter } from "vue-router";
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { onMounted, ref, watch } from "vue";
import SongItem from "@/components/common/SongItem.vue";
import { useStore } from "vuex";
import { useDateFormat } from '@vueuse/core'
import { useDateFormat } from '@vueuse/core';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getHotSearch } from '@/api/home';
import { getSearch } from '@/api/search';
import SongItem from '@/components/common/SongItem.vue';
import type { IHotSearch } from '@/type/search';
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'Search',
});
const route = useRoute();
const router = useRouter();
const searchDetail = ref<any>();
const searchType = ref(Number(route.query.type) || 1);
const searchDetailLoading = ref(false);
// 热搜列表
const hotSearchData = ref<IHotSearch>();
const loadHotSearch = async () => {
const { data } = await getHotSearch();
hotSearchData.value = data;
const { data } = await getHotSearch();
hotSearchData.value = data;
};
onMounted(() => {
loadHotSearch();
loadHotSearch();
});
const hotKeyword = ref(route.query.keyword || "搜索列表");
const hotKeyword = ref(route.query.keyword || '搜索列表');
const clickHotKeyword = (keyword: string) => {
hotKeyword.value = keyword;
router.push({
path: "/search",
query: {
keyword: keyword,
type: 1
},
});
// isHotSearchList.value = false;
hotKeyword.value = keyword;
router.push({
path: '/search',
query: {
keyword,
type: 1,
},
});
// isHotSearchList.value = false;
};
const dateFormat = (time:any) => useDateFormat(time, 'YYYY.MM.DD').value
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
const loadSearch = async (keywords: any) => {
hotKeyword.value = keywords;
searchDetail.value = undefined;
if (!keywords) return;
const { data } = await getSearch({keywords, type:searchType.value});
hotKeyword.value = keywords;
searchDetail.value = undefined;
if (!keywords) return;
const songs = data.result.songs || [];
const albums = data.result.albums || [];
searchDetailLoading.value = true;
const { data } = await getSearch({ keywords, type: searchType.value });
// songs map 替换属性
songs.map((item: any) => {
item.picUrl = item.al.picUrl;
item.song = item;
item.artists = item.ar;
});
albums.map((item: any) => {
item.desc = `${item.artist.name } ${ item.company } ${dateFormat(item.publishTime)}`;
});
searchDetail.value = {
songs,
albums
}
const songs = data.result.songs || [];
const albums = data.result.albums || [];
const mvs = (data.result.mvs || []).map((item: any) => ({
...item,
picUrl: item.cover,
playCount: item.playCount,
desc: item.artists.map((artist: any) => artist.name).join('/'),
type: 'mv',
}));
const playlists = (data.result.playlists || []).map((item: any) => ({
...item,
picUrl: item.coverImgUrl,
playCount: item.playCount,
desc: item.creator.nickname,
type: 'playlist',
}));
// songs map 替换属性
songs.forEach((item: any) => {
item.picUrl = item.al.picUrl;
item.song = item;
item.artists = item.ar;
});
albums.forEach((item: any) => {
item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`;
});
searchDetail.value = {
songs,
albums,
mvs,
playlists,
};
searchDetailLoading.value = false;
};
loadSearch(route.query.keyword);
watch(
() => route.query,
async newParams => {
searchType.value = Number(newParams.type || 1)
loadSearch(newParams.keyword);
}
)
() => route.query,
async (newParams) => {
searchType.value = Number(newParams.type || 1);
loadSearch(newParams.keyword);
},
);
const store = useStore()
const store = useStore();
const handlePlay = (item: any) => {
const tracks = searchDetail.value?.songs || []
store.commit('setPlayList', tracks)
}
const handlePlay = () => {
const tracks = searchDetail.value?.songs || [];
store.commit('setPlayList', tracks);
};
</script>
<style lang="scss" scoped>
.search-page {
@apply flex h-full;
@apply flex h-full;
}
.hot-search {
@apply mr-4 rounded-xl flex-1 overflow-hidden;
background-color: #0d0d0d;
animation-duration: 0.2s;
min-width: 400px;
height: 100%;
&-list{
@apply pb-28;
}
&-item {
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
&-count {
@apply text-green-500 inline-block ml-3 w-8;
&-3 {
@apply text-red-600 font-bold inline-block ml-3 w-8;
}
}
@apply mr-4 rounded-xl flex-1 overflow-hidden;
background-color: #0d0d0d;
animation-duration: 0.2s;
min-width: 400px;
height: 100%;
&-list {
@apply pb-28;
}
&-item {
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
&-count {
@apply text-green-500 inline-block ml-3 w-8;
&-3 {
@apply text-red-600 font-bold inline-block ml-3 w-8;
}
}
}
}
.search-list {
@apply flex-1 rounded-xl;
background-color: #0d0d0d;
height: 100%;
&-box{
@apply pb-28;
}
@apply flex-1 rounded-xl;
background-color: #0d0d0d;
height: 100%;
&-box {
@apply pb-28;
}
}
.title {
@apply text-gray-200 text-xl font-bold my-2 mx-4;
@apply text-gray-200 text-xl font-bold my-2 mx-4;
}
.mobile {
.hot-search {
@apply mr-0 w-full;
}
}
</style>

View File

@@ -5,7 +5,7 @@
<div class="set-item-title">代理</div>
<div class="set-item-content">无法听音乐时打开</div>
</div>
<n-switch v-model:value="setData.isProxy"/>
<n-switch v-model:value="setData.isProxy" />
</div>
<div class="set-item">
<div>
@@ -30,35 +30,40 @@
</template>
<script setup lang="ts">
import store from '@/store'
import { useRouter } from 'vue-router';
const setData = ref(store.state.setData)
const router = useRouter()
import store from '@/store';
defineOptions({
name: 'Setting',
});
const setData = ref(store.state.setData);
const router = useRouter();
const handelCancel = () => {
router.back()
}
router.back();
};
const windowData = window as any
const windowData = window as any;
const handleSave = () => {
store.commit('setSetData', setData.value)
windowData.electronAPI.restart()
}
store.commit('setSetData', setData.value);
windowData.electronAPI.restart();
};
</script>
<style scoped lang="scss">
.set-page{
.set-page {
@apply flex flex-col justify-center items-center pt-8;
}
.set-item{
.set-item {
@apply w-3/5 flex justify-between items-center mb-4;
.set-item-title{
.set-item-title {
@apply text-gray-200 text-base;
}
.set-item-content{
.set-item-content {
@apply text-gray-400 text-sm;
}
}
</style>
</style>

View File

@@ -1,89 +1,93 @@
<script lang="ts" setup>
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { getUserDetail, getUserPlaylist, getUserRecord } from "@/api/user";
import type { IUserDetail } from "@/type/user";
import { computed, ref } from "vue";
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import { getListDetail } from '@/api/list'
import SongItem from "@/components/common/SongItem.vue";
import MusicList from "@/components/MusicList.vue";
import { computed, ref } from 'vue';
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getListDetail } from '@/api/list';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from '@/components/common/SongItem.vue';
import MusicList from '@/components/MusicList.vue';
import type { Playlist } from '@/type/listDetail';
import PlayBottom from "@/components/common/PlayBottom.vue";
import type { IUserDetail } from '@/type/user';
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'User',
});
const store = useStore()
const router = useRouter()
const userDetail = ref<IUserDetail>()
const playList = ref<any[]>([])
const recordList = ref()
let user = store.state.user
const store = useStore();
const router = useRouter();
const userDetail = ref<IUserDetail>();
const playList = ref<any[]>([]);
const recordList = ref();
const user = computed(() => store.state.user);
const loadPage = async () => {
if (!user) {
router.push("/login")
return
if (!user.value) {
router.push('/login');
return;
}
const { data: userData } = await getUserDetail(user.userId)
userDetail.value = userData
const { data: userData } = await getUserDetail(user.value.userId);
userDetail.value = userData;
const { data: playlistData } = await getUserPlaylist(user.userId)
playList.value = playlistData.playlist
const { data: playlistData } = await getUserPlaylist(user.value.userId);
playList.value = playlistData.playlist;
const { data: recordData } = await getUserRecord(user.userId)
recordList.value = recordData.allData
}
const { data: recordData } = await getUserRecord(user.value.userId);
recordList.value = recordData.allData;
};
watchEffect(() => {
const localUser = localStorage.getItem('user')
store.state.user = localUser ? JSON.parse(localUser) : null
user = store.state.user
loadPage()
})
onActivated(() => {
if (!user.value) {
router.push('/login');
} else {
loadPage();
}
});
const isShowList = ref(false)
const list = ref<Playlist>()
const isShowList = ref(false);
const list = ref<Playlist>();
// 展示歌单
const showPlaylist = async (id: number) => {
const { data } = await getListDetail(id)
isShowList.value = true
list.value = data.playlist
}
const { data } = await getListDetail(id);
isShowList.value = true;
list.value = data.playlist;
};
// 格式化歌曲列表项
const formatDetail = computed(() => (detail: any) => {
let song = {
const song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
}
};
detail.song = song
detail.picUrl = detail.al.picUrl
return detail
})
const handlePlay = (item: any) => {
const tracks = recordList.value || []
store.commit('setPlayList', tracks)
}
detail.song = song;
detail.picUrl = detail.al.picUrl;
return detail;
});
const handlePlay = () => {
const tracks = recordList.value || [];
store.commit('setPlayList', tracks);
};
</script>
<template>
<div class="user-page" @click.stop="isShowList = false">
<div
class="left"
v-if="userDetail"
class="left"
:class="setAnimationClass('animate__fadeInLeft')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
>
<div class="page">
<div class="user-name">{{ user.nickname }}</div>
<div class="user-info">
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl,'50y50')" />
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
<div class="user-info-list">
<div class="user-info-item">
<div class="label">{{ userDetail.profile.followeds }}</div>
@@ -102,49 +106,39 @@ const handlePlay = (item: any) => {
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
<div class="play-list-title">创建的歌单</div>
<div class=" ">创建的歌单</div>
<n-scrollbar>
<div
class="play-list-item"
v-for="(item,index) in playList"
:key="index"
@click="showPlaylist(item.id)"
>
<n-image
:src="getImgUrl( item.coverImgUrl, '50y50')"
class="play-list-item-img"
lazy
preview-disabled
/>
<div v-for="(item, index) in playList" :key="index" class="play-list-item" @click="showPlaylist(item.id)">
<n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
<div class="play-list-item-info">
<div class="play-list-item-name">{{ item.name }}</div>
<div class="play-list-item-count">{{ item.trackCount }}播放{{ item.playCount }}</div>
</div>
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
</div>
</div>
</div>
<div class="right" :class="setAnimationClass('animate__fadeInRight')">
<div v-if="!isMobile" class="right" :class="setAnimationClass('animate__fadeInRight')">
<div class="title">听歌排行</div>
<div class="record-list">
<n-scrollbar>
<div
class="record-item"
v-for="(item, index) in recordList"
:key="item.song.id"
class="record-item"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)"
>
<SongItem class="song-item" :item="formatDetail(item.song)" @play="handlePlay"/>
<song-item class="song-item" :item="formatDetail(item.song)" @play="handlePlay" />
<div class="play-count">{{ item.playCount }}</div>
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
</div>
</div>
<MusicList v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
<music-list v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
</div>
</template>
@@ -179,10 +173,11 @@ const handlePlay = (item: any) => {
}
.right {
@apply flex-1 ml-4;
@apply flex-1 ml-4 overflow-hidden h-full;
.record-list {
background-color: #0d0d0d;
@apply rounded-2xl h-full;
@apply rounded-2xl;
height: calc(100% - 3.75rem);
.record-item {
@apply flex items-center px-4;
}
@@ -223,4 +218,9 @@ const handlePlay = (item: any) => {
}
}
</style>
.mobile {
.user-page {
@apply px-4;
}
}
</style>

View File

@@ -1,8 +1,5 @@
module.exports = {
purge: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}'
],
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
@@ -11,4 +8,4 @@ module.exports = {
extend: {},
},
plugins: [],
}
};

View File

@@ -1,26 +1,22 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import VueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue';
import path from 'path';
// import VueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import { defineConfig } from 'vite';
import viteCompression from 'vite-plugin-compression';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
VueDevTools(),
viteCompression(),
// VueDevTools(),
AutoImport({
imports: [
'vue',
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar',
],
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
},
],
}),
@@ -35,16 +31,10 @@ export default defineConfig({
},
},
server: {
host: '0.0.0.0', //允许本机
host: '0.0.0.0',
// 指定端口
port: 4678,
proxy: {
// string shorthand
'/mt': {
target: 'http://mt.myalger.top',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/mt/, ''),
},
// with options
'/api': {
target: 'http://110.42.251.190:9898',
@@ -69,4 +59,4 @@ export default defineConfig({
},
},
},
})
});