Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7365daf700 | ||
|
|
cebf313075 | ||
|
|
bb99049991 | ||
|
|
df74dafbc5 | ||
|
|
721d2a9704 | ||
|
|
1e60fa9a95 | ||
|
|
f24e8232f8 | ||
|
|
a1b1d861ac | ||
|
|
f24263b416 | ||
|
|
17795e5da2 | ||
|
|
f1030d3a78 | ||
|
|
b979ce250f | ||
|
|
d0d8966875 | ||
|
|
d39ba65263 | ||
|
|
62d400827e | ||
|
|
75b99c46b5 | ||
|
|
e7ae79144c | ||
|
|
04d6cbe7f3 | ||
|
|
bea1e5751f | ||
|
|
f2ebb04fab | ||
|
|
42048764d5 | ||
|
|
e326253fd8 | ||
|
|
edf5c77ea0 | ||
|
|
8870390770 | ||
|
|
c9514e6e19 | ||
|
|
08fa160de4 | ||
|
|
5d4c4922fd | ||
|
|
c5e7c87658 | ||
|
|
f6923b4c47 | ||
|
|
4cf7598a7d | ||
|
|
81b09bef0d | ||
|
|
b21df3de25 | ||
|
|
c49d814182 | ||
|
|
1cb3c72ab7 | ||
|
|
f03372de6a | ||
|
|
d925f40303 | ||
|
|
dc12d895d8 | ||
|
|
0bb14902f2 | ||
|
|
3027a5f6ff | ||
|
|
f320f4760b | ||
|
|
e939933d6f | ||
|
|
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,12 @@
|
|||||||
VITE_API = /api
|
# 你的接口地址 (必填)
|
||||||
VITE_API_MT = /mt
|
VITE_API_LOCAL = ***
|
||||||
VITE_API_MUSIC = /music
|
# 音乐破解接口地址
|
||||||
VITE_API_PROXY = http://110.42.251.190:9856
|
VITE_API_MUSIC = ***
|
||||||
|
# 代理地址
|
||||||
|
VITE_API_PROXY = ***
|
||||||
|
|
||||||
|
|
||||||
|
# 本地运行代理地址
|
||||||
|
VITE_API_PROXY = /api
|
||||||
|
VITE_API_MUSIC_PROXY = /music
|
||||||
|
VITE_API_PROXY_MUSIC = /music_proxy
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
VITE_API = http://110.42.251.190:9898
|
|
||||||
VITE_API_MT = http://mt.myalger.top
|
|
||||||
VITE_API_MUSIC = http://110.42.251.190:4100
|
|
||||||
VITE_API_PROXY = http://110.42.251.190:9856
|
|
||||||
13
.eslintignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
snapshot*
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
es
|
||||||
|
esm
|
||||||
|
node_modules
|
||||||
|
static
|
||||||
|
cypress
|
||||||
|
script/test/cypress
|
||||||
|
_site
|
||||||
|
temp*
|
||||||
|
static/
|
||||||
|
!.prettierrc.js
|
||||||
139
.eslintrc
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"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)
|
||||||
|
"consistent-return": "off",
|
||||||
|
"no-promise-executor-return": "off",
|
||||||
|
"prefer-promise-reject-errors": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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` 并合并为准
|
||||||
11
.gitignore
vendored
@@ -1,11 +1,20 @@
|
|||||||
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
|
||||||
|
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
.env.*.local
|
||||||
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||||
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.
|
||||||
103
README.md
@@ -1,5 +1,100 @@
|
|||||||
# Vue 3 + Typescript + Vite
|
# Alger Music Player
|
||||||
|
主要功能如下
|
||||||
|
|
||||||
vue3 + TypeScript + NaiveUI + animateCss + Vuex + VueRouter + Axios等实现音乐桌面web端
|
- 音乐推荐
|
||||||
实现各项功能
|
- 音乐播放
|
||||||
网站地址:http://mc.myalger.top/
|
- 网易云登录
|
||||||
|
- 播放历史
|
||||||
|
- 桌面歌词
|
||||||
|
- 歌单 mv 搜索 专辑等功能
|
||||||
|
- 识别无法播放歌曲 并代理播放
|
||||||
|
- 可听周杰伦(搜索专辑)
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
||||||
|
|
||||||
|
## 预览地址
|
||||||
|
[http://mc.alger.fun/](http://mc.alger.fun/)
|
||||||
|
|
||||||
|
## 软件截图
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 主要框架
|
||||||
|
- Vue 3 - 渐进式 JavaScript 框架
|
||||||
|
- TypeScript - JavaScript 的超集,添加了类型系统
|
||||||
|
- Electron - 跨平台桌面应用开发框架
|
||||||
|
- Vite - 下一代前端构建工具
|
||||||
|
|
||||||
|
### UI 框架
|
||||||
|
- Naive UI - 基于 Vue 3 的组件库
|
||||||
|
|
||||||
|
### 项目特点
|
||||||
|
- 完整的类型支持(TypeScript)
|
||||||
|
- 模块化设计
|
||||||
|
- 自动化组件和 API 导入
|
||||||
|
- 多平台支持(Web、Desktop、Mobile Web)
|
||||||
|
- 构建优化(代码分割、压缩)
|
||||||
|
|
||||||
|
## 咖啡☕️
|
||||||
|
| 微信 | 支付宝 |
|
||||||
|
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||||
|
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
|
||||||
|
|
||||||
|
## 项目运行
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 运行项目 web
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 运行项目 electron
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
# 打包项目 web
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 打包项目 electron
|
||||||
|
npm run win ...
|
||||||
|
# 具体看 package.json
|
||||||
|
```
|
||||||
|
#### 注意
|
||||||
|
- 本地运行需要配置 .env.development 文件
|
||||||
|
- 打包需要配置 .env.production 文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.development
|
||||||
|
VITE_API_LOCAL = /api
|
||||||
|
VITE_API_MUSIC_PROXY = /music
|
||||||
|
VITE_API_PROXY_MUSIC = /music_proxy
|
||||||
|
|
||||||
|
# 你的接口地址 (必填)
|
||||||
|
VITE_API = ***
|
||||||
|
# 音乐po接口地址
|
||||||
|
VITE_API_MUSIC = ***
|
||||||
|
VITE_API_PROXY = ***
|
||||||
|
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
# 你的接口地址 (必填)
|
||||||
|
VITE_API = ***
|
||||||
|
# 音乐po接口地址
|
||||||
|
VITE_API_MUSIC = ***
|
||||||
|
# 代理地址
|
||||||
|
VITE_API_PROXY = ***
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stargazers over time
|
||||||
|
[](https://starchart.cc/algerkong/AlgerMusicPlayer)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 欢迎提Issues
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
本软件仅用于学习交流,禁止用于商业用途,否则后果自负。
|
||||||
|
|||||||
129
app.js
@@ -1,139 +1,148 @@
|
|||||||
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');
|
||||||
|
const config = require('./electron/config');
|
||||||
|
|
||||||
let mainWin = null
|
let mainWin = null;
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWin = new BrowserWindow({
|
mainWin = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 780,
|
height: 780,
|
||||||
frame: false,
|
frame: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
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:${config.development.mainPort}/`);
|
||||||
} 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
|
||||||
const tray = new Tray(image)
|
.createFromPath(path.join(__dirname, 'public/icon_16x16.png'))
|
||||||
|
.resize({ width: 16, height: 16 });
|
||||||
|
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 || '';
|
||||||
})
|
});
|
||||||
|
|||||||
7
auto-imports.d.ts
vendored
@@ -3,6 +3,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
@@ -35,6 +36,7 @@ declare global {
|
|||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: typeof import('vue')['provide']
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: typeof import('vue')['reactive']
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: typeof import('vue')['readonly']
|
||||||
@@ -53,10 +55,13 @@ declare global {
|
|||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
const useDialog: typeof import('naive-ui')['useDialog']
|
const useDialog: typeof import('naive-ui')['useDialog']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useNotification: typeof import('naive-ui')['useNotification']
|
const useNotification: typeof import('naive-ui')['useNotification']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: typeof import('vue')['watch']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
@@ -65,6 +70,6 @@ declare global {
|
|||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
49
build/mac.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.alger.music",
|
||||||
|
"productName": "AlgerMusic",
|
||||||
|
"artifactName": "${productName}_${version}_${arch}.${ext}",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist_electron/mac"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"package.json",
|
||||||
|
"app.js",
|
||||||
|
"electron/**/*",
|
||||||
|
"**/*",
|
||||||
|
"public/**/*",
|
||||||
|
"node_modules/**/*"
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"icon": "public/icon.icns",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "dmg",
|
||||||
|
"arch": ["x64", "arm64"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": "public.app-category.music",
|
||||||
|
"darkModeSupport": true
|
||||||
|
},
|
||||||
|
"dmg": {
|
||||||
|
"title": "${productName} ${version}",
|
||||||
|
"icon": "public/icon.icns",
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"x": 410,
|
||||||
|
"y": 150,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 130,
|
||||||
|
"y": 150,
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"window": {
|
||||||
|
"width": 540,
|
||||||
|
"height": 380
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
components.d.ts
vendored
@@ -1,33 +1,44 @@
|
|||||||
/* 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 {
|
||||||
|
Coffee: typeof import('./src/components/Coffee.vue')['default']
|
||||||
|
InstallAppModal: typeof import('./src/components/common/InstallAppModal.vue')['default']
|
||||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
||||||
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
||||||
|
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
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-6.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/img/image-7.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
docs/img/image-8.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
electron/config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
// 开发环境配置
|
||||||
|
development: {
|
||||||
|
mainPort: 4488,
|
||||||
|
lyricPort: 4488,
|
||||||
|
},
|
||||||
|
// 生产环境配置
|
||||||
|
production: {
|
||||||
|
distPath: '../dist',
|
||||||
|
},
|
||||||
|
};
|
||||||
75
electron/lyric.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const { BrowserWindow } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
let lyricWindow = null;
|
||||||
|
|
||||||
|
const createWin = () => {
|
||||||
|
lyricWindow = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 300,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
transparent: true,
|
||||||
|
hasShadow: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: `${__dirname}/preload.js`,
|
||||||
|
webSecurity: 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:${config.development.lyricPort}/#/lyric`);
|
||||||
|
} else {
|
||||||
|
const distPath = path.resolve(__dirname, config.production.distPath);
|
||||||
|
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,5 +1,6 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron')
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 主进程通信
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
minimize: () => ipcRenderer.send('minimize-window'),
|
minimize: () => ipcRenderer.send('minimize-window'),
|
||||||
maximize: () => ipcRenderer.send('maximize-window'),
|
maximize: () => ipcRenderer.send('maximize-window'),
|
||||||
@@ -7,19 +8,23 @@ 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 = {
|
// 存储相关
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
setStoreValue: (key, value) => {
|
setStoreValue: (key, value) => ipcRenderer.send('setStore', key, value),
|
||||||
ipcRenderer.send("setStore", key, value)
|
getStoreValue: (key) => ipcRenderer.sendSync('getStore', key),
|
||||||
|
on: (channel, func) => {
|
||||||
|
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||||
},
|
},
|
||||||
|
once: (channel, func) => {
|
||||||
getStoreValue(key) {
|
ipcRenderer.once(channel, (event, ...args) => func(...args));
|
||||||
const resp = ipcRenderer.sendSync("getStore", key)
|
|
||||||
return resp
|
|
||||||
},
|
},
|
||||||
}
|
send: (channel, data) => {
|
||||||
}
|
ipcRenderer.send(channel, data);
|
||||||
|
},
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler)
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.0",
|
|
||||||
"isProxy": false,
|
"isProxy": false,
|
||||||
"author": "alger"
|
"noAnimate": false,
|
||||||
|
"animationSpeed": 1,
|
||||||
|
"author": "Alger",
|
||||||
|
"authorUrl": "https://github.com/algerkong"
|
||||||
}
|
}
|
||||||
|
|||||||
69
electron/update.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const { app, BrowserWindow } = require('electron');
|
||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
|
||||||
|
class Updater {
|
||||||
|
constructor(mainWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
this.updateUrl = 'http://your-server.com/update'; // 更新服务器地址
|
||||||
|
this.version = app.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
async checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.updateUrl}/check`, {
|
||||||
|
params: {
|
||||||
|
version: this.version,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.hasUpdate) {
|
||||||
|
await this.downloadUpdate(response.data.downloadUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查更新失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载更新
|
||||||
|
async downloadUpdate(downloadUrl) {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
url: downloadUrl,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempPath = path.join(app.getPath('temp'), 'update.zip');
|
||||||
|
fs.writeFileSync(tempPath, response.data);
|
||||||
|
|
||||||
|
await this.extractUpdate(tempPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载更新失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压更新
|
||||||
|
async extractUpdate(zipPath) {
|
||||||
|
try {
|
||||||
|
const zip = new AdmZip(zipPath);
|
||||||
|
const targetPath = path.join(__dirname, '../dist'); // 前端文件目录
|
||||||
|
|
||||||
|
// 解压文件
|
||||||
|
zip.extractAllTo(targetPath, true);
|
||||||
|
|
||||||
|
// 删除临时文件
|
||||||
|
fs.unlinkSync(zipPath);
|
||||||
|
|
||||||
|
// 刷新页面
|
||||||
|
this.mainWindow.webContents.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解压更新失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Updater;
|
||||||
100
index.html
@@ -1,25 +1,81 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<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, maximum-scale=1.0, user-scalable=no" />
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
<!-- SEO 元数据 -->
|
||||||
href="./public/icon/iconfont.css"
|
<title>网抑云音乐 | AlgerKong | AlgerMusicPlayer</title>
|
||||||
/>
|
<meta name="description" content="AlgerMusicPlayer 网抑云音乐 基于 网易云音乐API 的一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源,让您随时随地享受音乐。" />
|
||||||
<link rel="stylesheet" href="./public/css/animate.css" />
|
<meta name="keywords" content="AlgerMusic, AlgerMusicPlayer, 网抑云, 音乐播放器, 在线音乐, 免费音乐, 歌词显示, 音乐下载, AlgerKong, 网易云音乐" />
|
||||||
<link rel="stylesheet" href="./public/css/base.css" />
|
|
||||||
<style>
|
<!-- 作者信息 -->
|
||||||
:root {
|
<meta name="author" content="AlgerKong" />
|
||||||
--animate-delay: 0.5s;
|
<meta name="author-url" content="https://github.com/algerkong" />
|
||||||
}
|
|
||||||
</style>
|
<!-- PWA 相关 -->
|
||||||
</head>
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|
||||||
|
<!-- 资源预加载 -->
|
||||||
|
<link rel="preload" href="/icon/iconfont.css" as="style" />
|
||||||
|
<link rel="preload" href="/css/animate.css" as="style" />
|
||||||
|
<link rel="preload" href="/css/base.css" as="style" />
|
||||||
|
|
||||||
|
<!-- 样式表 -->
|
||||||
|
<link rel="stylesheet" href="/icon/iconfont.css" />
|
||||||
|
<link rel="stylesheet" href="/css/animate.css" />
|
||||||
|
<link rel="stylesheet" href="/css/base.css" />
|
||||||
|
<script defer src="https://cn.vercount.one/js"></script>
|
||||||
|
|
||||||
|
<!-- 动画配置 -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--animate-delay: 0.5s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<div style="display: none;">
|
||||||
|
Total Page View <span id="vercount_value_page_pv">Loading</span>
|
||||||
|
Total Visits <span id="vercount_value_site_pv">Loading</span>
|
||||||
|
Site Total Visitors <span id="vercount_value_site_uv">Loading</span>
|
||||||
|
</div>
|
||||||
|
<!-- 收款码图片预加载 -->
|
||||||
|
<link rel="preload" as="image" href="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" />
|
||||||
|
<link rel="preload" as="image" href="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" />
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
|
||||||
|
<!-- 结构化数据 -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebApplication",
|
||||||
|
"name": "网抑云音乐",
|
||||||
|
"applicationCategory": "MultimediaApplication",
|
||||||
|
"operatingSystem": "Web, Windows, MacOS",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "AlgerKong",
|
||||||
|
"url": "https://github.com/algerkong"
|
||||||
|
},
|
||||||
|
"description": "一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "CNY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
81
package.json
@@ -1,47 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "alger-music",
|
"name": "alger-music",
|
||||||
"version": "1.3.0",
|
"version": "2.1.0",
|
||||||
"description": "这是一个用于音乐播放的应用程序。",
|
"description": "这是一个用于音乐播放的应用程序。",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "cross-env NODE_ENV=production vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"es": "vite && electron .",
|
"start": "cross-env NODE_ENV=development electron .",
|
||||||
"start": "set NODE_ENV=development&&electron .",
|
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
|
||||||
"e:b": "electron-builder --config ./electron.config.json",
|
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
|
||||||
"eb": "vite build && e:b"
|
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
|
||||||
|
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
|
||||||
|
"b:mac": "cross-env NODE_ENV=production npm run build && electron-builder --config ./build/mac.json",
|
||||||
|
"b:win": "cross-env NODE_ENV=production npm run build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"@types/howler": "^2.2.12",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
|
||||||
"@vueuse/core": "^10.7.1",
|
|
||||||
"autoprefixer": "^9.8.6",
|
|
||||||
"axios": "^0.21.1",
|
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"howler": "^2.2.4"
|
||||||
"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",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"electron": "^32.2.7",
|
||||||
|
"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.49",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"remixicon": "^4.2.0",
|
||||||
|
"sass": "^1.82.0",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"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 |
BIN
public/icon.icns
Normal file
BIN
public/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 626 B |
10
public/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Alger Music PWA",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "256x256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
76
src/App.vue
@@ -1,37 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app-container" :class="{ mobile: isMobile }">
|
||||||
<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 setup lang="ts">
|
||||||
<script lang="ts" setup>
|
import { darkTheme } from 'naive-ui';
|
||||||
import { darkTheme } from 'naive-ui'
|
import { onMounted } from 'vue';
|
||||||
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)
|
|
||||||
// 是否播放
|
onMounted(() => {
|
||||||
const play = computed(() => store.state.play as boolean)
|
store.dispatch('initializeSettings');
|
||||||
const windowData = window as any
|
});
|
||||||
onMounted(()=>{
|
</script>
|
||||||
if(windowData.electron){
|
|
||||||
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
|
<style lang="scss" scoped>
|
||||||
store.commit('setSetData', setData)
|
.app-container {
|
||||||
}
|
@apply h-full w-full;
|
||||||
})
|
user-select: none;
|
||||||
</script>
|
}
|
||||||
|
|
||||||
<style lang="scss" scoped >
|
.mobile {
|
||||||
div {
|
.text-base {
|
||||||
box-sizing: border-box;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
.app {
|
}
|
||||||
user-select: none;
|
|
||||||
}
|
.html:has(.mobile) {
|
||||||
</style>
|
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,15 +1,18 @@
|
|||||||
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 = 30, offset = 0) => {
|
||||||
return request.get<IData<Array<IMvItem>>>('/top/mv', {
|
return request({
|
||||||
|
url: '/mv/all',
|
||||||
|
method: 'get',
|
||||||
params: {
|
params: {
|
||||||
limit,
|
limit,
|
||||||
|
offset,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取 mv 数据
|
// 获取 mv 数据
|
||||||
export const getMvDetail = (mvid: string) => {
|
export const getMvDetail = (mvid: string) => {
|
||||||
@@ -17,8 +20,8 @@ export const getMvDetail = (mvid: string) => {
|
|||||||
params: {
|
params: {
|
||||||
mvid,
|
mvid,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取 mv 地址
|
// 获取 mv 地址
|
||||||
export const getMvUrl = (id: Number) => {
|
export const getMvUrl = (id: Number) => {
|
||||||
@@ -26,5 +29,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 |
44
src/components/Coffee.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<n-popover trigger="hover" placement="top" :show-arrow="true" :raw="true" :delay="100">
|
||||||
|
<template #trigger>
|
||||||
|
<slot>
|
||||||
|
<n-button
|
||||||
|
quaternary
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 transition-all duration-300 hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
请我喝咖啡
|
||||||
|
</n-button>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-6 bg-black rounded-lg shadow-lg">
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||||
|
<span class="text-sm text-gray-100">支付宝</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||||
|
<span class="text-sm text-gray-100">微信支付</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
alipayQR: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
wechatQR: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,75 +1,299 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer :show="show" height="70vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
|
<n-drawer
|
||||||
|
:show="show"
|
||||||
|
:height="isMobile ? '100vh' : '80vh'"
|
||||||
|
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-header h-12 flex items-center justify-between">
|
||||||
<div class="music-title">{{ name }}</div>
|
<n-ellipsis :line-clamp="1">
|
||||||
<!-- 歌单歌曲列表 -->
|
<div class="music-title">
|
||||||
<div class="music-list">
|
{{ name }}
|
||||||
<n-scrollbar >
|
|
||||||
<div v-for="(item, index) in songList" :key="item.id" :class="setAnimationClass('animate__bounceInUp')"
|
|
||||||
:style="setAnimationDelay(index, 100)">
|
|
||||||
<SongItem :item="formatDetail(item)" @play="handlePlay" />
|
|
||||||
</div>
|
</div>
|
||||||
<PlayBottom/>
|
</n-ellipsis>
|
||||||
</n-scrollbar>
|
<div class="music-close">
|
||||||
|
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="music-content">
|
||||||
|
<!-- 左侧歌单信息 -->
|
||||||
|
<div class="music-info">
|
||||||
|
<div class="music-cover">
|
||||||
|
<n-image
|
||||||
|
:src="getImgUrl(listInfo?.coverImgUrl, '300y300')"
|
||||||
|
class="cover-img"
|
||||||
|
preview-disabled
|
||||||
|
:class="setAnimationClass('animate__fadeIn')"
|
||||||
|
object-fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="music-detail">
|
||||||
|
<div v-if="listInfo?.creator" class="creator-info">
|
||||||
|
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
||||||
|
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="listInfo?.description" class="music-desc">
|
||||||
|
<n-ellipsis :line-clamp="isMobile ? 3 : 10">
|
||||||
|
{{ listInfo.description }}
|
||||||
|
</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧歌曲列表 -->
|
||||||
|
<div class="music-list-container">
|
||||||
|
<div v-loading="loading" class="music-list">
|
||||||
|
<n-scrollbar @scroll="handleScroll">
|
||||||
|
<div v-loading="loading || !songList.length" class="music-list-content">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in displayedSongs"
|
||||||
|
:key="item.id"
|
||||||
|
class="double-item"
|
||||||
|
:class="setAnimationClass('animate__bounceInUp')"
|
||||||
|
:style="getItemAnimationDelay(index)"
|
||||||
|
>
|
||||||
|
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||||
|
<play-bottom />
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
<play-bottom />
|
||||||
|
</div>
|
||||||
</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 { getImgUrl, 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 props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
songList: any[]
|
songList: any[];
|
||||||
}>()
|
loading?: boolean;
|
||||||
const emit = defineEmits(['update:show'])
|
listInfo?: {
|
||||||
|
trackIds: { id: number }[];
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['update:show', 'update:loading']);
|
||||||
|
|
||||||
|
const page = ref(0);
|
||||||
|
const pageSize = 20;
|
||||||
|
const isLoadingMore = ref(false);
|
||||||
|
const displayedSongs = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 计算总数
|
||||||
|
const total = computed(() => {
|
||||||
|
if (props.listInfo?.trackIds) {
|
||||||
|
return props.listInfo.trackIds.length;
|
||||||
|
}
|
||||||
|
return props.songList.length;
|
||||||
|
});
|
||||||
|
|
||||||
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 = props.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 (isLoadingMore.value || displayedSongs.value.length >= total.value) return;
|
||||||
|
|
||||||
|
isLoadingMore.value = true;
|
||||||
|
try {
|
||||||
|
if (props.listInfo?.trackIds) {
|
||||||
|
// 如果有 trackIds,需要分批请求歌曲详情
|
||||||
|
const start = page.value * pageSize;
|
||||||
|
const end = Math.min((page.value + 1) * pageSize, total.value);
|
||||||
|
const trackIds = props.listInfo.trackIds.slice(start, end).map((item) => item.id);
|
||||||
|
|
||||||
|
if (trackIds.length > 0) {
|
||||||
|
const { data } = await getMusicDetail(trackIds);
|
||||||
|
displayedSongs.value = [...displayedSongs.value, ...data.songs];
|
||||||
|
page.value++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有 trackIds,直接使用 songList 分页
|
||||||
|
const start = page.value * pageSize;
|
||||||
|
const end = Math.min((page.value + 1) * pageSize, props.songList.length);
|
||||||
|
const newSongs = props.songList.slice(start, end);
|
||||||
|
displayedSongs.value = [...displayedSongs.value, ...newSongs];
|
||||||
|
page.value++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载歌曲失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemAnimationDelay = (index: number) => {
|
||||||
|
const currentPageIndex = index % pageSize;
|
||||||
|
return setAnimationDelay(currentPageIndex, 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改滚动处理函数
|
||||||
|
const handleScroll = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100 && !isLoadingMore.value) {
|
||||||
|
loadMoreSongs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 songList 变化,重置分页状态
|
||||||
|
watch(
|
||||||
|
() => props.songList,
|
||||||
|
(newSongs) => {
|
||||||
|
page.value = 0;
|
||||||
|
displayedSongs.value = newSongs.slice(0, pageSize);
|
||||||
|
if (newSongs.length > pageSize) {
|
||||||
|
page.value = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.music {
|
.music {
|
||||||
|
&-title {
|
||||||
|
@apply text-xl font-bold text-white;
|
||||||
|
}
|
||||||
|
|
||||||
&-page {
|
&-page {
|
||||||
@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 {
|
|
||||||
@apply text-lg font-bold text-white p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
|
@apply cursor-pointer text-white flex gap-2 items-center;
|
||||||
|
.icon {
|
||||||
|
@apply text-3xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
@apply flex h-[calc(100%-60px)];
|
||||||
|
}
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
|
||||||
|
|
||||||
|
.music-cover {
|
||||||
|
@apply w-full aspect-square rounded-lg overflow-hidden mb-4;
|
||||||
|
.cover-img {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-detail {
|
||||||
|
@apply flex flex-col flex-grow;
|
||||||
|
|
||||||
|
.creator-info {
|
||||||
|
@apply flex items-center mb-4;
|
||||||
|
.creator-name {
|
||||||
|
@apply ml-2 text-sm text-gray-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-desc {
|
||||||
|
@apply text-sm text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-list-container {
|
||||||
|
@apply flex-grow min-h-0 flex flex-col relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
height: calc(100% - 60px);
|
@apply flex-grow min-h-0;
|
||||||
|
|
||||||
|
:deep(.n-virtual-list__scroll) {
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.mobile {
|
||||||
|
.music-page {
|
||||||
|
@apply px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-content {
|
||||||
|
@apply flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-info {
|
||||||
|
@apply w-full pr-0 mb-2 flex flex-row;
|
||||||
|
|
||||||
|
.music-cover {
|
||||||
|
@apply w-[100px] h-[100px] rounded-lg overflow-hidden mb-4;
|
||||||
|
}
|
||||||
|
.music-detail {
|
||||||
|
@apply flex-1 ml-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
@apply text-center text-white py-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.double-list {
|
||||||
|
.double-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-item {
|
||||||
|
background-color: #191919;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
867
src/components/MvPlayer.vue
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
<template>
|
||||||
|
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
|
||||||
|
<div class="mv-detail">
|
||||||
|
<div ref="videoContainerRef" class="video-container" :class="{ 'cursor-hidden': !showCursor }">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
:src="mvUrl"
|
||||||
|
class="video-player"
|
||||||
|
@ended="handleEnded"
|
||||||
|
@timeupdate="handleTimeUpdate"
|
||||||
|
@loadedmetadata="handleLoadedMetadata"
|
||||||
|
@play="isPlaying = true"
|
||||||
|
@pause="isPlaying = false"
|
||||||
|
@click="togglePlay"
|
||||||
|
></video>
|
||||||
|
|
||||||
|
<div v-if="autoPlayBlocked" class="play-hint" @click="togglePlay">
|
||||||
|
<n-button quaternary circle size="large">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="48">
|
||||||
|
<i class="ri-play-circle-line"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="custom-controls" :class="{ 'controls-hidden': !showControls }">
|
||||||
|
<div class="progress-bar custom-slider">
|
||||||
|
<n-slider
|
||||||
|
v-model:value="progress"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:tooltip="false"
|
||||||
|
:step="0.1"
|
||||||
|
@update:value="handleProgressChange"
|
||||||
|
>
|
||||||
|
<template #rail>
|
||||||
|
<div class="progress-rail">
|
||||||
|
<div class="progress-buffer" :style="{ width: `${bufferedProgress}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-main">
|
||||||
|
<div class="left-controls">
|
||||||
|
<n-tooltip v-if="!props.noList" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="handlePrev">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<n-spin v-if="prevLoading" size="small" />
|
||||||
|
<i v-else class="ri-skip-back-line"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
上一个
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="togglePlay">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<n-spin v-if="playLoading" size="small" />
|
||||||
|
<i v-else :class="isPlaying ? 'ri-pause-line' : 'ri-play-line'"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ isPlaying ? '暂停' : '播放' }}
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip v-if="!props.noList" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="handleNext">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<n-spin v-if="nextLoading" size="small" />
|
||||||
|
<i v-else class="ri-skip-forward-line"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
下一个
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-controls">
|
||||||
|
<div v-if="!isMobile" class="volume-control custom-slider">
|
||||||
|
<n-tooltip placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="toggleMute">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<i :class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ volume === 0 ? '取消静音' : '静音' }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-slider v-model:value="volume" :min="0" :max="100" :tooltip="false" class="volume-slider" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-tooltip v-if="!props.noList" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ playMode === 'single' ? '单曲循环' : '列表循环' }}
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="toggleFullscreen">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<i :class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button quaternary circle @click="handleClose">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="24">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
关闭
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加模式切换提示 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="showModeHint" class="mode-hint">
|
||||||
|
<n-icon size="48" class="mode-icon">
|
||||||
|
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
|
||||||
|
</n-icon>
|
||||||
|
<div class="mode-text">
|
||||||
|
{{ playMode === 'single' ? '单曲循环' : '自动播放下一个' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mv-detail-title" :class="{ 'title-hidden': !showControls }">
|
||||||
|
<div class="title">
|
||||||
|
<n-ellipsis>{{ currentMv?.name }}</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NIcon, NSlider, NTooltip } from 'naive-ui';
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import { getMvUrl } from '@/api/mv';
|
||||||
|
import { IMvItem } from '@/type/mv';
|
||||||
|
|
||||||
|
type PlayMode = 'single' | 'auto';
|
||||||
|
const PLAY_MODE = {
|
||||||
|
Single: 'single' as PlayMode,
|
||||||
|
Auto: 'auto' as PlayMode,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
currentMv?: IMvItem;
|
||||||
|
noList?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
currentMv: undefined,
|
||||||
|
noList: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:show', value: boolean): void;
|
||||||
|
(e: 'next', loading: (value: boolean) => void): void;
|
||||||
|
(e: 'prev', loading: (value: boolean) => void): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const mvUrl = ref<string>();
|
||||||
|
const playMode = ref<PlayMode>(PLAY_MODE.Auto);
|
||||||
|
|
||||||
|
const videoRef = ref<HTMLVideoElement>();
|
||||||
|
const isPlaying = ref(false);
|
||||||
|
const currentTime = ref(0);
|
||||||
|
const duration = ref(0);
|
||||||
|
const progress = ref(0);
|
||||||
|
const bufferedProgress = ref(0);
|
||||||
|
const volume = ref(100);
|
||||||
|
const showControls = ref(true);
|
||||||
|
let controlsTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!videoRef.value) return;
|
||||||
|
if (isPlaying.value) {
|
||||||
|
videoRef.value.pause();
|
||||||
|
} else {
|
||||||
|
videoRef.value.play();
|
||||||
|
}
|
||||||
|
resetCursorTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
if (!videoRef.value) return;
|
||||||
|
if (volume.value === 0) {
|
||||||
|
volume.value = 100;
|
||||||
|
} else {
|
||||||
|
volume.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(volume, (newVolume) => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.volume = newVolume / 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleProgressChange = (value: number) => {
|
||||||
|
if (!videoRef.value || !duration.value) return;
|
||||||
|
const newTime = (value / 100) * duration.value;
|
||||||
|
videoRef.value.currentTime = newTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (!videoRef.value) return;
|
||||||
|
currentTime.value = videoRef.value.currentTime;
|
||||||
|
if (!isDragging.value) {
|
||||||
|
progress.value = (currentTime.value / duration.value) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoRef.value.buffered.length > 0) {
|
||||||
|
bufferedProgress.value = (videoRef.value.buffered.end(0) / duration.value) * 100;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
if (!videoRef.value) return;
|
||||||
|
duration.value = videoRef.value.duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetControlsTimer = () => {
|
||||||
|
if (controlsTimer) {
|
||||||
|
clearTimeout(controlsTimer);
|
||||||
|
}
|
||||||
|
showControls.value = true;
|
||||||
|
controlsTimer = setTimeout(() => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
showControls.value = false;
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
resetControlsTimer();
|
||||||
|
resetCursorTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
if (controlsTimer) {
|
||||||
|
clearTimeout(controlsTimer);
|
||||||
|
}
|
||||||
|
if (cursorTimer) {
|
||||||
|
clearTimeout(cursorTimer);
|
||||||
|
}
|
||||||
|
unlockScreenOrientation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 currentMv 的变化
|
||||||
|
watch(
|
||||||
|
() => props.currentMv,
|
||||||
|
async (newMv) => {
|
||||||
|
if (newMv) {
|
||||||
|
await loadMvUrl(newMv);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoPlayBlocked = ref(false);
|
||||||
|
|
||||||
|
const playLoading = ref(false);
|
||||||
|
|
||||||
|
const loadMvUrl = async (mv: IMvItem) => {
|
||||||
|
playLoading.value = true;
|
||||||
|
autoPlayBlocked.value = false;
|
||||||
|
try {
|
||||||
|
const res = await getMvUrl(mv.id);
|
||||||
|
mvUrl.value = res.data.data.url;
|
||||||
|
await nextTick();
|
||||||
|
if (videoRef.value) {
|
||||||
|
try {
|
||||||
|
await videoRef.value.play();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('自动播放失败,可能需要用户交互:', error);
|
||||||
|
autoPlayBlocked.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载MV地址失败:', error);
|
||||||
|
} finally {
|
||||||
|
playLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:show', false);
|
||||||
|
if (store.state.playMusicUrl) {
|
||||||
|
store.commit('setIsPlay', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
if (playMode.value === PLAY_MODE.Single) {
|
||||||
|
// 单曲循环模式,重新加载当前MV
|
||||||
|
if (props.currentMv) {
|
||||||
|
loadMvUrl(props.currentMv);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 自动播放模式,触发下一个
|
||||||
|
emit('next', (value: boolean) => {
|
||||||
|
nextLoading.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePlayMode = () => {
|
||||||
|
playMode.value = playMode.value === PLAY_MODE.Auto ? PLAY_MODE.Single : PLAY_MODE.Auto;
|
||||||
|
showModeHint.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showModeHint.value = false;
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDragging = ref(false);
|
||||||
|
|
||||||
|
// 添加全屏相关的状态和方法
|
||||||
|
const videoContainerRef = ref<HTMLElement>();
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 检查是否支持全屏API
|
||||||
|
const checkFullscreenAPI = () => {
|
||||||
|
const doc = document as any;
|
||||||
|
return {
|
||||||
|
requestFullscreen:
|
||||||
|
videoContainerRef.value?.requestFullscreen ||
|
||||||
|
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
|
||||||
|
(videoContainerRef.value as any)?.mozRequestFullScreen ||
|
||||||
|
(videoContainerRef.value as any)?.msRequestFullscreen,
|
||||||
|
exitFullscreen: doc.exitFullscreen || doc.webkitExitFullscreen || doc.mozCancelFullScreen || doc.msExitFullscreen,
|
||||||
|
fullscreenElement:
|
||||||
|
doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement,
|
||||||
|
fullscreenEnabled:
|
||||||
|
doc.fullscreenEnabled || doc.webkitFullscreenEnabled || doc.mozFullScreenEnabled || doc.msFullscreenEnabled,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加横屏锁定功能
|
||||||
|
const lockScreenOrientation = async () => {
|
||||||
|
try {
|
||||||
|
if ('orientation' in screen) {
|
||||||
|
await (screen as any).orientation.lock('landscape');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('无法锁定屏幕方向:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlockScreenOrientation = () => {
|
||||||
|
try {
|
||||||
|
if ('orientation' in screen) {
|
||||||
|
(screen as any).orientation.unlock();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('无法解锁屏幕方向:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改切换全屏状态的方法
|
||||||
|
const toggleFullscreen = async () => {
|
||||||
|
const api = checkFullscreenAPI();
|
||||||
|
|
||||||
|
if (!api.fullscreenEnabled) {
|
||||||
|
console.warn('全屏API不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!api.fullscreenElement) {
|
||||||
|
await videoContainerRef.value?.requestFullscreen();
|
||||||
|
isFullscreen.value = true;
|
||||||
|
// 在移动端进入全屏时锁定横屏
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
await lockScreenOrientation();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
isFullscreen.value = false;
|
||||||
|
// 退出全屏时解锁屏幕方向
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
unlockScreenOrientation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换全屏失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听全屏状态变化
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
const api = checkFullscreenAPI();
|
||||||
|
isFullscreen.value = !!api.fullscreenElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在组件挂载时添加全屏变化监听
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在组件卸载时移除监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||||
|
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加键盘快捷键支持
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'f' || e.key === 'F') {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 添加到现有的 onMounted 中
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 添加到现有的 onUnmounted 中
|
||||||
|
document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加提示状态
|
||||||
|
const showModeHint = ref(false);
|
||||||
|
|
||||||
|
// 添加加载状态
|
||||||
|
const prevLoading = ref(false);
|
||||||
|
const nextLoading = ref(false);
|
||||||
|
|
||||||
|
// 添加处理函数
|
||||||
|
const handlePrev = () => {
|
||||||
|
prevLoading.value = true;
|
||||||
|
emit('prev', (value: boolean) => {
|
||||||
|
prevLoading.value = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
nextLoading.value = true;
|
||||||
|
emit('next', (value: boolean) => {
|
||||||
|
nextLoading.value = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加鼠标显示状态
|
||||||
|
const showCursor = ref(true);
|
||||||
|
let cursorTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
// 添加重置鼠标计时器的函数
|
||||||
|
const resetCursorTimer = () => {
|
||||||
|
if (cursorTimer) {
|
||||||
|
clearTimeout(cursorTimer);
|
||||||
|
}
|
||||||
|
showCursor.value = true;
|
||||||
|
if (isPlaying.value && !showControls.value) {
|
||||||
|
cursorTimer = setTimeout(() => {
|
||||||
|
showCursor.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听播放状态变化
|
||||||
|
watch(isPlaying, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
showCursor.value = true;
|
||||||
|
if (cursorTimer) {
|
||||||
|
clearTimeout(cursorTimer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetCursorTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加控制栏状态监听
|
||||||
|
watch(showControls, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
showCursor.value = true;
|
||||||
|
if (cursorTimer) {
|
||||||
|
clearTimeout(cursorTimer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetCursorTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMobile = computed(() => store.state.isMobile);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mv-detail {
|
||||||
|
@apply w-full h-full bg-black relative;
|
||||||
|
|
||||||
|
// 添加横屏模式支持
|
||||||
|
@media screen and (orientation: landscape) {
|
||||||
|
height: 100vh !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
@apply w-full h-full relative;
|
||||||
|
transition: cursor 0.3s ease;
|
||||||
|
|
||||||
|
// 移动端适配
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.custom-controls {
|
||||||
|
.controls-main {
|
||||||
|
@apply flex-wrap gap-2 justify-center;
|
||||||
|
|
||||||
|
.left-controls,
|
||||||
|
.right-controls {
|
||||||
|
@apply w-full justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
@apply order-first w-full text-center mb-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整标题样式
|
||||||
|
.mv-detail-title {
|
||||||
|
.title {
|
||||||
|
@apply text-base max-w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整进度条
|
||||||
|
.progress-bar {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cursor-hidden {
|
||||||
|
* {
|
||||||
|
cursor: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制栏区域保持鼠标可见
|
||||||
|
.custom-controls {
|
||||||
|
* {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:fullscreen,
|
||||||
|
&:-webkit-full-screen,
|
||||||
|
&:-moz-full-screen,
|
||||||
|
&:-ms-fullscreen {
|
||||||
|
background: black;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
// 确保全屏时标题栏正确显示
|
||||||
|
.mv-detail-title {
|
||||||
|
@apply px-8 py-6;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保全屏时控制栏正确显示
|
||||||
|
.custom-controls {
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
@apply opacity-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
@apply w-full h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-controls {
|
||||||
|
@apply absolute bottom-0 left-0 w-full transition-opacity duration-300 ease-in-out;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
padding: 16px 20px;
|
||||||
|
|
||||||
|
&.controls-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
@apply mb-4;
|
||||||
|
|
||||||
|
.progress-rail {
|
||||||
|
@apply relative w-full h-full;
|
||||||
|
|
||||||
|
.progress-buffer {
|
||||||
|
@apply absolute h-full bg-gray-600 rounded-full;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-main {
|
||||||
|
@apply flex justify-between items-center;
|
||||||
|
|
||||||
|
.left-controls,
|
||||||
|
.right-controls {
|
||||||
|
@apply flex items-center gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
@apply text-sm text-white ml-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
@apply text-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply text-green-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-hint {
|
||||||
|
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 cursor-pointer;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
@apply text-white opacity-80 transform transition-all duration-300;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply opacity-100 scale-110;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mv-detail-title {
|
||||||
|
@apply absolute w-full left-0 top-0 px-6 py-4 transition-opacity duration-300 z-50;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
|
||||||
|
&.title-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply text-white text-lg font-medium;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider {
|
||||||
|
:deep(.n-slider) {
|
||||||
|
--n-rail-height: 4px;
|
||||||
|
--n-rail-color: rgba(255, 255, 255, 0.2);
|
||||||
|
--n-fill-color: var(--primary-color);
|
||||||
|
--n-handle-size: 12px;
|
||||||
|
--n-handle-color: var(--primary-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--n-rail-height: 6px;
|
||||||
|
--n-handle-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-rail {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-handle {
|
||||||
|
@apply transition-opacity duration-200;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .n-slider-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加模式提示样式
|
||||||
|
.mode-hint {
|
||||||
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
|
||||||
|
@apply flex flex-col items-center justify-center;
|
||||||
|
@apply bg-black bg-opacity-70 rounded-lg p-4;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
.mode-icon {
|
||||||
|
@apply text-white mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-text {
|
||||||
|
@apply text-white text-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加过渡动画
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 tooltip 样式
|
||||||
|
:deep(.n-tooltip) {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调左侧控制按钮的样式
|
||||||
|
.left-controls {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
@apply text-sm text-white ml-4; // 增加时间显示的左边距
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以添加按钮禁用状态的样式
|
||||||
|
:deep(.n-button--disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加加载动画样式
|
||||||
|
:deep(.n-spin) {
|
||||||
|
.n-spin-body {
|
||||||
|
@apply text-white;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加视频播放器样式
|
||||||
|
.video-player {
|
||||||
|
@apply w-full h-full cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加点击反馈效果
|
||||||
|
.video-container {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
@apply opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加鼠标隐藏样式
|
||||||
|
.video-container {
|
||||||
|
@apply w-full h-full relative;
|
||||||
|
transition: cursor 0.3s ease;
|
||||||
|
|
||||||
|
&.cursor-hidden {
|
||||||
|
* {
|
||||||
|
cursor: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制栏区域保持鼠标可见
|
||||||
|
.custom-controls {
|
||||||
|
* {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,79 +1,155 @@
|
|||||||
<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 || isHiding"
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
class="play-list-type-item"
|
||||||
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
|
:class="
|
||||||
v-show="isShowAllPlaylistCategory || index <= 19"
|
setAnimationClass(
|
||||||
@click="handleClickPlaylistType(item.name)"
|
index <= 19
|
||||||
>{{ item.name }}</span>
|
? 'animate__bounceIn'
|
||||||
</template>
|
: !isShowAllPlaylistCategory
|
||||||
<div
|
? 'animate__backOutLeft'
|
||||||
class="play-list-type-showall"
|
: 'animate__bounceIn',
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
) +
|
||||||
:style="
|
' ' +
|
||||||
setAnimationDelay(
|
'type-item-' +
|
||||||
!isShowAllPlaylistCategory
|
index
|
||||||
? 25
|
"
|
||||||
: playlistCategory?.sub.length || 100 + 30
|
:style="getAnimationDelay(index)"
|
||||||
)
|
@click="handleClickPlaylistType(item.name)"
|
||||||
"
|
>{{ item.name }}</span
|
||||||
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
|
>
|
||||||
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
|
</template>
|
||||||
</div>
|
<div
|
||||||
|
class="play-list-type-showall"
|
||||||
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
|
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
|
||||||
|
@click="handleToggleShowAllPlaylistCategory"
|
||||||
|
>
|
||||||
|
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { computed, 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>();
|
||||||
// 是否显示全部歌单分类
|
// 是否显示全部歌单分类
|
||||||
const isShowAllPlaylistCategory = ref<boolean>(false);
|
const isShowAllPlaylistCategory = ref<boolean>(false);
|
||||||
|
const DELAY_TIME = 40;
|
||||||
|
const getAnimationDelay = computed(() => {
|
||||||
|
return (index: number) => {
|
||||||
|
if (index <= 19) {
|
||||||
|
return setAnimationDelay(index, DELAY_TIME);
|
||||||
|
}
|
||||||
|
if (!isShowAllPlaylistCategory.value) {
|
||||||
|
const nowIndex = (playlistCategory.value?.sub.length || 0) - index;
|
||||||
|
return setAnimationDelay(nowIndex, DELAY_TIME);
|
||||||
|
}
|
||||||
|
return setAnimationDelay(index - 19, DELAY_TIME);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(isShowAllPlaylistCategory, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
const elements = playlistCategory.value?.sub.map((item, index) =>
|
||||||
|
document.querySelector(`.type-item-${index}`),
|
||||||
|
) as HTMLElement[];
|
||||||
|
elements
|
||||||
|
.slice(20)
|
||||||
|
.reverse()
|
||||||
|
.forEach((element, index) => {
|
||||||
|
if (element) {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
(element as HTMLElement).style.position = 'absolute';
|
||||||
|
},
|
||||||
|
index * DELAY_TIME + 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
isHiding.value = false;
|
||||||
|
document.querySelectorAll('.play-list-type-item').forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
console.log('element', element);
|
||||||
|
(element as HTMLElement).style.position = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('.play-list-type-item').forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
(element as HTMLElement).style.position = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 加载歌单分类
|
// 加载歌单分类
|
||||||
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,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHiding = ref<boolean>(false);
|
||||||
|
const handleToggleShowAllPlaylistCategory = () => {
|
||||||
|
isShowAllPlaylistCategory.value = !isShowAllPlaylistCategory.value;
|
||||||
|
if (!isShowAllPlaylistCategory.value) {
|
||||||
|
isHiding.value = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 页面初始化
|
// 页面初始化
|
||||||
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__fadeInRight')">最新专辑</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,157 @@
|
|||||||
<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();
|
||||||
},
|
// 处理数据
|
||||||
});
|
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>
|
||||||
|
|||||||
119
src/components/common/InstallAppModal.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal v-model:show="showModal" preset="dialog" :show-icon="false" :mask-closable="true" class="install-app-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="app-icon">
|
||||||
|
<img src="@/assets/logo.png" alt="App Icon" />
|
||||||
|
</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<h2 class="app-name">Alger Music</h2>
|
||||||
|
<p class="app-desc mb-2">在桌面安装应用,获得更好的体验</p>
|
||||||
|
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<n-button class="cancel-btn" @click="closeModal">暂不安装</n-button>
|
||||||
|
<n-button type="primary" class="install-btn" @click="handleInstall">立即安装</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const isElectron = ref((window as any).electron !== undefined);
|
||||||
|
const noPrompt = ref(false);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
if (noPrompt.value) {
|
||||||
|
localStorage.setItem('installPromptDismissed', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
// 新页面打开
|
||||||
|
// 识别当前环境是 mac 还是 windows
|
||||||
|
// const os = navigator.platform;
|
||||||
|
// const isMac = os.includes('Mac');
|
||||||
|
// const isWindows = os.includes('Win');
|
||||||
|
// const urls = {
|
||||||
|
// mac: 'http://file.alger.fun/d/ali/%E8%BD%AF%E4%BB%B6/AlgerMusic/AlgerMusic.dmg',
|
||||||
|
// windows: 'http://file.alger.fun/d/ali/%E8%BD%AF%E4%BB%B6/AlgerMusic/AlgerMusic.exe',
|
||||||
|
// };
|
||||||
|
// // 根据操作系统选择下载链接
|
||||||
|
// let downloadUrl = '';
|
||||||
|
// if (isMac) {
|
||||||
|
// downloadUrl = urls.mac;
|
||||||
|
// } else if (isWindows) {
|
||||||
|
// downloadUrl = urls.windows;
|
||||||
|
// }
|
||||||
|
const downloadUrl = 'https://github.com/algerkong/AlgerMusicPlayer/releases';
|
||||||
|
if (downloadUrl) {
|
||||||
|
window.open(downloadUrl, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果是 electron 环境,不显示安装提示
|
||||||
|
if (isElectron.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经点击过"暂不安装"
|
||||||
|
const isDismissed = localStorage.getItem('installPromptDismissed') === 'true';
|
||||||
|
if (isDismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showModal.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.install-app-modal {
|
||||||
|
:deep(.n-modal) {
|
||||||
|
@apply max-w-sm;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
@apply p-4;
|
||||||
|
.modal-header {
|
||||||
|
@apply flex items-center mb-6;
|
||||||
|
.app-icon {
|
||||||
|
@apply w-16 h-16 mr-4 rounded-2xl overflow-hidden;
|
||||||
|
img {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-info {
|
||||||
|
@apply flex-1;
|
||||||
|
.app-name {
|
||||||
|
@apply text-xl font-bold mb-1;
|
||||||
|
}
|
||||||
|
.app-desc {
|
||||||
|
@apply text-sm text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
@apply flex gap-3;
|
||||||
|
.n-button {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
@apply bg-gray-800 text-gray-300 border-none;
|
||||||
|
&:hover {
|
||||||
|
@apply bg-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.install-btn {
|
||||||
|
@apply bg-green-600 border-none;
|
||||||
|
&:hover {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
|
|||||||
73
src/components/common/PlayVideo.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
|
||||||
|
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);
|
||||||
|
audioService.getCurrentSound()?.pause();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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,121 @@
|
|||||||
<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"
|
||||||
|
/>
|
||||||
|
<mv-player v-if="item.type === 'mv'" v-model:show="showPop" :current-mv="getCurrentMv()" no-list />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getImgUrl } from '@/utils'
|
import { useStore } from 'vuex';
|
||||||
import type {Album} from '@/type/album'
|
|
||||||
import { getAlbum } from '@/api/list';
|
import { getAlbum, getListDetail } from '@/api/list';
|
||||||
|
import MvPlayer from '@/components/MvPlayer.vue';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
import { IMvItem } from '@/type/mv';
|
||||||
|
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 songList = ref<any[]>([]);
|
||||||
|
|
||||||
const showMusic = ref(false)
|
const showPop = ref(false);
|
||||||
|
const listInfo = ref<any>(null);
|
||||||
|
|
||||||
|
const getCurrentMv = () => {
|
||||||
|
return {
|
||||||
|
id: props.item.id,
|
||||||
|
name: props.item.name,
|
||||||
|
} as unknown as IMvItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
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') {
|
||||||
|
store.commit('setIsPlay', false);
|
||||||
|
store.commit('setPlayMusic', false);
|
||||||
|
audioService.getCurrentSound()?.pause();
|
||||||
|
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,38 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="song-item" :class="{'song-mini': mini}">
|
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }">
|
||||||
<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 v-if="list" class="song-item-content-wrapper">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{
|
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||||
item.name
|
<div class="song-item-content-divider">-</div>
|
||||||
}}</n-ellipsis>
|
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
||||||
</div>
|
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||||
<div class="song-item-content-name">
|
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
|
||||||
<span
|
|
||||||
v-for="(artists, artistsindex) in item.song.artists"
|
|
||||||
:key="artistsindex"
|
|
||||||
>{{ artists.name
|
|
||||||
}}{{
|
|
||||||
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
|
|
||||||
}}</span
|
|
||||||
>
|
>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="song-item-content-title">
|
||||||
|
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
<div class="song-item-content-name">
|
||||||
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
|
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||||
|
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
|
>
|
||||||
|
</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="song-item-operating">
|
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
||||||
<div class="song-item-operating-like">
|
<div v-if="favorite" class="song-item-operating-like">
|
||||||
<i class="iconfont icon-likefill"></i>
|
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click.stop="toggleFavorite"></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 +51,90 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useStore } from 'vuex'
|
import { computed, useTemplateRef } from 'vue';
|
||||||
import type { SongResult } from '@/type/music'
|
import { useStore } from 'vuex';
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
import { audioService } from '@/services/audioService';
|
||||||
item: SongResult
|
import type { SongResult } from '@/type/music';
|
||||||
mini?: boolean
|
import { getImgUrl } from '@/utils';
|
||||||
}>(), {
|
import { getImageBackground } from '@/utils/linearColor';
|
||||||
mini: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const store = useStore()
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
item: SongResult;
|
||||||
|
mini?: boolean;
|
||||||
|
list?: boolean;
|
||||||
|
favorite?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
mini: false,
|
||||||
|
list: false,
|
||||||
|
favorite: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
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);
|
||||||
}
|
audioService.getCurrentSound()?.pause();
|
||||||
|
} else {
|
||||||
|
store.commit('setPlayMusic', true);
|
||||||
|
audioService.getCurrentSound()?.play();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await store.commit('setPlay', item);
|
||||||
|
store.commit('setIsPlay', true);
|
||||||
|
emits('play', item);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否已收藏
|
||||||
|
const isFavorite = computed(() => {
|
||||||
|
return store.state.favoriteList.includes(props.item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换收藏状态
|
||||||
|
const toggleFavorite = async (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isFavorite.value) {
|
||||||
|
store.commit('removeFromFavorite', props.item.id);
|
||||||
|
} else {
|
||||||
|
store.commit('addToFavorite', props.item.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
// 配置文字不可选中
|
// 配置文字不可选中
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -101,7 +159,7 @@ const playMusicEvent = (item: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-operating {
|
&-operating {
|
||||||
@apply flex items-center pl-4 rounded-full border border-gray-700 ml-4;
|
@apply flex items-center rounded-full border border-gray-700 ml-4;
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-xl;
|
@apply text-xl;
|
||||||
@@ -111,42 +169,83 @@ const playMusicEvent = (item: any) => {
|
|||||||
@apply text-xl hover:text-red-600 transition;
|
@apply text-xl hover:text-red-600 transition;
|
||||||
}
|
}
|
||||||
&-like {
|
&-like {
|
||||||
@apply mr-2 cursor-pointer;
|
@apply mr-2 cursor-pointer ml-4;
|
||||||
|
}
|
||||||
|
.like-active {
|
||||||
|
@apply text-red-600;
|
||||||
}
|
}
|
||||||
&-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 ml-1;
|
||||||
}
|
}
|
||||||
&-play{
|
&-play {
|
||||||
@apply w-8 h-8;
|
@apply w-8 h-8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.song-list {
|
||||||
|
@apply p-2 rounded-lg hover:bg-gray-800/50 border border-gray-800/50 mb-2;
|
||||||
|
.song-item-img {
|
||||||
|
@apply w-10 h-10 rounded-lg mr-3;
|
||||||
|
}
|
||||||
|
.song-item-content {
|
||||||
|
@apply flex items-center flex-1;
|
||||||
|
&-wrapper {
|
||||||
|
@apply flex items-center flex-1 text-sm;
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
@apply text-white flex-shrink-0 max-w-[45%];
|
||||||
|
}
|
||||||
|
&-divider {
|
||||||
|
@apply mx-2 text-gray-400;
|
||||||
|
}
|
||||||
|
&-name {
|
||||||
|
@apply text-gray-400 flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.song-item-operating {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
&-like {
|
||||||
|
@apply cursor-pointer hover:scale-110 transition-transform;
|
||||||
|
.iconfont {
|
||||||
|
@apply text-base text-gray-400 hover:text-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-play {
|
||||||
|
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
|
||||||
|
.iconfont {
|
||||||
|
@apply text-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
183
src/hooks/IndexDBHook.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 创建一个使用 IndexedDB 的组合函数
|
||||||
|
const useIndexedDB = () => {
|
||||||
|
const db = ref<IDBDatabase | null>(null); // 数据库引用
|
||||||
|
|
||||||
|
// 打开数据库并创建表
|
||||||
|
const initDB = (dbName: string, version: number, stores: { name: string; keyPath?: string }[]) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(dbName, version); // 打开数据库请求
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event: any) => {
|
||||||
|
const db = event.target.result; // 获取数据库实例
|
||||||
|
stores.forEach((store) => {
|
||||||
|
if (!db.objectStoreNames.contains(store.name)) {
|
||||||
|
// 确保对象存储(表)创建
|
||||||
|
db.createObjectStore(store.name, {
|
||||||
|
keyPath: store.keyPath || 'id',
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event: any) => {
|
||||||
|
db.value = event.target.result; // 保存数据库实例
|
||||||
|
resolve(); // 成功时解析 Promise
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event: any) => {
|
||||||
|
reject(event.target.error); // 失败时拒绝 Promise
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用新增数据
|
||||||
|
const addData = (storeName: string, value: any) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化'); // 检查数据库是否已初始化
|
||||||
|
const tx = db.value.transaction(storeName, 'readwrite'); // 创建事务
|
||||||
|
const store = tx.objectStore(storeName); // 获取对象存储
|
||||||
|
|
||||||
|
const request = store.add(value); // 添加数据请求
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log('成功'); // 成功时输出
|
||||||
|
resolve(); // 解析 Promise
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('新增失败:', (event.target as IDBRequest).error); // 输出错误
|
||||||
|
reject((event.target as IDBRequest).error); // 拒绝 Promise
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用保存数据(新增或更新)
|
||||||
|
const saveData = (storeName: string, value: any) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const request = store.put(value);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log('成功');
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject((event.target as IDBRequest).error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用获取数据
|
||||||
|
const getData = (storeName: string, key: string | number) => {
|
||||||
|
return new Promise<any>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const request = store.get(key);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
if (event.target) {
|
||||||
|
resolve((event.target as IDBRequest).result);
|
||||||
|
} else {
|
||||||
|
reject('事件目标为空');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject((event.target as IDBRequest).error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除数据
|
||||||
|
const deleteData = (storeName: string, key: string | number) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const request = store.delete(key);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log('删除成功');
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject((event.target as IDBRequest).error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查询所有数据
|
||||||
|
const getAllData = (storeName: string) => {
|
||||||
|
return new Promise<any[]>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const request = store.getAll();
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
if (event.target) {
|
||||||
|
resolve((event.target as IDBRequest).result);
|
||||||
|
} else {
|
||||||
|
reject('事件目标为空');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject((event.target as IDBRequest).error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页查询数据
|
||||||
|
const getDataWithPagination = (storeName: string, page: number, pageSize: number) => {
|
||||||
|
return new Promise<any[]>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const request = store.openCursor(); // 打开游标请求
|
||||||
|
const results: any[] = []; // 存储结果的数组
|
||||||
|
let index = 0; // 当前索引
|
||||||
|
const skip = (page - 1) * pageSize; // 计算跳过的数量
|
||||||
|
|
||||||
|
request.onsuccess = (event: any) => {
|
||||||
|
const cursor = event.target.result; // 获取游标
|
||||||
|
if (!cursor) {
|
||||||
|
resolve(results); // 如果没有更多数据,解析结果
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= skip && results.length < pageSize) {
|
||||||
|
results.push(cursor.value); // 添加当前游标值到结果
|
||||||
|
}
|
||||||
|
|
||||||
|
index++; // 增加索引
|
||||||
|
cursor.continue(); // 继续游标
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event: any) => {
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
initDB,
|
||||||
|
addData,
|
||||||
|
saveData,
|
||||||
|
getData,
|
||||||
|
deleteData,
|
||||||
|
getAllData,
|
||||||
|
getDataWithPagination,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIndexedDB;
|
||||||
@@ -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,317 @@
|
|||||||
import { getMusicLrc } from '@/api/music'
|
import { computed, ref } from 'vue';
|
||||||
import { ILyric } from '@/type/lyric'
|
|
||||||
import { getIsMc } from '@/utils'
|
|
||||||
|
|
||||||
interface ILrcData {
|
import { audioService } from '@/services/audioService';
|
||||||
text: string
|
import store from '@/store';
|
||||||
trText: string
|
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 playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
|
||||||
|
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
||||||
|
|
||||||
function parseLyricLine(lyricLine: string) {
|
document.onkeyup = (e) => {
|
||||||
// [00:00.00] 作词 : 长友美知惠/
|
switch (e.code) {
|
||||||
const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''
|
case 'Space':
|
||||||
const time = parseTime(timeText)
|
if (store.state.play) {
|
||||||
const text = lyricLine.replace(LRC_REGEX, '').trim()
|
store.commit('setPlayMusic', false);
|
||||||
return { time, text }
|
audioService.getCurrentSound()?.pause();
|
||||||
}
|
} else {
|
||||||
|
store.commit('setPlayMusic', true);
|
||||||
interface ILyricText {
|
audioService.getCurrentSound()?.play();
|
||||||
text: string
|
}
|
||||||
trText: string
|
break;
|
||||||
}
|
default:
|
||||||
|
|
||||||
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
|
watch(
|
||||||
const correctionTime = ref(0.4)
|
() => store.state.playMusicUrl,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
audioService.play(newVal);
|
||||||
|
sound.value = audioService.getCurrentSound();
|
||||||
|
audioServiceOn(audioService);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.state.playMusic,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
||||||
|
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const audioServiceOn = (audio: typeof audioService) => {
|
||||||
|
let interval: any = null;
|
||||||
|
|
||||||
|
// 监听播放
|
||||||
|
audio.onPlay(() => {
|
||||||
|
store.commit('setPlayMusic', true);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
nowTime.value = sound.value?.seek() as number;
|
||||||
|
allTime.value = sound.value?.duration() as number;
|
||||||
|
const newIndex = getLrcIndex(nowTime.value);
|
||||||
|
if (newIndex !== nowIndex.value) {
|
||||||
|
nowIndex.value = newIndex;
|
||||||
|
currentLrcProgress.value = 0;
|
||||||
|
}
|
||||||
|
if (isElectron.value) {
|
||||||
|
sendLyricToWin();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听暂停
|
||||||
|
audio.onPause(() => {
|
||||||
|
store.commit('setPlayMusic', false);
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听结束
|
||||||
|
audio.onEnd(() => {
|
||||||
|
handleEnded();
|
||||||
|
store.commit('nextPlay');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const play = () => {
|
||||||
|
audioService.getCurrentSound()?.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pause = () => {
|
||||||
|
audioService.getCurrentSound()?.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPlaying = computed(() => store.state.play as boolean);
|
||||||
|
|
||||||
// 增加矫正时间
|
// 增加矫正时间
|
||||||
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 {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 播放进度
|
||||||
|
export const useLyricProgress = () => {
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
if (!isPlaying.value) return;
|
||||||
|
const currentSound = sound.value;
|
||||||
|
if (!currentSound) return;
|
||||||
|
|
||||||
|
const { start, end } = currentLrcTiming.value;
|
||||||
|
const duration = end - start;
|
||||||
|
const elapsed = (currentSound.seek() as number) - 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.currentTime = lrcTimeArray.value[index] as number
|
const currentSound = sound.value;
|
||||||
audio.play()
|
if (!currentSound) return;
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
currentSound.seek(lrcTimeArray.value[index]);
|
||||||
lrcData,
|
currentSound.play();
|
||||||
lrcArray,
|
};
|
||||||
lrcTimeArray,
|
|
||||||
newLrcIndex,
|
// 获取当前播放的歌词
|
||||||
loadLrc,
|
export const getCurrentLrc = () => {
|
||||||
isCurrentLrc,
|
const index = getLrcIndex(nowTime.value);
|
||||||
addCorrectionTime,
|
return {
|
||||||
reduceCorrectionTime,
|
currentLrc: lrcArray.value[index],
|
||||||
setAudioTime,
|
nextLrc: lrcArray.value[index + 1],
|
||||||
nowTime,
|
};
|
||||||
allTime,
|
};
|
||||||
}
|
|
||||||
|
// 获取一句歌词播放时间几秒到几秒
|
||||||
|
export const getLrcTimeRange = (index: number) => ({
|
||||||
|
currentTime: lrcTimeArray.value[index],
|
||||||
|
nextTime: lrcTimeArray.value[index + 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听歌词数组变化,当切换歌曲时重新初始化歌词窗口
|
||||||
|
watch(
|
||||||
|
() => lrcArray.value,
|
||||||
|
(newLrcArray) => {
|
||||||
|
if (newLrcArray.length > 0 && isElectron.value) {
|
||||||
|
// 重新初始化歌词数据
|
||||||
|
initLyricWindow();
|
||||||
|
// 发送当前状态
|
||||||
|
sendLyricToWin();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听播放状态变化
|
||||||
|
watch(isPlaying, (newIsPlaying) => {
|
||||||
|
if (isElectron.value) {
|
||||||
|
sendLyricToWin(newIsPlaying);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理歌曲结束
|
||||||
|
export const handleEnded = () => {
|
||||||
|
if (isElectron.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initLyricWindow();
|
||||||
|
sendLyricToWin();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化歌词数据
|
||||||
|
export const initLyricWindow = () => {
|
||||||
|
if (!isElectron.value) return;
|
||||||
|
try {
|
||||||
|
if (lrcArray.value.length > 0) {
|
||||||
|
console.log('Initializing lyric window with data:', {
|
||||||
|
lrcArray: lrcArray.value,
|
||||||
|
lrcTimeArray: lrcTimeArray.value,
|
||||||
|
allTime: allTime.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const staticData = {
|
||||||
|
type: 'init',
|
||||||
|
lrcArray: lrcArray.value,
|
||||||
|
lrcTimeArray: lrcTimeArray.value,
|
||||||
|
allTime: allTime.value,
|
||||||
|
};
|
||||||
|
windowData.electronAPI.sendLyric(JSON.stringify(staticData));
|
||||||
|
} else {
|
||||||
|
console.log('No lyrics available for initialization');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing lyric window:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送歌词更新数据
|
||||||
|
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||||
|
if (!isElectron.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (lrcArray.value.length > 0) {
|
||||||
|
const nowIndex = getLrcIndex(nowTime.value);
|
||||||
|
const updateData = {
|
||||||
|
type: 'update',
|
||||||
|
nowIndex,
|
||||||
|
nowTime: nowTime.value,
|
||||||
|
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||||
|
nextTime: lrcTimeArray.value[nowIndex + 1],
|
||||||
|
isPlay,
|
||||||
|
};
|
||||||
|
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending lyric update:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openLyric = () => {
|
||||||
|
if (!isElectron.value) return;
|
||||||
|
console.log('Opening lyric window');
|
||||||
|
windowData.electronAPI.openLyric();
|
||||||
|
|
||||||
|
// 延迟一下初始化,确保窗口已经创建
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Initializing lyric window after delay');
|
||||||
|
initLyricWindow();
|
||||||
|
sendLyricToWin();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|||||||
191
src/hooks/MusicListHook.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { Howl } from 'howler';
|
||||||
|
|
||||||
|
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||||
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
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) => {
|
||||||
|
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;
|
||||||
|
// 设置网页标题
|
||||||
|
document.title = `${updatedPlayMusic.name} - ${updatedPlayMusic?.song?.artists?.reduce((prev, curr) => `${prev}${curr.name}/`, '')}`;
|
||||||
|
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 sound = new Howl({
|
||||||
|
src: [nextSongUrl],
|
||||||
|
html5: true,
|
||||||
|
preload: true,
|
||||||
|
autoplay: false,
|
||||||
|
});
|
||||||
|
return sound;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
audioService.getCurrentSound()?.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
audioService.getCurrentSound()?.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handlePlayMusic,
|
||||||
|
nextPlay,
|
||||||
|
prevPlay,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,106 @@
|
|||||||
<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>
|
||||||
|
<install-app-modal></install-app-modal>
|
||||||
|
</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 InstallAppModal from '@/components/common/InstallAppModal.vue';
|
||||||
|
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 route = useRoute();
|
||||||
|
|
||||||
const route = useRoute()
|
const backgroundColor = ref('#000');
|
||||||
|
|
||||||
const audio = {
|
|
||||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowData = window as any
|
|
||||||
const isElectron = computed(() => {
|
|
||||||
return !!windowData.electronAPI
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 监听音乐是否播放
|
|
||||||
watch(
|
|
||||||
() => play.value,
|
|
||||||
value => {
|
|
||||||
if (value && audio.value) {
|
|
||||||
audioPlay()
|
|
||||||
} else {
|
|
||||||
audioPause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
document.onkeyup = (e) => {
|
|
||||||
switch (e.code) {
|
|
||||||
case 'Space':
|
|
||||||
playMusicEvent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 按下键盘按钮监听
|
|
||||||
document.onkeydown = (e) => {
|
|
||||||
switch (e.code) {
|
|
||||||
case 'Space':
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const audioPlay = () => {
|
|
||||||
if (audio.value) {
|
|
||||||
audio.value.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioPause = () => {
|
|
||||||
if (audio.value) {
|
|
||||||
audio.value.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const playMusicEvent = async () => {
|
|
||||||
if (play.value) {
|
|
||||||
store.commit('setPlayMusic', false)
|
|
||||||
} else {
|
|
||||||
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>
|
||||||
|
|||||||
@@ -3,113 +3,196 @@
|
|||||||
:show="musicFull"
|
:show="musicFull"
|
||||||
height="100vh"
|
height="100vh"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:drawer-style="{ backgroundColor: 'transparent' }"
|
:style="{ background: currentBackground || background }"
|
||||||
>
|
>
|
||||||
<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.ar || playMusic.song.artists" :key="index">
|
||||||
preview-disabled
|
{{ item.name }}{{ index < (playMusic.ar || 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, 'hover-text': item.text }"
|
||||||
@click="setAudioTime(index, audio)"
|
@click="setAudioTime(index)"
|
||||||
>
|
>
|
||||||
{{ 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 { onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import {
|
|
||||||
lrcArray,
|
|
||||||
newLrcIndex,
|
|
||||||
isCurrentLrc,
|
|
||||||
addCorrectionTime,
|
|
||||||
reduceCorrectionTime,
|
|
||||||
setAudioTime,
|
|
||||||
nowTime,
|
|
||||||
} from '@/hooks/MusicHook'
|
|
||||||
|
|
||||||
const store = useStore()
|
import { lrcArray, nowIndex, playMusic, setAudioTime, useLyricProgress } from '@/hooks/MusicHook';
|
||||||
|
import { getImgUrl } from '@/utils';
|
||||||
|
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
|
||||||
|
|
||||||
|
// 定义 refs
|
||||||
|
const lrcSider = ref<any>(null);
|
||||||
|
const isMouse = ref(false);
|
||||||
|
const lrcContainer = ref<HTMLElement | null>(null);
|
||||||
|
const currentBackground = ref('');
|
||||||
|
const animationFrame = ref<number | null>(null);
|
||||||
|
const isDark = ref(false);
|
||||||
|
|
||||||
|
// 初始化 textColors
|
||||||
|
const textColors = ref(getTextColors());
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
musicFull: {
|
musicFull: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
audio: {
|
background: {
|
||||||
type: HTMLAudioElement,
|
type: String,
|
||||||
default: null,
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听背景变化
|
||||||
|
watch(
|
||||||
|
() => props.background,
|
||||||
|
(newBg) => {
|
||||||
|
if (!newBg) {
|
||||||
|
textColors.value = getTextColors();
|
||||||
|
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));
|
||||||
|
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||||
|
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBackground.value) {
|
||||||
|
if (animationFrame.value) {
|
||||||
|
cancelAnimationFrame(animationFrame.value);
|
||||||
|
}
|
||||||
|
animationFrame.value = animateGradient(currentBackground.value, newBg, (gradient) => {
|
||||||
|
currentBackground.value = gradient;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentBackground.value = newBg;
|
||||||
|
}
|
||||||
|
|
||||||
|
textColors.value = getTextColors(newBg);
|
||||||
|
isDark.value = textColors.value.active === '#000000';
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(isDark.value));
|
||||||
|
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||||
|
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 修改 useLyricProgress 的使用方式
|
||||||
|
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
|
||||||
|
|
||||||
|
// 修改 getLrcStyle 函数
|
||||||
|
const getLrcStyle = (index: number) => {
|
||||||
|
const colors = textColors.value || getTextColors;
|
||||||
|
const originalStyle = originalLrcStyle(index);
|
||||||
|
|
||||||
|
if (index === nowIndex.value) {
|
||||||
|
// 当前播放的歌词,使用渐变效果
|
||||||
|
return {
|
||||||
|
...originalStyle,
|
||||||
|
backgroundImage: originalStyle.backgroundImage
|
||||||
|
?.replace(/#ffffff/g, colors.active)
|
||||||
|
.replace(/#ffffff8a/g, `${colors.primary}`),
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
color: 'transparent',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非当前播放的歌词,使用普通颜色
|
||||||
|
return {
|
||||||
|
color: colors.primary,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理动画
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (animationFrame.value) {
|
||||||
|
cancelAnimationFrame(animationFrame.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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 +201,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,56 +215,90 @@ 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);
|
|
||||||
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;
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
||||||
&-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;
|
transition: all 0.3s ease;
|
||||||
transition: all 0.2s ease-out;
|
background-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
span {
|
||||||
@apply font-bold text-xl text-red-500;
|
background-clip: text !important;
|
||||||
|
-webkit-background-clip: text !important;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tr {
|
||||||
|
@apply font-normal;
|
||||||
|
opacity: 0.7;
|
||||||
|
color: var(--text-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.now-text {
|
.hover-text {
|
||||||
@apply font-bold text-xl text-red-500;
|
&:hover {
|
||||||
|
@apply font-bold opacity-100 rounded-xl;
|
||||||
|
background-color: var(--hover-bg-color);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--text-color-active) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
#drawer-target {
|
||||||
|
@apply flex-col p-4 pt-8 justify-start;
|
||||||
|
.music-img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.music-lrc {
|
||||||
|
height: calc(100vh - 260px) !important;
|
||||||
|
width: 100vw;
|
||||||
|
span {
|
||||||
|
padding-right: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.music-lrc-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-drawer {
|
||||||
|
transition: none; // 移除之前的过渡效果,现在使用 JS 动画
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 展开全屏 -->
|
<!-- 展开全屏 -->
|
||||||
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio.value as HTMLAudioElement)" />
|
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||||
<!-- 底部播放栏 -->
|
<!-- 底部播放栏 -->
|
||||||
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
|
<div
|
||||||
<n-image
|
class="music-play-bar"
|
||||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
:class="setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')"
|
||||||
class="play-bar-img"
|
>
|
||||||
lazy
|
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
||||||
preview-disabled
|
<n-image :src="getImgUrl(playMusic?.picUrl, '300y300')" class="play-bar-img" lazy preview-disabled />
|
||||||
@click="setMusicFull"
|
<div class="hover-arrow">
|
||||||
/>
|
<div class="hover-content">
|
||||||
|
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
|
||||||
|
<i class="text-3xl" :class="musicFullVisible ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'"></i>
|
||||||
|
<span class="hover-text">{{ musicFullVisible ? '收起' : '展开' }}歌词</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="music-content">
|
<div class="music-content">
|
||||||
<div class="music-content-title">
|
<div class="music-content-title">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
@@ -18,42 +24,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<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="(artists, artistsindex) in playMusic.ar || playMusic.song.artists" :key="artistsindex"
|
||||||
{{ item.name
|
>{{ artists.name
|
||||||
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
}}{{ artistsindex < (playMusic.ar || 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 custom-slider">
|
||||||
<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 custom-slider">
|
||||||
<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 +66,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 +92,126 @@
|
|||||||
</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 { useThrottleFn } from '@vueuse/core';
|
||||||
import { secondToMinute, getImgUrl } from '@/utils'
|
import { useTemplateRef } from 'vue';
|
||||||
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, isElectron, nowTime, openLyric, sound } 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 background = ref('#000');
|
||||||
|
|
||||||
const audio = {
|
|
||||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = () => {
|
// 使用 useThrottleFn 创建节流版本的 seek 函数
|
||||||
if (audio.value) {
|
const throttledSeek = useThrottleFn((value: number) => {
|
||||||
audio.value.play()
|
if (!sound.value) return;
|
||||||
}
|
sound.value.seek((value * allTime.value) / 100);
|
||||||
}
|
}, 50); // 50ms 的节流延迟
|
||||||
|
|
||||||
const audioPause = () => {
|
// 修改 timeSlider 计算属性
|
||||||
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: throttledSeek,
|
||||||
if (!audio.value) return
|
});
|
||||||
audio.value.currentTime = (value * allTime.value) / 100
|
|
||||||
audioPlay()
|
|
||||||
store.commit('setPlayMusic', true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 音量条
|
// 音量条
|
||||||
const audioVolume = ref(1)
|
const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1);
|
||||||
const volumeSlider = computed({
|
const volumeSlider = computed({
|
||||||
get: () => audioVolume.value * 100,
|
get: () => audioVolume.value * 100,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if(!audio.value) return
|
if (!sound.value) return;
|
||||||
audio.value.volume = value / 100
|
localStorage.setItem('volume', (value / 100).toString());
|
||||||
|
sound.value.volume(value / 100);
|
||||||
|
audioVolume.value = 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 = () => {
|
|
||||||
if(audio.value){
|
|
||||||
audio.value.removeEventListener('timeupdate', handleGetAudioTime)
|
|
||||||
audio.value.removeEventListener('ended', handleEnded)
|
|
||||||
audio.value.addEventListener('timeupdate', handleGetAudioTime)
|
|
||||||
audio.value.addEventListener('ended', handleEnded)
|
|
||||||
// 监听音乐播放暂停
|
|
||||||
audio.value.addEventListener('pause', () => {
|
|
||||||
store.commit('setPlayMusic', false)
|
|
||||||
})
|
|
||||||
audio.value.addEventListener('play', () => {
|
|
||||||
store.commit('setPlayMusic', true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// 监听音频播放的实时时间事件
|
|
||||||
const audio = this as HTMLAudioElement
|
|
||||||
// 获取当前播放时间
|
|
||||||
nowTime.value = Math.floor(audio.currentTime)
|
|
||||||
// 获取总时间
|
|
||||||
allTime.value = audio.duration
|
|
||||||
// 获取音量
|
|
||||||
audioVolume.value = audio.volume
|
|
||||||
MusicFullRef.value?.lrcScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放暂停按钮事件
|
// 播放暂停按钮事件
|
||||||
const playMusicEvent = async () => {
|
const playMusicEvent = async () => {
|
||||||
if (play.value) {
|
if (play.value) {
|
||||||
store.commit('setPlayMusic', false)
|
if (sound.value) {
|
||||||
|
sound.value.pause();
|
||||||
|
}
|
||||||
|
store.commit('setPlayMusic', false);
|
||||||
} else {
|
} else {
|
||||||
store.commit('setPlayMusic', true)
|
if (sound.value) {
|
||||||
|
sound.value.play();
|
||||||
|
}
|
||||||
|
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 +220,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;
|
||||||
|
|
||||||
@@ -252,12 +231,16 @@ const setMusicFull = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply text-xs mt-1;
|
@apply text-xs mt-1 text-gray-100;
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 +263,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 +293,118 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自定义 slider 样式
|
||||||
|
.custom-slider {
|
||||||
|
:deep(.n-slider) {
|
||||||
|
--n-rail-height: 4px;
|
||||||
|
--n-rail-color: rgba(255, 255, 255, 0.2);
|
||||||
|
--n-fill-color: var(--primary-color);
|
||||||
|
--n-handle-size: 12px;
|
||||||
|
--n-handle-color: var(--primary-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--n-rail-height: 6px;
|
||||||
|
--n-handle-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-rail {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-handle {
|
||||||
|
@apply transition-opacity duration-200;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .n-slider-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-bar-img-wrapper {
|
||||||
|
@apply relative cursor-pointer w-14 h-14;
|
||||||
|
|
||||||
|
.hover-arrow {
|
||||||
|
@apply absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-300 rounded-2xl;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
.hover-content {
|
||||||
|
@apply flex flex-col items-center justify-center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
@apply text-white mb-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-text {
|
||||||
|
@apply text-white text-xs scale-90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.hover-arrow {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
@apply text-sm py-1 px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-bar-img {
|
||||||
|
@apply w-14 h-14 rounded-2xl;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,168 +1,174 @@
|
|||||||
<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>
|
<coffee
|
||||||
</template>
|
alipay-q-r="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true"
|
||||||
|
wechat-q-r="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true"
|
||||||
<script lang="ts" setup>
|
>
|
||||||
import { getSearchKeyword } from '@/api/home';
|
<div class="github" @click="toGithub">
|
||||||
import { getUserDetail, logout } from '@/api/login';
|
<i class="ri-github-fill"></i>
|
||||||
import { useRouter } from 'vue-router';
|
</div>
|
||||||
import { useStore } from 'vuex';
|
</coffee>
|
||||||
import request from '@/utils/request_mt'
|
</div>
|
||||||
import { getImgUrl } from '@/utils';
|
</template>
|
||||||
import {USER_SET_OPTIONS, SEARCH_TYPES} from '@/const/bar-const'
|
|
||||||
|
<script lang="ts" setup>
|
||||||
const router = useRouter()
|
import { useRouter } from 'vue-router';
|
||||||
const store = useStore();
|
import { useStore } from 'vuex';
|
||||||
const userSetOptions = ref(USER_SET_OPTIONS)
|
|
||||||
|
import { getSearchKeyword } from '@/api/home';
|
||||||
|
import { getUserDetail, logout } from '@/api/login';
|
||||||
// 推荐热搜词
|
import Coffee from '@/components/Coffee.vue';
|
||||||
const hotSearchKeyword = ref("搜索点什么吧...")
|
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||||
const hotSearchValue = ref("")
|
import { getImgUrl } from '@/utils';
|
||||||
const loadHotSearchKeyword = async () => {
|
|
||||||
const { data } = await getSearchKeyword();
|
const router = useRouter();
|
||||||
hotSearchKeyword.value = data.data.showKeyword
|
const store = useStore();
|
||||||
hotSearchValue.value = data.data.realkeyword
|
const userSetOptions = ref(USER_SET_OPTIONS);
|
||||||
}
|
|
||||||
|
// 推荐热搜词
|
||||||
const loadPage = async () => {
|
const hotSearchKeyword = ref('搜索点什么吧...');
|
||||||
const token = localStorage.getItem("token")
|
const hotSearchValue = ref('');
|
||||||
if (!token) return
|
const loadHotSearchKeyword = async () => {
|
||||||
const { data } = await getUserDetail()
|
const { data } = await getSearchKeyword();
|
||||||
store.state.user = data.profile
|
hotSearchKeyword.value = data.data.showKeyword;
|
||||||
localStorage.setItem('user', JSON.stringify(data.profile))
|
hotSearchValue.value = data.data.realkeyword;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const loadPage = async () => {
|
||||||
watchEffect(() => {
|
const token = localStorage.getItem('token');
|
||||||
loadPage()
|
if (!token) return;
|
||||||
if (store.state.user) {
|
const { data } = await getUserDetail();
|
||||||
userSetOptions.value = USER_SET_OPTIONS
|
store.state.user = data.profile;
|
||||||
} else {
|
localStorage.setItem('user', JSON.stringify(data.profile));
|
||||||
userSetOptions.value = USER_SET_OPTIONS.filter(item => item.key !== 'logout')
|
};
|
||||||
}
|
|
||||||
})
|
loadPage();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
const toLogin = () => {
|
if (store.state.user) {
|
||||||
router.push('/login')
|
userSetOptions.value = USER_SET_OPTIONS;
|
||||||
}
|
} else {
|
||||||
|
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
|
||||||
// 页面初始化
|
}
|
||||||
onMounted(() => {
|
});
|
||||||
loadHotSearchKeyword()
|
|
||||||
loadPage()
|
const toLogin = () => {
|
||||||
})
|
router.push('/login');
|
||||||
|
};
|
||||||
|
|
||||||
// 搜索词
|
// 页面初始化
|
||||||
const searchValue = ref("")
|
onMounted(() => {
|
||||||
const searchType = ref(1)
|
loadHotSearchKeyword();
|
||||||
const search = () => {
|
loadPage();
|
||||||
let value = searchValue.value
|
});
|
||||||
if (value == "") {
|
|
||||||
searchValue.value = hotSearchValue.value
|
// 搜索词
|
||||||
} else {
|
const searchValue = ref('');
|
||||||
router.push({
|
const search = () => {
|
||||||
path: "/search",
|
const { value } = searchValue;
|
||||||
query: {
|
if (value === '') {
|
||||||
keyword: value,
|
searchValue.value = hotSearchValue.value;
|
||||||
type: searchType.value
|
return;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
if (router.currentRoute.value.path === '/search') {
|
||||||
}
|
store.state.searchValue = value;
|
||||||
|
return;
|
||||||
const selectSearchType = (key: any) => {
|
}
|
||||||
searchType.value = key
|
|
||||||
}
|
router.push({
|
||||||
|
path: '/search',
|
||||||
|
query: {
|
||||||
const searchTypeOptions = ref(SEARCH_TYPES)
|
keyword: value,
|
||||||
|
},
|
||||||
const selectItem = async (key: any) => {
|
});
|
||||||
// switch 判断
|
};
|
||||||
switch (key) {
|
|
||||||
case 'card':
|
const selectSearchType = (key: number) => {
|
||||||
await request.get('/?do=sign')
|
store.state.searchType = key;
|
||||||
.then(res => {
|
};
|
||||||
console.log(res)
|
|
||||||
})
|
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||||
break;
|
|
||||||
case 'card_music':
|
const selectItem = async (key: string) => {
|
||||||
await request.get('/?do=daka')
|
// switch 判断
|
||||||
.then(res => {
|
switch (key) {
|
||||||
console.log(res)
|
case 'logout':
|
||||||
})
|
logout().then(() => {
|
||||||
break;
|
store.state.user = null;
|
||||||
case 'listen':
|
localStorage.clear();
|
||||||
await request.get('/?do=listen&id=1885175990&time=300')
|
router.push('/login');
|
||||||
.then(res => {
|
});
|
||||||
console.log(res)
|
break;
|
||||||
})
|
case 'login':
|
||||||
break;
|
router.push('/login');
|
||||||
case 'logout':
|
break;
|
||||||
logout().then(() => {
|
case 'set':
|
||||||
store.state.user = null
|
router.push('/set');
|
||||||
localStorage.clear()
|
break;
|
||||||
})
|
default:
|
||||||
break;
|
}
|
||||||
case 'login':
|
};
|
||||||
router.push("/login")
|
|
||||||
break;
|
const toGithub = () => {
|
||||||
case 'set':
|
window.open('https://github.com/algerkong/AlgerMusicPlayer', '_blank');
|
||||||
router.push("/set")
|
};
|
||||||
break;
|
</script>
|
||||||
}
|
|
||||||
}
|
<style lang="scss" scoped>
|
||||||
|
.user-box {
|
||||||
</script>
|
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
||||||
|
background: #1a1a1a;
|
||||||
<style lang="scss" scoped>
|
}
|
||||||
.user-box {
|
.search-box {
|
||||||
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
@apply pb-4 pr-4;
|
||||||
background: #1a1a1a;
|
}
|
||||||
}
|
.search-box-input {
|
||||||
.search-box{
|
@apply relative;
|
||||||
@apply pb-4 pr-4;
|
}
|
||||||
}
|
|
||||||
.search-box-input {
|
.mobile {
|
||||||
@apply relative;
|
.search-box {
|
||||||
}
|
@apply pl-4;
|
||||||
</style>
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.github {
|
||||||
|
@apply cursor-pointer text-gray-100 hover:text-gray-400 text-xl ml-4 rounded-full border border-gray-600 flex justify-center items-center px-2 h-full;
|
||||||
|
}
|
||||||
|
</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 as keyof typeof directives]);
|
||||||
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,19 +46,29 @@ 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'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/favorite',
|
||||||
|
name: 'favorite',
|
||||||
|
component: () => import('@/views/favorite/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '我的收藏',
|
||||||
|
icon: 'icon-likefill',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: '/user',
|
||||||
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(),
|
||||||
})
|
});
|
||||||
|
|||||||
55
src/services/audioService.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Howl } from 'howler';
|
||||||
|
|
||||||
|
class AudioService {
|
||||||
|
private currentSound: Howl | null = null;
|
||||||
|
|
||||||
|
play(url: string) {
|
||||||
|
if (this.currentSound) {
|
||||||
|
this.currentSound.unload();
|
||||||
|
}
|
||||||
|
this.currentSound = null;
|
||||||
|
this.currentSound = new Howl({
|
||||||
|
src: [url],
|
||||||
|
html5: true,
|
||||||
|
autoplay: true,
|
||||||
|
volume: localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.currentSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentSound() {
|
||||||
|
return this.currentSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.currentSound) {
|
||||||
|
this.currentSound.stop();
|
||||||
|
this.currentSound.unload();
|
||||||
|
this.currentSound = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听播放
|
||||||
|
onPlay(callback: () => void) {
|
||||||
|
if (this.currentSound) {
|
||||||
|
this.currentSound.on('play', callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听暂停
|
||||||
|
onPause(callback: () => void) {
|
||||||
|
if (this.currentSound) {
|
||||||
|
this.currentSound.on('pause', callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听结束
|
||||||
|
onEnd(callback: () => void) {
|
||||||
|
if (this.currentSound) {
|
||||||
|
this.currentSound.on('end', callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audioService = new AudioService();
|
||||||
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,33 @@
|
|||||||
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'
|
|
||||||
|
// 默认设置
|
||||||
|
const defaultSettings = {
|
||||||
|
isProxy: false,
|
||||||
|
noAnimate: false,
|
||||||
|
animationSpeed: 1,
|
||||||
|
author: 'Alger',
|
||||||
|
authorUrl: 'https://github.com/algerkong',
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
favoriteList: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@@ -23,89 +36,88 @@ 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: defaultSettings,
|
||||||
}
|
lyric: {},
|
||||||
|
isMobile: false,
|
||||||
|
searchValue: '',
|
||||||
|
searchType: 1,
|
||||||
|
favoriteList: localStorage.getItem('favoriteList') ? JSON.parse(localStorage.getItem('favoriteList') || '[]') : [],
|
||||||
|
};
|
||||||
|
|
||||||
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) {
|
setSetData(state: State, setData: any) {
|
||||||
state.setData = setData
|
state.setData = setData;
|
||||||
windowData.electron.ipcRenderer.setStoreValue(
|
const isElectron = (window as any).electronAPI !== undefined;
|
||||||
'set',
|
if (isElectron) {
|
||||||
JSON.parse(JSON.stringify(setData))
|
(window as any).electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
|
||||||
)
|
} else {
|
||||||
},
|
localStorage.setItem('appSettings', 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)
|
addToFavorite(state: State, songId: number) {
|
||||||
}
|
if (!state.favoriteList.includes(songId)) {
|
||||||
url = url ? url : data.data[0].url
|
state.favoriteList = [songId, ...state.favoriteList];
|
||||||
return getMusicProxyUrl(url)
|
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
removeFromFavorite(state: State, songId: number) {
|
||||||
|
state.favoriteList = state.favoriteList.filter((id) => id !== songId);
|
||||||
|
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const updatePlayMusic = async (state: State) => {
|
const actions = {
|
||||||
state.playMusic = state.playList[state.playListIndex]
|
initializeSettings({ commit }: { commit: any }) {
|
||||||
state.playMusicUrl = await getSongUrl(state.playMusic.id)
|
const isElectron = (window as any).electronAPI !== undefined;
|
||||||
state.play = true
|
|
||||||
musicHistory.addMusic(state.playMusic)
|
if (isElectron) {
|
||||||
}
|
const setData = (window as any).electron.ipcRenderer.getStoreValue('set');
|
||||||
|
commit('setSetData', setData || defaultSettings);
|
||||||
|
} else {
|
||||||
|
const savedSettings = localStorage.getItem('appSettings');
|
||||||
|
if (savedSettings) {
|
||||||
|
commit('setSetData', {
|
||||||
|
...defaultSettings,
|
||||||
|
...JSON.parse(savedSettings),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
commit('setSetData', defaultSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: state,
|
state,
|
||||||
mutations: mutations,
|
mutations,
|
||||||
})
|
actions,
|
||||||
|
});
|
||||||
|
|
||||||
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,216 @@
|
|||||||
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;
|
ar: Artist[];
|
||||||
downloadMaxbr: number;
|
}
|
||||||
rscl?: any;
|
|
||||||
freeTrialPrivilege: FreeTrialPrivilege;
|
interface Privilege {
|
||||||
chargeInfoList: ChargeInfoList[];
|
id: number;
|
||||||
}
|
fee: number;
|
||||||
|
payed: number;
|
||||||
interface ChargeInfoList {
|
st: number;
|
||||||
rate: number;
|
pl: number;
|
||||||
chargeUrl?: any;
|
dl: number;
|
||||||
chargeMessage?: any;
|
sp: number;
|
||||||
chargeType: number;
|
cp: number;
|
||||||
}
|
subp: number;
|
||||||
|
cs: boolean;
|
||||||
interface FreeTrialPrivilege {
|
maxbr: number;
|
||||||
resConsumable: boolean;
|
fl: number;
|
||||||
userConsumable: boolean;
|
toast: boolean;
|
||||||
}
|
flag: number;
|
||||||
|
preSell: boolean;
|
||||||
interface BMusic {
|
playMaxbr: number;
|
||||||
name?: any;
|
downloadMaxbr: number;
|
||||||
id: number;
|
rscl?: any;
|
||||||
size: number;
|
freeTrialPrivilege: FreeTrialPrivilege;
|
||||||
extension: string;
|
chargeInfoList: ChargeInfoList[];
|
||||||
sr: number;
|
}
|
||||||
dfsId: number;
|
|
||||||
bitrate: number;
|
interface ChargeInfoList {
|
||||||
playTime: number;
|
rate: number;
|
||||||
volumeDelta: number;
|
chargeUrl?: any;
|
||||||
}
|
chargeMessage?: any;
|
||||||
|
chargeType: number;
|
||||||
interface Album {
|
}
|
||||||
name: string;
|
|
||||||
id: number;
|
interface FreeTrialPrivilege {
|
||||||
type: string;
|
resConsumable: boolean;
|
||||||
size: number;
|
userConsumable: boolean;
|
||||||
picId: number;
|
}
|
||||||
blurPicUrl: string;
|
|
||||||
companyId: number;
|
interface BMusic {
|
||||||
pic: number;
|
name?: any;
|
||||||
picUrl: string;
|
id: number;
|
||||||
publishTime: number;
|
size: number;
|
||||||
description: string;
|
extension: string;
|
||||||
tags: string;
|
sr: number;
|
||||||
company: string;
|
dfsId: number;
|
||||||
briefDesc: string;
|
bitrate: number;
|
||||||
artist: Artist;
|
playTime: number;
|
||||||
songs: any[];
|
volumeDelta: number;
|
||||||
alias: string[];
|
}
|
||||||
status: number;
|
|
||||||
copyrightId: number;
|
interface Album {
|
||||||
commentThreadId: string;
|
name: string;
|
||||||
artists: Artist[];
|
id: number;
|
||||||
subType: string;
|
type: string;
|
||||||
transName?: any;
|
size: number;
|
||||||
onSale: boolean;
|
picId: number;
|
||||||
mark: number;
|
blurPicUrl: string;
|
||||||
picId_str: string;
|
companyId: number;
|
||||||
}
|
pic: number;
|
||||||
|
picUrl: string;
|
||||||
interface Artist {
|
publishTime: number;
|
||||||
name: string;
|
description: string;
|
||||||
id: number;
|
tags: string;
|
||||||
picId: number;
|
company: string;
|
||||||
img1v1Id: number;
|
briefDesc: string;
|
||||||
briefDesc: string;
|
artist: Artist;
|
||||||
picUrl: string;
|
songs: any[];
|
||||||
img1v1Url: string;
|
alias: string[];
|
||||||
albumSize: number;
|
status: number;
|
||||||
alias: any[];
|
copyrightId: number;
|
||||||
trans: string;
|
commentThreadId: string;
|
||||||
musicSize: number;
|
artists: Artist[];
|
||||||
topicPerson: number;
|
subType: string;
|
||||||
}
|
transName?: any;
|
||||||
|
onSale: boolean;
|
||||||
export interface IPlayMusicUrl {
|
mark: number;
|
||||||
data: Datum[];
|
picId_str: string;
|
||||||
code: number;
|
}
|
||||||
}
|
|
||||||
|
interface Artist {
|
||||||
interface Datum {
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
url: string;
|
picId: number;
|
||||||
br: number;
|
img1v1Id: number;
|
||||||
size: number;
|
briefDesc: string;
|
||||||
md5: string;
|
picUrl: string;
|
||||||
code: number;
|
img1v1Url: string;
|
||||||
expi: number;
|
albumSize: number;
|
||||||
type: string;
|
alias: any[];
|
||||||
gain: number;
|
trans: string;
|
||||||
fee: number;
|
musicSize: number;
|
||||||
uf?: any;
|
topicPerson: number;
|
||||||
payed: number;
|
}
|
||||||
flag: number;
|
|
||||||
canExtend: boolean;
|
export interface IPlayMusicUrl {
|
||||||
freeTrialInfo?: any;
|
data: Datum[];
|
||||||
level: string;
|
code: number;
|
||||||
encodeType: string;
|
}
|
||||||
freeTrialPrivilege: FreeTrialPrivilege;
|
|
||||||
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
|
interface Datum {
|
||||||
urlSource: number;
|
id: number;
|
||||||
}
|
url: string;
|
||||||
|
br: number;
|
||||||
interface FreeTimeTrialPrivilege {
|
size: number;
|
||||||
resConsumable: boolean;
|
md5: string;
|
||||||
userConsumable: boolean;
|
code: number;
|
||||||
type: number;
|
expi: number;
|
||||||
remainTime: number;
|
type: string;
|
||||||
}
|
gain: number;
|
||||||
|
fee: number;
|
||||||
interface FreeTrialPrivilege {
|
uf?: any;
|
||||||
resConsumable: boolean;
|
payed: number;
|
||||||
userConsumable: boolean;
|
flag: number;
|
||||||
}
|
canExtend: boolean;
|
||||||
|
freeTrialInfo?: any;
|
||||||
|
level: string;
|
||||||
|
encodeType: string;
|
||||||
|
freeTrialPrivilege: FreeTrialPrivilege;
|
||||||
|
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
|
||||||
|
urlSource: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FreeTimeTrialPrivilege {
|
||||||
|
resConsumable: boolean;
|
||||||
|
userConsumable: boolean;
|
||||||
|
type: number;
|
||||||
|
remainTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FreeTrialPrivilege {
|
||||||
|
resConsumable: boolean;
|
||||||
|
userConsumable: boolean;
|
||||||
|
}
|
||||||
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||