Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06bffe7618 | ||
|
|
7abc087d70 | ||
|
|
eb2ea1981d | ||
|
|
6dc14ec51b | ||
|
|
36f8257a3e | ||
|
|
c55544df46 | ||
|
|
008f2183de | ||
|
|
dd3a3c3bbb | ||
|
|
941eb2e66e | ||
|
|
a98fcb43d6 | ||
|
|
791121ae06 | ||
|
|
0c156e2708 | ||
|
|
017b47fded | ||
|
|
e27ed22c16 | ||
|
|
904d8744ef | ||
|
|
800e0b7360 | ||
|
|
b6a5461a1d | ||
|
|
a4eda61a86 | ||
|
|
a79d0712a4 | ||
|
|
8f782cdc9d | ||
|
|
2f851f3172 | ||
|
|
9fcf455c08 | ||
|
|
9b14906a46 | ||
|
|
14ce428951 | ||
|
|
8c93124311 | ||
|
|
c09707867b | ||
|
|
a2af0f3904 | ||
|
|
73982f0e84 | ||
|
|
449a6fd335 | ||
|
|
32b39c7927 | ||
|
|
c6f1e0b233 | ||
|
|
7c1a3ae4bc | ||
|
|
6bd6622484 | ||
|
|
433aff385d | ||
|
|
c37ad07f93 | ||
|
|
e4c1f855fb | ||
|
|
6978656061 | ||
|
|
973d60c98f | ||
|
|
5a43ba2576 | ||
|
|
e52a02cf3c | ||
|
|
da8216e2ca | ||
|
|
bd0e2ec35c | ||
|
|
7c8598ffa5 | ||
|
|
50e594b91d | ||
|
|
a9e5bb33e4 | ||
|
|
5e8676a039 |
@@ -1,4 +1,3 @@
|
|||||||
VITE_API = /api
|
VITE_API = /api
|
||||||
VITE_API_MT = /mt
|
|
||||||
VITE_API_MUSIC = /music
|
VITE_API_MUSIC = /music
|
||||||
VITE_API_PROXY = http://110.42.251.190:9856
|
VITE_API_PROXY = http://110.42.251.190:9856
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
VITE_API = http://110.42.251.190:9898
|
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_MUSIC = http://110.42.251.190:4100
|
||||||
VITE_API_PROXY = http://110.42.251.190:9856
|
VITE_API_PROXY = http://110.42.251.190:9856
|
||||||
13
.eslintignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
snapshot*
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
es
|
||||||
|
esm
|
||||||
|
node_modules
|
||||||
|
static
|
||||||
|
cypress
|
||||||
|
script/test/cypress
|
||||||
|
_site
|
||||||
|
temp*
|
||||||
|
static/
|
||||||
|
!.prettierrc.js
|
||||||
136
.eslintrc
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"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-return-assign": "off",
|
||||||
|
"no-unused-expressions": "off",
|
||||||
|
"no-return-await": "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
@@ -0,0 +1 @@
|
|||||||
|
* @algerkong
|
||||||
70
.github/ISSUE_TEMPLATE/bug-report.zh-CN.yml
vendored
Normal 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
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name:
|
||||||
|
url:
|
||||||
|
about:
|
||||||
29
.github/ISSUE_TEMPLATE/feature-report.zh-CN.yml
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
|||||||
|
## IssueShoot
|
||||||
|
- 预估时长: {{ .duration }}
|
||||||
|
- 期望完成时间: {{ .deadline }}
|
||||||
|
- 开发难度: {{ .level }}
|
||||||
|
- 参与人数: 1
|
||||||
|
- 需求对接人: ivringpeng
|
||||||
|
- 验收标准: 实现期望改造效果,提 PR 并通过验收无误
|
||||||
|
- 备注: 最终激励以实际提交 `pull request` 并合并为准
|
||||||
7
.gitignore
vendored
@@ -1,11 +1,16 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
dist_electron
|
dist_electron
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# lock
|
||||||
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
dist.zip
|
dist.zip
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|||||||
39
.prettierrc.js
Normal 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',
|
||||||
|
};
|
||||||
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"compile-hero.disable-compile-files-on-did-save-code": true
|
|
||||||
}
|
|
||||||
201
LICENSE
Normal 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.
|
||||||
46
README.md
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 软件截图
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 欢迎提Issues
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
本软件仅用于学习交流,禁止用于商业用途,否则后果自负。
|
||||||
|
|||||||
124
app.js
@@ -1,9 +1,10 @@
|
|||||||
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron')
|
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron');
|
||||||
const path = require('path')
|
const path = require('path');
|
||||||
const Store = require('electron-store');
|
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() {
|
function createWindow() {
|
||||||
mainWin = new BrowserWindow({
|
mainWin = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -11,129 +12,134 @@ function createWindow() {
|
|||||||
frame: false,
|
frame: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
// contextIsolation: false,
|
||||||
preload: path.join(__dirname, '/electron/preload.js'),
|
preload: path.join(__dirname, '/electron/preload.js'),
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
const win = mainWin
|
const win = mainWin;
|
||||||
win.setMinimumSize(1200, 780)
|
win.setMinimumSize(1200, 780);
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.webContents.openDevTools({ mode: 'detach' })
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
win.loadURL('http://localhost:4678/')
|
win.loadURL('http://localhost:4488/');
|
||||||
} else {
|
} 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 image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'));
|
||||||
const tray = new Tray(image)
|
const tray = new Tray(image);
|
||||||
|
|
||||||
// 创建一个上下文菜单
|
// 创建一个上下文菜单
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: '显示',
|
label: '显示',
|
||||||
click: () => {
|
click: () => {
|
||||||
win.show()
|
win.show();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '退出',
|
label: '退出',
|
||||||
click: () => {
|
click: () => {
|
||||||
win.destroy()
|
win.destroy();
|
||||||
|
app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
|
|
||||||
// 设置系统托盘图标的上下文菜单
|
// 设置系统托盘图标的上下文菜单
|
||||||
tray.setContextMenu(contextMenu)
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
||||||
tray.on('click', () => {
|
tray.on('click', () => {
|
||||||
if (win.isVisible()) {
|
if (win.isVisible()) {
|
||||||
win.hide()
|
win.hide();
|
||||||
} else {
|
} else {
|
||||||
win.show()
|
win.show();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const set = store.get('set')
|
const set = store.get('set');
|
||||||
// store.set('set', setJson)
|
// store.set('set', setJson)
|
||||||
|
|
||||||
if (!set) {
|
if (!set) {
|
||||||
store.set('set', setJson)
|
store.set('set', setJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadLyricWindow(ipcMain);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制只能启动一个应用
|
// 限制只能启动一个应用
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) {
|
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', () => {
|
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||||
if (mainWin.isVisible()) {
|
if (mainWin.isVisible()) {
|
||||||
mainWin.hide()
|
mainWin.hide();
|
||||||
} else {
|
} else {
|
||||||
mainWin.show()
|
mainWin.show();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
app.quit();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
app.on('will-quit', () => {
|
app.on('will-quit', () => {
|
||||||
globalShortcut.unregisterAll()
|
globalShortcut.unregisterAll();
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.on('minimize-window', (event) => {
|
ipcMain.on('minimize-window', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
win.minimize()
|
win.minimize();
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.on('maximize-window', (event) => {
|
ipcMain.on('maximize-window', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (win.isMaximized()) {
|
if (win.isMaximized()) {
|
||||||
win.unmaximize()
|
win.unmaximize();
|
||||||
} else {
|
} else {
|
||||||
win.maximize()
|
win.maximize();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.on('close-window', (event) => {
|
ipcMain.on('close-window', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
win.destroy()
|
win.destroy();
|
||||||
})
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('drag-start', (event, data) => {
|
ipcMain.on('drag-start', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
win.webContents.beginFrameSubscription((frameBuffer) => {
|
win.webContents.beginFrameSubscription((frameBuffer) => {
|
||||||
event.reply('frame-buffer', frameBuffer)
|
event.reply('frame-buffer', frameBuffer);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.on('mini-tray', (event) => {
|
ipcMain.on('mini-tray', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
win.hide()
|
win.hide();
|
||||||
})
|
});
|
||||||
|
|
||||||
// 重启
|
// 重启
|
||||||
ipcMain.on('restart', () => {
|
ipcMain.on('restart', () => {
|
||||||
app.relaunch()
|
app.relaunch();
|
||||||
app.exit(0)
|
app.exit(0);
|
||||||
})
|
});
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
// 定义ipcRenderer监听事件
|
// 定义ipcRenderer监听事件
|
||||||
ipcMain.on('setStore', (_, key, value) => {
|
ipcMain.on('setStore', (_, key, value) => {
|
||||||
store.set(key, value)
|
store.set(key, value);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.on('getStore', (_, key) => {
|
ipcMain.on('getStore', (_, key) => {
|
||||||
let value = store.get(key)
|
const value = store.get(key);
|
||||||
_.returnValue = value || ""
|
_.returnValue = value || '';
|
||||||
})
|
});
|
||||||
|
|||||||
31
build/win32.json
Normal 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
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
components.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
||||||
@@ -25,9 +25,11 @@ declare module 'vue' {
|
|||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
|
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||||
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
||||||
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
||||||
PlaylistType: typeof import('./src/components/PlaylistType.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']
|
RecommendAlbum: typeof import('./src/components/RecommendAlbum.vue')['default']
|
||||||
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
|
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
|
||||||
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']
|
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']
|
||||||
|
|||||||
BIN
docs/img/image-1.png
Normal file
|
After Width: | Height: | Size: 902 KiB |
BIN
docs/img/image-2.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
docs/img/image-3.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/img/image-4.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
docs/img/image-5.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
docs/img/image.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
@@ -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
@@ -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,
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron')
|
const { contextBridge, ipcRenderer, app } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
minimize: () => ipcRenderer.send('minimize-window'),
|
minimize: () => ipcRenderer.send('minimize-window'),
|
||||||
@@ -7,19 +7,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
||||||
miniTray: () => ipcRenderer.send('mini-tray'),
|
miniTray: () => ipcRenderer.send('mini-tray'),
|
||||||
restart: () => ipcRenderer.send('restart'),
|
restart: () => ipcRenderer.send('restart'),
|
||||||
})
|
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||||
|
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||||
|
});
|
||||||
|
|
||||||
const electronHandler = {
|
const electronHandler = {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
setStoreValue: (key, value) => {
|
setStoreValue: (key, value) => {
|
||||||
ipcRenderer.send("setStore", key, value)
|
ipcRenderer.send('setStore', key, value);
|
||||||
},
|
},
|
||||||
|
|
||||||
getStoreValue(key) {
|
getStoreValue(key) {
|
||||||
const resp = ipcRenderer.sendSync("getStore", key)
|
const resp = ipcRenderer.sendSync('getStore', key);
|
||||||
return resp
|
return resp;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
app,
|
||||||
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler)
|
contextBridge.exposeInMainWorld('electron', electronHandler);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.0",
|
"version": "1.5.1",
|
||||||
"isProxy": false,
|
"isProxy": false,
|
||||||
"author": "alger"
|
"author": "alger"
|
||||||
}
|
}
|
||||||
|
|||||||
44
index.html
@@ -1,25 +1,25 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>网抑云 | algerkong</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link
|
<title>网抑云 | algerkong</title>
|
||||||
rel="stylesheet"
|
<link rel="manifest" href="./public/manifest.json" />
|
||||||
href="./public/icon/iconfont.css"
|
<link rel="stylesheet" href="./public/icon/iconfont.css" />
|
||||||
/>
|
<link rel="stylesheet" href="./public/css/animate.css" />
|
||||||
<link rel="stylesheet" href="./public/css/animate.css" />
|
<link rel="stylesheet" href="./public/css/base.css" />
|
||||||
<link rel="stylesheet" href="./public/css/base.css" />
|
<style>
|
||||||
<style>
|
:root {
|
||||||
:root {
|
--animate-delay: 0.5s;
|
||||||
--animate-delay: 0.5s;
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
</head>
|
||||||
</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>
|
</html>
|
||||||
74
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "alger-music",
|
"name": "alger-music",
|
||||||
"version": "1.3.0",
|
"version": "2.0.0",
|
||||||
"description": "这是一个用于音乐播放的应用程序。",
|
"description": "这是一个用于音乐播放的应用程序。",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
@@ -8,40 +8,54 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"es": "vite && electron .",
|
|
||||||
"start": "set NODE_ENV=development&&electron .",
|
"start": "set NODE_ENV=development&&electron .",
|
||||||
"e:b": "electron-builder --config ./electron.config.json",
|
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
|
||||||
"eb": "vite build && e:b"
|
"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": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"electron-store": "^8.1.0"
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sicons/antd": "^0.10.0",
|
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||||
"@vicons/antd": "^0.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@vitejs/plugin-vue": "^5.1.3",
|
||||||
"electron": "^28.0.0",
|
"@vue/compiler-sfc": "^3.5.0",
|
||||||
"electron-builder": "^24.9.1",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"typescript": "^4.3.2",
|
"@vue/runtime-core": "^3.5.0",
|
||||||
"unplugin-auto-import": "^0.17.2",
|
"@vueuse/core": "^11.0.3",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"@vueuse/electron": "^11.0.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"electron": "^32.0.1",
|
||||||
|
"electron-builder": "^25.0.5",
|
||||||
|
"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.39.0",
|
||||||
|
"postcss": "^8.4.44",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"remixicon": "^4.2.0",
|
||||||
|
"sass": "^1.78.0",
|
||||||
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"unplugin-auto-import": "^0.18.2",
|
||||||
|
"unplugin-vue-components": "^0.27.4",
|
||||||
"vfonts": "^0.1.0",
|
"vfonts": "^0.1.0",
|
||||||
"vite": "^4.4.7",
|
"vite": "^5.4.3",
|
||||||
"vite-plugin-vue-devtools": "1.0.0-beta.5",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue-tsc": "^0.0.24"
|
"vite-plugin-vue-devtools": "7.4.0",
|
||||||
|
"vue": "^3.5.0",
|
||||||
|
"vue-router": "^4.4.3",
|
||||||
|
"vue-tsc": "^2.1.4",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
body{
|
body{
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-popover:has(.music-play){
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 178 KiB |
10
public/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Alger Music PWA",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "256x256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
85
src/App.vue
@@ -1,37 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app" :class="isMobile ? 'mobile' : ''">
|
||||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
||||||
<n-config-provider :theme="darkTheme">
|
<n-config-provider :theme="darkTheme">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { darkTheme } from 'naive-ui'
|
import { darkTheme } from 'naive-ui';
|
||||||
import store from '@/store'
|
|
||||||
|
import store from '@/store';
|
||||||
const audio = ref<HTMLAudioElement | null>(null)
|
|
||||||
|
import { isMobile } from './utils';
|
||||||
const playMusicUrl = computed(() => store.state.playMusicUrl as string)
|
|
||||||
// 是否播放
|
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
|
||||||
const play = computed(() => store.state.play as boolean)
|
// 是否播放
|
||||||
const windowData = window as any
|
const play = computed(() => store.state.play as boolean);
|
||||||
onMounted(()=>{
|
const windowData = window as any;
|
||||||
if(windowData.electron){
|
onMounted(() => {
|
||||||
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
|
if (windowData.electron) {
|
||||||
store.commit('setSetData', setData)
|
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
|
||||||
}
|
store.commit('setSetData', setData);
|
||||||
})
|
}
|
||||||
</script>
|
});
|
||||||
|
</script>
|
||||||
<style lang="scss" scoped >
|
|
||||||
div {
|
<style lang="scss" scoped>
|
||||||
box-sizing: border-box;
|
div {
|
||||||
}
|
box-sizing: border-box;
|
||||||
.app {
|
}
|
||||||
user-select: none;
|
.app {
|
||||||
}
|
user-select: none;
|
||||||
</style>
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.text-base {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.html:has(.mobile) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import request from "@/utils/request";
|
import { IData } from '@/type';
|
||||||
import { IHotSinger } from "@/type/singer";
|
import { IAlbumNew } from '@/type/album';
|
||||||
import { ISearchKeyword, IHotSearch } from "@/type/search";
|
import { IDayRecommend } from '@/type/day_recommend';
|
||||||
import { IPlayListSort } from "@/type/playlist";
|
import { IRecommendMusic } from '@/type/music';
|
||||||
import { IRecommendMusic } from "@/type/music";
|
import { IPlayListSort } from '@/type/playlist';
|
||||||
import { IAlbumNew } from "@/type/album";
|
import { IHotSearch, ISearchKeyword } from '@/type/search';
|
||||||
|
import { IHotSinger } from '@/type/singer';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
interface IHotSingerParams {
|
interface IHotSingerParams {
|
||||||
offset: number;
|
offset: number;
|
||||||
@@ -16,30 +18,35 @@ interface IRecommendMusicParams {
|
|||||||
|
|
||||||
// 获取热门歌手
|
// 获取热门歌手
|
||||||
export const getHotSinger = (params: IHotSingerParams) => {
|
export const getHotSinger = (params: IHotSingerParams) => {
|
||||||
return request.get<IHotSinger>("/top/artists", { params });
|
return request.get<IHotSinger>('/top/artists', { params });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取搜索推荐词
|
// 获取搜索推荐词
|
||||||
export const getSearchKeyword = () => {
|
export const getSearchKeyword = () => {
|
||||||
return request.get<ISearchKeyword>("/search/default");
|
return request.get<ISearchKeyword>('/search/default');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取热门搜索
|
// 获取热门搜索
|
||||||
export const getHotSearch = () => {
|
export const getHotSearch = () => {
|
||||||
return request.get<IHotSearch>("/search/hot/detail");
|
return request.get<IHotSearch>('/search/hot/detail');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取歌单分类
|
// 获取歌单分类
|
||||||
export const getPlaylistCategory = () => {
|
export const getPlaylistCategory = () => {
|
||||||
return request.get<IPlayListSort>("/playlist/catlist");
|
return request.get<IPlayListSort>('/playlist/catlist');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取推荐音乐
|
// 获取推荐音乐
|
||||||
export const getRecommendMusic = (params: IRecommendMusicParams) => {
|
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<IData<IDayRecommend>>>('/recommend/songs');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取最新专辑推荐
|
// 获取最新专辑推荐
|
||||||
export const getNewAlbum = () => {
|
export const getNewAlbum = () => {
|
||||||
return request.get<IAlbumNew>("/album/newest");
|
return request.get<IAlbumNew>('/album/newest');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import request from "@/utils/request";
|
import { IList } from '@/type/list';
|
||||||
import { IList } from "@/type/list";
|
import type { IListDetail } from '@/type/listDetail';
|
||||||
import type { IListDetail } from "@/type/listDetail";
|
import request from '@/utils/request';
|
||||||
|
|
||||||
interface IListByTagParams {
|
interface IListByTagParams {
|
||||||
tag: string;
|
tag: string;
|
||||||
before: number;
|
before: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IListByCatParams {
|
interface IListByCatParams {
|
||||||
cat: string;
|
cat: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据tag 获取歌单列表
|
// 根据tag 获取歌单列表
|
||||||
export function getListByTag(params: IListByTagParams) {
|
export function getListByTag(params: IListByTagParams) {
|
||||||
return request.get<IList>("/top/playlist/highquality", { params: params });
|
return request.get<IList>('/top/playlist/highquality', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据cat 获取歌单列表
|
// 根据cat 获取歌单列表
|
||||||
export function getListByCat(params: IListByCatParams) {
|
export function getListByCat(params: IListByCatParams) {
|
||||||
return request.get("/top/playlist", {
|
return request.get('/top/playlist', {
|
||||||
params: params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取推荐歌单
|
// 获取推荐歌单
|
||||||
export function getRecommendList(limit: number = 30) {
|
export function getRecommendList(limit: number = 30) {
|
||||||
return request.get("/personalized", { params: { limit } });
|
return request.get('/personalized', { params: { limit } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取歌单详情
|
// 获取歌单详情
|
||||||
export function getListDetail(id: number | string) {
|
export function getListDetail(id: number | string) {
|
||||||
return request.get<IListDetail>("/playlist/detail", { params: { id } });
|
return request.get<IListDetail>('/playlist/detail', { params: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取专辑内容
|
// 获取专辑内容
|
||||||
export function getAlbum(id: number | string) {
|
export function getAlbum(id: number | string) {
|
||||||
return request.get("/album", { params: { id } });
|
return request.get('/album', { params: { id } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import request from "@/utils/request";
|
import request from '@/utils/request';
|
||||||
|
|
||||||
// 创建二维码key
|
// 创建二维码key
|
||||||
// /login/qr/key
|
// /login/qr/key
|
||||||
export function getQrKey() {
|
export function getQrKey() {
|
||||||
return request.get("/login/qr/key");
|
return request.get('/login/qr/key');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建二维码
|
// 创建二维码
|
||||||
// /login/qr/create
|
// /login/qr/create
|
||||||
export function createQr(key: any) {
|
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
|
// /login/qr/check
|
||||||
export function checkQr(key: any) {
|
export function checkQr(key: any) {
|
||||||
return request.get("/login/qr/check", { params: { key: key } });
|
return request.get('/login/qr/check', { params: { key } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取登录状态
|
// 获取登录状态
|
||||||
// /login/status
|
// /login/status
|
||||||
export function getLoginStatus() {
|
export function getLoginStatus() {
|
||||||
return request.get("/login/status");
|
return request.get('/login/status');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
// /user/account
|
// /user/account
|
||||||
export function getUserDetail() {
|
export function getUserDetail() {
|
||||||
return request.get("/user/account");
|
return request.get('/user/account');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
// /logout
|
// /logout
|
||||||
export function logout() {
|
export function logout() {
|
||||||
return request.get("/logout");
|
return request.get('/logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手机号登录
|
// 手机号登录
|
||||||
// /login/cellphone
|
// /login/cellphone
|
||||||
export function loginByCellphone(phone: any, password: any) {
|
export function loginByCellphone(phone: string, password: string) {
|
||||||
return request.post("/login/cellphone", {
|
return request.post('/login/cellphone', {
|
||||||
phone: phone,
|
phone,
|
||||||
password: password,
|
password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { IPlayMusicUrl } from "@/type/music"
|
import { ILyric } from '@/type/lyric';
|
||||||
import { ILyric } from "@/type/lyric"
|
import { IPlayMusicUrl } from '@/type/music';
|
||||||
import request from "@/utils/request"
|
import request from '@/utils/request';
|
||||||
import requestMusic from "@/utils/request_music"
|
import requestMusic from '@/utils/request_music';
|
||||||
// 根据音乐Id获取音乐播放URl
|
// 根据音乐Id获取音乐播放URl
|
||||||
export const getMusicUrl = (id: number) => {
|
export const getMusicUrl = (id: number) => {
|
||||||
return request.get<IPlayMusicUrl>("/song/url", { params: { id: id } })
|
return request.get<IPlayMusicUrl>('/song/url', { params: { id } });
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取歌曲详情
|
// 获取歌曲详情
|
||||||
export const getMusicDetail = (ids: Array<number>) => {
|
export const getMusicDetail = (ids: Array<number>) => {
|
||||||
return request.get("/song/detail", { params: { ids: ids.join(",")}})
|
return request.get('/song/detail', { params: { ids: ids.join(',') } });
|
||||||
}
|
};
|
||||||
|
|
||||||
// 根据音乐Id获取音乐歌词
|
// 根据音乐Id获取音乐歌词
|
||||||
export const getMusicLrc = (id: number) => {
|
export const getMusicLrc = (id: number) => {
|
||||||
return request.get<ILyric>("/lyric", { params: { id: id } })
|
return request.get<ILyric>('/lyric', { params: { id } });
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number) => {
|
export const getParsingMusicUrl = (id: number) => {
|
||||||
return requestMusic.get<any>("/music", { params: { id: id } })
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IData } from '@/type'
|
import { IData } from '@/type';
|
||||||
import { IMvItem, IMvUrlData } from '@/type/mv'
|
import { IMvItem, IMvUrlData } from '@/type/mv';
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request';
|
||||||
|
|
||||||
// 获取 mv 排行
|
// 获取 mv 排行
|
||||||
export const getTopMv = (limit: number) => {
|
export const getTopMv = (limit: number) => {
|
||||||
@@ -8,8 +8,8 @@ export const getTopMv = (limit: number) => {
|
|||||||
params: {
|
params: {
|
||||||
limit,
|
limit,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取 mv 数据
|
// 获取 mv 数据
|
||||||
export const getMvDetail = (mvid: string) => {
|
export const getMvDetail = (mvid: string) => {
|
||||||
@@ -17,8 +17,8 @@ export const getMvDetail = (mvid: string) => {
|
|||||||
params: {
|
params: {
|
||||||
mvid,
|
mvid,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取 mv 地址
|
// 获取 mv 地址
|
||||||
export const getMvUrl = (id: Number) => {
|
export const getMvUrl = (id: Number) => {
|
||||||
@@ -26,5 +26,5 @@ export const getMvUrl = (id: Number) => {
|
|||||||
params: {
|
params: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import request from "@/utils/request"
|
import request from '@/utils/request';
|
||||||
import { ISearchDetail } from "@/type/search"
|
|
||||||
|
|
||||||
interface IParams {
|
interface IParams {
|
||||||
keywords: string
|
keywords: string;
|
||||||
type: number
|
type: number;
|
||||||
}
|
}
|
||||||
// 搜索内容
|
// 搜索内容
|
||||||
export const getSearch = (params: IParams) => {
|
export const getSearch = (params: IParams) => {
|
||||||
return request.get<any>('/cloudsearch', {
|
return request.get<any>('/cloudsearch', {
|
||||||
params,
|
params,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import request from "@/utils/request";
|
import request from '@/utils/request';
|
||||||
|
|
||||||
// /user/detail
|
// /user/detail
|
||||||
export function getUserDetail(uid: number) {
|
export function getUserDetail(uid: number) {
|
||||||
return request.get("/user/detail", { params: { uid } });
|
return request.get('/user/detail', { params: { uid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// /user/playlist
|
// /user/playlist
|
||||||
export function getUserPlaylist(uid: number) {
|
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
|
// /user/record?uid=32953014&type=1
|
||||||
export function getUserRecord(uid: number, type: number = 0) {
|
export function getUserRecord(uid: number, type: number = 0) {
|
||||||
return request.get("/user/record", { params: { uid, type } });
|
return request.get('/user/record', { params: { uid, type } });
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,57 +1,151 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer :show="show" height="70vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
|
<n-drawer
|
||||||
|
:show="show"
|
||||||
|
:height="isMobile ? '100vh' : '70vh'"
|
||||||
|
placement="bottom"
|
||||||
|
block-scroll
|
||||||
|
mask-closable
|
||||||
|
:style="{ backgroundColor: 'transparent' }"
|
||||||
|
@mask-click="close"
|
||||||
|
>
|
||||||
<div class="music-page">
|
<div class="music-page">
|
||||||
<i class="iconfont icon-icon_error music-close" @click="close"></i>
|
<div class="music-close">
|
||||||
<div class="music-title">{{ name }}</div>
|
<i class="icon ri-layout-column-line" @click="doubleDisply = !doubleDisply"></i>
|
||||||
|
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
||||||
|
</div>
|
||||||
|
<div class="music-title text-el">{{ name }}</div>
|
||||||
<!-- 歌单歌曲列表 -->
|
<!-- 歌单歌曲列表 -->
|
||||||
<div class="music-list">
|
<div class="music-list">
|
||||||
<n-scrollbar >
|
<n-scrollbar @scroll="handleScroll">
|
||||||
<div v-for="(item, index) in songList" :key="item.id" :class="setAnimationClass('animate__bounceInUp')"
|
<div
|
||||||
:style="setAnimationDelay(index, 100)">
|
v-loading="loading || !songList.length"
|
||||||
<SongItem :item="formatDetail(item)" @play="handlePlay" />
|
class="music-list-content"
|
||||||
|
:class="{ 'double-list': doubleDisply }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in displayedSongs"
|
||||||
|
:key="item.id"
|
||||||
|
class="double-item"
|
||||||
|
:class="setAnimationClass('animate__bounceInUp')"
|
||||||
|
:style="setAnimationDelay(index, 5)"
|
||||||
|
>
|
||||||
|
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||||
</div>
|
</div>
|
||||||
<PlayBottom/>
|
<play-bottom />
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
|
|
||||||
|
<!-- <n-virtual-list :item-size="42" :items="displayedSongs" item-resizable @scroll="handleScroll">
|
||||||
|
<template #default="{ item, index }">
|
||||||
|
<div :key="item.id" class="double-item">
|
||||||
|
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
|
<play-bottom /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex';
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
|
||||||
import SongItem from "@/components/common/SongItem.vue";
|
import { getMusicDetail } from '@/api/music';
|
||||||
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
|
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
import PlayBottom from './common/PlayBottom.vue';
|
import PlayBottom from './common/PlayBottom.vue';
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
|
|
||||||
const props = defineProps<{
|
const {
|
||||||
|
songList,
|
||||||
|
loading = false,
|
||||||
|
listInfo,
|
||||||
|
} = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
songList: any[]
|
songList: any[];
|
||||||
}>()
|
loading?: boolean;
|
||||||
const emit = defineEmits(['update:show'])
|
listInfo?: any;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['update:show', 'update:loading']);
|
||||||
|
|
||||||
|
const page = ref(0);
|
||||||
|
const pageSize = 20;
|
||||||
|
const total = ref(0);
|
||||||
|
const isLoadingMore = ref(false);
|
||||||
|
const displayedSongs = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 双排显示开关
|
||||||
|
const doubleDisply = ref(false);
|
||||||
|
|
||||||
const formatDetail = computed(() => (detail: any) => {
|
const formatDetail = computed(() => (detail: any) => {
|
||||||
let song = {
|
const song = {
|
||||||
artists: detail.ar,
|
artists: detail.ar,
|
||||||
name: detail.al.name,
|
name: detail.al.name,
|
||||||
id: detail.al.id,
|
id: detail.al.id,
|
||||||
}
|
};
|
||||||
|
|
||||||
detail.song = song
|
detail.song = song;
|
||||||
detail.picUrl = detail.al.picUrl
|
detail.picUrl = detail.al.picUrl;
|
||||||
return detail
|
return detail;
|
||||||
})
|
});
|
||||||
|
|
||||||
const handlePlay = (item: any) => {
|
const handlePlay = () => {
|
||||||
const tracks = props.songList || []
|
const tracks = songList || [];
|
||||||
store.commit('setPlayList', tracks)
|
store.commit(
|
||||||
}
|
'setPlayList',
|
||||||
|
tracks.map((item) => ({
|
||||||
|
...item,
|
||||||
|
picUrl: item.al.picUrl,
|
||||||
|
song: {
|
||||||
|
artists: item.ar,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('update:show', false)
|
emit('update:show', false);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const loadMoreSongs = async () => {
|
||||||
|
if (displayedSongs.value.length >= total.value) return;
|
||||||
|
|
||||||
|
isLoadingMore.value = true;
|
||||||
|
try {
|
||||||
|
const trackIds = listInfo.trackIds
|
||||||
|
.slice(page.value * pageSize, (page.value + 1) * pageSize)
|
||||||
|
.map((item: any) => item.id);
|
||||||
|
const reslist = await getMusicDetail(trackIds);
|
||||||
|
// displayedSongs.value = displayedSongs.value.concat(reslist.data.songs);
|
||||||
|
displayedSongs.value = JSON.parse(JSON.stringify([...displayedSongs.value, ...reslist.data.songs]));
|
||||||
|
page.value++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = (e: any) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 50 && !isLoadingMore.value) {
|
||||||
|
loadMoreSongs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => songList,
|
||||||
|
(newSongs) => {
|
||||||
|
displayedSongs.value = JSON.parse(JSON.stringify(newSongs));
|
||||||
|
total.value = listInfo ? listInfo.trackIds.length : displayedSongs.value.length;
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -60,16 +154,46 @@ const close = () => {
|
|||||||
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-lg font-bold text-white p-4;
|
@apply text-lg font-bold text-white p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
|
@apply absolute top-4 right-8 cursor-pointer text-white flex gap-2 items-center;
|
||||||
|
.icon {
|
||||||
|
@apply text-3xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.mobile {
|
||||||
|
.music-page {
|
||||||
|
@apply px-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
@apply text-center text-white py-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.double-list {
|
||||||
|
@apply flex flex-wrap gap-5;
|
||||||
|
|
||||||
|
.double-item {
|
||||||
|
width: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-item {
|
||||||
|
background-color: #191919;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,39 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 歌单分类列表 -->
|
<!-- 歌单分类列表 -->
|
||||||
<div class="play-list-type">
|
<div class="play-list-type">
|
||||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
|
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
|
||||||
<div>
|
<div>
|
||||||
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
|
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
|
||||||
<span
|
<span
|
||||||
class="play-list-type-item"
|
v-show="isShowAllPlaylistCategory || index <= 19"
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
class="play-list-type-item"
|
||||||
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
v-show="isShowAllPlaylistCategory || index <= 19"
|
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
|
||||||
@click="handleClickPlaylistType(item.name)"
|
@click="handleClickPlaylistType(item.name)"
|
||||||
>{{ item.name }}</span>
|
>{{ item.name }}</span
|
||||||
</template>
|
>
|
||||||
<div
|
</template>
|
||||||
class="play-list-type-showall"
|
<div
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
class="play-list-type-showall"
|
||||||
:style="
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
setAnimationDelay(
|
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
|
||||||
!isShowAllPlaylistCategory
|
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
|
||||||
? 25
|
>
|
||||||
: playlistCategory?.sub.length || 100 + 30
|
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
||||||
)
|
</div>
|
||||||
"
|
|
||||||
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
|
|
||||||
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from 'vue';
|
||||||
import { getPlaylistCategory } from "@/api/home";
|
import { useRouter } from 'vue-router';
|
||||||
import type { IPlayListSort } from "@/type/playlist";
|
|
||||||
import { setAnimationDelay, setAnimationClass } from "@/utils";
|
import { getPlaylistCategory } from '@/api/home';
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import type { IPlayListSort } from '@/type/playlist';
|
||||||
|
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
// 歌单分类
|
// 歌单分类
|
||||||
const playlistCategory = ref<IPlayListSort>();
|
const playlistCategory = ref<IPlayListSort>();
|
||||||
// 是否显示全部歌单分类
|
// 是否显示全部歌单分类
|
||||||
@@ -41,39 +39,45 @@ const isShowAllPlaylistCategory = ref<boolean>(false);
|
|||||||
|
|
||||||
// 加载歌单分类
|
// 加载歌单分类
|
||||||
const loadPlaylistCategory = async () => {
|
const loadPlaylistCategory = async () => {
|
||||||
const { data } = await getPlaylistCategory();
|
const { data } = await getPlaylistCategory();
|
||||||
playlistCategory.value = data;
|
playlistCategory.value = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const handleClickPlaylistType = (type: any) => {
|
const handleClickPlaylistType = (type: string) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: "/list",
|
path: '/list',
|
||||||
query: {
|
query: {
|
||||||
type: type,
|
type,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 页面初始化
|
// 页面初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPlaylistCategory();
|
loadPlaylistCategory();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold mb-4;
|
@apply text-lg font-bold mb-4;
|
||||||
}
|
}
|
||||||
.play-list-type {
|
.play-list-type {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
@apply mx-6;
|
@apply mx-6;
|
||||||
&-item,
|
&-item,
|
||||||
&-showall {
|
&-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;
|
@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;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
&-showall {
|
&-showall {
|
||||||
@apply block text-center;
|
@apply block text-center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.play-list-type {
|
||||||
|
@apply mx-0 w-full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,86 +1,86 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="recommend-album">
|
<div class="recommend-album">
|
||||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
|
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
|
||||||
<div class="recommend-album-list">
|
<div class="recommend-album-list">
|
||||||
<template v-for="(item,index) in albumData?.albums" :key="item.id">
|
<template v-for="(item, index) in albumData?.albums" :key="item.id">
|
||||||
<div
|
<div
|
||||||
v-if="index < 6"
|
v-if="index < 6"
|
||||||
class="recommend-album-list-item"
|
class="recommend-album-list-item"
|
||||||
:class="setAnimationClass('animate__backInUp')"
|
:class="setAnimationClass('animate__backInUp')"
|
||||||
:style="setAnimationDelay(index, 100)"
|
:style="setAnimationDelay(index, 100)"
|
||||||
@click="handleClick(item)"
|
@click="handleClick(item)"
|
||||||
>
|
>
|
||||||
<n-image
|
<n-image
|
||||||
class="recommend-album-list-item-img"
|
class="recommend-album-list-item-img"
|
||||||
:src="getImgUrl( item.blurPicUrl, '200y200')"
|
:src="getImgUrl(item.blurPicUrl, '200y200')"
|
||||||
lazy
|
lazy
|
||||||
preview-disabled
|
preview-disabled
|
||||||
/>
|
/>
|
||||||
<div class="recommend-album-list-item-content">{{ item.name }}</div>
|
<div class="recommend-album-list-item-content">{{ item.name }}</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getNewAlbum } from "@/api/home"
|
import { onMounted, ref } from 'vue';
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import type { IAlbumNew } from "@/type/album"
|
|
||||||
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
|
|
||||||
import { getAlbum } from "@/api/list";
|
|
||||||
|
|
||||||
|
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 loadAlbumList = async () => {
|
||||||
const { data } = await getNewAlbum();
|
const { data } = await getNewAlbum();
|
||||||
albumData.value = data
|
albumData.value = data;
|
||||||
}
|
};
|
||||||
|
|
||||||
const showMusic = ref(false)
|
const showMusic = ref(false);
|
||||||
const songList = ref([])
|
const songList = ref([]);
|
||||||
const albumName = ref('')
|
const albumName = ref('');
|
||||||
|
|
||||||
const handleClick = async (item:any) => {
|
const handleClick = async (item: any) => {
|
||||||
albumName.value = item.name
|
albumName.value = item.name;
|
||||||
showMusic.value = true
|
showMusic.value = true;
|
||||||
const res = await getAlbum(item.id)
|
const res = await getAlbum(item.id);
|
||||||
songList.value = res.data.songs.map((song:any)=>{
|
songList.value = res.data.songs.map((song: any) => {
|
||||||
song.al.picUrl = song.al.picUrl || item.picUrl
|
song.al.picUrl = song.al.picUrl || item.picUrl;
|
||||||
return song
|
return song;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAlbumList()
|
loadAlbumList();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.recommend-album {
|
.recommend-album {
|
||||||
@apply flex-1 mx-5;
|
@apply flex-1 mx-5;
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold mb-4;
|
@apply text-lg font-bold mb-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommend-album-list {
|
.recommend-album-list {
|
||||||
@apply grid grid-cols-2 grid-rows-3 gap-2;
|
@apply grid grid-cols-2 grid-rows-3 gap-2;
|
||||||
&-item {
|
&-item {
|
||||||
@apply rounded-xl overflow-hidden relative;
|
@apply rounded-xl overflow-hidden relative;
|
||||||
&-img {
|
&-img {
|
||||||
@apply rounded-xl transition w-full h-full;
|
@apply rounded-xl transition w-full h-full;
|
||||||
}
|
}
|
||||||
&:hover img {
|
&:hover img {
|
||||||
filter: brightness(50%);
|
filter: brightness(50%);
|
||||||
}
|
}
|
||||||
&-content {
|
&-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;
|
@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 {
|
&-content:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,85 +1,158 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 推荐歌手 -->
|
<!-- 推荐歌手 -->
|
||||||
|
<n-scrollbar :size="100" :x-scrollable="true">
|
||||||
<div class="recommend-singer">
|
<div class="recommend-singer">
|
||||||
<div class="recommend-singer-list">
|
<div class="recommend-singer-list">
|
||||||
<div
|
<div
|
||||||
class="recommend-singer-item relative"
|
v-if="dayRecommendData"
|
||||||
:class="setAnimationClass('animate__backInRight')"
|
class="recommend-singer-item relative"
|
||||||
v-for="(item, index) in hotSingerData?.artists"
|
:class="setAnimationClass('animate__backInRight')"
|
||||||
:style="setAnimationDelay(index, 100)"
|
:style="setAnimationDelay(0, 100)"
|
||||||
:key="item.id"
|
>
|
||||||
>
|
<div
|
||||||
<div
|
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '300y300'))"
|
||||||
:style="setBackgroundImg(getImgUrl(item.picUrl,'300y300'))"
|
class="recommend-singer-item-bg"
|
||||||
class="recommend-singer-item-bg"
|
></div>
|
||||||
></div>
|
<div
|
||||||
<div
|
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer"
|
||||||
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10"
|
@click="showMusic = true"
|
||||||
>{{ item.musicSize }}首</div>
|
>
|
||||||
<div class="recommend-singer-item-info z-10">
|
<div class="font-bold text-xl">每日推荐</div>
|
||||||
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
|
||||||
<i class="iconfont icon-playfill text-xl"></i>
|
<div class="mt-2">
|
||||||
</div>
|
<p v-for="item in dayRecommendData?.dailySongs.slice(0, 5)" :key="item.id" class="text-el">
|
||||||
<div class="ml-4">
|
{{ item.name }}
|
||||||
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
|
<br />
|
||||||
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in hotSingerData?.artists"
|
||||||
|
: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>
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { setBackgroundImg, setAnimationDelay, setAnimationClass,getImgUrl } from "@/utils";
|
import { onMounted, ref } from 'vue';
|
||||||
import { onMounted, ref } from "vue";
|
import { useStore } from 'vuex';
|
||||||
import { getHotSinger } from "@/api/home";
|
|
||||||
import type { IHotSinger } from "@/type/singer";
|
import { getDayRecommend, getHotSinger } from '@/api/home';
|
||||||
import router from "@/router";
|
import router from '@/router';
|
||||||
|
import { IDayRecommend } from '@/type/day_recommend';
|
||||||
|
import type { IHotSinger } from '@/type/singer';
|
||||||
|
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
// 歌手信息
|
// 歌手信息
|
||||||
const hotSingerData = ref<IHotSinger>();
|
const hotSingerData = ref<IHotSinger>();
|
||||||
|
const dayRecommendData = ref<IDayRecommend>();
|
||||||
|
const showMusic = ref(false);
|
||||||
|
|
||||||
//加载推荐歌手
|
onMounted(async () => {
|
||||||
const loadSingerList = async () => {
|
await loadData();
|
||||||
const { data } = await getHotSinger({ offset: 0, limit: 5 });
|
|
||||||
hotSingerData.value = data;
|
|
||||||
};
|
|
||||||
// 页面初始化
|
|
||||||
onMounted(() => {
|
|
||||||
loadSingerList();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
// 第一个请求:获取热门歌手
|
||||||
|
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
|
||||||
|
|
||||||
const toSearchSinger = (keyword: string) => {
|
// 第二个请求:获取每日推荐
|
||||||
router.push({
|
try {
|
||||||
path: "/search",
|
const {
|
||||||
query: {
|
data: { data: dayRecommend },
|
||||||
keyword: keyword,
|
} = await getDayRecommend();
|
||||||
},
|
console.log('dayRecommend', dayRecommend);
|
||||||
});
|
// 处理数据
|
||||||
|
if (dayRecommend) {
|
||||||
|
singerData.artists = singerData.artists.slice(0, 4);
|
||||||
|
}
|
||||||
|
dayRecommendData.value = dayRecommend;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
hotSingerData.value = singerData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toSearchSinger = (keyword: string) => {
|
||||||
|
router.push({
|
||||||
|
path: '/search',
|
||||||
|
query: {
|
||||||
|
keyword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听登录状态
|
||||||
|
watchEffect(() => {
|
||||||
|
if (store.state.user) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.recommend-singer {
|
.recommend-singer {
|
||||||
&-list {
|
&-list {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
height: 280px;
|
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 {
|
&-info {
|
||||||
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
|
@apply flex items-center p-2;
|
||||||
&-bg {
|
&-play {
|
||||||
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
|
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .recommend-singer {
|
||||||
|
&-list {
|
||||||
|
height: 180px;
|
||||||
|
@apply ml-4;
|
||||||
|
}
|
||||||
|
&-item {
|
||||||
|
@apply p-4 rounded-xl;
|
||||||
|
&-bg {
|
||||||
|
@apply rounded-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="recommend-music">
|
<div class="recommend-music">
|
||||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">
|
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">本周最热音乐</div>
|
||||||
本周最热音乐
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
|
v-show="recommendMusic?.result"
|
||||||
|
v-loading="loading"
|
||||||
class="recommend-music-list"
|
class="recommend-music-list"
|
||||||
:class="setAnimationClass('animate__bounceInUp')"
|
:class="setAnimationClass('animate__bounceInUp')"
|
||||||
v-show="recommendMusic?.result"
|
|
||||||
>
|
>
|
||||||
<!-- 推荐音乐列表 -->
|
<!-- 推荐音乐列表 -->
|
||||||
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
|
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
|
||||||
<div
|
<div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
|
||||||
:class="setAnimationClass('animate__bounceInUp')"
|
|
||||||
:style="setAnimationDelay(index, 100)"
|
|
||||||
>
|
|
||||||
<song-item :item="item" @play="handlePlay" />
|
<song-item :item="item" @play="handlePlay" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,30 +18,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getRecommendMusic } from '@/api/home'
|
import { useStore } from 'vuex';
|
||||||
import type { IRecommendMusic } from '@/type/music'
|
|
||||||
import { setAnimationClass, setAnimationDelay } from '@/utils'
|
import { getRecommendMusic } from '@/api/home';
|
||||||
import SongItem from './common/SongItem.vue'
|
import type { IRecommendMusic } from '@/type/music';
|
||||||
import { useStore } from 'vuex'
|
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
|
import SongItem from './common/SongItem.vue';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
// 推荐歌曲
|
// 推荐歌曲
|
||||||
const recommendMusic = ref<IRecommendMusic>()
|
const recommendMusic = ref<IRecommendMusic>();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
// 加载推荐歌曲
|
// 加载推荐歌曲
|
||||||
const loadRecommendMusic = async () => {
|
const loadRecommendMusic = async () => {
|
||||||
const { data } = await getRecommendMusic({ limit: 10 })
|
loading.value = true;
|
||||||
recommendMusic.value = data
|
const { data } = await getRecommendMusic({ limit: 10 });
|
||||||
}
|
recommendMusic.value = data;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 页面初始化
|
// 页面初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadRecommendMusic()
|
loadRecommendMusic();
|
||||||
})
|
});
|
||||||
|
|
||||||
const handlePlay = (item: any) => {
|
const handlePlay = () => {
|
||||||
store.commit('setPlayList', recommendMusic.value?.result)
|
store.commit('setPlayList', recommendMusic.value?.result);
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,42 +1,40 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
import { setAnimationClass } from '@/utils';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showPop: {
|
showPop: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
showClose: {
|
showClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const musicFullClass = computed(() => {
|
const musicFullClass = computed(() => {
|
||||||
if (props.showPop) {
|
if (props.showPop) {
|
||||||
return setAnimationClass('animate__fadeInUp')
|
return setAnimationClass('animate__fadeInUp');
|
||||||
} else {
|
}
|
||||||
return setAnimationClass('animate__fadeOutDown')
|
return setAnimationClass('animate__fadeOutDown');
|
||||||
}
|
});
|
||||||
})
|
</script>
|
||||||
|
|
||||||
</script>
|
<template>
|
||||||
|
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
|
||||||
<template>
|
<i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
|
||||||
<div class="pop-page" v-show="props.showPop" :class="musicFullClass">
|
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
|
||||||
<i class="iconfont icon-icon_error close" v-if="props.showClose"></i>
|
<slot></slot>
|
||||||
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
|
</div>
|
||||||
<slot></slot>
|
</template>
|
||||||
</div>
|
|
||||||
</template>
|
<style lang="scss" scoped>
|
||||||
|
.pop-page {
|
||||||
<style lang="scss" scoped>
|
height: 800px;
|
||||||
.pop-page {
|
@apply absolute top-4 left-0 w-full;
|
||||||
height: 800px;
|
background-color: #000000f0;
|
||||||
@apply absolute top-4 left-0 w-full;
|
.close {
|
||||||
background-color: #000000f0;
|
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
|
||||||
.close {
|
}
|
||||||
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
|
}
|
||||||
}
|
</style>
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bottom" v-if="isPlay"></div>
|
<div v-if="isPlay" class="bottom" :style="{ height }"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStore } from 'vuex';
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.bottom{
|
.bottom {
|
||||||
@apply h-28;
|
@apply h-28;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
70
src/components/common/PlayVideo.vue
Normal 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>
|
||||||
@@ -1,70 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-item" @click="handleClick">
|
<div class="search-item" :class="item.type" @click="handleClick">
|
||||||
<div class="search-item-img">
|
<div class="search-item-img">
|
||||||
<n-image
|
<n-image :src="getImgUrl(item.picUrl, '320y180')" lazy preview-disabled />
|
||||||
:src="getImgUrl(item.picUrl, 'album')"
|
<div v-if="item.type === 'mv'" class="play">
|
||||||
lazy
|
<i class="iconfont icon icon-play"></i>
|
||||||
preview-disabled
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="search-item-info">
|
<div class="search-item-info">
|
||||||
<div class="search-item-name">{{ item.name }}</div>
|
<p class="search-item-name">{{ item.name }}</p>
|
||||||
<div class="search-item-artist">{{ item.desc}}</div>
|
<p class="search-item-artist">{{ item.desc }}</p>
|
||||||
</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"
|
||||||
|
:list-info="listInfo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PlayVideo v-if="item.type === 'mv'" v-model:show="showPop" :title="item.name" :url="url" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getImgUrl } from '@/utils'
|
import { getAlbum, getListDetail } from '@/api/list';
|
||||||
import type {Album} from '@/type/album'
|
import { getMvUrl } from '@/api/mv';
|
||||||
import { getAlbum } from '@/api/list';
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item: {
|
item: {
|
||||||
picUrl: string
|
picUrl: string;
|
||||||
name: string
|
name: string;
|
||||||
desc: string
|
desc: string;
|
||||||
type: string
|
type: string;
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
}
|
};
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const songList = ref([])
|
const url = ref('');
|
||||||
|
|
||||||
const showMusic = ref(false)
|
const songList = ref<any[]>([]);
|
||||||
|
|
||||||
|
const showPop = ref(false);
|
||||||
|
const listInfo = ref<any>(null);
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
showMusic.value = true
|
listInfo.value = null;
|
||||||
if(props.item.type === '专辑'){
|
if (props.item.type === '专辑') {
|
||||||
const res = await getAlbum(props.item.id)
|
showPop.value = true;
|
||||||
songList.value = res.data.songs.map((song:any)=>{
|
const res = await getAlbum(props.item.id);
|
||||||
song.al.picUrl = song.al.picUrl || props.item.picUrl
|
songList.value = res.data.songs.map((song: any) => {
|
||||||
return song
|
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;
|
||||||
|
listInfo.value = res.data.playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.item.type === 'mv') {
|
||||||
|
const res = await getMvUrl(props.item.id);
|
||||||
|
url.value = res.data.data.url;
|
||||||
|
showPop.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.search-item {
|
||||||
.search-item{
|
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition cursor-pointer;
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
|
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
.search-item-img{
|
.search-item-img {
|
||||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||||
}
|
}
|
||||||
.search-item-info{
|
.search-item-info {
|
||||||
&-name{
|
@apply flex-1 overflow-hidden;
|
||||||
|
&-name {
|
||||||
@apply text-white text-sm text-center;
|
@apply text-white text-sm text-center;
|
||||||
}
|
}
|
||||||
&-artist{
|
&-artist {
|
||||||
@apply text-gray-400 text-xs text-center;
|
@apply text-gray-400 text-xs text-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
.mv {
|
||||||
|
&:hover {
|
||||||
|
.play {
|
||||||
|
@apply opacity-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-item-img {
|
||||||
|
width: 160px;
|
||||||
|
height: 90px;
|
||||||
|
@apply rounded-lg relative;
|
||||||
|
}
|
||||||
|
.play {
|
||||||
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity;
|
||||||
|
.icon {
|
||||||
|
@apply text-white text-5xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="song-item" :class="{'song-mini': mini}">
|
<div class="song-item" :class="{ 'song-mini': mini }">
|
||||||
<n-image
|
<n-image
|
||||||
v-if="item.picUrl "
|
v-if="item.picUrl"
|
||||||
:src="getImgUrl( item.picUrl, '40y40')"
|
ref="songImg"
|
||||||
|
:src="getImgUrl(item.picUrl, '40y40')"
|
||||||
class="song-item-img"
|
class="song-item-img"
|
||||||
lazy
|
|
||||||
preview-disabled
|
preview-disabled
|
||||||
|
:img-props="{
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
}"
|
||||||
|
@load="imageLoad"
|
||||||
/>
|
/>
|
||||||
<div class="song-item-content">
|
<div class="song-item-content">
|
||||||
<div class="song-item-content-title">
|
<div class="song-item-content-title">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{
|
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||||
item.name
|
|
||||||
}}</n-ellipsis>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="song-item-content-name">
|
<div class="song-item-content-name">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
<span
|
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||||
v-for="(artists, artistsindex) in item.song.artists"
|
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
:key="artistsindex"
|
|
||||||
>{{ artists.name
|
|
||||||
}}{{
|
|
||||||
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
|
|
||||||
}}</span
|
|
||||||
>
|
>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,8 +28,8 @@
|
|||||||
<i class="iconfont icon-likefill"></i>
|
<i class="iconfont icon-likefill"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="song-item-operating-play bg-black"
|
class="song-item-operating-play bg-black animate__animated"
|
||||||
:class="isPlaying ? 'bg-green-600' : ''"
|
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
||||||
@click="playMusicEvent(item)"
|
@click="playMusicEvent(item)"
|
||||||
>
|
>
|
||||||
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
||||||
@@ -43,40 +40,68 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useStore } from 'vuex'
|
import { useTemplateRef } from 'vue';
|
||||||
import type { SongResult } from '@/type/music'
|
import { useStore } from 'vuex';
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
import type { SongResult } from '@/type/music';
|
||||||
item: SongResult
|
import { getImgUrl } from '@/utils';
|
||||||
mini?: boolean
|
import { getImageBackground } from '@/utils/linearColor';
|
||||||
}>(), {
|
|
||||||
mini: false
|
|
||||||
})
|
|
||||||
|
|
||||||
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(() => {
|
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 songImageRef = useTemplateRef('songImg');
|
||||||
|
|
||||||
|
const imageLoad = async () => {
|
||||||
|
if (!songImageRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { backgroundColor } = await getImageBackground(
|
||||||
|
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
props.item.backgroundColor = backgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
// 播放音乐 设置音乐详情 打开音乐底栏
|
// 播放音乐 设置音乐详情 打开音乐底栏
|
||||||
const playMusicEvent = (item: any) => {
|
const playMusicEvent = async (item: SongResult) => {
|
||||||
store.commit('setPlay', item)
|
if (playMusic.value.id === item.id) {
|
||||||
store.commit('setIsPlay', true)
|
if (play.value) {
|
||||||
emits('play', item)
|
store.commit('setPlayMusic', false);
|
||||||
}
|
} else {
|
||||||
|
store.commit('setPlayMusic', true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await store.commit('setPlay', item);
|
||||||
|
store.commit('setIsPlay', true);
|
||||||
|
emits('play', item);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
// 配置文字不可选中
|
// 配置文字不可选中
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -115,35 +140,36 @@ const playMusicEvent = (item: any) => {
|
|||||||
}
|
}
|
||||||
&-play {
|
&-play {
|
||||||
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
|
@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;
|
@apply p-2 rounded-2xl;
|
||||||
.song-item{
|
.song-item {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
&-img{
|
&-img {
|
||||||
@apply w-10 h-10 mr-2;
|
@apply w-10 h-10 mr-2;
|
||||||
}
|
}
|
||||||
&-content{
|
&-content {
|
||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
&-title{
|
&-title {
|
||||||
@apply text-sm;
|
@apply text-sm;
|
||||||
}
|
}
|
||||||
&-name{
|
&-name {
|
||||||
@apply text-xs;
|
@apply text-xs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-operating{
|
&-operating {
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
.iconfont{
|
.iconfont {
|
||||||
@apply text-base;
|
@apply text-base;
|
||||||
}
|
}
|
||||||
&-like{
|
&-like {
|
||||||
@apply mr-1;
|
@apply mr-1;
|
||||||
}
|
}
|
||||||
&-play{
|
&-play {
|
||||||
@apply w-8 h-8;
|
@apply w-8 h-8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const USER_SET_OPTIONS = [
|
|||||||
label: '设置',
|
label: '设置',
|
||||||
key: 'set',
|
key: 'set',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export const SEARCH_TYPES = [
|
export const SEARCH_TYPES = [
|
||||||
{
|
{
|
||||||
@@ -30,36 +30,36 @@ export const SEARCH_TYPES = [
|
|||||||
label: '专辑',
|
label: '专辑',
|
||||||
key: 10,
|
key: 10,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: '歌手',
|
// label: '歌手',
|
||||||
key: 100,
|
// key: 100,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: '歌单',
|
label: '歌单',
|
||||||
key: 1000,
|
key: 1000,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: '用户',
|
// label: '用户',
|
||||||
key: 1002,
|
// key: 1002,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: 'MV',
|
label: 'MV',
|
||||||
key: 1004,
|
key: 1004,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: '歌词',
|
// label: '歌词',
|
||||||
key: 1006,
|
// key: 1006,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: '电台',
|
// label: '电台',
|
||||||
key: 1009,
|
// key: 1009,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: '视频',
|
// label: '视频',
|
||||||
key: 1014,
|
// key: 1014,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: '综合',
|
// label: '综合',
|
||||||
key: 1018,
|
// key: 1018,
|
||||||
},
|
// },
|
||||||
]
|
];
|
||||||
|
|||||||
7
src/directive/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { vLoading } from './loading/index';
|
||||||
|
|
||||||
|
const directives = {
|
||||||
|
loading: vLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default directives;
|
||||||
40
src/directive/loading/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { createVNode, render, VNode } from 'vue';
|
||||||
|
|
||||||
|
import Loading from './index.vue';
|
||||||
|
|
||||||
|
const vnode: VNode = createVNode(Loading) as VNode;
|
||||||
|
|
||||||
|
export const vLoading = {
|
||||||
|
// 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用
|
||||||
|
mounted: (el: HTMLElement, binding: any) => {
|
||||||
|
render(vnode, el);
|
||||||
|
},
|
||||||
|
// 在绑定元素的父组件 及他自己的所有子节点都更新后调用
|
||||||
|
updated: (el: HTMLElement, binding: any) => {
|
||||||
|
if (binding.value) {
|
||||||
|
vnode?.component?.exposed.show();
|
||||||
|
} else {
|
||||||
|
vnode?.component?.exposed.hide();
|
||||||
|
}
|
||||||
|
// 动态添加删除自定义class: loading-parent
|
||||||
|
formatterClass(el, binding);
|
||||||
|
},
|
||||||
|
// 绑定元素的父组件卸载后调用
|
||||||
|
unmounted: () => {
|
||||||
|
vnode?.component?.exposed.hide();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatterClass(el: HTMLElement, binding: any) {
|
||||||
|
const classStr = el.getAttribute('class');
|
||||||
|
const tagetClass: number = classStr?.indexOf('loading-parent') as number;
|
||||||
|
if (binding.value) {
|
||||||
|
if (tagetClass === -1) {
|
||||||
|
el.setAttribute('class', `${classStr} loading-parent`);
|
||||||
|
}
|
||||||
|
} else if (tagetClass > -1) {
|
||||||
|
const classArray: Array<string> = classStr?.split('') as string[];
|
||||||
|
classArray.splice(tagetClass - 1, tagetClass + 15);
|
||||||
|
el.setAttribute('class', classArray?.join(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/directive/loading/index.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!-- -->
|
||||||
|
<template>
|
||||||
|
<div v-if="isShow" class="loading-box">
|
||||||
|
<div class="mask" :style="{ background: maskBackground }"></div>
|
||||||
|
<div class="loading-content-box">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<div :style="{ color: textColor }" class="tip">{{ tip }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NSpin } from 'naive-ui';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
tip: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '加载中...';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maskBackground: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'rgba(0, 0, 0, 0.8)';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadingColor: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'rgba(255, 255, 255, 1)';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
textColor: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'rgba(255, 255, 255, 1)';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isShow = ref(false);
|
||||||
|
const show = () => {
|
||||||
|
isShow.value = true;
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
isShow.value = false;
|
||||||
|
};
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
isShow,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-box {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 9999;
|
||||||
|
.n-spin {
|
||||||
|
// color: #ccc;
|
||||||
|
}
|
||||||
|
.mask {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.loading-content-box {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
src/electron.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
minimize: () => void
|
minimize: () => void;
|
||||||
maximize: () => void
|
maximize: () => void;
|
||||||
close: () => void
|
close: () => void;
|
||||||
dragStart: () => void
|
dragStart: () => void;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
// musicHistoryHooks
|
// musicHistoryHooks
|
||||||
import { RemovableRef, useLocalStorage } from '@vueuse/core'
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import type { SongResult } from '@/type/music'
|
|
||||||
|
import type { SongResult } from '@/type/music';
|
||||||
|
|
||||||
export const useMusicHistory = () => {
|
export const useMusicHistory = () => {
|
||||||
const musicHistory = useLocalStorage<SongResult[]>('musicHistory', [])
|
const musicHistory = useLocalStorage<SongResult[]>('musicHistory', []);
|
||||||
|
|
||||||
const addMusic = (music: SongResult) => {
|
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) {
|
if (index !== -1) {
|
||||||
musicHistory.value[index].count =
|
musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1;
|
||||||
(musicHistory.value[index].count || 0) + 1
|
musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]);
|
||||||
musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0])
|
|
||||||
} else {
|
} else {
|
||||||
musicHistory.value.unshift({ ...music, count: 1 })
|
musicHistory.value.unshift({ ...music, count: 1 });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const delMusic = (music: any) => {
|
const delMusic = (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) {
|
if (index !== -1) {
|
||||||
musicHistory.value.splice(index, 1)
|
musicHistory.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const musicList = ref(musicHistory.value)
|
const musicList = ref(musicHistory.value);
|
||||||
watch(
|
watch(
|
||||||
() => musicHistory.value,
|
() => musicHistory.value,
|
||||||
() => {
|
() => {
|
||||||
musicList.value = musicHistory.value
|
musicList.value = musicHistory.value;
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
musicHistory,
|
musicHistory,
|
||||||
musicList,
|
musicList,
|
||||||
addMusic,
|
addMusic,
|
||||||
delMusic,
|
delMusic,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,104 +1,198 @@
|
|||||||
import { getMusicLrc } from '@/api/music'
|
import { computed, ref } from 'vue';
|
||||||
import { ILyric } from '@/type/lyric'
|
|
||||||
import { getIsMc } from '@/utils'
|
|
||||||
|
|
||||||
interface ILrcData {
|
import { getMusicLrc } from '@/api/music';
|
||||||
text: string
|
import store from '@/store';
|
||||||
trText: string
|
import { ILyric } from '@/type/lyric';
|
||||||
}
|
import type { ILyricText, SongResult } from '@/type/music';
|
||||||
|
|
||||||
const lrcData = ref<ILyric>()
|
const windowData = window as any;
|
||||||
const newLrcIndex = ref<number>(0)
|
|
||||||
const lrcArray = ref<Array<ILrcData>>([])
|
|
||||||
const lrcTimeArray = ref<Array<Number>>([])
|
|
||||||
|
|
||||||
const parseTime = (timeString: string) => {
|
export const isElectron = computed(() => !!windowData.electronAPI);
|
||||||
const [minutes, seconds] = timeString.split(':')
|
|
||||||
return parseInt(minutes) * 60 + parseFloat(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g
|
export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
|
||||||
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g
|
export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
|
||||||
|
export const nowTime = ref(0); // 当前播放时间
|
||||||
|
export const allTime = ref(0); // 总播放时间
|
||||||
|
export const nowIndex = ref(0); // 当前播放歌词
|
||||||
|
export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
|
||||||
|
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
|
||||||
|
export const audio = ref<HTMLAudioElement>(); // 音频对象
|
||||||
|
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
|
||||||
|
|
||||||
function parseLyricLine(lyricLine: string) {
|
watch(
|
||||||
// [00:00.00] 作词 : 长友美知惠/
|
() => store.state.playMusic,
|
||||||
const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''
|
() => {
|
||||||
const time = parseTime(timeText)
|
nextTick(() => {
|
||||||
const text = lyricLine.replace(LRC_REGEX, '').trim()
|
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
||||||
return { time, text }
|
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
||||||
}
|
});
|
||||||
|
},
|
||||||
interface ILyricText {
|
{
|
||||||
text: string
|
deep: true,
|
||||||
trText: string
|
},
|
||||||
}
|
);
|
||||||
|
const isPlaying = computed(() => store.state.play as boolean);
|
||||||
function parseLyrics(lyricsString: string) {
|
|
||||||
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 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
|
|
||||||
} catch (err) {
|
|
||||||
console.error('err', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 歌词矫正时间Correction time
|
|
||||||
const correctionTime = ref(0.4)
|
|
||||||
|
|
||||||
// 增加矫正时间
|
// 增加矫正时间
|
||||||
const addCorrectionTime = (time: number) => {
|
export const addCorrectionTime = (time: number) => (correctionTime.value += time);
|
||||||
correctionTime.value += time
|
|
||||||
}
|
|
||||||
|
|
||||||
// 减少矫正时间
|
// 减少矫正时间
|
||||||
const reduceCorrectionTime = (time: number) => {
|
export const reduceCorrectionTime = (time: number) => (correctionTime.value -= time);
|
||||||
correctionTime.value -= time
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentLrc = (index: any, time: number) => {
|
// 获取当前播放歌词
|
||||||
const currentTime = Number(lrcTimeArray.value[index])
|
export const isCurrentLrc = (index: number, time: number): boolean => {
|
||||||
const nextTime = Number(lrcTimeArray.value[index + 1])
|
const currentTime = lrcTimeArray.value[index];
|
||||||
const nowTime = time + correctionTime.value
|
const nextTime = lrcTimeArray.value[index + 1];
|
||||||
const isTrue = nowTime > currentTime && nowTime < nextTime
|
const nowTime = time + correctionTime.value;
|
||||||
if (isTrue) {
|
const isTrue = nowTime > currentTime && nowTime < nextTime;
|
||||||
newLrcIndex.value = index
|
return isTrue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前播放歌词INDEX
|
||||||
|
export const getLrcIndex = (time: number): number => {
|
||||||
|
for (let i = 0; i < lrcTimeArray.value.length; i++) {
|
||||||
|
if (isCurrentLrc(i, time)) {
|
||||||
|
nowIndex.value = i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return isTrue
|
return nowIndex.value;
|
||||||
}
|
};
|
||||||
|
|
||||||
const nowTime = ref(0)
|
// 获取当前播放歌词进度
|
||||||
const allTime = ref(0)
|
const currentLrcTiming = computed(() => {
|
||||||
|
const start = lrcTimeArray.value[nowIndex.value] || 0;
|
||||||
|
const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1;
|
||||||
|
return { start, end };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取歌词样式
|
||||||
|
export const getLrcStyle = (index: number) => {
|
||||||
|
if (index === nowIndex.value) {
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(to right, #ffffff ${currentLrcProgress.value}%, #ffffff8a ${currentLrcProgress.value}%)`,
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
color: 'transparent',
|
||||||
|
transition: 'background-image 0.1s linear',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(nowTime, (newTime) => {
|
||||||
|
const newIndex = getLrcIndex(newTime);
|
||||||
|
if (newIndex !== nowIndex.value) {
|
||||||
|
nowIndex.value = newIndex;
|
||||||
|
currentLrcProgress.value = 0; // 重置进度
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放进度
|
||||||
|
export const useLyricProgress = () => {
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
if (!isPlaying.value) return;
|
||||||
|
audio.value = audio.value || (document.querySelector('#MusicAudio') as HTMLAudioElement);
|
||||||
|
if (!audio.value) return;
|
||||||
|
const { start, end } = currentLrcTiming.value;
|
||||||
|
const duration = end - start;
|
||||||
|
const elapsed = audio.value.currentTime - start;
|
||||||
|
currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100);
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(updateProgress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startProgressAnimation = () => {
|
||||||
|
if (!animationFrameId && isPlaying.value) {
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopProgressAnimation = () => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(isPlaying, (newIsPlaying) => {
|
||||||
|
if (newIsPlaying) {
|
||||||
|
startProgressAnimation();
|
||||||
|
} else {
|
||||||
|
stopProgressAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
startProgressAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopProgressAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentLrcProgress,
|
||||||
|
getLrcStyle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// 设置当前播放时间
|
// 设置当前播放时间
|
||||||
const setAudioTime = (index: any, audio: HTMLAudioElement) => {
|
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
|
||||||
audio.currentTime = lrcTimeArray.value[index] as number
|
audio.currentTime = lrcTimeArray.value[index];
|
||||||
audio.play()
|
audio.play();
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
// 获取当前播放的歌词
|
||||||
lrcData,
|
export const getCurrentLrc = () => {
|
||||||
lrcArray,
|
const index = getLrcIndex(nowTime.value);
|
||||||
lrcTimeArray,
|
return {
|
||||||
newLrcIndex,
|
currentLrc: lrcArray.value[index],
|
||||||
loadLrc,
|
nextLrc: lrcArray.value[index + 1],
|
||||||
isCurrentLrc,
|
};
|
||||||
addCorrectionTime,
|
};
|
||||||
reduceCorrectionTime,
|
|
||||||
setAudioTime,
|
// 获取一句歌词播放时间是 几秒到几秒
|
||||||
nowTime,
|
export const getLrcTimeRange = (index: number) => ({
|
||||||
allTime,
|
currentTime: lrcTimeArray.value[index],
|
||||||
}
|
nextTime: lrcTimeArray.value[index + 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||||
|
if (!isElectron.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (lrcArray.value.length > 0) {
|
||||||
|
const nowIndex = getLrcIndex(nowTime.value);
|
||||||
|
const { currentLrc, nextLrc } = getCurrentLrc();
|
||||||
|
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
|
||||||
|
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
|
||||||
|
const lyricWinData = {
|
||||||
|
currentLrc,
|
||||||
|
nextLrc,
|
||||||
|
currentTime,
|
||||||
|
nextTime,
|
||||||
|
nowIndex,
|
||||||
|
lrcTimeArray: lrcTimeArray.value,
|
||||||
|
lrcArray: lrcArray.value,
|
||||||
|
nowTime: nowTime.value,
|
||||||
|
allTime: allTime.value,
|
||||||
|
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||||
|
isPlay,
|
||||||
|
};
|
||||||
|
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending lyric to window:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openLyric = () => {
|
||||||
|
if (!isElectron.value) return;
|
||||||
|
windowData.electronAPI.openLyric();
|
||||||
|
sendLyricToWin();
|
||||||
|
};
|
||||||
|
|||||||
175
src/hooks/MusicListHook.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||||
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
|
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
||||||
|
import { getImgUrl, getMusicProxyUrl } from '@/utils';
|
||||||
|
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||||
|
|
||||||
|
const musicHistory = useMusicHistory();
|
||||||
|
|
||||||
|
// 获取歌曲url
|
||||||
|
const getSongUrl = async (id: number) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
}
|
||||||
|
url = url || data.data[0].url;
|
||||||
|
return getMusicProxyUrl(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSongDetail = async (playMusic: SongResult) => {
|
||||||
|
if (playMusic.playMusicUrl) {
|
||||||
|
return playMusic;
|
||||||
|
}
|
||||||
|
playMusic.playLoading = true;
|
||||||
|
const playMusicUrl = await getSongUrl(playMusic.id);
|
||||||
|
const { backgroundColor, primaryColor } =
|
||||||
|
playMusic.backgroundColor && playMusic.primaryColor
|
||||||
|
? playMusic
|
||||||
|
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
|
||||||
|
|
||||||
|
playMusic.playLoading = false;
|
||||||
|
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据
|
||||||
|
export const useMusicListHook = () => {
|
||||||
|
const handlePlayMusic = async (state: any, playMusic: SongResult) => {
|
||||||
|
const updatedPlayMusic = await getSongDetail(playMusic);
|
||||||
|
state.playMusic = updatedPlayMusic;
|
||||||
|
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
|
||||||
|
state.play = true;
|
||||||
|
loadLrcAsync(state, updatedPlayMusic.id);
|
||||||
|
musicHistory.addMusic(state.playMusic);
|
||||||
|
const playListIndex = state.playList.findIndex((item: SongResult) => item.id === playMusic.id);
|
||||||
|
state.playListIndex = playListIndex;
|
||||||
|
// 请求后续五首歌曲的详情
|
||||||
|
fetchSongs(state, playListIndex + 1, playListIndex + 6);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于预加载下一首歌曲的 MP3 数据
|
||||||
|
const preloadNextSong = (nextSongUrl: string) => {
|
||||||
|
const audio = new Audio(nextSongUrl);
|
||||||
|
audio.preload = 'auto'; // 设置预加载
|
||||||
|
audio.load(); // 手动加载
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
|
||||||
|
const songs = state.playList.slice(Math.max(0, startIndex), Math.min(endIndex, state.playList.length));
|
||||||
|
|
||||||
|
const detailedSongs = await Promise.all(
|
||||||
|
songs.map(async (song: SongResult) => {
|
||||||
|
// 如果歌曲详情已经存在,就不重复请求
|
||||||
|
if (!song.playMusicUrl) {
|
||||||
|
return await getSongDetail(song);
|
||||||
|
}
|
||||||
|
return song;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// 加载下一首的歌词
|
||||||
|
const nextSong = detailedSongs[0];
|
||||||
|
if (!(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
|
||||||
|
nextSong.lyric = await loadLrc(nextSong.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新播放列表中的歌曲详情
|
||||||
|
detailedSongs.forEach((song, index) => {
|
||||||
|
state.playList[startIndex + index] = song;
|
||||||
|
});
|
||||||
|
preloadNextSong(nextSong.playMusicUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPlay = async (state: any) => {
|
||||||
|
if (state.playList.length === 0) {
|
||||||
|
state.play = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const playListIndex = (state.playListIndex + 1) % state.playList.length;
|
||||||
|
await handlePlayMusic(state, state.playList[playListIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevPlay = async (state: any) => {
|
||||||
|
if (state.playList.length === 0) {
|
||||||
|
state.play = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
|
||||||
|
await handlePlayMusic(state, state.playList[playListIndex]);
|
||||||
|
await fetchSongs(state, playListIndex - 5, playListIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTime = (timeString: string): number => {
|
||||||
|
const [minutes, seconds] = timeString.split(':');
|
||||||
|
return Number(minutes) * 60 + Number(seconds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLyricLine = (lyricLine: string): { time: number; text: string } => {
|
||||||
|
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
|
||||||
|
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
|
||||||
|
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
|
||||||
|
const time = parseTime(timeText);
|
||||||
|
const text = lyricLine.replace(LRC_REGEX, '').trim();
|
||||||
|
return { time, text };
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {
|
||||||
|
const lines = lyricsString.split('\n');
|
||||||
|
const lyrics: ILyricText[] = [];
|
||||||
|
const times: number[] = [];
|
||||||
|
lines.forEach((line) => {
|
||||||
|
const { time, text } = parseLyricLine(line);
|
||||||
|
times.push(time);
|
||||||
|
lyrics.push({ text, trText: '' });
|
||||||
|
});
|
||||||
|
return { lyrics, times };
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLrc = async (playMusicId: number): Promise<ILyric> => {
|
||||||
|
try {
|
||||||
|
const { data } = await getMusicLrc(playMusicId);
|
||||||
|
const { lyrics, times } = parseLyrics(data.lrc.lyric);
|
||||||
|
const tlyric: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (data.tlyric.lyric) {
|
||||||
|
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
|
||||||
|
tLyrics.forEach((lyric, index) => {
|
||||||
|
tlyric[tTimes[index].toString()] = lyric.text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics.forEach((item, index) => {
|
||||||
|
item.trText = item.text ? tlyric[times[index].toString()] || '' : '';
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
lrcTimeArray: times,
|
||||||
|
lrcArray: lyrics,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading lyrics:', err);
|
||||||
|
return {
|
||||||
|
lrcTimeArray: [],
|
||||||
|
lrcArray: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 异步加载歌词的方法
|
||||||
|
const loadLrcAsync = async (state: any, playMusicId: number) => {
|
||||||
|
if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lyrics = await loadLrc(playMusicId);
|
||||||
|
state.playMusic.lyric = lyrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handlePlayMusic,
|
||||||
|
nextPlay,
|
||||||
|
prevPlay,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,4 +8,8 @@
|
|||||||
.n-image img {
|
.n-image img {
|
||||||
background-color: #111111;
|
background-color: #111111;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-el {
|
||||||
|
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,144 +1,163 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-page">
|
<div class="layout-page">
|
||||||
<div class="layout-main">
|
<div class="layout-main" :style="{ background: backgroundColor }">
|
||||||
<title-bar v-if="isElectron" />
|
<title-bar v-if="isElectron" />
|
||||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
||||||
<!-- 侧边菜单栏 -->
|
<!-- 侧边菜单栏 -->
|
||||||
<app-menu class="menu" :menus="menus" />
|
<app-menu v-if="!isMobile" class="menu" :menus="menus" />
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<search-bar />
|
<search-bar />
|
||||||
<!-- 主页面路由 -->
|
<!-- 主页面路由 -->
|
||||||
<div class="main-content bg-black pb-" :native-scrollbar="false" :class="isPlay ? 'pb-20' : ''">
|
<div class="main-content" :native-scrollbar="false">
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<router-view class="main-page" v-slot="{ Component }" :class="route.meta.noScroll? 'pr-3' : ''">
|
<router-view
|
||||||
<template v-if="route.meta.noScroll">
|
v-slot="{ Component }"
|
||||||
<keep-alive v-if="!route.meta.noKeepAlive">
|
class="main-page"
|
||||||
<component :is="Component" />
|
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
|
||||||
</keep-alive>
|
>
|
||||||
<component v-else :is="Component"/>
|
<keep-alive :include="keepAliveInclude">
|
||||||
</template>
|
<component :is="Component" />
|
||||||
<template v-else>
|
</keep-alive>
|
||||||
<n-scrollbar>
|
</router-view>
|
||||||
<keep-alive v-if="!route.meta.noKeepAlive">
|
</n-message-provider>
|
||||||
<component :is="Component" />
|
</div>
|
||||||
</keep-alive>
|
<play-bottom height="5rem" />
|
||||||
<component v-else :is="Component"/>
|
<app-menu v-if="isMobile" class="menu" :menus="menus" />
|
||||||
</n-scrollbar>
|
|
||||||
</template>
|
|
||||||
</router-view>
|
|
||||||
</n-message-provider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 底部音乐播放 -->
|
|
||||||
<play-bar v-if="isPlay" />
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 底部音乐播放 -->
|
||||||
|
<play-bar v-if="isPlay" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
|
import { isElectron } from '@/hooks/MusicHook';
|
||||||
|
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 AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
|
||||||
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
||||||
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
||||||
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
||||||
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const isPlay = computed(() => store.state.isPlay as boolean)
|
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||||
const menus = store.state.menus;
|
const { menus } = store.state;
|
||||||
const play = computed(() => store.state.play as boolean)
|
const play = computed(() => store.state.play as boolean);
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
|
|
||||||
const audio = {
|
const audio = {
|
||||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement
|
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||||
}
|
};
|
||||||
|
|
||||||
const windowData = window as any
|
|
||||||
const isElectron = computed(() => {
|
|
||||||
return !!windowData.electronAPI
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const backgroundColor = ref('#000');
|
||||||
|
// watch(
|
||||||
|
// () => store.state.playMusic,
|
||||||
|
// () => {
|
||||||
|
// backgroundColor.value = store.state.playMusic.backgroundColor;
|
||||||
|
// console.log('backgroundColor.value', backgroundColor.value);
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// immediate: true,
|
||||||
|
// deep: true,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 监听音乐是否播放
|
// 监听音乐是否播放
|
||||||
watch(
|
watch(
|
||||||
() => play.value,
|
() => play.value,
|
||||||
value => {
|
(value) => {
|
||||||
if (value && audio.value) {
|
if (value && audio.value) {
|
||||||
audioPlay()
|
audioPlay();
|
||||||
} else {
|
} else {
|
||||||
audioPause()
|
audioPause();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
document.onkeyup = (e) => {
|
document.onkeyup = (e) => {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'Space':
|
case 'Space':
|
||||||
playMusicEvent()
|
playMusicEvent();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
// 按下键盘按钮监听
|
});
|
||||||
document.onkeydown = (e) => {
|
|
||||||
switch (e.code) {
|
|
||||||
case 'Space':
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const audioPlay = () => {
|
const audioPlay = () => {
|
||||||
if (audio.value) {
|
if (audio.value) {
|
||||||
audio.value.play()
|
audio.value.play();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const audioPause = () => {
|
const audioPause = () => {
|
||||||
if (audio.value) {
|
if (audio.value) {
|
||||||
audio.value.pause()
|
audio.value.pause();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const playMusicEvent = async () => {
|
const playMusicEvent = async () => {
|
||||||
if (play.value) {
|
if (play.value) {
|
||||||
store.commit('setPlayMusic', false)
|
store.commit('setPlayMusic', false);
|
||||||
} else {
|
} else {
|
||||||
store.commit('setPlayMusic', true)
|
store.commit('setPlayMusic', true);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.layout-page {
|
.layout-page {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@apply flex justify-center items-center overflow-hidden;
|
@apply flex justify-center items-center overflow-hidden bg-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-main {
|
.layout-main {
|
||||||
@apply bg-black text-white shadow-xl flex flex-col relative;
|
@apply text-white shadow-xl flex flex-col relative transition-all;
|
||||||
|
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%;
|
height: 100%;
|
||||||
width: 100%;
|
&-content {
|
||||||
overflow: hidden;
|
@apply box-border flex-1 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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// :deep(.n-scrollbar-content) {
|
||||||
|
// @apply pr-3;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.mobile {
|
||||||
|
.layout-main {
|
||||||
|
&-page {
|
||||||
|
@apply pt-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -8,13 +8,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-menu-list">
|
<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">
|
<router-link class="app-menu-item-link" :to="item.path">
|
||||||
<i
|
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></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>
|
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,44 +20,47 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isText: {
|
isText: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '26px'
|
default: '26px',
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '#aaa'
|
default: '#aaa',
|
||||||
},
|
},
|
||||||
selectColor: {
|
selectColor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '#10B981'
|
default: '#10B981',
|
||||||
},
|
},
|
||||||
menus: {
|
menus: {
|
||||||
type: Array as any,
|
type: Array as any,
|
||||||
default: []
|
default: () => [],
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const path = ref(route.path);
|
const path = ref(route.path);
|
||||||
watch(() => route.path, async newParams => {
|
watch(
|
||||||
path.value = newParams
|
() => route.path,
|
||||||
})
|
async (newParams) => {
|
||||||
|
path.value = newParams;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const iconStyle = (index: any) => {
|
const iconStyle = (index: number) => {
|
||||||
let style = {
|
const style = {
|
||||||
fontSize: props.size,
|
fontSize: props.size,
|
||||||
color: path.value === props.menus[index].path ? props.selectColor : props.color
|
color: path.value === props.menus[index].path ? props.selectColor : props.color,
|
||||||
}
|
};
|
||||||
return style
|
return style;
|
||||||
}
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -83,4 +82,25 @@ const iconStyle = (index: any) => {
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
transition: 0.2s ease-in-out;
|
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>
|
||||||
|
|||||||
@@ -1,72 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer
|
<n-drawer :show="musicFull" height="100vh" placement="bottom" :style="{ background: background }">
|
||||||
:show="musicFull"
|
|
||||||
height="100vh"
|
|
||||||
placement="bottom"
|
|
||||||
:drawer-style="{ backgroundColor: 'transparent' }"
|
|
||||||
>
|
|
||||||
<div id="drawer-target">
|
<div id="drawer-target">
|
||||||
<div class="drawer-back" :class="{'paused': !isPlaying}" :style="{backgroundImage:`url(${getImgUrl(playMusic?.picUrl, '300y300')})`}"></div>
|
<div class="drawer-back"></div>
|
||||||
<div class="music-img">
|
<div class="music-img">
|
||||||
<n-image
|
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
|
||||||
ref="PicImgRef"
|
<div>
|
||||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
<div class="music-content-name">{{ playMusic.name }}</div>
|
||||||
class="img"
|
<div class="music-content-singer">
|
||||||
lazy
|
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
||||||
preview-disabled
|
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
||||||
/>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-content">
|
<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 ? ' / ' : '' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<n-layout
|
<n-layout
|
||||||
class="music-lrc"
|
|
||||||
style="height: 55vh"
|
|
||||||
ref="lrcSider"
|
ref="lrcSider"
|
||||||
|
class="music-lrc"
|
||||||
|
style="height: 60vh"
|
||||||
:native-scrollbar="false"
|
:native-scrollbar="false"
|
||||||
@mouseover="mouseOverLayout"
|
@mouseover="mouseOverLayout"
|
||||||
@mouseleave="mouseLeaveLayout"
|
@mouseleave="mouseLeaveLayout"
|
||||||
>
|
>
|
||||||
<template v-for="(item, index) in lrcArray" :key="index">
|
<div ref="lrcContainer">
|
||||||
<div
|
<div
|
||||||
|
v-for="(item, index) in lrcArray"
|
||||||
|
:id="`music-lrc-text-${index}`"
|
||||||
|
:key="index"
|
||||||
class="music-lrc-text"
|
class="music-lrc-text"
|
||||||
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
|
:class="{ 'now-text': index === nowIndex }"
|
||||||
@click="setAudioTime(index, audio)"
|
@click="setAudioTime(index, audio)"
|
||||||
>
|
>
|
||||||
{{ item.text }}
|
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||||
|
<div class="music-lrc-text-tr">{{ item.trText }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
<!-- 时间矫正 -->
|
<!-- 时间矫正 -->
|
||||||
<div class="music-content-time">
|
<!-- <div class="music-content-time">
|
||||||
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
||||||
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SongResult } from '@/type/music'
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import {
|
import {
|
||||||
lrcArray,
|
|
||||||
newLrcIndex,
|
|
||||||
isCurrentLrc,
|
|
||||||
addCorrectionTime,
|
addCorrectionTime,
|
||||||
|
lrcArray,
|
||||||
|
nowIndex,
|
||||||
|
playMusic,
|
||||||
reduceCorrectionTime,
|
reduceCorrectionTime,
|
||||||
setAudioTime,
|
setAudioTime,
|
||||||
nowTime,
|
useLyricProgress,
|
||||||
} from '@/hooks/MusicHook'
|
} from '@/hooks/MusicHook';
|
||||||
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
const store = useStore()
|
const { getLrcStyle } = useLyricProgress();
|
||||||
|
|
||||||
|
// const isPlaying = computed(() => store.state.play as boolean);
|
||||||
|
// 获取歌词滚动dom
|
||||||
|
const lrcSider = ref<any>(null);
|
||||||
|
const isMouse = ref(false);
|
||||||
|
const lrcContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
musicFull: {
|
musicFull: {
|
||||||
@@ -77,39 +77,57 @@ const props = defineProps({
|
|||||||
type: HTMLAudioElement,
|
type: HTMLAudioElement,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
})
|
background: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:musicFull'])
|
|
||||||
|
|
||||||
// 播放的音乐信息
|
|
||||||
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 lrcScroll = () => {
|
const lrcScroll = (behavior = 'smooth') => {
|
||||||
if (props.musicFull && !isMouse.value) {
|
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
||||||
let top = newLrcIndex.value * 50 - 225
|
if (props.musicFull && !isMouse.value && nowEl && lrcContainer.value) {
|
||||||
lrcSider.value.scrollTo({ top: top, behavior: 'smooth' })
|
const containerRect = lrcContainer.value.getBoundingClientRect();
|
||||||
|
const nowElRect = nowEl.getBoundingClientRect();
|
||||||
|
const relativeTop = nowElRect.top - containerRect.top;
|
||||||
|
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
|
||||||
|
lrcSider.value.scrollTo({ top: scrollTop, behavior });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
||||||
|
|
||||||
const mouseOverLayout = () => {
|
const mouseOverLayout = () => {
|
||||||
isMouse.value = true
|
isMouse.value = true;
|
||||||
}
|
};
|
||||||
const mouseLeaveLayout = () => {
|
const mouseLeaveLayout = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isMouse.value = false
|
isMouse.value = false;
|
||||||
}, 3000)
|
lrcScroll();
|
||||||
}
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(nowIndex, () => {
|
||||||
|
debouncedLrcScroll();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.musicFull,
|
||||||
|
() => {
|
||||||
|
if (props.musicFull) {
|
||||||
|
nextTick(() => {
|
||||||
|
lrcScroll('instant');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
lrcScroll,
|
lrcScroll,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
||||||
@keyframes round {
|
@keyframes round {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@@ -118,15 +136,13 @@ defineExpose({
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.drawer-back{
|
.drawer-back {
|
||||||
@apply absolute bg-cover bg-center opacity-70;
|
@apply absolute bg-cover bg-center;
|
||||||
filter: blur(80px) brightness(80%);
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
width: 200%;
|
width: 200%;
|
||||||
height: 200%;
|
height: 200%;
|
||||||
top: -50%;
|
top: -50%;
|
||||||
left: -50%;
|
left: -50%;
|
||||||
animation: round 20s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-back.paused {
|
.drawer-back.paused {
|
||||||
@@ -134,55 +150,67 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
#drawer-target {
|
#drawer-target {
|
||||||
@apply top-0 left-0 absolute w-full h-full overflow-hidden rounded px-24 pt-24 pb-48 flex items-center;
|
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
background-color: rgba(0, 0, 0, 0.747);
|
|
||||||
animation-duration: 300ms;
|
animation-duration: 300ms;
|
||||||
|
|
||||||
.music-img {
|
.music-img {
|
||||||
@apply flex-1 flex justify-center mr-24;
|
@apply flex-1 flex justify-center mr-16 flex-col;
|
||||||
|
max-width: 360px;
|
||||||
|
max-height: 360px;
|
||||||
.img {
|
.img {
|
||||||
width: 350px;
|
@apply rounded-xl w-full h-full shadow-2xl;
|
||||||
height: 350px;
|
|
||||||
@apply rounded-xl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-content {
|
.music-content {
|
||||||
@apply flex flex-col justify-center items-center;
|
@apply flex flex-col justify-center items-center relative;
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply font-bold text-3xl py-2;
|
@apply font-bold text-xl pb-1 pt-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-singer {
|
&-singer {
|
||||||
@apply text-base py-2;
|
@apply text-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-content-time{
|
.music-content-time {
|
||||||
display: none;
|
display: none;
|
||||||
@apply flex justify-center items-center;
|
@apply flex justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-lrc {
|
.music-lrc {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 550px;
|
height: 550px;
|
||||||
|
|
||||||
&-text {
|
&-text {
|
||||||
@apply text-white text-lg flex justify-center items-center cursor-pointer;
|
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
||||||
height: 50px;
|
color: #ffffff8a;
|
||||||
transition: all 0.2s ease-out;
|
// transition: all 0.5s ease;
|
||||||
|
span {
|
||||||
|
padding-right: 100px;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply font-bold text-xl text-red-500;
|
@apply font-bold opacity-100 rounded-xl;
|
||||||
|
background-color: #ffffff26;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tr {
|
||||||
|
@apply font-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.now-text {
|
.mobile {
|
||||||
@apply font-bold text-xl text-red-500;
|
#drawer-target {
|
||||||
|
@apply flex-col p-4 pt-8;
|
||||||
|
.music-img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.music-lrc {
|
||||||
|
height: calc(100vh - 260px) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 展开全屏 -->
|
<!-- 展开全屏 -->
|
||||||
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio.value as HTMLAudioElement)" />
|
<music-full
|
||||||
|
ref="MusicFullRef"
|
||||||
|
v-model:music-full="musicFullVisible"
|
||||||
|
:audio="audio.value as HTMLAudioElement"
|
||||||
|
:background="background"
|
||||||
|
/>
|
||||||
<!-- 底部播放栏 -->
|
<!-- 底部播放栏 -->
|
||||||
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
|
<div
|
||||||
|
class="music-play-bar"
|
||||||
|
:class="setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')"
|
||||||
|
>
|
||||||
<n-image
|
<n-image
|
||||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
||||||
class="play-bar-img"
|
class="play-bar-img"
|
||||||
@@ -19,41 +27,32 @@
|
|||||||
<div class="music-content-name">
|
<div class="music-content-name">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
||||||
{{ item.name
|
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
||||||
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
|
||||||
</span>
|
</span>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-buttons">
|
<div class="music-buttons">
|
||||||
<div @click="handlePrev">
|
<div class="music-buttons-prev" @click="handlePrev">
|
||||||
<i class="iconfont icon-prev"></i>
|
<i class="iconfont icon-prev"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-buttons-play" @click="playMusicEvent">
|
<div class="music-buttons-play" @click="playMusicEvent">
|
||||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||||||
</div>
|
</div>
|
||||||
<div @click="handleEnded">
|
<div class="music-buttons-next" @click="handleEnded">
|
||||||
<i class="iconfont icon-next"></i>
|
<i class="iconfont icon-next"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-time">
|
<div class="music-time">
|
||||||
<div class="time">{{ getNowTime }}</div>
|
<div class="time">{{ getNowTime }}</div>
|
||||||
<n-slider
|
<n-slider v-model:value="timeSlider" :step="0.05" :tooltip="false"></n-slider>
|
||||||
v-model:value="timeSlider"
|
|
||||||
:step="0.05"
|
|
||||||
:tooltip="false"
|
|
||||||
></n-slider>
|
|
||||||
<div class="time">{{ getAllTime }}</div>
|
<div class="time">{{ getAllTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="audio-volume">
|
<div class="audio-volume">
|
||||||
<div>
|
<div>
|
||||||
<i class="iconfont icon-notificationfill"></i>
|
<i class="iconfont icon-notificationfill"></i>
|
||||||
</div>
|
</div>
|
||||||
<n-slider
|
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false"></n-slider>
|
||||||
v-model:value="volumeSlider"
|
|
||||||
:step="0.01"
|
|
||||||
:tooltip="false"
|
|
||||||
></n-slider>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="audio-button">
|
<div class="audio-button">
|
||||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
||||||
@@ -68,13 +67,22 @@
|
|||||||
</template>
|
</template>
|
||||||
解析播放
|
解析播放
|
||||||
</n-tooltip> -->
|
</n-tooltip> -->
|
||||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
<n-tooltip v-if="isElectron" class="music-lyric" trigger="hover" :z-index="9999999">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<i class="iconfont icon-full" @click="setMusicFull"></i>
|
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
|
||||||
</template>
|
</template>
|
||||||
歌词
|
歌词
|
||||||
</n-tooltip> -->
|
</n-tooltip>
|
||||||
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
|
<n-popover
|
||||||
|
trigger="click"
|
||||||
|
:z-index="99999999"
|
||||||
|
content-class="music-play"
|
||||||
|
raw
|
||||||
|
:show-arrow="false"
|
||||||
|
:delay="200"
|
||||||
|
arrow-wrapper-style=" border-radius:1.5rem"
|
||||||
|
@update-show="scrollToPlayList"
|
||||||
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-tooltip trigger="manual" :z-index="9999999">
|
<n-tooltip trigger="manual" :z-index="9999999">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
@@ -85,156 +93,161 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="music-play-list">
|
<div class="music-play-list">
|
||||||
<div class="music-play-list-back"></div>
|
<div class="music-play-list-back"></div>
|
||||||
<n-scrollbar>
|
<n-virtual-list ref="palyListRef" :item-size="62" item-resizable :items="playList">
|
||||||
<div class="music-play-list-content">
|
<template #default="{ item }">
|
||||||
<song-item v-for="(item, index) in playList" :key="item.id" :item="item" mini></song-item>
|
<div class="music-play-list-content">
|
||||||
</div>
|
<song-item :key="item.id" :item="item" mini></song-item>
|
||||||
</n-scrollbar>
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
</div>
|
</div>
|
||||||
</n-popover>
|
</n-popover>
|
||||||
</div>
|
</div>
|
||||||
<!-- 播放音乐 -->
|
<!-- 播放音乐 -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SongResult } from '@/type/music'
|
import { useTemplateRef } from 'vue';
|
||||||
import { secondToMinute, getImgUrl } from '@/utils'
|
import { useStore } from 'vuex';
|
||||||
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'
|
|
||||||
|
|
||||||
const store = useStore()
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
|
import { allTime, getCurrentLrc, isElectron, 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 = {
|
const audio = {
|
||||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement
|
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||||
}
|
};
|
||||||
|
const background = ref('#000');
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.playMusicUrl,
|
() => store.state.playMusic,
|
||||||
() => {
|
async () => {
|
||||||
loadLrc(playMusic.value.id)
|
background.value = playMusic.value.backgroundColor as string;
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true, deep: true },
|
||||||
)
|
);
|
||||||
|
|
||||||
const audioPlay = () => {
|
const audioPlay = () => {
|
||||||
if (audio.value) {
|
if (audio.value) {
|
||||||
audio.value.play()
|
audio.value.play();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const audioPause = () => {
|
|
||||||
if (audio.value) {
|
|
||||||
audio.value.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性 获取当前播放时间的进度
|
// 计算属性 获取当前播放时间的进度
|
||||||
const timeSlider = computed({
|
const timeSlider = computed({
|
||||||
get: () => (nowTime.value / allTime.value) * 100,
|
get: () => (nowTime.value / allTime.value) * 100,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (!audio.value) return
|
if (!audio.value) return;
|
||||||
audio.value.currentTime = (value * allTime.value) / 100
|
audio.value.currentTime = (value * allTime.value) / 100;
|
||||||
audioPlay()
|
audioPlay();
|
||||||
store.commit('setPlayMusic', true)
|
store.commit('setPlayMusic', true);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// 音量条
|
// 音量条
|
||||||
const audioVolume = ref(1)
|
const audioVolume = ref(1);
|
||||||
const volumeSlider = computed({
|
const volumeSlider = computed({
|
||||||
get: () => audioVolume.value * 100,
|
get: () => audioVolume.value * 100,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if(!audio.value) return
|
if (!audio.value) return;
|
||||||
audio.value.volume = value / 100
|
audio.value.volume = value / 100;
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
// 获取当前播放时间
|
// 获取当前播放时间
|
||||||
const getNowTime = computed(() => {
|
const getNowTime = computed(() => {
|
||||||
return secondToMinute(nowTime.value)
|
return secondToMinute(nowTime.value);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 获取总时间
|
// 获取总时间
|
||||||
const getAllTime = computed(() => {
|
const getAllTime = computed(() => {
|
||||||
return secondToMinute(allTime.value)
|
return secondToMinute(allTime.value);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 监听音乐播放 获取时间
|
// 监听音乐播放 获取时间
|
||||||
const onAudio = () => {
|
const onAudio = () => {
|
||||||
if(audio.value){
|
if (audio.value) {
|
||||||
audio.value.removeEventListener('timeupdate', handleGetAudioTime)
|
audio.value.removeEventListener('timeupdate', handleGetAudioTime);
|
||||||
audio.value.removeEventListener('ended', handleEnded)
|
audio.value.removeEventListener('ended', handleEnded);
|
||||||
audio.value.addEventListener('timeupdate', handleGetAudioTime)
|
audio.value.addEventListener('timeupdate', handleGetAudioTime);
|
||||||
audio.value.addEventListener('ended', handleEnded)
|
audio.value.addEventListener('ended', handleEnded);
|
||||||
// 监听音乐播放暂停
|
// 监听音乐播放暂停
|
||||||
audio.value.addEventListener('pause', () => {
|
audio.value.addEventListener('pause', () => {
|
||||||
store.commit('setPlayMusic', false)
|
store.commit('setPlayMusic', false);
|
||||||
})
|
});
|
||||||
audio.value.addEventListener('play', () => {
|
audio.value.addEventListener('play', () => {
|
||||||
store.commit('setPlayMusic', true)
|
store.commit('setPlayMusic', true);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onAudio()
|
onAudio();
|
||||||
|
|
||||||
function handleEnded() {
|
function handleEnded() {
|
||||||
store.commit('nextPlay')
|
store.commit('nextPlay');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePrev() {
|
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 = audio.currentTime;
|
||||||
|
getCurrentLrc();
|
||||||
// 获取总时间
|
// 获取总时间
|
||||||
allTime.value = audio.duration
|
allTime.value = audio.duration;
|
||||||
// 获取音量
|
// 获取音量
|
||||||
audioVolume.value = audio.volume
|
audioVolume.value = audio.volume;
|
||||||
MusicFullRef.value?.lrcScroll()
|
sendLyricToWin(store.state.isPlay);
|
||||||
|
// if (musicFullVisible.value) {
|
||||||
|
// MusicFullRef.value?.lrcScroll();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放暂停按钮事件
|
// 播放暂停按钮事件
|
||||||
const playMusicEvent = async () => {
|
const playMusicEvent = async () => {
|
||||||
if (play.value) {
|
if (play.value) {
|
||||||
store.commit('setPlayMusic', false)
|
store.commit('setPlayMusic', false);
|
||||||
} else {
|
} else {
|
||||||
store.commit('setPlayMusic', true)
|
store.commit('setPlayMusic', true);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const musicFull = ref(false)
|
const musicFullVisible = ref(false);
|
||||||
|
|
||||||
// 设置musicFull
|
// 设置musicFull
|
||||||
const setMusicFull = () => {
|
const setMusicFull = () => {
|
||||||
musicFull.value = !musicFull.value
|
musicFullVisible.value = !musicFullVisible.value;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const palyListRef = useTemplateRef('palyListRef');
|
||||||
|
|
||||||
|
const scrollToPlayList = (val: boolean) => {
|
||||||
|
if (!val) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
palyListRef.value?.scrollTo({ top: store.state.playListIndex * 62 });
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -243,7 +256,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;
|
@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;
|
z-index: 9999;
|
||||||
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
||||||
background-color: rgba(0, 0, 0, 0.747); .music-content {
|
background-color: #212121;
|
||||||
|
animation-duration: 0.5s !important;
|
||||||
|
.music-content {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
@apply ml-4;
|
@apply ml-4;
|
||||||
|
|
||||||
@@ -258,6 +273,11 @@ const setMusicFull = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.play-bar-opcity {
|
||||||
|
@apply bg-transparent;
|
||||||
|
box-shadow: 0 0 20px 5px #0000001d;
|
||||||
|
}
|
||||||
|
|
||||||
.play-bar-img {
|
.play-bar-img {
|
||||||
@apply w-14 h-14 rounded-2xl;
|
@apply w-14 h-14 rounded-2xl;
|
||||||
}
|
}
|
||||||
@@ -280,8 +300,8 @@ const setMusicFull = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
|
|
||||||
background: #383838;
|
background: #383838;
|
||||||
|
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition bg-opacity-40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,18 +330,50 @@ const setMusicFull = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-play{
|
.music-play {
|
||||||
|
&-list {
|
||||||
&-list{
|
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
@apply relative rounded-3xl overflow-hidden;
|
width: 300px;
|
||||||
&-back{
|
@apply relative rounded-3xl overflow-hidden py-2;
|
||||||
|
&-back {
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
|
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
|
||||||
}
|
}
|
||||||
&-content{
|
&-content {
|
||||||
padding: 10px;
|
@apply mx-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -1,168 +1,157 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-box flex">
|
<div class="search-box flex">
|
||||||
<div class="search-box-input flex-1">
|
<div class="search-box-input flex-1">
|
||||||
<n-input
|
<n-input
|
||||||
size="medium"
|
v-model:value="searchValue"
|
||||||
round
|
size="medium"
|
||||||
v-model:value="searchValue"
|
round
|
||||||
:placeholder="hotSearchKeyword"
|
:placeholder="hotSearchKeyword"
|
||||||
class="border border-gray-600"
|
class="border border-gray-600"
|
||||||
@keydown.enter="search"
|
@keydown.enter="search"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<i class="iconfont icon-search"></i>
|
<i class="iconfont icon-search"></i>
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<div class="w-20 px-3 flex justify-between items-center">
|
<div class="w-20 px-3 flex justify-between items-center">
|
||||||
<div>{{ searchTypeOptions.find(item => item.key === searchType)?.label }}</div>
|
<div>{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}</div>
|
||||||
<n-dropdown trigger="hover" @select="selectSearchType" :options="searchTypeOptions">
|
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-box">
|
<div class="user-box">
|
||||||
<n-dropdown trigger="hover" @select="selectItem" :options="userSetOptions">
|
<n-dropdown trigger="hover" :options="userSetOptions" @select="selectItem">
|
||||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
<n-avatar
|
<n-avatar
|
||||||
class="ml-2 cursor-pointer"
|
v-if="store.state.user"
|
||||||
circle
|
class="ml-2 cursor-pointer"
|
||||||
size="medium"
|
circle
|
||||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
size="medium"
|
||||||
v-if="store.state.user"
|
:src="getImgUrl(store.state.user.avatarUrl)"
|
||||||
/>
|
/>
|
||||||
<div class="mx-2 rounded-full cursor-pointer text-sm" v-else @click="toLogin">登录</div>
|
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getSearchKeyword } from '@/api/home';
|
import { useRouter } from 'vue-router';
|
||||||
import { getUserDetail, logout } from '@/api/login';
|
import { useStore } from 'vuex';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useStore } from 'vuex';
|
import { getSearchKeyword } from '@/api/home';
|
||||||
import request from '@/utils/request_mt'
|
import { getUserDetail, logout } from '@/api/login';
|
||||||
import { getImgUrl } from '@/utils';
|
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||||
import {USER_SET_OPTIONS, SEARCH_TYPES} from '@/const/bar-const'
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const userSetOptions = ref(USER_SET_OPTIONS)
|
const userSetOptions = ref(USER_SET_OPTIONS);
|
||||||
|
|
||||||
|
// 推荐热搜词
|
||||||
// 推荐热搜词
|
const hotSearchKeyword = ref('搜索点什么吧...');
|
||||||
const hotSearchKeyword = ref("搜索点什么吧...")
|
const hotSearchValue = ref('');
|
||||||
const hotSearchValue = ref("")
|
const loadHotSearchKeyword = async () => {
|
||||||
const loadHotSearchKeyword = async () => {
|
const { data } = await getSearchKeyword();
|
||||||
const { data } = await getSearchKeyword();
|
hotSearchKeyword.value = data.data.showKeyword;
|
||||||
hotSearchKeyword.value = data.data.showKeyword
|
hotSearchValue.value = data.data.realkeyword;
|
||||||
hotSearchValue.value = data.data.realkeyword
|
};
|
||||||
}
|
|
||||||
|
const loadPage = async () => {
|
||||||
const loadPage = async () => {
|
const token = localStorage.getItem('token');
|
||||||
const token = localStorage.getItem("token")
|
if (!token) return;
|
||||||
if (!token) return
|
const { data } = await getUserDetail();
|
||||||
const { data } = await getUserDetail()
|
store.state.user = data.profile;
|
||||||
store.state.user = data.profile
|
localStorage.setItem('user', JSON.stringify(data.profile));
|
||||||
localStorage.setItem('user', JSON.stringify(data.profile))
|
};
|
||||||
}
|
|
||||||
|
loadPage();
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
loadPage()
|
if (store.state.user) {
|
||||||
if (store.state.user) {
|
userSetOptions.value = USER_SET_OPTIONS;
|
||||||
userSetOptions.value = USER_SET_OPTIONS
|
} else {
|
||||||
} else {
|
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
|
||||||
userSetOptions.value = USER_SET_OPTIONS.filter(item => item.key !== 'logout')
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
const toLogin = () => {
|
||||||
|
router.push('/login');
|
||||||
const toLogin = () => {
|
};
|
||||||
router.push('/login')
|
|
||||||
}
|
// 页面初始化
|
||||||
|
onMounted(() => {
|
||||||
// 页面初始化
|
loadHotSearchKeyword();
|
||||||
onMounted(() => {
|
loadPage();
|
||||||
loadHotSearchKeyword()
|
});
|
||||||
loadPage()
|
|
||||||
})
|
// 搜索词
|
||||||
|
const searchValue = ref('');
|
||||||
|
const search = () => {
|
||||||
// 搜索词
|
const { value } = searchValue;
|
||||||
const searchValue = ref("")
|
if (value === '') {
|
||||||
const searchType = ref(1)
|
searchValue.value = hotSearchValue.value;
|
||||||
const search = () => {
|
return;
|
||||||
let value = searchValue.value
|
}
|
||||||
if (value == "") {
|
|
||||||
searchValue.value = hotSearchValue.value
|
if (router.currentRoute.value.path === '/search') {
|
||||||
} else {
|
store.state.searchValue = value;
|
||||||
router.push({
|
return;
|
||||||
path: "/search",
|
}
|
||||||
query: {
|
|
||||||
keyword: value,
|
router.push({
|
||||||
type: searchType.value
|
path: '/search',
|
||||||
}
|
query: {
|
||||||
})
|
keyword: value,
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
};
|
||||||
const selectSearchType = (key: any) => {
|
|
||||||
searchType.value = key
|
const selectSearchType = (key: number) => {
|
||||||
}
|
store.state.searchType = key;
|
||||||
|
};
|
||||||
|
|
||||||
const searchTypeOptions = ref(SEARCH_TYPES)
|
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||||
|
|
||||||
const selectItem = async (key: any) => {
|
const selectItem = async (key: string) => {
|
||||||
// switch 判断
|
// switch 判断
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'card':
|
case 'logout':
|
||||||
await request.get('/?do=sign')
|
logout().then(() => {
|
||||||
.then(res => {
|
store.state.user = null;
|
||||||
console.log(res)
|
localStorage.clear();
|
||||||
})
|
router.push('/login');
|
||||||
break;
|
});
|
||||||
case 'card_music':
|
break;
|
||||||
await request.get('/?do=daka')
|
case 'login':
|
||||||
.then(res => {
|
router.push('/login');
|
||||||
console.log(res)
|
break;
|
||||||
})
|
case 'set':
|
||||||
break;
|
router.push('/set');
|
||||||
case 'listen':
|
break;
|
||||||
await request.get('/?do=listen&id=1885175990&time=300')
|
default:
|
||||||
.then(res => {
|
}
|
||||||
console.log(res)
|
};
|
||||||
})
|
</script>
|
||||||
break;
|
|
||||||
case 'logout':
|
<style lang="scss" scoped>
|
||||||
logout().then(() => {
|
.user-box {
|
||||||
store.state.user = null
|
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
||||||
localStorage.clear()
|
background: #1a1a1a;
|
||||||
})
|
}
|
||||||
break;
|
.search-box {
|
||||||
case 'login':
|
@apply pb-4 pr-4;
|
||||||
router.push("/login")
|
}
|
||||||
break;
|
.search-box-input {
|
||||||
case 'set':
|
@apply relative;
|
||||||
router.push("/set")
|
}
|
||||||
break;
|
|
||||||
}
|
.mobile {
|
||||||
}
|
.search-box {
|
||||||
|
@apply pl-4;
|
||||||
</script>
|
}
|
||||||
|
}
|
||||||
<style lang="scss" scoped>
|
</style>
|
||||||
.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>
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
<button @click="minimize">
|
<button @click="minimize">
|
||||||
<i class="iconfont icon-minisize"></i>
|
<i class="iconfont icon-minisize"></i>
|
||||||
</button>
|
</button>
|
||||||
<!-- <button @click="maximize">
|
|
||||||
<i class="iconfont icon-maxsize"></i>
|
|
||||||
</button> -->
|
|
||||||
<button @click="close">
|
<button @click="close">
|
||||||
<i class="iconfont icon-close"></i>
|
<i class="iconfont icon-close"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -16,18 +13,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDialog } from 'naive-ui'
|
import { useDialog } from 'naive-ui';
|
||||||
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog();
|
||||||
const windowData = window as any
|
const windowData = window as any;
|
||||||
|
|
||||||
const minimize = () => {
|
const minimize = () => {
|
||||||
windowData.electronAPI.minimize()
|
windowData.electronAPI.minimize();
|
||||||
}
|
};
|
||||||
|
|
||||||
const maximize = () => {
|
|
||||||
windowData.electronAPI.maximize()
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
@@ -36,17 +29,17 @@ const close = () => {
|
|||||||
positiveText: '最小化',
|
positiveText: '最小化',
|
||||||
negativeText: '关闭',
|
negativeText: '关闭',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: () => {
|
||||||
windowData.electronAPI.miniTray()
|
windowData.electronAPI.miniTray();
|
||||||
},
|
},
|
||||||
onNegativeClick: () => {
|
onNegativeClick: () => {
|
||||||
windowData.electronAPI.close()
|
windowData.electronAPI.close();
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const drag = (event: MouseEvent) => {
|
const drag = (event: MouseEvent) => {
|
||||||
windowData.electronAPI.dragStart(event)
|
windowData.electronAPI.dragStart(event);
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import AppMenu from "./AppMenu.vue";
|
import AppMenu from './AppMenu.vue';
|
||||||
import PlayBar from "./PlayBar.vue";
|
import PlayBar from './PlayBar.vue';
|
||||||
import SearchBar from "./SearchBar.vue";
|
import SearchBar from './SearchBar.vue';
|
||||||
|
|
||||||
export { AppMenu, PlayBar, SearchBar };
|
export { AppMenu, PlayBar, SearchBar };
|
||||||
|
|||||||
@@ -5,20 +5,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
lrcList: {
|
lrcList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
lrcIndex: {
|
lrcIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
lrcTime: {
|
lrcTime: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
41
src/main.ts
@@ -1,19 +1,22 @@
|
|||||||
import { createApp } from "vue";
|
import 'vfonts/Lato.css';
|
||||||
import App from "./App.vue";
|
import 'vfonts/FiraCode.css';
|
||||||
|
// tailwind css
|
||||||
import naive from "naive-ui";
|
import './index.css';
|
||||||
import "vfonts/Lato.css";
|
import 'remixicon/fonts/remixicon.css';
|
||||||
import "vfonts/FiraCode.css";
|
|
||||||
|
import { createApp } from 'vue';
|
||||||
// tailwind css
|
|
||||||
import "./index.css";
|
import router from '@/router';
|
||||||
|
import store from '@/store';
|
||||||
import router from "@/router";
|
|
||||||
|
import App from './App.vue';
|
||||||
import store from "@/store";
|
import directives from './directive';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(router);
|
|
||||||
app.use(store);
|
Object.keys(directives).forEach((key: string) => {
|
||||||
// app.use(naive);
|
app.directive(key, directives[key]);
|
||||||
app.mount("#app");
|
});
|
||||||
|
app.use(router);
|
||||||
|
app.use(store);
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const layoutRouter = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '首页',
|
title: '首页',
|
||||||
icon: 'icon-Home',
|
icon: 'icon-Home',
|
||||||
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/home/index.vue'),
|
component: () => import('@/views/home/index.vue'),
|
||||||
},
|
},
|
||||||
@@ -14,8 +15,8 @@ const layoutRouter = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '搜索',
|
title: '搜索',
|
||||||
noScroll: true,
|
noScroll: true,
|
||||||
noKeepAlive: true,
|
|
||||||
icon: 'icon-Search',
|
icon: 'icon-Search',
|
||||||
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/search/index.vue'),
|
component: () => import('@/views/search/index.vue'),
|
||||||
},
|
},
|
||||||
@@ -25,6 +26,7 @@ const layoutRouter = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '歌单',
|
title: '歌单',
|
||||||
icon: 'icon-Paper',
|
icon: 'icon-Paper',
|
||||||
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/list/index.vue'),
|
component: () => import('@/views/list/index.vue'),
|
||||||
},
|
},
|
||||||
@@ -34,6 +36,7 @@ const layoutRouter = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'MV',
|
title: 'MV',
|
||||||
icon: 'icon-recordfill',
|
icon: 'icon-recordfill',
|
||||||
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/mv/index.vue'),
|
component: () => import('@/views/mv/index.vue'),
|
||||||
},
|
},
|
||||||
@@ -43,6 +46,7 @@ const layoutRouter = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '历史',
|
title: '历史',
|
||||||
icon: 'icon-a-TicketStar',
|
icon: 'icon-a-TicketStar',
|
||||||
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/history/index.vue'),
|
component: () => import('@/views/history/index.vue'),
|
||||||
},
|
},
|
||||||
@@ -51,11 +55,11 @@ const layoutRouter = [
|
|||||||
name: 'user',
|
name: 'user',
|
||||||
meta: {
|
meta: {
|
||||||
title: '用户',
|
title: '用户',
|
||||||
noKeepAlive: true,
|
|
||||||
icon: 'icon-Profile',
|
icon: 'icon-Profile',
|
||||||
|
keepAlive: true,
|
||||||
noScroll: true,
|
noScroll: true,
|
||||||
},
|
},
|
||||||
component: () => import('@/views/user/index.vue'),
|
component: () => import('@/views/user/index.vue'),
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
export default layoutRouter;
|
export default layoutRouter;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import AppLayout from '@/layout/AppLayout.vue'
|
|
||||||
import homeRouter from '@/router/home'
|
import AppLayout from '@/layout/AppLayout.vue';
|
||||||
|
import homeRouter from '@/router/home';
|
||||||
|
|
||||||
const loginRouter = {
|
const loginRouter = {
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -11,7 +12,7 @@ const loginRouter = {
|
|||||||
icon: 'icon-Home',
|
icon: 'icon-Home',
|
||||||
},
|
},
|
||||||
component: () => import('@/views/login/index.vue'),
|
component: () => import('@/views/login/index.vue'),
|
||||||
}
|
};
|
||||||
|
|
||||||
const setRouter = {
|
const setRouter = {
|
||||||
path: '/set',
|
path: '/set',
|
||||||
@@ -22,7 +23,7 @@ const setRouter = {
|
|||||||
icon: 'icon-Home',
|
icon: 'icon-Home',
|
||||||
},
|
},
|
||||||
component: () => import('@/views/set/index.vue'),
|
component: () => import('@/views/set/index.vue'),
|
||||||
}
|
};
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -30,9 +31,13 @@ const routes = [
|
|||||||
component: AppLayout,
|
component: AppLayout,
|
||||||
children: [...homeRouter, loginRouter, setRouter],
|
children: [...homeRouter, loginRouter, setRouter],
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
path: '/lyric',
|
||||||
|
component: () => import('@/views/lyric/index.vue'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
routes: routes,
|
routes,
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
})
|
});
|
||||||
|
|||||||
11
src/shims-vue.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import { DefineComponent } from 'vue'
|
import { DefineComponent } from 'vue';
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
const component: DefineComponent<{}, {}, any>;
|
||||||
}
|
export default component;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex';
|
||||||
import { SongResult } from '@/type/music'
|
|
||||||
import { getMusicUrl, getParsingMusicUrl } from '@/api/music'
|
import { useMusicListHook } from '@/hooks/MusicListHook';
|
||||||
import homeRouter from '@/router/home'
|
import homeRouter from '@/router/home';
|
||||||
import { getMusicProxyUrl } from '@/utils'
|
import type { SongResult } from '@/type/music';
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook'
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
menus: any[]
|
menus: any[];
|
||||||
play: boolean
|
play: boolean;
|
||||||
isPlay: boolean
|
isPlay: boolean;
|
||||||
playMusic: SongResult
|
playMusic: SongResult;
|
||||||
playMusicUrl: string
|
playMusicUrl: string;
|
||||||
user: any
|
user: any;
|
||||||
playList: SongResult[]
|
playList: SongResult[];
|
||||||
playListIndex: number
|
playListIndex: number;
|
||||||
setData: any
|
setData: any;
|
||||||
|
lyric: any;
|
||||||
|
isMobile: boolean;
|
||||||
|
searchValue: string;
|
||||||
|
searchType: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@@ -23,89 +26,51 @@ const state: State = {
|
|||||||
isPlay: false,
|
isPlay: false,
|
||||||
playMusic: {} as SongResult,
|
playMusic: {} as SongResult,
|
||||||
playMusicUrl: '',
|
playMusicUrl: '',
|
||||||
user: null,
|
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') as string) : null,
|
||||||
playList: [],
|
playList: [],
|
||||||
playListIndex: 0,
|
playListIndex: 0,
|
||||||
setData: null,
|
setData: null,
|
||||||
}
|
lyric: {},
|
||||||
|
isMobile: false,
|
||||||
|
searchValue: '',
|
||||||
|
searchType: 1,
|
||||||
|
};
|
||||||
|
const windowData = window as any;
|
||||||
|
|
||||||
const windowData = window as any
|
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||||
|
|
||||||
const musicHistory = useMusicHistory()
|
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
setMenus(state: State, menus: any[]) {
|
setMenus(state: State, menus: any[]) {
|
||||||
state.menus = menus
|
state.menus = menus;
|
||||||
},
|
},
|
||||||
async setPlay(state: State, playMusic: SongResult) {
|
async setPlay(state: State, playMusic: SongResult) {
|
||||||
state.playMusic = playMusic
|
await handlePlayMusic(state, playMusic);
|
||||||
state.playMusicUrl = await getSongUrl(playMusic.id)
|
|
||||||
state.play = true
|
|
||||||
musicHistory.addMusic(playMusic)
|
|
||||||
},
|
},
|
||||||
setIsPlay(state: State, isPlay: boolean) {
|
setIsPlay(state: State, isPlay: boolean) {
|
||||||
state.isPlay = isPlay
|
state.isPlay = isPlay;
|
||||||
},
|
},
|
||||||
setPlayMusic(state: State, play: boolean) {
|
setPlayMusic(state: State, play: boolean) {
|
||||||
state.play = play
|
state.play = play;
|
||||||
},
|
},
|
||||||
setPlayList(state: State, playList: SongResult[]) {
|
setPlayList(state: State, playList: SongResult[]) {
|
||||||
state.playListIndex = playList.findIndex(
|
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
|
||||||
(item) => item.id === state.playMusic.id
|
state.playList = playList;
|
||||||
)
|
|
||||||
state.playList = playList
|
|
||||||
},
|
},
|
||||||
async nextPlay(state: State) {
|
async nextPlay(state: State) {
|
||||||
if (state.playList.length === 0) {
|
await nextPlay(state);
|
||||||
state.play = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.playListIndex = (state.playListIndex + 1) % state.playList.length
|
|
||||||
await updatePlayMusic(state)
|
|
||||||
},
|
},
|
||||||
async prevPlay(state: State) {
|
async prevPlay(state: State) {
|
||||||
if (state.playList.length === 0) {
|
await prevPlay(state);
|
||||||
state.play = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.playListIndex =
|
|
||||||
(state.playListIndex - 1 + state.playList.length) % state.playList.length
|
|
||||||
await updatePlayMusic(state)
|
|
||||||
},
|
},
|
||||||
async setSetData(state: State, setData: any) {
|
async setSetData(state: State, setData: any) {
|
||||||
state.setData = setData
|
state.setData = setData;
|
||||||
windowData.electron.ipcRenderer.setStoreValue(
|
window.electron && window.electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
|
||||||
'set',
|
|
||||||
JSON.parse(JSON.stringify(setData))
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const getSongUrl = async (id: number) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: state,
|
state,
|
||||||
mutations: mutations,
|
mutations,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default store
|
export default store;
|
||||||
|
|||||||
@@ -4,30 +4,30 @@ export interface IAlbumNew {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
name: string
|
name: string;
|
||||||
id: number
|
id: number;
|
||||||
type: string
|
type: string;
|
||||||
size: number
|
size: number;
|
||||||
picId: number
|
picId: number;
|
||||||
blurPicUrl: string
|
blurPicUrl: string;
|
||||||
companyId: number
|
companyId: number;
|
||||||
pic: number
|
pic: number;
|
||||||
picUrl: string
|
picUrl: string;
|
||||||
publishTime: number
|
publishTime: number;
|
||||||
description: string
|
description: string;
|
||||||
tags: string
|
tags: string;
|
||||||
company: string
|
company: string;
|
||||||
briefDesc: string
|
briefDesc: string;
|
||||||
artist: Artist
|
artist: Artist;
|
||||||
songs?: any
|
songs?: any;
|
||||||
alias: string[]
|
alias: string[];
|
||||||
status: number
|
status: number;
|
||||||
copyrightId: number
|
copyrightId: number;
|
||||||
commentThreadId: string
|
commentThreadId: string;
|
||||||
artists: Artist2[]
|
artists: Artist2[];
|
||||||
paid: boolean
|
paid: boolean;
|
||||||
onSale: boolean
|
onSale: boolean;
|
||||||
picId_str: string
|
picId_str: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Artist2 {
|
interface Artist2 {
|
||||||
|
|||||||
168
src/type/day_recommend.ts
Normal 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[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface IData<T> {
|
export interface IData<T> {
|
||||||
code: number
|
code: number;
|
||||||
data: T
|
data: T;
|
||||||
|
result: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,42 +7,42 @@ export interface IList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Playlist {
|
export interface Playlist {
|
||||||
name: string
|
name: string;
|
||||||
id: number
|
id: number;
|
||||||
trackNumberUpdateTime: number
|
trackNumberUpdateTime: number;
|
||||||
status: number
|
status: number;
|
||||||
userId: number
|
userId: number;
|
||||||
createTime: number
|
createTime: number;
|
||||||
updateTime: number
|
updateTime: number;
|
||||||
subscribedCount: number
|
subscribedCount: number;
|
||||||
trackCount: number
|
trackCount: number;
|
||||||
cloudTrackCount: number
|
cloudTrackCount: number;
|
||||||
coverImgUrl: string
|
coverImgUrl: string;
|
||||||
coverImgId: number
|
coverImgId: number;
|
||||||
description: string
|
description: string;
|
||||||
tags: string[]
|
tags: string[];
|
||||||
playCount: number
|
playCount: number;
|
||||||
trackUpdateTime: number
|
trackUpdateTime: number;
|
||||||
specialType: number
|
specialType: number;
|
||||||
totalDuration: number
|
totalDuration: number;
|
||||||
creator: Creator
|
creator: Creator;
|
||||||
tracks?: any
|
tracks?: any;
|
||||||
subscribers: Subscriber[]
|
subscribers: Subscriber[];
|
||||||
subscribed: boolean
|
subscribed: boolean;
|
||||||
commentThreadId: string
|
commentThreadId: string;
|
||||||
newImported: boolean
|
newImported: boolean;
|
||||||
adType: number
|
adType: number;
|
||||||
highQuality: boolean
|
highQuality: boolean;
|
||||||
privacy: number
|
privacy: number;
|
||||||
ordered: boolean
|
ordered: boolean;
|
||||||
anonimous: boolean
|
anonimous: boolean;
|
||||||
coverStatus: number
|
coverStatus: number;
|
||||||
recommendInfo?: any
|
recommendInfo?: any;
|
||||||
shareCount: number
|
shareCount: number;
|
||||||
coverImgId_str?: string
|
coverImgId_str?: string;
|
||||||
commentCount: number
|
commentCount: number;
|
||||||
copywriter: string
|
copywriter: string;
|
||||||
tag: string
|
tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Subscriber {
|
interface Subscriber {
|
||||||
@@ -120,8 +120,8 @@ interface AvatarDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Expert {
|
interface Expert {
|
||||||
"2": string;
|
'2': string;
|
||||||
"1"?: string;
|
'1'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推荐歌单
|
// 推荐歌单
|
||||||
|
|||||||
@@ -1,203 +1,203 @@
|
|||||||
export interface IListDetail {
|
export interface IListDetail {
|
||||||
code: number;
|
code: number;
|
||||||
relatedVideos?: any;
|
relatedVideos?: any;
|
||||||
playlist: Playlist;
|
playlist: Playlist;
|
||||||
urls?: any;
|
urls?: any;
|
||||||
privileges: Privilege[];
|
privileges: Privilege[];
|
||||||
sharedPrivilege?: any;
|
sharedPrivilege?: any;
|
||||||
resEntrance?: any;
|
resEntrance?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Privilege {
|
interface Privilege {
|
||||||
id: number;
|
id: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
payed: number;
|
payed: number;
|
||||||
realPayed: number;
|
realPayed: number;
|
||||||
st: number;
|
st: number;
|
||||||
pl: number;
|
pl: number;
|
||||||
dl: number;
|
dl: number;
|
||||||
sp: number;
|
sp: number;
|
||||||
cp: number;
|
cp: number;
|
||||||
subp: number;
|
subp: number;
|
||||||
cs: boolean;
|
cs: boolean;
|
||||||
maxbr: number;
|
maxbr: number;
|
||||||
fl: number;
|
fl: number;
|
||||||
pc?: any;
|
pc?: any;
|
||||||
toast: boolean;
|
toast: boolean;
|
||||||
flag: number;
|
flag: number;
|
||||||
paidBigBang: boolean;
|
paidBigBang: boolean;
|
||||||
preSell: boolean;
|
preSell: boolean;
|
||||||
playMaxbr: number;
|
playMaxbr: number;
|
||||||
downloadMaxbr: number;
|
downloadMaxbr: number;
|
||||||
rscl?: any;
|
rscl?: any;
|
||||||
freeTrialPrivilege: FreeTrialPrivilege;
|
freeTrialPrivilege: FreeTrialPrivilege;
|
||||||
chargeInfoList: ChargeInfoList[];
|
chargeInfoList: ChargeInfoList[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChargeInfoList {
|
interface ChargeInfoList {
|
||||||
rate: number;
|
rate: number;
|
||||||
chargeUrl?: any;
|
chargeUrl?: any;
|
||||||
chargeMessage?: any;
|
chargeMessage?: any;
|
||||||
chargeType: number;
|
chargeType: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FreeTrialPrivilege {
|
interface FreeTrialPrivilege {
|
||||||
resConsumable: boolean;
|
resConsumable: boolean;
|
||||||
userConsumable: boolean;
|
userConsumable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Playlist {
|
export interface Playlist {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
coverImgId: number
|
coverImgId: number;
|
||||||
coverImgUrl: string
|
coverImgUrl: string;
|
||||||
coverImgId_str: string
|
coverImgId_str: string;
|
||||||
adType: number
|
adType: number;
|
||||||
userId: number
|
userId: number;
|
||||||
createTime: number
|
createTime: number;
|
||||||
status: number
|
status: number;
|
||||||
opRecommend: boolean
|
opRecommend: boolean;
|
||||||
highQuality: boolean
|
highQuality: boolean;
|
||||||
newImported: boolean
|
newImported: boolean;
|
||||||
updateTime: number
|
updateTime: number;
|
||||||
trackCount: number
|
trackCount: number;
|
||||||
specialType: number
|
specialType: number;
|
||||||
privacy: number
|
privacy: number;
|
||||||
trackUpdateTime: number
|
trackUpdateTime: number;
|
||||||
commentThreadId: string
|
commentThreadId: string;
|
||||||
playCount: number
|
playCount: number;
|
||||||
trackNumberUpdateTime: number
|
trackNumberUpdateTime: number;
|
||||||
subscribedCount: number
|
subscribedCount: number;
|
||||||
cloudTrackCount: number
|
cloudTrackCount: number;
|
||||||
ordered: boolean
|
ordered: boolean;
|
||||||
description: string
|
description: string;
|
||||||
tags: string[]
|
tags: string[];
|
||||||
updateFrequency?: any
|
updateFrequency?: any;
|
||||||
backgroundCoverId: number
|
backgroundCoverId: number;
|
||||||
backgroundCoverUrl?: any
|
backgroundCoverUrl?: any;
|
||||||
titleImage: number
|
titleImage: number;
|
||||||
titleImageUrl?: any
|
titleImageUrl?: any;
|
||||||
englishTitle?: any
|
englishTitle?: any;
|
||||||
officialPlaylistType?: any
|
officialPlaylistType?: any;
|
||||||
subscribers: Subscriber[]
|
subscribers: Subscriber[];
|
||||||
subscribed: boolean
|
subscribed: boolean;
|
||||||
creator: Subscriber
|
creator: Subscriber;
|
||||||
tracks: Track[]
|
tracks: Track[];
|
||||||
videoIds?: any
|
videoIds?: any;
|
||||||
videos?: any
|
videos?: any;
|
||||||
trackIds: TrackId[]
|
trackIds: TrackId[];
|
||||||
shareCount: number
|
shareCount: number;
|
||||||
commentCount: number
|
commentCount: number;
|
||||||
remixVideo?: any
|
remixVideo?: any;
|
||||||
sharedUsers?: any
|
sharedUsers?: any;
|
||||||
historySharedUsers?: any
|
historySharedUsers?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrackId {
|
interface TrackId {
|
||||||
id: number;
|
id: number;
|
||||||
v: number;
|
v: number;
|
||||||
t: number;
|
t: number;
|
||||||
at: number;
|
at: number;
|
||||||
alg?: any;
|
alg?: any;
|
||||||
uid: number;
|
uid: number;
|
||||||
rcmdReason: string;
|
rcmdReason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Track {
|
interface Track {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
pst: number;
|
pst: number;
|
||||||
t: number;
|
t: number;
|
||||||
ar: Ar[];
|
ar: Ar[];
|
||||||
alia: string[];
|
alia: string[];
|
||||||
pop: number;
|
pop: number;
|
||||||
st: number;
|
st: number;
|
||||||
rt?: string;
|
rt?: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
v: number;
|
v: number;
|
||||||
crbt?: any;
|
crbt?: any;
|
||||||
cf: string;
|
cf: string;
|
||||||
al: Al;
|
al: Al;
|
||||||
dt: number;
|
dt: number;
|
||||||
h: H;
|
h: H;
|
||||||
m: H;
|
m: H;
|
||||||
l?: H;
|
l?: H;
|
||||||
a?: any;
|
a?: any;
|
||||||
cd: string;
|
cd: string;
|
||||||
no: number;
|
no: number;
|
||||||
rtUrl?: any;
|
rtUrl?: any;
|
||||||
ftype: number;
|
ftype: number;
|
||||||
rtUrls: any[];
|
rtUrls: any[];
|
||||||
djId: number;
|
djId: number;
|
||||||
copyright: number;
|
copyright: number;
|
||||||
s_id: number;
|
s_id: number;
|
||||||
mark: number;
|
mark: number;
|
||||||
originCoverType: number;
|
originCoverType: number;
|
||||||
originSongSimpleData?: any;
|
originSongSimpleData?: any;
|
||||||
single: number;
|
single: number;
|
||||||
noCopyrightRcmd?: any;
|
noCopyrightRcmd?: any;
|
||||||
mst: number;
|
mst: number;
|
||||||
cp: number;
|
cp: number;
|
||||||
mv: number;
|
mv: number;
|
||||||
rtype: number;
|
rtype: number;
|
||||||
rurl?: any;
|
rurl?: any;
|
||||||
publishTime: number;
|
publishTime: number;
|
||||||
tns?: string[];
|
tns?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface H {
|
interface H {
|
||||||
br: number;
|
br: number;
|
||||||
fid: number;
|
fid: number;
|
||||||
size: number;
|
size: number;
|
||||||
vd: number;
|
vd: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Al {
|
interface Al {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
picUrl: string;
|
picUrl: string;
|
||||||
tns: any[];
|
tns: any[];
|
||||||
pic_str?: string;
|
pic_str?: string;
|
||||||
pic: number;
|
pic: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ar {
|
interface Ar {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
tns: any[];
|
tns: any[];
|
||||||
alias: any[];
|
alias: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Subscriber {
|
interface Subscriber {
|
||||||
defaultAvatar: boolean;
|
defaultAvatar: boolean;
|
||||||
province: number;
|
province: number;
|
||||||
authStatus: number;
|
authStatus: number;
|
||||||
followed: boolean;
|
followed: boolean;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
accountStatus: number;
|
accountStatus: number;
|
||||||
gender: number;
|
gender: number;
|
||||||
city: number;
|
city: number;
|
||||||
birthday: number;
|
birthday: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
userType: number;
|
userType: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
description: string;
|
description: string;
|
||||||
detailDescription: string;
|
detailDescription: string;
|
||||||
avatarImgId: number;
|
avatarImgId: number;
|
||||||
backgroundImgId: number;
|
backgroundImgId: number;
|
||||||
backgroundUrl: string;
|
backgroundUrl: string;
|
||||||
authority: number;
|
authority: number;
|
||||||
mutual: boolean;
|
mutual: boolean;
|
||||||
expertTags?: any;
|
expertTags?: any;
|
||||||
experts?: any;
|
experts?: any;
|
||||||
djStatus: number;
|
djStatus: number;
|
||||||
vipType: number;
|
vipType: number;
|
||||||
remarkName?: any;
|
remarkName?: any;
|
||||||
authenticationTypes: number;
|
authenticationTypes: number;
|
||||||
avatarDetail?: any;
|
avatarDetail?: any;
|
||||||
backgroundImgIdStr: string;
|
backgroundImgIdStr: string;
|
||||||
anchor: boolean;
|
anchor: boolean;
|
||||||
avatarImgIdStr: string;
|
avatarImgIdStr: string;
|
||||||
avatarImgId_str: string;
|
avatarImgId_str: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export interface ILyric {
|
export interface ILyric {
|
||||||
sgc: boolean;
|
sgc: boolean;
|
||||||
sfy: boolean;
|
sfy: boolean;
|
||||||
qfy: boolean;
|
qfy: boolean;
|
||||||
lrc: Lrc;
|
lrc: Lrc;
|
||||||
klyric: Lrc;
|
klyric: Lrc;
|
||||||
tlyric: Lrc;
|
tlyric: Lrc;
|
||||||
code: number;
|
code: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Lrc {
|
interface Lrc {
|
||||||
version: number;
|
version: number;
|
||||||
lyric: string;
|
lyric: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,197 +1,215 @@
|
|||||||
export interface IRecommendMusic {
|
export interface IRecommendMusic {
|
||||||
code: number;
|
code: number;
|
||||||
category: number;
|
category: number;
|
||||||
result: SongResult[];
|
result: SongResult[];
|
||||||
}
|
}
|
||||||
|
export interface ILyricText {
|
||||||
export interface SongResult {
|
text: string;
|
||||||
id: number
|
trText: string;
|
||||||
type: number
|
}
|
||||||
name: string
|
export interface ILyric {
|
||||||
copywriter?: any
|
lrcTimeArray: number[];
|
||||||
picUrl: string
|
lrcArray: ILyricText[];
|
||||||
canDislike: boolean
|
}
|
||||||
trackNumberUpdateTime?: any
|
|
||||||
song: Song
|
export interface SongResult {
|
||||||
alg: string
|
id: number;
|
||||||
count?: number
|
type: number;
|
||||||
}
|
name: string;
|
||||||
|
copywriter?: any;
|
||||||
interface Song {
|
picUrl: string;
|
||||||
name: string;
|
canDislike: boolean;
|
||||||
id: number;
|
trackNumberUpdateTime?: any;
|
||||||
position: number;
|
song: Song;
|
||||||
alias: string[];
|
alg: string;
|
||||||
status: number;
|
count?: number;
|
||||||
fee: number;
|
playLoading?: boolean;
|
||||||
copyrightId: number;
|
ar?: Artist[];
|
||||||
disc: string;
|
al?: Album;
|
||||||
no: number;
|
backgroundColor?: string;
|
||||||
artists: Artist[];
|
primaryColor?: string;
|
||||||
album: Album;
|
playMusicUrl?: string;
|
||||||
starred: boolean;
|
lyric?: ILyric;
|
||||||
popularity: number;
|
}
|
||||||
score: number;
|
|
||||||
starredNum: number;
|
export interface Song {
|
||||||
duration: number;
|
name: string;
|
||||||
playedNum: number;
|
id: number;
|
||||||
dayPlays: number;
|
position: number;
|
||||||
hearTime: number;
|
alias: string[];
|
||||||
ringtone: string;
|
status: number;
|
||||||
crbt?: any;
|
fee: number;
|
||||||
audition?: any;
|
copyrightId: number;
|
||||||
copyFrom: string;
|
disc: string;
|
||||||
commentThreadId: string;
|
no: number;
|
||||||
rtUrl?: any;
|
artists: Artist[];
|
||||||
ftype: number;
|
album: Album;
|
||||||
rtUrls: any[];
|
starred: boolean;
|
||||||
copyright: number;
|
popularity: number;
|
||||||
transName?: any;
|
score: number;
|
||||||
sign?: any;
|
starredNum: number;
|
||||||
mark: number;
|
duration: number;
|
||||||
originCoverType: number;
|
playedNum: number;
|
||||||
originSongSimpleData?: any;
|
dayPlays: number;
|
||||||
single: number;
|
hearTime: number;
|
||||||
noCopyrightRcmd?: any;
|
ringtone: string;
|
||||||
rtype: number;
|
crbt?: any;
|
||||||
rurl?: any;
|
audition?: any;
|
||||||
mvid: number;
|
copyFrom: string;
|
||||||
bMusic: BMusic;
|
commentThreadId: string;
|
||||||
mp3Url?: any;
|
rtUrl?: any;
|
||||||
hMusic: BMusic;
|
ftype: number;
|
||||||
mMusic: BMusic;
|
rtUrls: any[];
|
||||||
lMusic: BMusic;
|
copyright: number;
|
||||||
exclusive: boolean;
|
transName?: any;
|
||||||
privilege: Privilege;
|
sign?: any;
|
||||||
}
|
mark: number;
|
||||||
|
originCoverType: number;
|
||||||
interface Privilege {
|
originSongSimpleData?: any;
|
||||||
id: number;
|
single: number;
|
||||||
fee: number;
|
noCopyrightRcmd?: any;
|
||||||
payed: number;
|
rtype: number;
|
||||||
st: number;
|
rurl?: any;
|
||||||
pl: number;
|
mvid: number;
|
||||||
dl: number;
|
bMusic: BMusic;
|
||||||
sp: number;
|
mp3Url?: any;
|
||||||
cp: number;
|
hMusic: BMusic;
|
||||||
subp: number;
|
mMusic: BMusic;
|
||||||
cs: boolean;
|
lMusic: BMusic;
|
||||||
maxbr: number;
|
exclusive: boolean;
|
||||||
fl: number;
|
privilege: Privilege;
|
||||||
toast: boolean;
|
count?: number;
|
||||||
flag: number;
|
playLoading?: boolean;
|
||||||
preSell: boolean;
|
picUrl?: string;
|
||||||
playMaxbr: number;
|
}
|
||||||
downloadMaxbr: number;
|
|
||||||
rscl?: any;
|
interface Privilege {
|
||||||
freeTrialPrivilege: FreeTrialPrivilege;
|
id: number;
|
||||||
chargeInfoList: ChargeInfoList[];
|
fee: number;
|
||||||
}
|
payed: number;
|
||||||
|
st: number;
|
||||||
interface ChargeInfoList {
|
pl: number;
|
||||||
rate: number;
|
dl: number;
|
||||||
chargeUrl?: any;
|
sp: number;
|
||||||
chargeMessage?: any;
|
cp: number;
|
||||||
chargeType: number;
|
subp: number;
|
||||||
}
|
cs: boolean;
|
||||||
|
maxbr: number;
|
||||||
interface FreeTrialPrivilege {
|
fl: number;
|
||||||
resConsumable: boolean;
|
toast: boolean;
|
||||||
userConsumable: boolean;
|
flag: number;
|
||||||
}
|
preSell: boolean;
|
||||||
|
playMaxbr: number;
|
||||||
interface BMusic {
|
downloadMaxbr: number;
|
||||||
name?: any;
|
rscl?: any;
|
||||||
id: number;
|
freeTrialPrivilege: FreeTrialPrivilege;
|
||||||
size: number;
|
chargeInfoList: ChargeInfoList[];
|
||||||
extension: string;
|
}
|
||||||
sr: number;
|
|
||||||
dfsId: number;
|
interface ChargeInfoList {
|
||||||
bitrate: number;
|
rate: number;
|
||||||
playTime: number;
|
chargeUrl?: any;
|
||||||
volumeDelta: number;
|
chargeMessage?: any;
|
||||||
}
|
chargeType: number;
|
||||||
|
}
|
||||||
interface Album {
|
|
||||||
name: string;
|
interface FreeTrialPrivilege {
|
||||||
id: number;
|
resConsumable: boolean;
|
||||||
type: string;
|
userConsumable: boolean;
|
||||||
size: number;
|
}
|
||||||
picId: number;
|
|
||||||
blurPicUrl: string;
|
interface BMusic {
|
||||||
companyId: number;
|
name?: any;
|
||||||
pic: number;
|
id: number;
|
||||||
picUrl: string;
|
size: number;
|
||||||
publishTime: number;
|
extension: string;
|
||||||
description: string;
|
sr: number;
|
||||||
tags: string;
|
dfsId: number;
|
||||||
company: string;
|
bitrate: number;
|
||||||
briefDesc: string;
|
playTime: number;
|
||||||
artist: Artist;
|
volumeDelta: number;
|
||||||
songs: any[];
|
}
|
||||||
alias: string[];
|
|
||||||
status: number;
|
interface Album {
|
||||||
copyrightId: number;
|
name: string;
|
||||||
commentThreadId: string;
|
id: number;
|
||||||
artists: Artist[];
|
type: string;
|
||||||
subType: string;
|
size: number;
|
||||||
transName?: any;
|
picId: number;
|
||||||
onSale: boolean;
|
blurPicUrl: string;
|
||||||
mark: number;
|
companyId: number;
|
||||||
picId_str: string;
|
pic: number;
|
||||||
}
|
picUrl: string;
|
||||||
|
publishTime: number;
|
||||||
interface Artist {
|
description: string;
|
||||||
name: string;
|
tags: string;
|
||||||
id: number;
|
company: string;
|
||||||
picId: number;
|
briefDesc: string;
|
||||||
img1v1Id: number;
|
artist: Artist;
|
||||||
briefDesc: string;
|
songs: any[];
|
||||||
picUrl: string;
|
alias: string[];
|
||||||
img1v1Url: string;
|
status: number;
|
||||||
albumSize: number;
|
copyrightId: number;
|
||||||
alias: any[];
|
commentThreadId: string;
|
||||||
trans: string;
|
artists: Artist[];
|
||||||
musicSize: number;
|
subType: string;
|
||||||
topicPerson: number;
|
transName?: any;
|
||||||
}
|
onSale: boolean;
|
||||||
|
mark: number;
|
||||||
export interface IPlayMusicUrl {
|
picId_str: string;
|
||||||
data: Datum[];
|
}
|
||||||
code: number;
|
|
||||||
}
|
interface Artist {
|
||||||
|
name: string;
|
||||||
interface Datum {
|
id: number;
|
||||||
id: number;
|
picId: number;
|
||||||
url: string;
|
img1v1Id: number;
|
||||||
br: number;
|
briefDesc: string;
|
||||||
size: number;
|
picUrl: string;
|
||||||
md5: string;
|
img1v1Url: string;
|
||||||
code: number;
|
albumSize: number;
|
||||||
expi: number;
|
alias: any[];
|
||||||
type: string;
|
trans: string;
|
||||||
gain: number;
|
musicSize: number;
|
||||||
fee: number;
|
topicPerson: number;
|
||||||
uf?: any;
|
}
|
||||||
payed: number;
|
|
||||||
flag: number;
|
export interface IPlayMusicUrl {
|
||||||
canExtend: boolean;
|
data: Datum[];
|
||||||
freeTrialInfo?: any;
|
code: number;
|
||||||
level: string;
|
}
|
||||||
encodeType: string;
|
|
||||||
freeTrialPrivilege: FreeTrialPrivilege;
|
interface Datum {
|
||||||
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
|
id: number;
|
||||||
urlSource: number;
|
url: string;
|
||||||
}
|
br: number;
|
||||||
|
size: number;
|
||||||
interface FreeTimeTrialPrivilege {
|
md5: string;
|
||||||
resConsumable: boolean;
|
code: number;
|
||||||
userConsumable: boolean;
|
expi: number;
|
||||||
type: number;
|
type: string;
|
||||||
remainTime: number;
|
gain: number;
|
||||||
}
|
fee: number;
|
||||||
|
uf?: any;
|
||||||
interface FreeTrialPrivilege {
|
payed: number;
|
||||||
resConsumable: boolean;
|
flag: number;
|
||||||
userConsumable: boolean;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
160
src/type/mv.ts
@@ -1,84 +1,84 @@
|
|||||||
export interface IMvItem {
|
export interface IMvItem {
|
||||||
id: number
|
id: number;
|
||||||
cover: string
|
cover: string;
|
||||||
name: string
|
name: string;
|
||||||
playCount: number
|
playCount: number;
|
||||||
briefDesc?: any
|
briefDesc?: any;
|
||||||
desc?: any
|
desc?: any;
|
||||||
artistName: string
|
artistName: string;
|
||||||
artistId: number
|
artistId: number;
|
||||||
duration: number
|
duration: number;
|
||||||
mark: number
|
mark: number;
|
||||||
mv: IMvData
|
mv: IMvData;
|
||||||
lastRank: number
|
lastRank: number;
|
||||||
score: number
|
score: number;
|
||||||
subed: boolean
|
subed: boolean;
|
||||||
artists: Artist[]
|
artists: Artist[];
|
||||||
transNames?: string[]
|
transNames?: string[];
|
||||||
alias?: string[]
|
alias?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMvData {
|
export interface IMvData {
|
||||||
authId: number
|
authId: number;
|
||||||
status: number
|
status: number;
|
||||||
id: number
|
id: number;
|
||||||
title: string
|
title: string;
|
||||||
subTitle: string
|
subTitle: string;
|
||||||
appTitle: string
|
appTitle: string;
|
||||||
aliaName: string
|
aliaName: string;
|
||||||
transName: string
|
transName: string;
|
||||||
pic4v3: number
|
pic4v3: number;
|
||||||
pic16v9: number
|
pic16v9: number;
|
||||||
caption: number
|
caption: number;
|
||||||
captionLanguage: string
|
captionLanguage: string;
|
||||||
style?: any
|
style?: any;
|
||||||
mottos: string
|
mottos: string;
|
||||||
oneword?: any
|
oneword?: any;
|
||||||
appword: string
|
appword: string;
|
||||||
stars?: any
|
stars?: any;
|
||||||
desc: string
|
desc: string;
|
||||||
area: string
|
area: string;
|
||||||
type: string
|
type: string;
|
||||||
subType: string
|
subType: string;
|
||||||
neteaseonly: number
|
neteaseonly: number;
|
||||||
upban: number
|
upban: number;
|
||||||
topWeeks: string
|
topWeeks: string;
|
||||||
publishTime: string
|
publishTime: string;
|
||||||
online: number
|
online: number;
|
||||||
score: number
|
score: number;
|
||||||
plays: number
|
plays: number;
|
||||||
monthplays: number
|
monthplays: number;
|
||||||
weekplays: number
|
weekplays: number;
|
||||||
dayplays: number
|
dayplays: number;
|
||||||
fee: number
|
fee: number;
|
||||||
artists: Artist[]
|
artists: Artist[];
|
||||||
videos: Video[]
|
videos: Video[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Video {
|
interface Video {
|
||||||
tagSign: TagSign
|
tagSign: TagSign;
|
||||||
tag: string
|
tag: string;
|
||||||
url: string
|
url: string;
|
||||||
duration: number
|
duration: number;
|
||||||
size: number
|
size: number;
|
||||||
width: number
|
width: number;
|
||||||
height: number
|
height: number;
|
||||||
container: string
|
container: string;
|
||||||
md5: string
|
md5: string;
|
||||||
check: boolean
|
check: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TagSign {
|
interface TagSign {
|
||||||
br: number
|
br: number;
|
||||||
type: string
|
type: string;
|
||||||
tagSign: string
|
tagSign: string;
|
||||||
resolution: number
|
resolution: number;
|
||||||
mvtype: string
|
mvtype: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Artist {
|
interface Artist {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -97,16 +97,16 @@ interface Artist {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export interface IMvUrlData {
|
export interface IMvUrlData {
|
||||||
id: number
|
id: number;
|
||||||
url: string
|
url: string;
|
||||||
r: number
|
r: number;
|
||||||
size: number
|
size: number;
|
||||||
md5: string
|
md5: string;
|
||||||
code: number
|
code: number;
|
||||||
expi: number
|
expi: number;
|
||||||
fee: number
|
fee: number;
|
||||||
mvFee: number
|
mvFee: number;
|
||||||
st: number
|
st: number;
|
||||||
promotionVo: null | any
|
promotionVo: null | any;
|
||||||
msg: string
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export interface IPlayListSort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SortCategories {
|
interface SortCategories {
|
||||||
"0": string;
|
'0': string;
|
||||||
"1": string;
|
'1': string;
|
||||||
"2": string;
|
'2': string;
|
||||||
"3": string;
|
'3': string;
|
||||||
"4": string;
|
'4': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortAll {
|
interface SortAll {
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ interface Song2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface KsongInfos {
|
interface KsongInfos {
|
||||||
"347230": _347230;
|
'347230': _347230;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface _347230 {
|
interface _347230 {
|
||||||
|
|||||||
@@ -1,64 +1,86 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import store from '@/store';
|
||||||
|
|
||||||
// 设置歌手背景图片
|
// 设置歌手背景图片
|
||||||
export const setBackgroundImg = (url: String) => {
|
export const setBackgroundImg = (url: String) => {
|
||||||
return 'background-image:' + 'url(' + url + ')'
|
return `background-image:url(${url})`;
|
||||||
}
|
};
|
||||||
// 设置动画类型
|
// 设置动画类型
|
||||||
export const setAnimationClass = (type: String) => {
|
export const setAnimationClass = (type: String) => {
|
||||||
return 'animate__animated ' + type
|
return `animate__animated ${type}`;
|
||||||
}
|
};
|
||||||
// 设置动画延时
|
// 设置动画延时
|
||||||
export const setAnimationDelay = (index: number = 6, time: number = 50) => {
|
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) => {
|
export const secondToMinute = (s: number) => {
|
||||||
if (!s) {
|
if (!s) {
|
||||||
return '00:00'
|
return '00:00';
|
||||||
}
|
}
|
||||||
let minute: number = Math.floor(s / 60)
|
const minute: number = Math.floor(s / 60);
|
||||||
let second: number = Math.floor(s % 60)
|
const second: number = Math.floor(s % 60);
|
||||||
let minuteStr: string =
|
const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`;
|
||||||
minute > 9 ? minute.toString() : '0' + minute.toString()
|
const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`;
|
||||||
let secondStr: string =
|
return `${minuteStr}:${secondStr}`;
|
||||||
second > 9 ? second.toString() : '0' + second.toString()
|
};
|
||||||
return minuteStr + ':' + secondStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化数字 千,万, 百万, 千万,亿
|
// 格式化数字 千,万, 百万, 千万,亿
|
||||||
export const formatNumber = (num: any) => {
|
const units = [
|
||||||
num = num * 1
|
{ value: 1e8, symbol: '亿' },
|
||||||
if (num < 10000) {
|
{ value: 1e4, symbol: '万' },
|
||||||
return num
|
];
|
||||||
|
|
||||||
|
export const formatNumber = (num: string | number) => {
|
||||||
|
num = Number(num);
|
||||||
|
for (let i = 0; i < units.length; i++) {
|
||||||
|
if (num >= units[i].value) {
|
||||||
|
return `${(num / units[i].value).toFixed(1)}${units[i].symbol}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (num < 100000000) {
|
return num.toString();
|
||||||
return (num / 10000).toFixed(1) + '万'
|
};
|
||||||
}
|
|
||||||
return (num / 100000000).toFixed(1) + '亿'
|
const windowData = window as any;
|
||||||
}
|
|
||||||
const windowData = window as any
|
|
||||||
export const getIsMc = () => {
|
export const getIsMc = () => {
|
||||||
if (!windowData.electron) {
|
if (!windowData.electron) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
|
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return false
|
if(window.location.origin.includes('localhost')){}
|
||||||
}
|
return false;
|
||||||
const ProxyUrl =
|
};
|
||||||
import.meta.env.VITE_API_PROXY + '' || 'http://110.42.251.190:9856'
|
const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856';
|
||||||
|
|
||||||
export const getMusicProxyUrl = (url: string) => {
|
export const getMusicProxyUrl = (url: string) => {
|
||||||
if (!getIsMc()) {
|
if (!getIsMc()) {
|
||||||
return url
|
return url;
|
||||||
}
|
}
|
||||||
const PUrl = url.split('').join('+')
|
const PUrl = url.split('').join('+');
|
||||||
return `${ProxyUrl}/mc?url=${PUrl}`
|
return `${ProxyUrl}/mc?url=${PUrl}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getImgUrl = computed(() => (url: string, size: string = '') => {
|
export const getImgUrl = (url: string | undefined, size: string = '') => {
|
||||||
const bdUrl = 'https://image.baidu.com/search/down?url='
|
const bdUrl = 'https://image.baidu.com/search/down?url=';
|
||||||
const imgUrl = encodeURIComponent(`${url}?param=${size}`)
|
const imgUrl = `${url}?param=${size}`;
|
||||||
return `${bdUrl}${imgUrl}`
|
if (!getIsMc()) {
|
||||||
})
|
return imgUrl;
|
||||||
|
}
|
||||||
|
return `${bdUrl}${encodeURIComponent(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;
|
||||||
|
});
|
||||||
|
|||||||
183
src/utils/linearColor.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
interface IColor {
|
||||||
|
backgroundColor: string;
|
||||||
|
primaryColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
|
||||||
|
try {
|
||||||
|
const primaryColor = await getImagePrimaryColor(imageSrc);
|
||||||
|
return {
|
||||||
|
backgroundColor: generateGradientBackground(primaryColor),
|
||||||
|
primaryColor,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
return {
|
||||||
|
backgroundColor: '',
|
||||||
|
primaryColor: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {
|
||||||
|
try {
|
||||||
|
const primaryColor = await getImageColor(img);
|
||||||
|
return {
|
||||||
|
backgroundColor: generateGradientBackground(primaryColor),
|
||||||
|
primaryColor,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error);
|
||||||
|
return {
|
||||||
|
backgroundColor: '',
|
||||||
|
primaryColor: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageColor = (img: HTMLImageElement): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('无法获取canvas上下文'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const color = getAverageColor(imageData.data);
|
||||||
|
resolve(`rgb(${color.join(',')})`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImagePrimaryColor = (imageSrc: string): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
img.src = imageSrc;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('无法获取canvas上下文'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const color = getAverageColor(imageData.data);
|
||||||
|
resolve(`rgb(${color.join(',')})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => reject(new Error('图片加载失败'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAverageColor = (data: Uint8ClampedArray): number[] => {
|
||||||
|
let r = 0;
|
||||||
|
let g = 0;
|
||||||
|
let b = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
r += data[i];
|
||||||
|
g += data[i + 1];
|
||||||
|
b += data[i + 2];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateGradientBackground = (color: string): string => {
|
||||||
|
const [r, g, b] = color.match(/\d+/g)?.map(Number) || [0, 0, 0];
|
||||||
|
const [h, s, l] = rgbToHsl(r, g, b);
|
||||||
|
|
||||||
|
// 增加亮度和暗度的差异
|
||||||
|
const lightL = Math.min(l + 0.2, 0.95);
|
||||||
|
const darkL = Math.max(l - 0.3, 0.05);
|
||||||
|
const midL = (lightL + darkL) / 2;
|
||||||
|
|
||||||
|
// 调整饱和度以增强效果
|
||||||
|
const lightS = Math.min(s * 0.8, 1);
|
||||||
|
const darkS = Math.min(s * 1.2, 1);
|
||||||
|
|
||||||
|
const [lightR, lightG, lightB] = hslToRgb(h, lightS, lightL);
|
||||||
|
const [midR, midG, midB] = hslToRgb(h, s, midL);
|
||||||
|
const [darkR, darkG, darkB] = hslToRgb(h, darkS, darkL);
|
||||||
|
|
||||||
|
const lightColor = `rgb(${lightR}, ${lightG}, ${lightB})`;
|
||||||
|
const midColor = `rgb(${midR}, ${midG}, ${midB})`;
|
||||||
|
const darkColor = `rgb(${darkR}, ${darkG}, ${darkB})`;
|
||||||
|
|
||||||
|
// 使用三个颜色点创建更丰富的渐变
|
||||||
|
return `linear-gradient(to bottom, ${lightColor} 0%, ${midColor} 50%, ${darkColor} 100%)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions (unchanged)
|
||||||
|
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
let h = 0;
|
||||||
|
let s;
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = s = 0;
|
||||||
|
} else {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [h, s, l];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||||
|
let r;
|
||||||
|
let g;
|
||||||
|
let b;
|
||||||
|
|
||||||
|
if (s === 0) {
|
||||||
|
r = g = b = l;
|
||||||
|
} else {
|
||||||
|
const hue2rgb = (p: number, q: number, t: number) => {
|
||||||
|
if (t < 0) t += 1;
|
||||||
|
if (t > 1) t -= 1;
|
||||||
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1 / 2) return q;
|
||||||
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1 / 3);
|
||||||
|
g = hue2rgb(p, q, h);
|
||||||
|
b = hue2rgb(p, q, h - 1 / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
let baseURL = import.meta.env.VITE_API + "";
|
|
||||||
|
const baseURL = `${import.meta.env.VITE_API}`;
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: baseURL,
|
baseURL,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ request.interceptors.request.use(
|
|||||||
(config) => {
|
(config) => {
|
||||||
// 在请求发送之前做一些处理
|
// 在请求发送之前做一些处理
|
||||||
// 在get请求params中添加timestamp
|
// 在get请求params中添加timestamp
|
||||||
if (config.method === "get") {
|
if (config.method === 'get') {
|
||||||
config.params = {
|
config.params = {
|
||||||
...config.params,
|
...config.params,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
let token = localStorage.getItem("token");
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.params.cookie = token;
|
config.params.cookie = token;
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ request.interceptors.request.use(
|
|||||||
(error) => {
|
(error) => {
|
||||||
// 当请求异常时做一些处理
|
// 当请求异常时做一些处理
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default request;
|
export default request;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import axios from "axios"
|
import axios from 'axios';
|
||||||
let baseURL = import.meta.env.VITE_API_MUSIC + ""
|
|
||||||
|
const baseURL = `${import.meta.env.VITE_API_MUSIC}`;
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: baseURL,
|
baseURL,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
})
|
});
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
return config
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 当请求异常时做一些处理
|
// 当请求异常时做一些处理
|
||||||
return Promise.reject(error)
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
export default request
|
export default request;
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
<div class="title">播放历史</div>
|
<div class="title">播放历史</div>
|
||||||
<n-scrollbar :size="100">
|
<n-scrollbar :size="100">
|
||||||
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||||
<div class="history-item" v-for="(item, index) in musicList" :key="item.id"
|
<div
|
||||||
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
|
v-for="(item, index) in musicList"
|
||||||
<song-item class="history-item-content" :item="item" />
|
:key="item.id"
|
||||||
|
class="history-item"
|
||||||
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
|
:style="setAnimationDelay(index, 30)"
|
||||||
|
>
|
||||||
|
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||||
<div class="history-item-count">
|
<div class="history-item-count">
|
||||||
{{ item.count }}
|
{{ item.count }}
|
||||||
</div>
|
</div>
|
||||||
@@ -19,33 +24,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook'
|
import { useStore } from 'vuex';
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
|
||||||
|
|
||||||
const {delMusic, musicList} = useMusicHistory();
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
|
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'History',
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { delMusic, musicList } = useMusicHistory();
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
store.commit('setPlayList', musicList.value);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.history-page{
|
.history-page {
|
||||||
@apply h-full w-full pt-2;
|
@apply h-full w-full pt-2;
|
||||||
.title{
|
.title {
|
||||||
@apply text-xl font-bold;
|
@apply pl-4 text-xl font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list-content{
|
.history-list-content {
|
||||||
@apply px-4 mt-2;
|
@apply px-4 mt-2 pb-28;
|
||||||
.history-item{
|
.history-item {
|
||||||
@apply flex items-center justify-between;
|
@apply flex items-center justify-between;
|
||||||
&-content{
|
&-content {
|
||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
}
|
}
|
||||||
&-count{
|
&-count {
|
||||||
@apply px-4 text-lg;
|
@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;
|
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,30 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-page">
|
<n-scrollbar :size="100" :x-scrollable="false">
|
||||||
|
<div class="main-page">
|
||||||
<!-- 推荐歌手 -->
|
<!-- 推荐歌手 -->
|
||||||
<recommend-singer />
|
<recommend-singer />
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- 歌单分类列表 -->
|
<!-- 歌单分类列表 -->
|
||||||
<playlist-type />
|
<playlist-type v-if="!isMobile" />
|
||||||
<!-- 本周最热音乐 -->
|
<!-- 本周最热音乐 -->
|
||||||
<recommend-songlist />
|
<recommend-songlist />
|
||||||
<!-- 推荐最新专辑 -->
|
<!-- 推荐最新专辑 -->
|
||||||
<recommend-album />
|
<recommend-album />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const RecommendSinger = defineAsyncComponent(() => import("@/components/RecommendSinger.vue"));
|
import { isMobile } from '@/utils';
|
||||||
const PlaylistType = defineAsyncComponent(() => import("@/components/PlaylistType.vue"));
|
|
||||||
const RecommendSonglist = defineAsyncComponent(() => import("@/components/RecommendSonglist.vue"));
|
const RecommendSinger = defineAsyncComponent(() => import('@/components/RecommendSinger.vue'));
|
||||||
const RecommendAlbum = defineAsyncComponent(() => import("@/components/RecommendAlbum.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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.main-page{
|
.main-page {
|
||||||
@apply h-full w-full;
|
@apply h-full w-full overflow-hidden;
|
||||||
}
|
}
|
||||||
.main-content {
|
.main-content {
|
||||||
@apply mt-6 flex pb-28;
|
@apply mt-6 flex mb-28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .main-content {
|
||||||
|
@apply flex-col mx-4;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,75 +1,79 @@
|
|||||||
<script lang="ts" setup>
|
<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 { useRoute } from 'vue-router';
|
||||||
import MusicList from "@/components/MusicList.vue";
|
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
|
||||||
|
|
||||||
const recommendList = ref()
|
import { getListByCat, getListDetail, getRecommendList } from '@/api/list';
|
||||||
const showMusic = ref(false)
|
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>()
|
defineOptions({
|
||||||
const listDetail = ref<IListDetail>()
|
name: 'List',
|
||||||
|
});
|
||||||
|
|
||||||
|
const recommendList = ref();
|
||||||
|
const showMusic = ref(false);
|
||||||
|
|
||||||
|
const recommendItem = ref<IRecommendItem | null>();
|
||||||
|
const listDetail = ref<IListDetail | null>();
|
||||||
|
const listLoading = ref(true);
|
||||||
const selectRecommendItem = async (item: IRecommendItem) => {
|
const selectRecommendItem = async (item: IRecommendItem) => {
|
||||||
showMusic.value = true
|
listLoading.value = true;
|
||||||
const { data } = await getListDetail(item.id)
|
recommendItem.value = null;
|
||||||
recommendItem.value = item
|
listDetail.value = null;
|
||||||
listDetail.value = data
|
showMusic.value = true;
|
||||||
}
|
recommendItem.value = item;
|
||||||
|
const { data } = await getListDetail(item.id);
|
||||||
|
listDetail.value = data;
|
||||||
|
listLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const listTitle = ref(route.query.type || "歌单列表");
|
const listTitle = ref(route.query.type || '歌单列表');
|
||||||
|
|
||||||
const loadList = async (type: any) => {
|
const loading = ref(false);
|
||||||
|
const loadList = async (type: string) => {
|
||||||
|
loading.value = true;
|
||||||
const params = {
|
const params = {
|
||||||
cat: type || '',
|
cat: type || '',
|
||||||
limit: 30,
|
limit: 30,
|
||||||
offset: 0
|
offset: 0,
|
||||||
}
|
};
|
||||||
const { data } = await getListByCat(params);
|
const { data } = await getListByCat(params);
|
||||||
recommendList.value = data.playlists
|
recommendList.value = data.playlists;
|
||||||
}
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
if (route.query.type) {
|
if (route.query.type) {
|
||||||
loadList(route.query.type)
|
loadList(route.query.type as string);
|
||||||
} else {
|
} else {
|
||||||
getRecommendList().then((res: { data: { result: any; }; }) => {
|
getRecommendList().then((res: { data: { result: any } }) => {
|
||||||
recommendList.value = res.data.result
|
recommendList.value = res.data.result;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
async newParams => {
|
async (newParams) => {
|
||||||
if(newParams.type){
|
if (newParams.type) {
|
||||||
const params = {
|
recommendList.value = null;
|
||||||
tag: newParams.type || '',
|
listTitle.value = newParams.type || '歌单列表';
|
||||||
limit: 30,
|
loadList(newParams.type as string);
|
||||||
before: 0
|
|
||||||
}
|
|
||||||
loadList(newParams.type);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="list-page">
|
<div class="list-page">
|
||||||
<div
|
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
|
||||||
class="recommend-title"
|
|
||||||
:class="setAnimationClass('animate__bounceInLeft')"
|
|
||||||
>{{ listTitle }}</div>
|
|
||||||
<!-- 歌单列表 -->
|
<!-- 歌单列表 -->
|
||||||
<n-scrollbar class="recommend" @click="showMusic = false" :size="100">
|
<n-scrollbar class="recommend" :size="100" @click="showMusic = false">
|
||||||
<div class="recommend-list" v-if="recommendList">
|
<div v-loading="loading" class="recommend-list">
|
||||||
<div
|
<div
|
||||||
|
v-for="(item, index) in recommendList"
|
||||||
|
:key="item.id"
|
||||||
class="recommend-item"
|
class="recommend-item"
|
||||||
v-for="(item,index) in recommendList"
|
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
:style="setAnimationDelay(index, 30)"
|
:style="setAnimationDelay(index, 30)"
|
||||||
@click.stop="selectRecommendItem(item)"
|
@click.stop="selectRecommendItem(item)"
|
||||||
@@ -77,7 +81,7 @@ watch(
|
|||||||
<div class="recommend-item-img">
|
<div class="recommend-item-img">
|
||||||
<n-image
|
<n-image
|
||||||
class="recommend-item-img-img"
|
class="recommend-item-img-img"
|
||||||
:src="getImgUrl( (item.picUrl || item.coverImgUrl), '200y200')"
|
:src="getImgUrl(item.picUrl || item.coverImgUrl, '200y200')"
|
||||||
width="200"
|
width="200"
|
||||||
height="200"
|
height="200"
|
||||||
lazy
|
lazy
|
||||||
@@ -91,15 +95,20 @@ watch(
|
|||||||
<div class="recommend-item-title">{{ item.name }}</div>
|
<div class="recommend-item-title">{{ item.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PlayBottom/>
|
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
<MusicList v-if="listDetail?.playlist" v-model:show="showMusic" :name="listDetail?.playlist.name" :song-list="listDetail?.playlist.tracks" />
|
<music-list
|
||||||
|
v-model:show="showMusic"
|
||||||
|
v-model:loading="listLoading"
|
||||||
|
:name="recommendItem?.name || ''"
|
||||||
|
:song-list="listDetail?.playlist.tracks || []"
|
||||||
|
:list-info="listDetail?.playlist"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.list-page {
|
.list-page {
|
||||||
@apply relative h-full w-full;
|
@apply relative h-full w-full px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommend {
|
.recommend {
|
||||||
@@ -151,4 +160,9 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
.mobile {
|
||||||
|
.recommend-list {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(25%, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,170 +1,194 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getQrKey, createQr, checkQr, getLoginStatus } from '@/api/login'
|
import { useMessage } from 'naive-ui';
|
||||||
import { onMounted } from '@vue/runtime-core';
|
import { onMounted } from 'vue';
|
||||||
import { getUserDetail, loginByCellphone } from '@/api/login';
|
import { useRouter } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
|
||||||
import { useRouter } from 'vue-router';
|
import { isMobile, setAnimationClass } from '@/utils';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
const message = useMessage()
|
name: 'Login',
|
||||||
const store = useStore();
|
});
|
||||||
const router = useRouter()
|
|
||||||
|
const message = useMessage();
|
||||||
const qrUrl = ref<string>()
|
const store = useStore();
|
||||||
onMounted(() => {
|
const router = useRouter();
|
||||||
loadLogin()
|
|
||||||
})
|
const qrUrl = ref<string>();
|
||||||
|
onMounted(() => {
|
||||||
const loadLogin = async () => {
|
loadLogin();
|
||||||
const qrKey = await getQrKey()
|
});
|
||||||
const key = qrKey.data.data.unikey
|
|
||||||
const { data } = await createQr(key)
|
const timerRef = ref(null);
|
||||||
qrUrl.value = data.data.qrimg
|
|
||||||
timerIsQr(key)
|
const loadLogin = async () => {
|
||||||
}
|
try {
|
||||||
|
const qrKey = await getQrKey();
|
||||||
|
const key = qrKey.data.data.unikey;
|
||||||
|
const { data } = await createQr(key);
|
||||||
const timerIsQr = (key: string) => {
|
qrUrl.value = data.data.qrimg;
|
||||||
const timer = setInterval(async () => {
|
|
||||||
const { data } = await checkQr(key)
|
const timer = timerIsQr(key);
|
||||||
|
// 添加对定时器的引用,以便在出现错误时可以清除
|
||||||
if (data.code === 800) {
|
timerRef.value = timer as any;
|
||||||
clearInterval(timer)
|
} catch (error) {
|
||||||
}
|
console.error('加载登录信息时出错:', error);
|
||||||
if (data.code === 803) {
|
}
|
||||||
// 将token存入localStorage
|
};
|
||||||
localStorage.setItem('token', data.cookie)
|
|
||||||
const user = await getUserDetail()
|
// 使用 ref 来保存定时器,便于在任何地方清除它
|
||||||
store.state.user = user.data.profile
|
|
||||||
message.success('登录成功')
|
const timerIsQr = (key: string) => {
|
||||||
|
const timer = setInterval(async () => {
|
||||||
await getLoginStatus().then(res => {
|
try {
|
||||||
console.log(res);
|
const { data } = await checkQr(key);
|
||||||
})
|
|
||||||
clearInterval(timer)
|
if (data.code === 800) {
|
||||||
setTimeout(() => {
|
clearInterval(timer);
|
||||||
router.push('/user')
|
timerRef.value = null;
|
||||||
}, 1000);
|
}
|
||||||
}
|
if (data.code === 803) {
|
||||||
}, 5000);
|
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('登录成功');
|
||||||
const isQr = ref(true)
|
|
||||||
const chooseQr = () => {
|
clearInterval(timer);
|
||||||
isQr.value = !isQr.value
|
timerRef.value = null;
|
||||||
}
|
router.push('/user');
|
||||||
|
}
|
||||||
// 手机号登录
|
} catch (error) {
|
||||||
const phone = ref('')
|
console.error('检查二维码状态时出错:', error);
|
||||||
const password = ref('')
|
// 在出现错误时清除定时器
|
||||||
const loginPhone = async () => {
|
clearInterval(timer);
|
||||||
const { data } = await loginByCellphone(phone.value, password.value)
|
timerRef.value = null;
|
||||||
if (data.code === 200) {
|
}
|
||||||
message.success('登录成功')
|
}, 2000);
|
||||||
store.state.user = data.profile
|
|
||||||
localStorage.setItem('token', data.cookie)
|
return timer;
|
||||||
setTimeout(() => {
|
};
|
||||||
router.push('/user')
|
|
||||||
}, 1000);
|
// 离开页面时
|
||||||
}
|
onBeforeUnmount(() => {
|
||||||
}
|
if (timerRef.value) {
|
||||||
|
clearInterval(timerRef.value);
|
||||||
</script>
|
timerRef.value = null;
|
||||||
|
}
|
||||||
<template>
|
});
|
||||||
<div class="login-page">
|
|
||||||
<div class="phone-login">
|
// 是否扫码登陆
|
||||||
<div class="bg"></div>
|
const isQr = ref(!isMobile.value);
|
||||||
<div class="content">
|
const chooseQr = () => {
|
||||||
<div class="phone" v-if="isQr" :class="setAnimationClass('animate__fadeInUp')">
|
isQr.value = !isQr.value;
|
||||||
<div class="login-title">扫码登陆</div>
|
};
|
||||||
<img class="qr-img" :src="qrUrl" />
|
|
||||||
<div class="text">使用网易云APP扫码登录</div>
|
// 手机号登录
|
||||||
</div>
|
const phone = ref('');
|
||||||
<div class="phone" v-else :class="setAnimationClass('animate__fadeInUp')">
|
const password = ref('');
|
||||||
<div class="login-title">手机号登录</div>
|
const loginPhone = async () => {
|
||||||
<div class="phone-page">
|
const { data } = await loginByCellphone(phone.value, password.value);
|
||||||
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
|
if (data.code === 200) {
|
||||||
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
|
message.success('登录成功');
|
||||||
</div>
|
store.state.user = data.profile;
|
||||||
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
|
localStorage.setItem('token', data.cookie);
|
||||||
</div>
|
setTimeout(() => {
|
||||||
</div>
|
router.push('/user');
|
||||||
<div class="bottom">
|
}, 1000);
|
||||||
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
|
}
|
||||||
</div>
|
};
|
||||||
</div>
|
</script>
|
||||||
</div>
|
|
||||||
</template>
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
<style lang="scss" scoped>
|
<div class="phone-login">
|
||||||
.login-page {
|
<div class="bg"></div>
|
||||||
@apply flex flex-col items-center justify-center p-20 pt-20;
|
<div class="content">
|
||||||
}
|
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||||
|
<div class="login-title">扫码登陆</div>
|
||||||
.login-title {
|
<img class="qr-img" :src="qrUrl" />
|
||||||
@apply text-2xl font-bold mb-6;
|
<div class="text">使用网易云APP扫码登录</div>
|
||||||
}
|
</div>
|
||||||
|
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||||
.text {
|
<div class="login-title">手机号登录</div>
|
||||||
@apply mt-4 text-green-500 text-xs;
|
<div class="phone-page">
|
||||||
}
|
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
|
||||||
|
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
|
||||||
.phone-login {
|
</div>
|
||||||
width: 350px;
|
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
|
||||||
height: 550px;
|
</div>
|
||||||
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
|
</div>
|
||||||
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
|
<div class="bottom">
|
||||||
background-color: #383838;
|
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
|
||||||
box-shadow: inset 0px 0px 20px 5px #0000005e;
|
</div>
|
||||||
|
</div>
|
||||||
.bg {
|
</div>
|
||||||
@apply absolute w-full h-full bg-black opacity-30;
|
</template>
|
||||||
}
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
.bottom {
|
.login-page {
|
||||||
width: 200%;
|
@apply flex flex-col items-center justify-center p-20 pt-20;
|
||||||
height: 250px;
|
}
|
||||||
bottom: -180px;
|
|
||||||
border-radius: 50%;
|
.login-title {
|
||||||
left: 50%;
|
@apply text-2xl font-bold mb-6;
|
||||||
padding: 10px;
|
}
|
||||||
transform: translateX(-50%);
|
|
||||||
color: #ffffff99;
|
.text {
|
||||||
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
|
@apply mt-4 text-green-500 text-xs;
|
||||||
box-shadow: 10px 0px 20px #000000a9;
|
}
|
||||||
}
|
|
||||||
|
.phone-login {
|
||||||
.content {
|
width: 350px;
|
||||||
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
|
height: 550px;
|
||||||
.qr-img {
|
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
|
||||||
@apply opacity-80 rounded-2xl cursor-pointer;
|
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
|
||||||
}
|
background-color: #383838;
|
||||||
|
box-shadow: inset 0px 0px 20px 5px #0000005e;
|
||||||
.phone {
|
|
||||||
animation-duration: 0.5s;
|
.bg {
|
||||||
&-page {
|
@apply absolute w-full h-full bg-black opacity-30;
|
||||||
background-color: #ffffffdd;
|
}
|
||||||
width: 250px;
|
|
||||||
@apply rounded-2xl overflow-hidden;
|
.bottom {
|
||||||
}
|
width: 200%;
|
||||||
|
height: 250px;
|
||||||
&-input {
|
bottom: -180px;
|
||||||
height: 40px;
|
border-radius: 50%;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
left: 50%;
|
||||||
@apply w-full text-black px-4 outline-none;
|
padding: 10px;
|
||||||
}
|
transform: translateX(-50%);
|
||||||
}
|
color: #ffffff99;
|
||||||
.btn-login {
|
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
|
||||||
width: 250px;
|
box-shadow: 10px 0px 20px #000000a9;
|
||||||
height: 40px;
|
}
|
||||||
@apply mt-10 text-white rounded-xl bg-black opacity-60;
|
|
||||||
}
|
.content {
|
||||||
}
|
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
|
||||||
}
|
.qr-img {
|
||||||
</style>
|
@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
@@ -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>
|
||||||
@@ -4,11 +4,23 @@
|
|||||||
<h2>推荐MV</h2>
|
<h2>推荐MV</h2>
|
||||||
</div>
|
</div>
|
||||||
<n-scrollbar :size="100">
|
<n-scrollbar :size="100">
|
||||||
<div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
<div v-loading="loading" class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||||
<div class="mv-item" v-for="(item, index) in mvList" :key="item.id"
|
<div
|
||||||
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
|
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)">
|
<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="top">
|
||||||
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
|
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
|
||||||
<i class="iconfont icon-videofill"></i>
|
<i class="iconfont icon-videofill"></i>
|
||||||
@@ -20,7 +32,7 @@
|
|||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
|
|
||||||
<n-drawer :show="showMv" height="100vh" placement="bottom" :z-index="999999999">
|
<n-drawer :show="showMv" height="100vh" placement="bottom" :z-index="999999999">
|
||||||
<div class="mv-detail">
|
<div v-loading="mvLoading" class="mv-detail">
|
||||||
<video :src="playMvUrl" controls autoplay></video>
|
<video :src="playMvUrl" controls autoplay></video>
|
||||||
<div class="mv-detail-title">
|
<div class="mv-detail-title">
|
||||||
<div class="title">{{ playMvItem?.name }}</div>
|
<div class="title">{{ playMvItem?.name }}</div>
|
||||||
@@ -35,51 +47,60 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
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';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
const showMv = ref(false)
|
import { getMvUrl, getTopMv } from '@/api/mv';
|
||||||
const mvList = ref<Array<IMvItem>>([])
|
import { IMvItem } from '@/type/mv';
|
||||||
const playMvItem = ref<IMvItem>()
|
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
const playMvUrl = ref<string>()
|
|
||||||
const store = useStore()
|
defineOptions({
|
||||||
|
name: 'Mv',
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMv = ref(false);
|
||||||
|
const mvList = ref<Array<IMvItem>>([]);
|
||||||
|
const playMvItem = ref<IMvItem>();
|
||||||
|
const playMvUrl = ref<string>();
|
||||||
|
const store = useStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const res = await getTopMv(30)
|
loading.value = true;
|
||||||
mvList.value = res.data.data
|
const res = await getTopMv(30);
|
||||||
console.log('mvList.value', mvList.value)
|
mvList.value = res.data.data;
|
||||||
})
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mvLoading = ref(false);
|
||||||
const handleShowMv = async (item: IMvItem) => {
|
const handleShowMv = async (item: IMvItem) => {
|
||||||
store.commit('setIsPlay', false)
|
mvLoading.value = true;
|
||||||
store.commit('setPlayMusic', false)
|
store.commit('setIsPlay', false);
|
||||||
showMv.value = true
|
store.commit('setPlayMusic', false);
|
||||||
const res = await getMvUrl(item.id)
|
showMv.value = true;
|
||||||
|
const res = await getMvUrl(item.id);
|
||||||
playMvItem.value = item;
|
playMvItem.value = item;
|
||||||
playMvUrl.value = res.data.data.url
|
playMvUrl.value = res.data.data.url;
|
||||||
}
|
mvLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
showMv.value = false
|
showMv.value = false;
|
||||||
if (store.state.playMusicUrl) {
|
if (store.state.playMusicUrl) {
|
||||||
store.commit('setIsPlay', true)
|
store.commit('setIsPlay', true);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mv-list {
|
.mv-list {
|
||||||
@apply relative h-full w-full;
|
@apply relative h-full w-full px-4;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-xl font-bold;
|
@apply text-xl font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-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));
|
grid-template-columns: repeat(auto-fill, minmax(14%, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,4 +178,11 @@ const close = () => {
|
|||||||
.mv-detail-title:hover {
|
.mv-detail-title:hover {
|
||||||
@apply top-0;
|
@apply top-0;
|
||||||
}
|
}
|
||||||
}</style>
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.mv-list-content {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,182 +1,206 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-page">
|
<div class="search-page">
|
||||||
<n-layout
|
<n-layout
|
||||||
class="hot-search"
|
v-if="isMobile ? !searchDetail : true"
|
||||||
:class="setAnimationClass('animate__fadeInDown')"
|
class="hot-search"
|
||||||
:native-scrollbar="false"
|
:class="setAnimationClass('animate__fadeInDown')"
|
||||||
>
|
:native-scrollbar="false"
|
||||||
<div class="title">热搜列表</div>
|
>
|
||||||
<div class="hot-search-list">
|
<div class="title">热搜列表</div>
|
||||||
<template v-for="(item, index) in hotSearchData?.data">
|
<div class="hot-search-list">
|
||||||
<div
|
<template v-for="(item, index) in hotSearchData?.data" :key="index">
|
||||||
:class="setAnimationClass('animate__bounceInLeft')"
|
<div
|
||||||
:style="setAnimationDelay(index, 10)"
|
:class="setAnimationClass('animate__bounceInLeft')"
|
||||||
class="hot-search-item"
|
:style="setAnimationDelay(index, 10)"
|
||||||
@click.stop="clickHotKeyword(item.searchWord)"
|
class="hot-search-item"
|
||||||
>
|
@click.stop="loadSearch(item.searchWord, 1)"
|
||||||
<span
|
>
|
||||||
class="hot-search-item-count"
|
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
|
||||||
:class="{ 'hot-search-item-count-3': index < 3 }"
|
{{ item.searchWord }}
|
||||||
>{{ index + 1 }}</span>
|
</div>
|
||||||
{{ item.searchWord }}
|
</template>
|
||||||
</div>
|
</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>
|
||||||
|
<div v-loading="searchDetailLoading" 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>
|
||||||
|
<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>
|
||||||
</div>
|
</template>
|
||||||
</n-layout>
|
</template>
|
||||||
<!-- 搜索到的歌曲列表 -->
|
</div>
|
||||||
<n-layout
|
</n-layout>
|
||||||
class="search-list"
|
</div>
|
||||||
: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>
|
|
||||||
</n-layout>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getSearch } from "@/api/search";
|
import { useDateFormat } from '@vueuse/core';
|
||||||
import type { IHotSearch } from "@/type/search";
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { getHotSearch } from "@/api/home";
|
import { useRoute } from 'vue-router';
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useStore } from 'vuex';
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { getHotSearch } from '@/api/home';
|
||||||
import SongItem from "@/components/common/SongItem.vue";
|
import { getSearch } from '@/api/search';
|
||||||
import { useStore } from "vuex";
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import { useDateFormat } from '@vueuse/core'
|
import type { IHotSearch } from '@/type/search';
|
||||||
|
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Search',
|
||||||
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const store = useStore();
|
||||||
|
|
||||||
const searchDetail = ref<any>();
|
const searchDetail = ref<any>();
|
||||||
const searchType = ref(Number(route.query.type) || 1);
|
const searchType = computed(() => store.state.searchType as number);
|
||||||
|
const searchDetailLoading = ref(false);
|
||||||
|
|
||||||
// 热搜列表
|
// 热搜列表
|
||||||
const hotSearchData = ref<IHotSearch>();
|
const hotSearchData = ref<IHotSearch>();
|
||||||
const loadHotSearch = async () => {
|
const loadHotSearch = async () => {
|
||||||
const { data } = await getHotSearch();
|
const { data } = await getHotSearch();
|
||||||
hotSearchData.value = data;
|
hotSearchData.value = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadHotSearch();
|
loadHotSearch();
|
||||||
|
loadSearch(route.query.keyword);
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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});
|
|
||||||
|
|
||||||
const songs = data.result.songs || [];
|
|
||||||
const albums = data.result.albums || [];
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSearch(route.query.keyword);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => store.state.searchValue,
|
||||||
async newParams => {
|
(value) => {
|
||||||
searchType.value = Number(newParams.type || 1)
|
loadSearch(value);
|
||||||
loadSearch(newParams.keyword);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
|
||||||
|
const loadSearch = async (keywords: any, type: any = null) => {
|
||||||
|
hotKeyword.value = keywords;
|
||||||
|
searchDetail.value = undefined;
|
||||||
|
if (!keywords) return;
|
||||||
|
|
||||||
|
searchDetailLoading.value = true;
|
||||||
|
const { data } = await getSearch({ keywords, type: type || searchType.value });
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
async (path) => {
|
||||||
|
if (path === '/search') {
|
||||||
|
store.state.searchValue = route.query.keyword;
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const store = useStore()
|
const handlePlay = () => {
|
||||||
|
const tracks = searchDetail.value?.songs || [];
|
||||||
const handlePlay = (item: any) => {
|
store.commit('setPlayList', tracks);
|
||||||
const tracks = searchDetail.value?.songs || []
|
};
|
||||||
store.commit('setPlayList', tracks)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.search-page {
|
.search-page {
|
||||||
@apply flex h-full;
|
@apply flex h-full;
|
||||||
}
|
}
|
||||||
.hot-search {
|
.hot-search {
|
||||||
@apply mr-4 rounded-xl flex-1 overflow-hidden;
|
@apply mr-4 rounded-xl flex-1 overflow-hidden;
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
animation-duration: 0.2s;
|
animation-duration: 0.2s;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&-list{
|
&-list {
|
||||||
@apply pb-28;
|
@apply pb-28;
|
||||||
}
|
}
|
||||||
&-item {
|
&-item {
|
||||||
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
|
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
|
||||||
&-count {
|
&-count {
|
||||||
@apply text-green-500 inline-block ml-3 w-8;
|
@apply text-green-500 inline-block ml-3 w-8;
|
||||||
&-3 {
|
&-3 {
|
||||||
@apply text-red-600 font-bold inline-block ml-3 w-8;
|
@apply text-red-600 font-bold inline-block ml-3 w-8;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-list {
|
.search-list {
|
||||||
@apply flex-1 rounded-xl;
|
@apply flex-1 rounded-xl;
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&-box{
|
&-box {
|
||||||
@apply pb-28;
|
@apply pb-28;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="set-item-title">代理</div>
|
<div class="set-item-title">代理</div>
|
||||||
<div class="set-item-content">无法听音乐时打开</div>
|
<div class="set-item-content">无法听音乐时打开</div>
|
||||||
</div>
|
</div>
|
||||||
<n-switch v-model:value="setData.isProxy"/>
|
<n-switch v-model:value="setData.isProxy" />
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item">
|
<div class="set-item">
|
||||||
<div>
|
<div>
|
||||||
@@ -30,35 +30,42 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import store from '@/store'
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const setData = ref(store.state.setData)
|
import store from '@/store';
|
||||||
const router = useRouter()
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Setting',
|
||||||
|
});
|
||||||
|
|
||||||
|
const setData = ref(store.state.setData);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handelCancel = () => {
|
const handelCancel = () => {
|
||||||
router.back()
|
router.back();
|
||||||
}
|
};
|
||||||
|
|
||||||
const windowData = window as any
|
const windowData = window as any;
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
store.commit('setSetData', setData.value)
|
store.commit('setSetData', setData.value);
|
||||||
windowData.electronAPI.restart()
|
if (windowData.electronAPI) {
|
||||||
}
|
windowData.electronAPI.restart();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.set-page{
|
.set-page {
|
||||||
@apply flex flex-col justify-center items-center pt-8;
|
@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;
|
@apply w-3/5 flex justify-between items-center mb-4;
|
||||||
.set-item-title{
|
.set-item-title {
|
||||||
@apply text-gray-200 text-base;
|
@apply text-gray-200 text-base;
|
||||||
}
|
}
|
||||||
.set-item-content{
|
.set-item-content {
|
||||||
@apply text-gray-400 text-sm;
|
@apply text-gray-400 text-sm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||