Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba992b7c33 | ||
|
|
24d7c839c7 | ||
|
|
a4f3df80c9 | ||
|
|
866fec6ee3 | ||
|
|
8f7d6fbb8d | ||
|
|
62e26cae7d | ||
|
|
ddb814da10 | ||
|
|
e266ea8ef8 | ||
|
|
a894954641 | ||
|
|
f640ab9969 | ||
|
|
9eb17fd978 | ||
|
|
020aca7384 | ||
|
|
fcc47dc0ff | ||
|
|
17ce268da6 | ||
|
|
43c64b1b43 | ||
|
|
11ced6b418 | ||
|
|
3d3992154a | ||
|
|
81e7b67c7f | ||
|
|
d7e94a342b | ||
|
|
46f8067577 | ||
|
|
1dc7d0ceca | ||
|
|
ba64631a17 | ||
|
|
cdb9524f04 | ||
|
|
5213aa13c5 | ||
|
|
d870d0198f | ||
|
|
976a9afd2f | ||
|
|
018218a5bf | ||
|
|
38a9d6ed31 | ||
|
|
8dab799939 | ||
|
|
1ddbe6f24e | ||
|
|
4d5bcba6c7 | ||
|
|
f833306b60 | ||
|
|
4d92ed9963 | ||
|
|
a22285156a | ||
|
|
d1029f16d6 | ||
|
|
4908555635 | ||
|
|
750cf7a484 | ||
|
|
a334743f6f | ||
|
|
14747cac10 | ||
|
|
cc239aeaba | ||
|
|
eeda296589 | ||
|
|
edb7ea201c | ||
|
|
17d20fa299 | ||
|
|
f8d421c9b1 | ||
|
|
dfdf02a17f | ||
|
|
abdb2bcd50 | ||
|
|
f728191a8f | ||
|
|
dfa8b51a53 | ||
|
|
b2c13121fd | ||
|
|
d28adb61a4 | ||
|
|
9a7d5a3834 | ||
|
|
2037798fbe | ||
|
|
85bd0ad015 | ||
|
|
e1557a51a3 | ||
|
|
1ecc6f136f | ||
|
|
53b3061b03 | ||
|
|
3d2f6a2330 | ||
|
|
3b1470f28f | ||
|
|
100268448a | ||
|
|
51f67bb2c2 | ||
|
|
7be126cf5f | ||
|
|
f2f5d3ac15 | ||
|
|
34c45e0105 | ||
|
|
f9333f5f78 | ||
|
|
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
|
|
||||||
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.gitignore
|
||||||
135
.eslintrc.cjs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'eslint-config-airbnb-base',
|
||||||
|
'@vue/typescript/recommended',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:vue-scoped-css/base',
|
||||||
|
'@electron-toolkit',
|
||||||
|
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||||
|
'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: {
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'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',
|
||||||
|
'@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',
|
||||||
|
'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']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
// 需要行尾分号
|
||||||
|
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'dot-notation': 'off',
|
||||||
|
'constructor-super': 'off',
|
||||||
|
'getter-return': 'off',
|
||||||
|
'no-const-assign': 'off',
|
||||||
|
'no-dupe-args': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'no-dupe-keys': 'off',
|
||||||
|
'no-func-assign': 'off',
|
||||||
|
'no-import-assign': 'off',
|
||||||
|
'no-new-symbol': 'off',
|
||||||
|
'no-obj-calls': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-setter-return': 'off',
|
||||||
|
'no-this-before-super': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'no-unreachable': 'off',
|
||||||
|
'no-unsafe-negation': 'off',
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'prefer-rest-params': 'error',
|
||||||
|
'prefer-spread': 'error',
|
||||||
|
'valid-typeof': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'no-promise-executor-return': 'off',
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': '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` 并合并为准
|
||||||
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# MacOS Build
|
||||||
|
- name: Build MacOS
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
export ELECTRON_BUILDER_EXTRA_ARGS="--universal"
|
||||||
|
npm run build:mac
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
|
DEBUG: electron-builder
|
||||||
|
|
||||||
|
# Windows Build
|
||||||
|
- name: Build Windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: npm run build:win
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Linux Build
|
||||||
|
- name: Build Linux
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
npm run build:linux
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Get version from tag
|
||||||
|
- name: Get version from tag
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Read release notes
|
||||||
|
- name: Read release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md)
|
||||||
|
echo "NOTES<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$NOTES" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dist/*.dmg
|
||||||
|
dist/*.exe
|
||||||
|
dist/*.deb
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/latest*.yml
|
||||||
|
dist/*.blockmap
|
||||||
|
body: ${{ env.NOTES }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
13
.gitignore
vendored
@@ -1,11 +1,22 @@
|
|||||||
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
|
||||||
|
|
||||||
|
out
|
||||||
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
5
.prettierrc.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
singleQuote: true
|
||||||
|
semi: true
|
||||||
|
printWidth: 100
|
||||||
|
trailingComma: none
|
||||||
|
endOfLine: auto
|
||||||
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"compile-hero.disable-compile-files-on-did-save-code": true
|
|
||||||
}
|
|
||||||
8
CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [v3.4.0] - 2025-01-11
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- 添加捐赠支持列表
|
||||||
|
- 添加收藏列表 批量下载功能
|
||||||
|
- 优化搜藏列表 可本地和线上收藏合并 如果收藏失败 会自动收藏到本地
|
||||||
99
README.md
@@ -1,5 +1,96 @@
|
|||||||
# 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/)
|
||||||
|
|
||||||
|
QQ群:789288579
|
||||||
|
|
||||||
|
## 软件截图
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 主要框架
|
||||||
|
- Vue 3 - 渐进式 JavaScript 框架
|
||||||
|
- TypeScript - JavaScript 的超集,添加了类型系统
|
||||||
|
- Electron - 跨平台桌面应用开发框架
|
||||||
|
- Vite - 下一代前端构建工具
|
||||||
|
- Naive UI - 基于 Vue 3 的组件库
|
||||||
|
|
||||||
|
|
||||||
|
## 咖啡☕️
|
||||||
|
| 微信 | 支付宝 |
|
||||||
|
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||||
|
| <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
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
本软件仅用于学习交流,禁止用于商业用途,否则后果自负。
|
||||||
|
|||||||
139
app.js
@@ -1,139 +0,0 @@
|
|||||||
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron')
|
|
||||||
const path = require('path')
|
|
||||||
const Store = require('electron-store');
|
|
||||||
const setJson = require('./electron/set.json')
|
|
||||||
|
|
||||||
let mainWin = null
|
|
||||||
function createWindow() {
|
|
||||||
mainWin = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 780,
|
|
||||||
frame: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
preload: path.join(__dirname, '/electron/preload.js'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const win = mainWin
|
|
||||||
win.setMinimumSize(1200, 780)
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
win.webContents.openDevTools({ mode: 'detach' })
|
|
||||||
win.loadURL('http://localhost:4678/')
|
|
||||||
} else {
|
|
||||||
win.loadURL(`file://${__dirname}/dist/index.html`)
|
|
||||||
}
|
|
||||||
const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'))
|
|
||||||
const tray = new Tray(image)
|
|
||||||
|
|
||||||
// 创建一个上下文菜单
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: '显示',
|
|
||||||
click: () => {
|
|
||||||
win.show()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
win.destroy()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 设置系统托盘图标的上下文菜单
|
|
||||||
tray.setContextMenu(contextMenu)
|
|
||||||
|
|
||||||
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
|
||||||
tray.on('click', () => {
|
|
||||||
if (win.isVisible()) {
|
|
||||||
win.hide()
|
|
||||||
} else {
|
|
||||||
win.show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const set = store.get('set')
|
|
||||||
// store.set('set', setJson)
|
|
||||||
|
|
||||||
if (!set) {
|
|
||||||
store.set('set', setJson)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制只能启动一个应用
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(createWindow)
|
|
||||||
|
|
||||||
app.on('ready',()=>{
|
|
||||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
|
||||||
if (mainWin.isVisible()) {
|
|
||||||
mainWin.hide()
|
|
||||||
} else {
|
|
||||||
mainWin.show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('will-quit', () => {
|
|
||||||
globalShortcut.unregisterAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('minimize-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
|
||||||
win.minimize()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('maximize-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
|
||||||
if (win.isMaximized()) {
|
|
||||||
win.unmaximize()
|
|
||||||
} else {
|
|
||||||
win.maximize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('close-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
|
||||||
win.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('drag-start', (event, data) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
|
||||||
win.webContents.beginFrameSubscription((frameBuffer) => {
|
|
||||||
event.reply('frame-buffer', frameBuffer)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('mini-tray', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
|
||||||
win.hide()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重启
|
|
||||||
ipcMain.on('restart', () => {
|
|
||||||
app.relaunch()
|
|
||||||
app.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
// 定义ipcRenderer监听事件
|
|
||||||
ipcMain.on('setStore', (_, key, value) => {
|
|
||||||
store.set(key, value)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('getStore', (_, key) => {
|
|
||||||
let value = store.get(key)
|
|
||||||
_.returnValue = value || ""
|
|
||||||
})
|
|
||||||
136
auto-imports.d.ts
vendored
@@ -3,68 +3,88 @@
|
|||||||
// @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'];
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: (typeof import('vue'))['computed'];
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: (typeof import('vue'))['createApp'];
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: (typeof import('vue'))['customRef'];
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'];
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: (typeof import('vue'))['defineComponent'];
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: (typeof import('vue'))['effectScope'];
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'];
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: (typeof import('vue'))['getCurrentScope'];
|
||||||
const h: typeof import('vue')['h']
|
const h: (typeof import('vue'))['h'];
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: (typeof import('vue'))['inject'];
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: (typeof import('vue'))['isProxy'];
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: (typeof import('vue'))['isReactive'];
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: (typeof import('vue'))['isReadonly'];
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: (typeof import('vue'))['isRef'];
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: (typeof import('vue'))['markRaw'];
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: (typeof import('vue'))['nextTick'];
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: (typeof import('vue'))['onActivated'];
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: (typeof import('vue'))['onBeforeMount'];
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'];
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'];
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: (typeof import('vue'))['onDeactivated'];
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'];
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
const onMounted: (typeof import('vue'))['onMounted'];
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
const onRenderTracked: (typeof import('vue'))['onRenderTracked'];
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'];
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
const onScopeDispose: (typeof import('vue'))['onScopeDispose'];
|
||||||
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 provide: typeof import('vue')['provide']
|
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'];
|
||||||
const reactive: typeof import('vue')['reactive']
|
const provide: (typeof import('vue'))['provide'];
|
||||||
const readonly: typeof import('vue')['readonly']
|
const reactive: (typeof import('vue'))['reactive'];
|
||||||
const ref: typeof import('vue')['ref']
|
const readonly: (typeof import('vue'))['readonly'];
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const ref: (typeof import('vue'))['ref'];
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const resolveComponent: (typeof import('vue'))['resolveComponent'];
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReactive: (typeof import('vue'))['shallowReactive'];
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowReadonly: (typeof import('vue'))['shallowReadonly'];
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const shallowRef: (typeof import('vue'))['shallowRef'];
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRaw: (typeof import('vue'))['toRaw'];
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRef: (typeof import('vue'))['toRef'];
|
||||||
const toValue: typeof import('vue')['toValue']
|
const toRefs: (typeof import('vue'))['toRefs'];
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const toValue: (typeof import('vue'))['toValue'];
|
||||||
const unref: typeof import('vue')['unref']
|
const triggerRef: (typeof import('vue'))['triggerRef'];
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const unref: (typeof import('vue'))['unref'];
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useAttrs: (typeof import('vue'))['useAttrs'];
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssModule: (typeof import('vue'))['useCssModule'];
|
||||||
const useDialog: typeof import('naive-ui')['useDialog']
|
const useCssVars: (typeof import('vue'))['useCssVars'];
|
||||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
const useDialog: (typeof import('naive-ui'))['useDialog'];
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useId: (typeof import('vue'))['useId'];
|
||||||
const useNotification: typeof import('naive-ui')['useNotification']
|
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar'];
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useMessage: (typeof import('naive-ui'))['useMessage'];
|
||||||
const watch: typeof import('vue')['watch']
|
const useModel: (typeof import('vue'))['useModel'];
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const useNotification: (typeof import('naive-ui'))['useNotification'];
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const useSlots: (typeof import('vue'))['useSlots'];
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
const useTemplateRef: (typeof import('vue'))['useTemplateRef'];
|
||||||
|
const watch: (typeof import('vue'))['watch'];
|
||||||
|
const watchEffect: (typeof import('vue'))['watchEffect'];
|
||||||
|
const watchPostEffect: (typeof import('vue'))['watchPostEffect'];
|
||||||
|
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'];
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
import('vue')
|
Component,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedRef,
|
||||||
|
DirectiveBinding,
|
||||||
|
ExtractDefaultPropTypes,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ExtractPublicPropTypes,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
Ref,
|
||||||
|
MaybeRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
VNode,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue';
|
||||||
|
import('vue');
|
||||||
}
|
}
|
||||||
|
|||||||
20
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
build/icon.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
13
build/installer.nsh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 设置 Windows 7 兼容性
|
||||||
|
ManifestDPIAware true
|
||||||
|
ManifestSupportedOS all
|
||||||
|
|
||||||
|
!macro customInit
|
||||||
|
# 检查系统版本
|
||||||
|
${If} ${AtLeastWin7}
|
||||||
|
# Windows 7 或更高版本
|
||||||
|
${Else}
|
||||||
|
MessageBox MB_OK|MB_ICONSTOP "此应用程序需要 Windows 7 或更高版本。"
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
20
components.d.ts
vendored
@@ -1,39 +1,33 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
export {}
|
export {};
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
|
||||||
MusicList: typeof import('./src/components/MusicList.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']
|
||||||
|
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']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
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']
|
||||||
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']
|
|
||||||
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
|
||||||
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
|
||||||
PlaylistType: typeof import('./src/components/PlaylistType.vue')['default']
|
|
||||||
RecommendAlbum: typeof import('./src/components/RecommendAlbum.vue')['default']
|
|
||||||
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
|
|
||||||
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SearchItem: typeof import('./src/components/common/SearchItem.vue')['default']
|
|
||||||
SongItem: typeof import('./src/components/common/SongItem.vue')['default']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
updaterCacheDirName: electron-lan-file-updater
|
||||||
BIN
docs/image.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
docs/image1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/image2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/image3.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
45
electron-builder.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
appId: com.electron.app
|
||||||
|
productName: electron-lan-file
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
executableName: electron-lan-file
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
notarize: false
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- snap
|
||||||
|
- deb
|
||||||
|
maintainer: electronjs.org
|
||||||
|
category: Utility
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
npmRebuild: false
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
electronDownload:
|
||||||
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
electron.vite.config.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src/renderer'),
|
||||||
|
'@renderer': resolve('src/renderer')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
viteCompression(),
|
||||||
|
// VueDevTools(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
{
|
||||||
|
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// with options
|
||||||
|
[process.env.VITE_API_LOCAL as string]: {
|
||||||
|
target: process.env.VITE_API,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), '')
|
||||||
|
},
|
||||||
|
[process.env.VITE_API_MUSIC_PROXY as string]: {
|
||||||
|
target: process.env.VITE_API_MUSIC,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_MUSIC_PROXY}`), '')
|
||||||
|
},
|
||||||
|
[process.env.VITE_API_PROXY_MUSIC as string]: {
|
||||||
|
target: process.env.VITE_API_PROXY,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY_MUSIC}`), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron')
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
minimize: () => ipcRenderer.send('minimize-window'),
|
|
||||||
maximize: () => ipcRenderer.send('maximize-window'),
|
|
||||||
close: () => ipcRenderer.send('close-window'),
|
|
||||||
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
|
||||||
miniTray: () => ipcRenderer.send('mini-tray'),
|
|
||||||
restart: () => ipcRenderer.send('restart'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const electronHandler = {
|
|
||||||
ipcRenderer: {
|
|
||||||
setStoreValue: (key, value) => {
|
|
||||||
ipcRenderer.send("setStore", key, value)
|
|
||||||
},
|
|
||||||
|
|
||||||
getStoreValue(key) {
|
|
||||||
const resp = ipcRenderer.sendSync("getStore", key)
|
|
||||||
return resp
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.3.0",
|
|
||||||
"isProxy": false,
|
|
||||||
"author": "alger"
|
|
||||||
}
|
|
||||||
25
index.html
@@ -1,25 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>网抑云 | algerkong</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="./public/icon/iconfont.css"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="./public/css/animate.css" />
|
|
||||||
<link rel="stylesheet" href="./public/css/base.css" />
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--animate-delay: 0.5s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
187
package.json
@@ -1,47 +1,162 @@
|
|||||||
{
|
{
|
||||||
"name": "alger-music",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "1.3.0",
|
"version": "3.4.0",
|
||||||
"description": "这是一个用于音乐播放的应用程序。",
|
"description": "Alger Music Player",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "app.js",
|
"main": "./out/main/index.js",
|
||||||
|
"homepage": "https://github.com/algerkong/AlgerMusicPlayer",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"format": "prettier --write .",
|
||||||
"build": "vite build",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||||
"serve": "vite preview",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"es": "vite && electron .",
|
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"start": "set NODE_ENV=development&&electron .",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"e:b": "electron-builder --config ./electron.config.json",
|
"start": "electron-vite preview",
|
||||||
"eb": "vite build && e:b"
|
"dev": "electron-vite dev",
|
||||||
|
"build": "npm run typecheck && electron-vite build",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
"build:win": "npm run build && electron-builder --win",
|
||||||
|
"build:mac": "npm run build && electron-builder --mac",
|
||||||
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@vueuse/core": "^10.7.1",
|
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
||||||
"autoprefixer": "^9.8.6",
|
|
||||||
"axios": "^0.21.1",
|
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"electron-updater": "^6.1.7",
|
||||||
"naive-ui": "^2.34.4",
|
"netease-cloud-music-api-alger": "^4.25.0"
|
||||||
"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",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
"@vicons/antd": "^0.10.0",
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"electron": "^28.0.0",
|
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||||
"electron-builder": "^24.9.1",
|
"@types/howler": "^2.2.12",
|
||||||
"typescript": "^4.3.2",
|
"@types/node": "^20.14.8",
|
||||||
"unplugin-auto-import": "^0.17.2",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"@vue/compiler-sfc": "^3.5.0",
|
||||||
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
|
"@vue/runtime-core": "^3.5.0",
|
||||||
|
"@vueuse/core": "^11.0.3",
|
||||||
|
"@vueuse/electron": "^11.0.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"electron": "^31.0.2",
|
||||||
|
"electron-builder": "^24.13.3",
|
||||||
|
"electron-vite": "^2.3.0",
|
||||||
|
"eslint": "^8.57.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.26.0",
|
||||||
|
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||||
|
"howler": "^2.2.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"marked": "^15.0.4",
|
||||||
|
"naive-ui": "^2.39.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"remixicon": "^4.2.0",
|
||||||
|
"sass": "^1.82.0",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "^5.5.2",
|
||||||
|
"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.3.1",
|
||||||
"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.4.30",
|
||||||
|
"vue-router": "^4.4.3",
|
||||||
|
"vue-tsc": "^2.0.22",
|
||||||
|
"vuex": "^4.1.0",
|
||||||
|
"animate.css": "^4.1.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.alger.music",
|
||||||
|
"productName": "AlgerMusicPlayer",
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "algerkong",
|
||||||
|
"repo": "AlgerMusicPlayer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"icon": "resources/icon.icns",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "dmg",
|
||||||
|
"arch": [
|
||||||
|
"universal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-mac-${arch}.${ext}",
|
||||||
|
"darkModeSupport": true,
|
||||||
|
"hardenedRuntime": false,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||||
|
"notarize": false,
|
||||||
|
"identity": null,
|
||||||
|
"type": "distribution",
|
||||||
|
"binaries": [
|
||||||
|
"Contents/MacOS/AlgerMusicPlayer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"icon": "resources/favicon.ico",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"ia32"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
|
||||||
|
"requestedExecutionLevel": "asInvoker"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"icon": "resources/icon.png",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "AppImage",
|
||||||
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "deb",
|
||||||
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
||||||
|
"category": "Audio",
|
||||||
|
"maintainer": "Alger <algerkc@qq.com>"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"installerIcon": "resources/favicon.ico",
|
||||||
|
"uninstallerIcon": "resources/favicon.ico",
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "AlgerMusicPlayer",
|
||||||
|
"include": "build/installer.nsh"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
7
public/css/animate.css
vendored
@@ -1,3 +0,0 @@
|
|||||||
body{
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
BIN
public/icon1.png
|
Before Width: | Height: | Size: 149 KiB |
BIN
resources/favicon.ico
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
resources/icon.icns
Normal file
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
resources/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 626 B |
10
resources/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Alger Music PWA",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "256x256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
src/App.vue
@@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app">
|
|
||||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
|
||||||
<n-config-provider :theme="darkTheme">
|
|
||||||
<n-dialog-provider>
|
|
||||||
<router-view></router-view>
|
|
||||||
</n-dialog-provider>
|
|
||||||
</n-config-provider>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { darkTheme } from 'naive-ui'
|
|
||||||
import store from '@/store'
|
|
||||||
|
|
||||||
const audio = ref<HTMLAudioElement | null>(null)
|
|
||||||
|
|
||||||
const playMusicUrl = computed(() => store.state.playMusicUrl as string)
|
|
||||||
// 是否播放
|
|
||||||
const play = computed(() => store.state.play as boolean)
|
|
||||||
const windowData = window as any
|
|
||||||
onMounted(()=>{
|
|
||||||
if(windowData.electron){
|
|
||||||
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
|
|
||||||
store.commit('setSetData', setData)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped >
|
|
||||||
div {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.app {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import request from "@/utils/request";
|
|
||||||
import { IHotSinger } from "@/type/singer";
|
|
||||||
import { ISearchKeyword, IHotSearch } from "@/type/search";
|
|
||||||
import { IPlayListSort } from "@/type/playlist";
|
|
||||||
import { IRecommendMusic } from "@/type/music";
|
|
||||||
import { IAlbumNew } from "@/type/album";
|
|
||||||
|
|
||||||
interface IHotSingerParams {
|
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRecommendMusicParams {
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取热门歌手
|
|
||||||
export const getHotSinger = (params: IHotSingerParams) => {
|
|
||||||
return request.get<IHotSinger>("/top/artists", { params });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取搜索推荐词
|
|
||||||
export const getSearchKeyword = () => {
|
|
||||||
return request.get<ISearchKeyword>("/search/default");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取热门搜索
|
|
||||||
export const getHotSearch = () => {
|
|
||||||
return request.get<IHotSearch>("/search/hot/detail");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取歌单分类
|
|
||||||
export const getPlaylistCategory = () => {
|
|
||||||
return request.get<IPlayListSort>("/playlist/catlist");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取推荐音乐
|
|
||||||
export const getRecommendMusic = (params: IRecommendMusicParams) => {
|
|
||||||
return request.get<IRecommendMusic>("/personalized/newsong", { params });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取最新专辑推荐
|
|
||||||
export const getNewAlbum = () => {
|
|
||||||
return request.get<IAlbumNew>("/album/newest");
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { IPlayMusicUrl } from "@/type/music"
|
|
||||||
import { ILyric } from "@/type/lyric"
|
|
||||||
import request from "@/utils/request"
|
|
||||||
import requestMusic from "@/utils/request_music"
|
|
||||||
// 根据音乐Id获取音乐播放URl
|
|
||||||
export const getMusicUrl = (id: number) => {
|
|
||||||
return request.get<IPlayMusicUrl>("/song/url", { params: { id: id } })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取歌曲详情
|
|
||||||
export const getMusicDetail = (ids: Array<number>) => {
|
|
||||||
return request.get("/song/detail", { params: { ids: ids.join(",")}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据音乐Id获取音乐歌词
|
|
||||||
export const getMusicLrc = (id: number) => {
|
|
||||||
return request.get<ILyric>("/lyric", { params: { id: id } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number) => {
|
|
||||||
return requestMusic.get<any>("/music", { params: { id: id } })
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { IData } from '@/type'
|
|
||||||
import { IMvItem, IMvUrlData } from '@/type/mv'
|
|
||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
// 获取 mv 排行
|
|
||||||
export const getTopMv = (limit: number) => {
|
|
||||||
return request.get<IData<Array<IMvItem>>>('/top/mv', {
|
|
||||||
params: {
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 mv 数据
|
|
||||||
export const getMvDetail = (mvid: string) => {
|
|
||||||
return request.get('/mv/detail', {
|
|
||||||
params: {
|
|
||||||
mvid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 mv 地址
|
|
||||||
export const getMvUrl = (id: Number) => {
|
|
||||||
return request.get<IData<IMvUrlData>>('/mv/url', {
|
|
||||||
params: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import request from "@/utils/request"
|
|
||||||
import { ISearchDetail } from "@/type/search"
|
|
||||||
|
|
||||||
interface IParams {
|
|
||||||
keywords: string
|
|
||||||
type: number
|
|
||||||
}
|
|
||||||
// 搜索内容
|
|
||||||
export const getSearch = (params: IParams) => {
|
|
||||||
return request.get<any>('/cloudsearch', {
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-drawer :show="show" height="70vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
|
|
||||||
<div class="music-page">
|
|
||||||
<i class="iconfont icon-icon_error music-close" @click="close"></i>
|
|
||||||
<div class="music-title">{{ name }}</div>
|
|
||||||
<!-- 歌单歌曲列表 -->
|
|
||||||
<div class="music-list">
|
|
||||||
<n-scrollbar >
|
|
||||||
<div v-for="(item, index) in songList" :key="item.id" :class="setAnimationClass('animate__bounceInUp')"
|
|
||||||
:style="setAnimationDelay(index, 100)">
|
|
||||||
<SongItem :item="formatDetail(item)" @play="handlePlay" />
|
|
||||||
</div>
|
|
||||||
<PlayBottom/>
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import { setAnimationClass, setAnimationDelay } from "@/utils";
|
|
||||||
import SongItem from "@/components/common/SongItem.vue";
|
|
||||||
import PlayBottom from './common/PlayBottom.vue';
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean;
|
|
||||||
name: string;
|
|
||||||
songList: any[]
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits(['update:show'])
|
|
||||||
|
|
||||||
const formatDetail = computed(() => (detail: any) => {
|
|
||||||
let song = {
|
|
||||||
artists: detail.ar,
|
|
||||||
name: detail.al.name,
|
|
||||||
id: detail.al.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
detail.song = song
|
|
||||||
detail.picUrl = detail.al.picUrl
|
|
||||||
return detail
|
|
||||||
})
|
|
||||||
|
|
||||||
const handlePlay = (item: any) => {
|
|
||||||
const tracks = props.songList || []
|
|
||||||
store.commit('setPlayList', tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('update:show', false)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.music {
|
|
||||||
&-page {
|
|
||||||
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
}
|
|
||||||
&-title {
|
|
||||||
@apply text-lg font-bold text-white p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-close {
|
|
||||||
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-list {
|
|
||||||
height: calc(100% - 60px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 歌单分类列表 -->
|
|
||||||
<div class="play-list-type">
|
|
||||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
|
|
||||||
<div>
|
|
||||||
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
|
|
||||||
<span
|
|
||||||
class="play-list-type-item"
|
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
|
||||||
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
|
|
||||||
v-show="isShowAllPlaylistCategory || index <= 19"
|
|
||||||
@click="handleClickPlaylistType(item.name)"
|
|
||||||
>{{ item.name }}</span>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
class="play-list-type-showall"
|
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
|
||||||
:style="
|
|
||||||
setAnimationDelay(
|
|
||||||
!isShowAllPlaylistCategory
|
|
||||||
? 25
|
|
||||||
: playlistCategory?.sub.length || 100 + 30
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
|
|
||||||
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { getPlaylistCategory } from "@/api/home";
|
|
||||||
import type { IPlayListSort } from "@/type/playlist";
|
|
||||||
import { setAnimationDelay, setAnimationClass } from "@/utils";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
// 歌单分类
|
|
||||||
const playlistCategory = ref<IPlayListSort>();
|
|
||||||
// 是否显示全部歌单分类
|
|
||||||
const isShowAllPlaylistCategory = ref<boolean>(false);
|
|
||||||
|
|
||||||
// 加载歌单分类
|
|
||||||
const loadPlaylistCategory = async () => {
|
|
||||||
const { data } = await getPlaylistCategory();
|
|
||||||
playlistCategory.value = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const handleClickPlaylistType = (type: any) => {
|
|
||||||
router.push({
|
|
||||||
path: "/list",
|
|
||||||
query: {
|
|
||||||
type: type,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 页面初始化
|
|
||||||
onMounted(() => {
|
|
||||||
loadPlaylistCategory();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.title {
|
|
||||||
@apply text-lg font-bold mb-4;
|
|
||||||
}
|
|
||||||
.play-list-type {
|
|
||||||
width: 250px;
|
|
||||||
@apply mx-6;
|
|
||||||
&-item,
|
|
||||||
&-showall {
|
|
||||||
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
}
|
|
||||||
&-showall {
|
|
||||||
@apply block text-center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="recommend-album">
|
|
||||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
|
|
||||||
<div class="recommend-album-list">
|
|
||||||
<template v-for="(item,index) in albumData?.albums" :key="item.id">
|
|
||||||
<div
|
|
||||||
v-if="index < 6"
|
|
||||||
class="recommend-album-list-item"
|
|
||||||
:class="setAnimationClass('animate__backInUp')"
|
|
||||||
:style="setAnimationDelay(index, 100)"
|
|
||||||
@click="handleClick(item)"
|
|
||||||
>
|
|
||||||
<n-image
|
|
||||||
class="recommend-album-list-item-img"
|
|
||||||
:src="getImgUrl( item.blurPicUrl, '200y200')"
|
|
||||||
lazy
|
|
||||||
preview-disabled
|
|
||||||
/>
|
|
||||||
<div class="recommend-album-list-item-content">{{ item.name }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { getNewAlbum } from "@/api/home"
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import type { IAlbumNew } from "@/type/album"
|
|
||||||
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
|
|
||||||
import { getAlbum } from "@/api/list";
|
|
||||||
|
|
||||||
|
|
||||||
const albumData = ref<IAlbumNew>()
|
|
||||||
const loadAlbumList = async () => {
|
|
||||||
const { data } = await getNewAlbum();
|
|
||||||
albumData.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const showMusic = ref(false)
|
|
||||||
const songList = ref([])
|
|
||||||
const albumName = ref('')
|
|
||||||
|
|
||||||
const handleClick = async (item:any) => {
|
|
||||||
albumName.value = item.name
|
|
||||||
showMusic.value = true
|
|
||||||
const res = await getAlbum(item.id)
|
|
||||||
songList.value = res.data.songs.map((song:any)=>{
|
|
||||||
song.al.picUrl = song.al.picUrl || item.picUrl
|
|
||||||
return song
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadAlbumList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.recommend-album {
|
|
||||||
@apply flex-1 mx-5;
|
|
||||||
.title {
|
|
||||||
@apply text-lg font-bold mb-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-album-list {
|
|
||||||
@apply grid grid-cols-2 grid-rows-3 gap-2;
|
|
||||||
&-item {
|
|
||||||
@apply rounded-xl overflow-hidden relative;
|
|
||||||
&-img {
|
|
||||||
@apply rounded-xl transition w-full h-full;
|
|
||||||
}
|
|
||||||
&:hover img {
|
|
||||||
filter: brightness(50%);
|
|
||||||
}
|
|
||||||
&-content {
|
|
||||||
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
|
|
||||||
}
|
|
||||||
&-content:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 推荐歌手 -->
|
|
||||||
<div class="recommend-singer">
|
|
||||||
<div class="recommend-singer-list">
|
|
||||||
<div
|
|
||||||
class="recommend-singer-item relative"
|
|
||||||
:class="setAnimationClass('animate__backInRight')"
|
|
||||||
v-for="(item, index) in hotSingerData?.artists"
|
|
||||||
:style="setAnimationDelay(index, 100)"
|
|
||||||
:key="item.id"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:style="setBackgroundImg(getImgUrl(item.picUrl,'300y300'))"
|
|
||||||
class="recommend-singer-item-bg"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10"
|
|
||||||
>{{ item.musicSize }}首</div>
|
|
||||||
<div class="recommend-singer-item-info z-10">
|
|
||||||
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
|
||||||
<i class="iconfont icon-playfill text-xl"></i>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
|
|
||||||
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { setBackgroundImg, setAnimationDelay, setAnimationClass,getImgUrl } from "@/utils";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { getHotSinger } from "@/api/home";
|
|
||||||
import type { IHotSinger } from "@/type/singer";
|
|
||||||
import router from "@/router";
|
|
||||||
|
|
||||||
// 歌手信息
|
|
||||||
const hotSingerData = ref<IHotSinger>();
|
|
||||||
|
|
||||||
//加载推荐歌手
|
|
||||||
const loadSingerList = async () => {
|
|
||||||
const { data } = await getHotSinger({ offset: 0, limit: 5 });
|
|
||||||
hotSingerData.value = data;
|
|
||||||
};
|
|
||||||
// 页面初始化
|
|
||||||
onMounted(() => {
|
|
||||||
loadSingerList();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const toSearchSinger = (keyword: string) => {
|
|
||||||
router.push({
|
|
||||||
path: "/search",
|
|
||||||
query: {
|
|
||||||
keyword: keyword,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.recommend-singer {
|
|
||||||
&-list {
|
|
||||||
@apply flex;
|
|
||||||
height: 280px;
|
|
||||||
}
|
|
||||||
&-item {
|
|
||||||
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
|
|
||||||
&-bg {
|
|
||||||
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
|
|
||||||
filter: brightness(80%);
|
|
||||||
}
|
|
||||||
&-info {
|
|
||||||
@apply flex items-center p-2;
|
|
||||||
&-play {
|
|
||||||
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="bottom" v-if="isPlay"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
const store = useStore()
|
|
||||||
const isPlay = computed(() => store.state.isPlay as boolean)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.bottom{
|
|
||||||
@apply h-28;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search-item" @click="handleClick">
|
|
||||||
<div class="search-item-img">
|
|
||||||
<n-image
|
|
||||||
:src="getImgUrl(item.picUrl, 'album')"
|
|
||||||
lazy
|
|
||||||
preview-disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="search-item-info">
|
|
||||||
<div class="search-item-name">{{ item.name }}</div>
|
|
||||||
<div class="search-item-artist">{{ item.desc}}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MusicList v-model:show="showMusic" :name="item.name" :song-list="songList" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
import type {Album} from '@/type/album'
|
|
||||||
import { getAlbum } from '@/api/list';
|
|
||||||
const props = defineProps<{
|
|
||||||
item: {
|
|
||||||
picUrl: string
|
|
||||||
name: string
|
|
||||||
desc: string
|
|
||||||
type: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const songList = ref([])
|
|
||||||
|
|
||||||
const showMusic = ref(false)
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
showMusic.value = true
|
|
||||||
if(props.item.type === '专辑'){
|
|
||||||
const res = await getAlbum(props.item.id)
|
|
||||||
songList.value = res.data.songs.map((song:any)=>{
|
|
||||||
song.al.picUrl = song.al.picUrl || props.item.picUrl
|
|
||||||
return song
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
.search-item{
|
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
|
|
||||||
margin: 0 10px;
|
|
||||||
.search-item-img{
|
|
||||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
|
||||||
}
|
|
||||||
.search-item-info{
|
|
||||||
&-name{
|
|
||||||
@apply text-white text-sm text-center;
|
|
||||||
}
|
|
||||||
&-artist{
|
|
||||||
@apply text-gray-400 text-xs text-center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="song-item" :class="{'song-mini': mini}">
|
|
||||||
<n-image
|
|
||||||
v-if="item.picUrl "
|
|
||||||
:src="getImgUrl( item.picUrl, '40y40')"
|
|
||||||
class="song-item-img"
|
|
||||||
lazy
|
|
||||||
preview-disabled
|
|
||||||
/>
|
|
||||||
<div class="song-item-content">
|
|
||||||
<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.song.artists"
|
|
||||||
:key="artistsindex"
|
|
||||||
>{{ artists.name
|
|
||||||
}}{{
|
|
||||||
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
</n-ellipsis>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="song-item-operating">
|
|
||||||
<div class="song-item-operating-like">
|
|
||||||
<i class="iconfont icon-likefill"></i>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="song-item-operating-play bg-black"
|
|
||||||
:class="isPlaying ? 'bg-green-600' : ''"
|
|
||||||
@click="playMusicEvent(item)"
|
|
||||||
>
|
|
||||||
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
|
||||||
<i v-else class="iconfont icon-playfill"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import type { SongResult } from '@/type/music'
|
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
item: SongResult
|
|
||||||
mini?: boolean
|
|
||||||
}>(), {
|
|
||||||
mini: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
const play = computed(() => store.state.play as boolean)
|
|
||||||
|
|
||||||
const playMusic = computed(() => store.state.playMusic)
|
|
||||||
|
|
||||||
// 判断是否为正在播放的音乐
|
|
||||||
const isPlaying = computed(() => {
|
|
||||||
return playMusic.value.id == props.item.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const emits = defineEmits(['play'])
|
|
||||||
|
|
||||||
// 播放音乐 设置音乐详情 打开音乐底栏
|
|
||||||
const playMusicEvent = (item: any) => {
|
|
||||||
store.commit('setPlay', item)
|
|
||||||
store.commit('setIsPlay', true)
|
|
||||||
emits('play', item)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
// 配置文字不可选中
|
|
||||||
.text-ellipsis {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.song-item {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
|
|
||||||
&-img {
|
|
||||||
@apply w-12 h-12 rounded-2xl mr-4;
|
|
||||||
}
|
|
||||||
&-content {
|
|
||||||
@apply flex-1;
|
|
||||||
&-title {
|
|
||||||
@apply text-base text-white;
|
|
||||||
}
|
|
||||||
&-name {
|
|
||||||
@apply text-xs;
|
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-operating {
|
|
||||||
@apply flex items-center pl-4 rounded-full border border-gray-700 ml-4;
|
|
||||||
background-color: #0d0d0d;
|
|
||||||
.iconfont {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
|
||||||
.icon-likefill {
|
|
||||||
color: #868686;
|
|
||||||
@apply text-xl hover:text-red-600 transition;
|
|
||||||
}
|
|
||||||
&-like {
|
|
||||||
@apply mr-2 cursor-pointer;
|
|
||||||
}
|
|
||||||
&-play {
|
|
||||||
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-mini{
|
|
||||||
@apply p-2 rounded-2xl;
|
|
||||||
.song-item{
|
|
||||||
@apply p-0;
|
|
||||||
&-img{
|
|
||||||
@apply w-10 h-10 mr-2;
|
|
||||||
}
|
|
||||||
&-content{
|
|
||||||
@apply flex-1;
|
|
||||||
&-title{
|
|
||||||
@apply text-sm;
|
|
||||||
}
|
|
||||||
&-name{
|
|
||||||
@apply text-xs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-operating{
|
|
||||||
@apply pl-2;
|
|
||||||
.iconfont{
|
|
||||||
@apply text-base;
|
|
||||||
}
|
|
||||||
&-like{
|
|
||||||
@apply mr-1;
|
|
||||||
}
|
|
||||||
&-play{
|
|
||||||
@apply w-8 h-8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
export const USER_SET_OPTIONS = [
|
|
||||||
// {
|
|
||||||
// label: '打卡',
|
|
||||||
// key: 'card',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: '听歌升级',
|
|
||||||
// key: 'card_music',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: '歌曲次数',
|
|
||||||
// key: 'listen',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: '退出登录',
|
|
||||||
key: 'logout',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '设置',
|
|
||||||
key: 'set',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const SEARCH_TYPES = [
|
|
||||||
{
|
|
||||||
label: '单曲',
|
|
||||||
key: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '专辑',
|
|
||||||
key: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '歌手',
|
|
||||||
key: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '歌单',
|
|
||||||
key: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '用户',
|
|
||||||
key: 1002,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MV',
|
|
||||||
key: 1004,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '歌词',
|
|
||||||
key: 1006,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '电台',
|
|
||||||
key: 1009,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '视频',
|
|
||||||
key: 1014,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '综合',
|
|
||||||
key: 1018,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
10
src/electron.d.ts
vendored
@@ -1,10 +0,0 @@
|
|||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electronAPI: {
|
|
||||||
minimize: () => void
|
|
||||||
maximize: () => void
|
|
||||||
close: () => void
|
|
||||||
dragStart: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { getMusicLrc } from '@/api/music'
|
|
||||||
import { ILyric } from '@/type/lyric'
|
|
||||||
import { getIsMc } from '@/utils'
|
|
||||||
|
|
||||||
interface ILrcData {
|
|
||||||
text: string
|
|
||||||
trText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const lrcData = ref<ILyric>()
|
|
||||||
const newLrcIndex = ref<number>(0)
|
|
||||||
const lrcArray = ref<Array<ILrcData>>([])
|
|
||||||
const lrcTimeArray = ref<Array<Number>>([])
|
|
||||||
|
|
||||||
const parseTime = (timeString: string) => {
|
|
||||||
const [minutes, seconds] = timeString.split(':')
|
|
||||||
return parseInt(minutes) * 60 + parseFloat(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g
|
|
||||||
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g
|
|
||||||
|
|
||||||
function parseLyricLine(lyricLine: string) {
|
|
||||||
// [00:00.00] 作词 : 长友美知惠/
|
|
||||||
const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''
|
|
||||||
const time = parseTime(timeText)
|
|
||||||
const text = lyricLine.replace(LRC_REGEX, '').trim()
|
|
||||||
return { time, text }
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILyricText {
|
|
||||||
text: string
|
|
||||||
trText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseLyrics(lyricsString: string) {
|
|
||||||
const lines = lyricsString.split('\n')
|
|
||||||
const lyrics: Array<ILyricText> = []
|
|
||||||
const times: number[] = []
|
|
||||||
lines.forEach((line) => {
|
|
||||||
const { time, text } = parseLyricLine(line)
|
|
||||||
times.push(time)
|
|
||||||
lyrics.push({ text, trText: '' })
|
|
||||||
})
|
|
||||||
return { lyrics, times }
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadLrc = async (playMusicId: number): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { data } = await getMusicLrc(playMusicId)
|
|
||||||
const { lyrics, times } = parseLyrics(data.lrc.lyric)
|
|
||||||
lrcTimeArray.value = times
|
|
||||||
lrcArray.value = lyrics
|
|
||||||
} catch (err) {
|
|
||||||
console.error('err', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 歌词矫正时间Correction time
|
|
||||||
const correctionTime = ref(0.4)
|
|
||||||
|
|
||||||
// 增加矫正时间
|
|
||||||
const addCorrectionTime = (time: number) => {
|
|
||||||
correctionTime.value += time
|
|
||||||
}
|
|
||||||
|
|
||||||
// 减少矫正时间
|
|
||||||
const reduceCorrectionTime = (time: number) => {
|
|
||||||
correctionTime.value -= time
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentLrc = (index: any, time: number) => {
|
|
||||||
const currentTime = Number(lrcTimeArray.value[index])
|
|
||||||
const nextTime = Number(lrcTimeArray.value[index + 1])
|
|
||||||
const nowTime = time + correctionTime.value
|
|
||||||
const isTrue = nowTime > currentTime && nowTime < nextTime
|
|
||||||
if (isTrue) {
|
|
||||||
newLrcIndex.value = index
|
|
||||||
}
|
|
||||||
return isTrue
|
|
||||||
}
|
|
||||||
|
|
||||||
const nowTime = ref(0)
|
|
||||||
const allTime = ref(0)
|
|
||||||
|
|
||||||
// 设置当前播放时间
|
|
||||||
const setAudioTime = (index: any, audio: HTMLAudioElement) => {
|
|
||||||
audio.currentTime = lrcTimeArray.value[index] as number
|
|
||||||
audio.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
lrcData,
|
|
||||||
lrcArray,
|
|
||||||
lrcTimeArray,
|
|
||||||
newLrcIndex,
|
|
||||||
loadLrc,
|
|
||||||
isCurrentLrc,
|
|
||||||
addCorrectionTime,
|
|
||||||
reduceCorrectionTime,
|
|
||||||
setAudioTime,
|
|
||||||
nowTime,
|
|
||||||
allTime,
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/* ./src/index.css */
|
|
||||||
|
|
||||||
/*! @import */
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
.n-image img {
|
|
||||||
background-color: #111111;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="layout-page">
|
|
||||||
<div class="layout-main">
|
|
||||||
<title-bar v-if="isElectron" />
|
|
||||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
|
||||||
<!-- 侧边菜单栏 -->
|
|
||||||
<app-menu class="menu" :menus="menus" />
|
|
||||||
<div class="main">
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<search-bar />
|
|
||||||
<!-- 主页面路由 -->
|
|
||||||
<div class="main-content bg-black pb-" :native-scrollbar="false" :class="isPlay ? 'pb-20' : ''">
|
|
||||||
<n-message-provider>
|
|
||||||
<router-view class="main-page" v-slot="{ Component }" :class="route.meta.noScroll? 'pr-3' : ''">
|
|
||||||
<template v-if="route.meta.noScroll">
|
|
||||||
<keep-alive v-if="!route.meta.noKeepAlive">
|
|
||||||
<component :is="Component" />
|
|
||||||
</keep-alive>
|
|
||||||
<component v-else :is="Component"/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<n-scrollbar>
|
|
||||||
<keep-alive v-if="!route.meta.noKeepAlive">
|
|
||||||
<component :is="Component" />
|
|
||||||
</keep-alive>
|
|
||||||
<component v-else :is="Component"/>
|
|
||||||
</n-scrollbar>
|
|
||||||
</template>
|
|
||||||
</router-view>
|
|
||||||
</n-message-provider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 底部音乐播放 -->
|
|
||||||
<play-bar v-if="isPlay" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
|
|
||||||
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
|
||||||
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
|
||||||
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
|
||||||
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
const isPlay = computed(() => store.state.isPlay as boolean)
|
|
||||||
const menus = store.state.menus;
|
|
||||||
const play = computed(() => store.state.play as boolean)
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.layout-page {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
@apply flex justify-center items-center overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-main {
|
|
||||||
@apply bg-black text-white shadow-xl flex flex-col relative;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
&-page{
|
|
||||||
@apply flex flex-1 overflow-hidden;
|
|
||||||
}
|
|
||||||
.menu {
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
.main {
|
|
||||||
@apply flex-1 box-border flex flex-col;
|
|
||||||
height: 100%;
|
|
||||||
&-content {
|
|
||||||
@apply box-border flex-1 overflow-hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.n-scrollbar-content){
|
|
||||||
@apply pr-3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- menu -->
|
|
||||||
<div class="app-menu">
|
|
||||||
<div class="app-menu-header">
|
|
||||||
<div class="app-menu-logo">
|
|
||||||
<img src="/icon.png" class="w-9 h-9" alt="logo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="app-menu-list">
|
|
||||||
<div class="app-menu-item" v-for="(item,index) in menus">
|
|
||||||
<router-link class="app-menu-item-link" :to="item.path">
|
|
||||||
<i
|
|
||||||
class="iconfont app-menu-item-icon"
|
|
||||||
:style="iconStyle(index)"
|
|
||||||
:class="item.meta.icon"
|
|
||||||
></i>
|
|
||||||
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
const props = defineProps({
|
|
||||||
isText: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: '26px'
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: '#aaa'
|
|
||||||
},
|
|
||||||
selectColor: {
|
|
||||||
type: String,
|
|
||||||
default: '#10B981'
|
|
||||||
},
|
|
||||||
menus: {
|
|
||||||
type: Array as any,
|
|
||||||
default: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const path = ref(route.path);
|
|
||||||
watch(() => route.path, async newParams => {
|
|
||||||
path.value = newParams
|
|
||||||
})
|
|
||||||
|
|
||||||
const iconStyle = (index: any) => {
|
|
||||||
let style = {
|
|
||||||
fontSize: props.size,
|
|
||||||
color: path.value === props.menus[index].path ? props.selectColor : props.color
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.app-menu {
|
|
||||||
@apply flex-col items-center justify-center px-6;
|
|
||||||
max-width: 100px;
|
|
||||||
}
|
|
||||||
.app-menu-item-link,
|
|
||||||
.app-menu-header {
|
|
||||||
@apply flex items-center justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu-item-link {
|
|
||||||
@apply mb-6 mt-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu-item-icon:hover {
|
|
||||||
color: #10b981 !important;
|
|
||||||
transform: scale(1.05);
|
|
||||||
transition: 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-drawer
|
|
||||||
:show="musicFull"
|
|
||||||
height="100vh"
|
|
||||||
placement="bottom"
|
|
||||||
:drawer-style="{ backgroundColor: 'transparent' }"
|
|
||||||
>
|
|
||||||
<div id="drawer-target">
|
|
||||||
<div class="drawer-back" :class="{'paused': !isPlaying}" :style="{backgroundImage:`url(${getImgUrl(playMusic?.picUrl, '300y300')})`}"></div>
|
|
||||||
<div class="music-img">
|
|
||||||
<n-image
|
|
||||||
ref="PicImgRef"
|
|
||||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
|
||||||
class="img"
|
|
||||||
lazy
|
|
||||||
preview-disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="music-content">
|
|
||||||
<div class="music-content-name">{{ playMusic.song.name }}</div>
|
|
||||||
<div class="music-content-singer">
|
|
||||||
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
|
||||||
{{ item.name
|
|
||||||
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<n-layout
|
|
||||||
class="music-lrc"
|
|
||||||
style="height: 55vh"
|
|
||||||
ref="lrcSider"
|
|
||||||
:native-scrollbar="false"
|
|
||||||
@mouseover="mouseOverLayout"
|
|
||||||
@mouseleave="mouseLeaveLayout"
|
|
||||||
>
|
|
||||||
<template v-for="(item, index) in lrcArray" :key="index">
|
|
||||||
<div
|
|
||||||
class="music-lrc-text"
|
|
||||||
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
|
|
||||||
@click="setAudioTime(index, audio)"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-layout>
|
|
||||||
<!-- 时间矫正 -->
|
|
||||||
<div class="music-content-time">
|
|
||||||
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
|
||||||
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { SongResult } from '@/type/music'
|
|
||||||
import { getImgUrl } from '@/utils'
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import {
|
|
||||||
lrcArray,
|
|
||||||
newLrcIndex,
|
|
||||||
isCurrentLrc,
|
|
||||||
addCorrectionTime,
|
|
||||||
reduceCorrectionTime,
|
|
||||||
setAudioTime,
|
|
||||||
nowTime,
|
|
||||||
} from '@/hooks/MusicHook'
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
musicFull: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
audio: {
|
|
||||||
type: HTMLAudioElement,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:musicFull'])
|
|
||||||
|
|
||||||
// 播放的音乐信息
|
|
||||||
const playMusic = computed(() => store.state.playMusic as SongResult)
|
|
||||||
const isPlaying = computed(() => store.state.play as boolean)
|
|
||||||
// 获取歌词滚动dom
|
|
||||||
const lrcSider = ref<any>(null)
|
|
||||||
const isMouse = ref(false)
|
|
||||||
// 歌词滚动方法
|
|
||||||
const lrcScroll = () => {
|
|
||||||
if (props.musicFull && !isMouse.value) {
|
|
||||||
let top = newLrcIndex.value * 50 - 225
|
|
||||||
lrcSider.value.scrollTo({ top: top, behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mouseOverLayout = () => {
|
|
||||||
isMouse.value = true
|
|
||||||
}
|
|
||||||
const mouseLeaveLayout = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
isMouse.value = false
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
lrcScroll,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
@keyframes round {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.drawer-back{
|
|
||||||
@apply absolute bg-cover bg-center opacity-70;
|
|
||||||
filter: blur(80px) brightness(80%);
|
|
||||||
z-index: -1;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
top: -50%;
|
|
||||||
left: -50%;
|
|
||||||
animation: round 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-back.paused {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
#drawer-target {
|
|
||||||
@apply top-0 left-0 absolute w-full h-full overflow-hidden rounded px-24 pt-24 pb-48 flex items-center;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
background-color: rgba(0, 0, 0, 0.747);
|
|
||||||
animation-duration: 300ms;
|
|
||||||
|
|
||||||
.music-img {
|
|
||||||
@apply flex-1 flex justify-center mr-24;
|
|
||||||
|
|
||||||
.img {
|
|
||||||
width: 350px;
|
|
||||||
height: 350px;
|
|
||||||
@apply rounded-xl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-content {
|
|
||||||
@apply flex flex-col justify-center items-center;
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
@apply font-bold text-3xl py-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-singer {
|
|
||||||
@apply text-base py-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-content-time{
|
|
||||||
display: none;
|
|
||||||
@apply flex justify-center items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-lrc {
|
|
||||||
background-color: inherit;
|
|
||||||
width: 500px;
|
|
||||||
height: 550px;
|
|
||||||
|
|
||||||
&-text {
|
|
||||||
@apply text-white text-lg flex justify-center items-center cursor-pointer;
|
|
||||||
height: 50px;
|
|
||||||
transition: all 0.2s ease-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply font-bold text-xl text-red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.now-text {
|
|
||||||
@apply font-bold text-xl text-red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 展开全屏 -->
|
|
||||||
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio.value as HTMLAudioElement)" />
|
|
||||||
<!-- 底部播放栏 -->
|
|
||||||
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
|
|
||||||
<n-image
|
|
||||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
|
||||||
class="play-bar-img"
|
|
||||||
lazy
|
|
||||||
preview-disabled
|
|
||||||
@click="setMusicFull"
|
|
||||||
/>
|
|
||||||
<div class="music-content">
|
|
||||||
<div class="music-content-title">
|
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
|
||||||
{{ playMusic.name }}
|
|
||||||
</n-ellipsis>
|
|
||||||
</div>
|
|
||||||
<div class="music-content-name">
|
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
|
||||||
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
|
||||||
{{ item.name
|
|
||||||
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
|
||||||
</span>
|
|
||||||
</n-ellipsis>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="music-buttons">
|
|
||||||
<div @click="handlePrev">
|
|
||||||
<i class="iconfont icon-prev"></i>
|
|
||||||
</div>
|
|
||||||
<div class="music-buttons-play" @click="playMusicEvent">
|
|
||||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
|
||||||
</div>
|
|
||||||
<div @click="handleEnded">
|
|
||||||
<i class="iconfont icon-next"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="music-time">
|
|
||||||
<div class="time">{{ getNowTime }}</div>
|
|
||||||
<n-slider
|
|
||||||
v-model:value="timeSlider"
|
|
||||||
:step="0.05"
|
|
||||||
:tooltip="false"
|
|
||||||
></n-slider>
|
|
||||||
<div class="time">{{ getAllTime }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="audio-volume">
|
|
||||||
<div>
|
|
||||||
<i class="iconfont icon-notificationfill"></i>
|
|
||||||
</div>
|
|
||||||
<n-slider
|
|
||||||
v-model:value="volumeSlider"
|
|
||||||
:step="0.01"
|
|
||||||
:tooltip="false"
|
|
||||||
></n-slider>
|
|
||||||
</div>
|
|
||||||
<div class="audio-button">
|
|
||||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
|
||||||
<template #trigger>
|
|
||||||
<i class="iconfont icon-likefill"></i>
|
|
||||||
</template>
|
|
||||||
喜欢
|
|
||||||
</n-tooltip> -->
|
|
||||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
|
||||||
<template #trigger>
|
|
||||||
<i class="iconfont icon-Play" @click="parsingMusic"></i>
|
|
||||||
</template>
|
|
||||||
解析播放
|
|
||||||
</n-tooltip> -->
|
|
||||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
|
||||||
<template #trigger>
|
|
||||||
<i class="iconfont icon-full" @click="setMusicFull"></i>
|
|
||||||
</template>
|
|
||||||
歌词
|
|
||||||
</n-tooltip> -->
|
|
||||||
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
|
|
||||||
<template #trigger>
|
|
||||||
<n-tooltip trigger="manual" :z-index="9999999">
|
|
||||||
<template #trigger>
|
|
||||||
<i class="iconfont icon-list"></i>
|
|
||||||
</template>
|
|
||||||
播放列表
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
<div class="music-play-list">
|
|
||||||
<div class="music-play-list-back"></div>
|
|
||||||
<n-scrollbar>
|
|
||||||
<div class="music-play-list-content">
|
|
||||||
<song-item v-for="(item, index) in playList" :key="item.id" :item="item" mini></song-item>
|
|
||||||
</div>
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
|
||||||
</n-popover>
|
|
||||||
</div>
|
|
||||||
<!-- 播放音乐 -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { SongResult } from '@/type/music'
|
|
||||||
import { secondToMinute, getImgUrl } from '@/utils'
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import { setAnimationClass } from '@/utils'
|
|
||||||
import {
|
|
||||||
loadLrc,
|
|
||||||
nowTime,
|
|
||||||
allTime
|
|
||||||
} from '@/hooks/MusicHook'
|
|
||||||
import MusicFull from './MusicFull.vue'
|
|
||||||
import SongItem from '@/components/common/SongItem.vue'
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
// 播放的音乐信息
|
|
||||||
const playMusic = computed(() => store.state.playMusic as SongResult)
|
|
||||||
// 是否播放
|
|
||||||
const play = computed(() => store.state.play as boolean)
|
|
||||||
|
|
||||||
const playList = computed(() => store.state.playList as SongResult[])
|
|
||||||
|
|
||||||
const audio = {
|
|
||||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => store.state.playMusicUrl,
|
|
||||||
() => {
|
|
||||||
loadLrc(playMusic.value.id)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const audioPlay = () => {
|
|
||||||
if (audio.value) {
|
|
||||||
audio.value.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioPause = () => {
|
|
||||||
if (audio.value) {
|
|
||||||
audio.value.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性 获取当前播放时间的进度
|
|
||||||
const timeSlider = computed({
|
|
||||||
get: () => (nowTime.value / allTime.value) * 100,
|
|
||||||
set: (value) => {
|
|
||||||
if (!audio.value) return
|
|
||||||
audio.value.currentTime = (value * allTime.value) / 100
|
|
||||||
audioPlay()
|
|
||||||
store.commit('setPlayMusic', true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 音量条
|
|
||||||
const audioVolume = ref(1)
|
|
||||||
const volumeSlider = computed({
|
|
||||||
get: () => audioVolume.value * 100,
|
|
||||||
set: (value) => {
|
|
||||||
if(!audio.value) return
|
|
||||||
audio.value.volume = value / 100
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// 获取当前播放时间
|
|
||||||
const getNowTime = computed(() => {
|
|
||||||
return secondToMinute(nowTime.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取总时间
|
|
||||||
const getAllTime = computed(() => {
|
|
||||||
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() {
|
|
||||||
store.commit('nextPlay')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePrev() {
|
|
||||||
store.commit('prevPlay')
|
|
||||||
}
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
if (play.value) {
|
|
||||||
store.commit('setPlayMusic', false)
|
|
||||||
} else {
|
|
||||||
store.commit('setPlayMusic', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const musicFull = ref(false)
|
|
||||||
|
|
||||||
// 设置musicFull
|
|
||||||
const setMusicFull = () => {
|
|
||||||
musicFull.value = !musicFull.value
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.text-ellipsis {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-play-bar {
|
|
||||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
|
|
||||||
z-index: 9999;
|
|
||||||
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
|
||||||
background-color: rgba(0, 0, 0, 0.747); .music-content {
|
|
||||||
width: 140px;
|
|
||||||
@apply ml-4;
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
@apply text-base text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
@apply text-xs mt-1;
|
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-bar-img {
|
|
||||||
@apply w-14 h-14 rounded-2xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-buttons {
|
|
||||||
@apply mx-6;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
@apply text-2xl hover:text-green-500 transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
@apply text-xl hover:text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@apply flex items-center;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-play {
|
|
||||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
|
|
||||||
background: #383838;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-time {
|
|
||||||
@apply flex flex-1 items-center;
|
|
||||||
|
|
||||||
.time {
|
|
||||||
@apply mx-4 mt-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-volume {
|
|
||||||
width: 140px;
|
|
||||||
@apply flex items-center mx-4;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
@apply text-2xl hover:text-green-500 transition cursor-pointer mr-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-button {
|
|
||||||
@apply flex items-center mx-4;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
@apply text-2xl hover:text-green-500 transition cursor-pointer m-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-play{
|
|
||||||
|
|
||||||
&-list{
|
|
||||||
height: 50vh;
|
|
||||||
@apply relative rounded-3xl overflow-hidden;
|
|
||||||
&-back{
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
|
|
||||||
}
|
|
||||||
&-content{
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search-box flex">
|
|
||||||
<div class="search-box-input flex-1">
|
|
||||||
<n-input
|
|
||||||
size="medium"
|
|
||||||
round
|
|
||||||
v-model:value="searchValue"
|
|
||||||
:placeholder="hotSearchKeyword"
|
|
||||||
class="border border-gray-600"
|
|
||||||
@keydown.enter="search"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<i class="iconfont icon-search"></i>
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<div class="w-20 px-3 flex justify-between items-center">
|
|
||||||
<div>{{ searchTypeOptions.find(item => item.key === searchType)?.label }}</div>
|
|
||||||
<n-dropdown trigger="hover" @select="selectSearchType" :options="searchTypeOptions">
|
|
||||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
|
||||||
</n-dropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</div>
|
|
||||||
<div class="user-box">
|
|
||||||
<n-dropdown trigger="hover" @select="selectItem" :options="userSetOptions">
|
|
||||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
|
||||||
</n-dropdown>
|
|
||||||
<n-avatar
|
|
||||||
class="ml-2 cursor-pointer"
|
|
||||||
circle
|
|
||||||
size="medium"
|
|
||||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
|
||||||
v-if="store.state.user"
|
|
||||||
/>
|
|
||||||
<div class="mx-2 rounded-full cursor-pointer text-sm" v-else @click="toLogin">登录</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { getSearchKeyword } from '@/api/home';
|
|
||||||
import { getUserDetail, logout } from '@/api/login';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
import request from '@/utils/request_mt'
|
|
||||||
import { getImgUrl } from '@/utils';
|
|
||||||
import {USER_SET_OPTIONS, SEARCH_TYPES} from '@/const/bar-const'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const store = useStore();
|
|
||||||
const userSetOptions = ref(USER_SET_OPTIONS)
|
|
||||||
|
|
||||||
|
|
||||||
// 推荐热搜词
|
|
||||||
const hotSearchKeyword = ref("搜索点什么吧...")
|
|
||||||
const hotSearchValue = ref("")
|
|
||||||
const loadHotSearchKeyword = async () => {
|
|
||||||
const { data } = await getSearchKeyword();
|
|
||||||
hotSearchKeyword.value = data.data.showKeyword
|
|
||||||
hotSearchValue.value = data.data.realkeyword
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPage = async () => {
|
|
||||||
const token = localStorage.getItem("token")
|
|
||||||
if (!token) return
|
|
||||||
const { data } = await getUserDetail()
|
|
||||||
store.state.user = data.profile
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
loadPage()
|
|
||||||
if (store.state.user) {
|
|
||||||
userSetOptions.value = USER_SET_OPTIONS
|
|
||||||
} else {
|
|
||||||
userSetOptions.value = USER_SET_OPTIONS.filter(item => item.key !== 'logout')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const toLogin = () => {
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面初始化
|
|
||||||
onMounted(() => {
|
|
||||||
loadHotSearchKeyword()
|
|
||||||
loadPage()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// 搜索词
|
|
||||||
const searchValue = ref("")
|
|
||||||
const searchType = ref(1)
|
|
||||||
const search = () => {
|
|
||||||
let value = searchValue.value
|
|
||||||
if (value == "") {
|
|
||||||
searchValue.value = hotSearchValue.value
|
|
||||||
} else {
|
|
||||||
router.push({
|
|
||||||
path: "/search",
|
|
||||||
query: {
|
|
||||||
keyword: value,
|
|
||||||
type: searchType.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectSearchType = (key: any) => {
|
|
||||||
searchType.value = key
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const searchTypeOptions = ref(SEARCH_TYPES)
|
|
||||||
|
|
||||||
const selectItem = async (key: any) => {
|
|
||||||
// switch 判断
|
|
||||||
switch (key) {
|
|
||||||
case 'card':
|
|
||||||
await request.get('/?do=sign')
|
|
||||||
.then(res => {
|
|
||||||
console.log(res)
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case 'card_music':
|
|
||||||
await request.get('/?do=daka')
|
|
||||||
.then(res => {
|
|
||||||
console.log(res)
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case 'listen':
|
|
||||||
await request.get('/?do=listen&id=1885175990&time=300')
|
|
||||||
.then(res => {
|
|
||||||
console.log(res)
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case 'logout':
|
|
||||||
logout().then(() => {
|
|
||||||
store.state.user = null
|
|
||||||
localStorage.clear()
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case 'login':
|
|
||||||
router.push("/login")
|
|
||||||
break;
|
|
||||||
case 'set':
|
|
||||||
router.push("/set")
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.user-box {
|
|
||||||
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
|
||||||
background: #1a1a1a;
|
|
||||||
}
|
|
||||||
.search-box{
|
|
||||||
@apply pb-4 pr-4;
|
|
||||||
}
|
|
||||||
.search-box-input {
|
|
||||||
@apply relative;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="title-bar" @mousedown="drag">
|
|
||||||
<div id="title">Alger Music</div>
|
|
||||||
<div id="buttons">
|
|
||||||
<button @click="minimize">
|
|
||||||
<i class="iconfont icon-minisize"></i>
|
|
||||||
</button>
|
|
||||||
<!-- <button @click="maximize">
|
|
||||||
<i class="iconfont icon-maxsize"></i>
|
|
||||||
</button> -->
|
|
||||||
<button @click="close">
|
|
||||||
<i class="iconfont icon-close"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useDialog } from 'naive-ui'
|
|
||||||
|
|
||||||
const dialog = useDialog()
|
|
||||||
const windowData = window as any
|
|
||||||
|
|
||||||
const minimize = () => {
|
|
||||||
windowData.electronAPI.minimize()
|
|
||||||
}
|
|
||||||
|
|
||||||
const maximize = () => {
|
|
||||||
windowData.electronAPI.maximize()
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
dialog.warning({
|
|
||||||
title: '提示',
|
|
||||||
content: '确定要退出吗?',
|
|
||||||
positiveText: '最小化',
|
|
||||||
negativeText: '关闭',
|
|
||||||
onPositiveClick: () => {
|
|
||||||
windowData.electronAPI.miniTray()
|
|
||||||
},
|
|
||||||
onNegativeClick: () => {
|
|
||||||
windowData.electronAPI.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const drag = (event: MouseEvent) => {
|
|
||||||
windowData.electronAPI.dragStart(event)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
#title-bar {
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
@apply flex justify-between text-white px-6 py-2 select-none relative;
|
|
||||||
z-index: 9999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buttons {
|
|
||||||
@apply flex gap-4;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
@apply hover:text-green-500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import AppMenu from "./AppMenu.vue";
|
|
||||||
import PlayBar from "./PlayBar.vue";
|
|
||||||
import SearchBar from "./SearchBar.vue";
|
|
||||||
|
|
||||||
export { AppMenu, PlayBar, SearchBar };
|
|
||||||
19
src/main.ts
@@ -1,19 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
import naive from "naive-ui";
|
|
||||||
import "vfonts/Lato.css";
|
|
||||||
import "vfonts/FiraCode.css";
|
|
||||||
|
|
||||||
// tailwind css
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
import router from "@/router";
|
|
||||||
|
|
||||||
import store from "@/store";
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
app.use(router);
|
|
||||||
app.use(store);
|
|
||||||
// app.use(naive);
|
|
||||||
app.mount("#app");
|
|
||||||
92
src/main/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||||
|
import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { loadLyricWindow } from './lyric';
|
||||||
|
import { initializeConfig } from './modules/config';
|
||||||
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
|
import { initializeTray } from './modules/tray';
|
||||||
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
|
import { startMusicApi } from './server';
|
||||||
|
|
||||||
|
// 导入所有图标
|
||||||
|
const iconPath = join(__dirname, '../../resources');
|
||||||
|
const icon = nativeImage.createFromPath(
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? join(iconPath, 'icon.icns')
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? join(iconPath, 'favicon.ico')
|
||||||
|
: join(iconPath, 'icon.png')
|
||||||
|
);
|
||||||
|
|
||||||
|
let mainWindow: Electron.BrowserWindow;
|
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
function initialize() {
|
||||||
|
// 初始化各个模块
|
||||||
|
initializeConfig();
|
||||||
|
initializeFileManager();
|
||||||
|
|
||||||
|
// 创建主窗口
|
||||||
|
mainWindow = createMainWindow(icon);
|
||||||
|
|
||||||
|
// 初始化窗口管理
|
||||||
|
initializeWindowManager();
|
||||||
|
|
||||||
|
// 初始化托盘
|
||||||
|
initializeTray(iconPath, mainWindow);
|
||||||
|
|
||||||
|
// 启动音乐API
|
||||||
|
startMusicApi();
|
||||||
|
|
||||||
|
// 加载歌词窗口
|
||||||
|
loadLyricWindow(ipcMain, mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用程序准备就绪时的处理
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
// 设置应用ID
|
||||||
|
electronApp.setAppUserModelId('com.alger.music');
|
||||||
|
|
||||||
|
// 监听窗口创建事件
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
// macOS 激活应用时的处理
|
||||||
|
app.on('activate', function () {
|
||||||
|
if (mainWindow === null) initialize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用程序准备就绪后的快捷键设置
|
||||||
|
app.on('ready', () => {
|
||||||
|
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有窗口关闭时的处理
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重启应用
|
||||||
|
ipcMain.on('restart', () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取系统架构信息
|
||||||
|
ipcMain.on('get-arch', (event) => {
|
||||||
|
event.returnValue = process.arch;
|
||||||
|
});
|
||||||
176
src/main/lyric.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { BrowserWindow, IpcMain, screen } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import path, { join } from 'path';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
let lyricWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
const createWin = () => {
|
||||||
|
console.log('Creating lyric window');
|
||||||
|
|
||||||
|
// 获取保存的窗口位置
|
||||||
|
const windowBounds =
|
||||||
|
(store.get('lyricWindowBounds') as {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) || {};
|
||||||
|
const { x, y, width, height } = windowBounds;
|
||||||
|
|
||||||
|
// 获取屏幕尺寸
|
||||||
|
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
|
||||||
|
// 验证保存的位置是否有效
|
||||||
|
const validPosition =
|
||||||
|
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
||||||
|
|
||||||
|
lyricWindow = new BrowserWindow({
|
||||||
|
width: width || 800,
|
||||||
|
height: height || 200,
|
||||||
|
x: validPosition ? x : undefined,
|
||||||
|
y: validPosition ? y : undefined,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
transparent: true,
|
||||||
|
hasShadow: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听窗口关闭事件
|
||||||
|
lyricWindow.on('closed', () => {
|
||||||
|
if (lyricWindow) {
|
||||||
|
lyricWindow.destroy();
|
||||||
|
lyricWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return lyricWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
|
||||||
|
const showLyricWindow = () => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
if (lyricWindow.isMinimized()) {
|
||||||
|
lyricWindow.restore();
|
||||||
|
}
|
||||||
|
lyricWindow.focus();
|
||||||
|
lyricWindow.show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.on('open-lyric', () => {
|
||||||
|
console.log('Received open-lyric request');
|
||||||
|
|
||||||
|
if (showLyricWindow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating new lyric window');
|
||||||
|
const win = createWin();
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
console.error('Failed to create lyric window');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);
|
||||||
|
} else {
|
||||||
|
const distPath = path.resolve(__dirname, '../renderer');
|
||||||
|
win.loadURL(`file://${distPath}/index.html#/lyric`);
|
||||||
|
}
|
||||||
|
|
||||||
|
win.setMinimumSize(600, 200);
|
||||||
|
win.setSkipTaskbar(true);
|
||||||
|
|
||||||
|
win.once('ready-to-show', () => {
|
||||||
|
console.log('Lyric window ready to show');
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('send-lyric', (_, data) => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
lyricWindow.webContents.send('receive-lyric', data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing lyric data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('top-lyric', (_, data) => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.setAlwaysOnTop(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('close-lyric', () => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.webContents.send('lyric-window-close');
|
||||||
|
mainWin.webContents.send('lyric-control-back', 'close');
|
||||||
|
lyricWindow.destroy();
|
||||||
|
lyricWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理鼠标事件
|
||||||
|
ipcMain.on('mouseenter-lyric', () => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.setIgnoreMouseEvents(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('mouseleave-lyric', () => {
|
||||||
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.setIgnoreMouseEvents(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理拖动移动
|
||||||
|
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
|
||||||
|
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||||
|
|
||||||
|
const [currentX, currentY] = lyricWindow.getPosition();
|
||||||
|
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
const [windowWidth, windowHeight] = lyricWindow.getSize();
|
||||||
|
|
||||||
|
// 计算新位置,确保窗口不会移出屏幕
|
||||||
|
const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth));
|
||||||
|
const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight));
|
||||||
|
|
||||||
|
lyricWindow.setPosition(newX, newY);
|
||||||
|
|
||||||
|
// 保存新位置
|
||||||
|
store.set('lyricWindowBounds', {
|
||||||
|
...lyricWindow.getBounds(),
|
||||||
|
x: newX,
|
||||||
|
y: newY
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加鼠标穿透事件处理
|
||||||
|
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
|
||||||
|
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||||
|
|
||||||
|
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加播放控制处理
|
||||||
|
ipcMain.on('control-back', (_, command) => {
|
||||||
|
console.log('command', command);
|
||||||
|
if (mainWin && !mainWin.isDestroyed()) {
|
||||||
|
console.log('Sending control-back command:', command);
|
||||||
|
mainWin.webContents.send('lyric-control-back', command);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
43
src/main/modules/config.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
import set from '../set.json';
|
||||||
|
|
||||||
|
interface StoreType {
|
||||||
|
set: {
|
||||||
|
isProxy: boolean;
|
||||||
|
noAnimate: boolean;
|
||||||
|
animationSpeed: number;
|
||||||
|
author: string;
|
||||||
|
authorUrl: string;
|
||||||
|
musicApiPort: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let store: Store<StoreType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置管理
|
||||||
|
*/
|
||||||
|
export function initializeConfig() {
|
||||||
|
store = new Store<StoreType>({
|
||||||
|
name: 'config',
|
||||||
|
defaults: {
|
||||||
|
set
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads'));
|
||||||
|
|
||||||
|
// 定义ipcRenderer监听事件
|
||||||
|
ipcMain.on('set-store-value', (_, key, value) => {
|
||||||
|
store.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('get-store-value', (_, key) => {
|
||||||
|
const value = store.get(key);
|
||||||
|
_.returnValue = value || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
72
src/main/modules/fileManager.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { app, dialog, ipcMain, shell } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化文件管理相关的IPC监听
|
||||||
|
*/
|
||||||
|
export function initializeFileManager() {
|
||||||
|
// 通用的选择目录处理
|
||||||
|
ipcMain.handle('select-directory', async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
title: '选择目录'
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用的打开目录处理
|
||||||
|
ipcMain.on('open-directory', (_, path) => {
|
||||||
|
shell.openPath(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载音乐处理
|
||||||
|
ipcMain.on('download-music', downloadMusic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载音乐功能
|
||||||
|
*/
|
||||||
|
async function downloadMusic(
|
||||||
|
event: Electron.IpcMainEvent,
|
||||||
|
{ url, filename }: { url: string; filename: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const store = new Store();
|
||||||
|
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||||
|
|
||||||
|
// 直接使用配置的下载路径
|
||||||
|
const filePath = path.join(downloadPath, `${filename}.mp3`);
|
||||||
|
|
||||||
|
// 检查文件是否已存在,如果存在则添加序号
|
||||||
|
let finalFilePath = filePath;
|
||||||
|
let counter = 1;
|
||||||
|
while (fs.existsSync(finalFilePath)) {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const nameWithoutExt = filePath.slice(0, -ext.length);
|
||||||
|
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'stream'
|
||||||
|
});
|
||||||
|
|
||||||
|
const writer = fs.createWriteStream(finalFilePath);
|
||||||
|
response.data.pipe(writer);
|
||||||
|
|
||||||
|
writer.on('finish', () => {
|
||||||
|
event.reply('music-download-complete', { success: true, path: finalFilePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
writer.on('error', (err) => {
|
||||||
|
event.reply('music-download-complete', { success: false, error: err.message });
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
event.reply('music-download-complete', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/modules/tray.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
let tray: Tray | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化系统托盘
|
||||||
|
*/
|
||||||
|
export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
||||||
|
const trayIcon = nativeImage
|
||||||
|
.createFromPath(join(iconPath, 'icon_16x16.png'))
|
||||||
|
.resize({ width: 16, height: 16 });
|
||||||
|
tray = new Tray(trayIcon);
|
||||||
|
|
||||||
|
// 创建一个上下文菜单
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.destroy();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 设置系统托盘图标的上下文菜单
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
119
src/main/modules/window.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { is } from '@electron-toolkit/utils';
|
||||||
|
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化代理设置
|
||||||
|
*/
|
||||||
|
function initializeProxy() {
|
||||||
|
const defaultConfig = {
|
||||||
|
enable: false,
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7890
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyConfig = store.get('set.proxyConfig', defaultConfig) as {
|
||||||
|
enable: boolean;
|
||||||
|
protocol: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxyConfig?.enable) {
|
||||||
|
const proxyRules = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;
|
||||||
|
session.defaultSession.setProxy({ proxyRules });
|
||||||
|
} else {
|
||||||
|
session.defaultSession.setProxy({ proxyRules: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化窗口管理相关的IPC监听
|
||||||
|
*/
|
||||||
|
export function initializeWindowManager() {
|
||||||
|
// 初始化代理设置
|
||||||
|
initializeProxy();
|
||||||
|
|
||||||
|
ipcMain.on('minimize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('maximize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
if (win.isMaximized()) {
|
||||||
|
win.unmaximize();
|
||||||
|
} else {
|
||||||
|
win.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('close-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.destroy();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('mini-tray', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听代理设置变化
|
||||||
|
store.onDidChange('set.proxyConfig', () => {
|
||||||
|
initializeProxy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主窗口
|
||||||
|
*/
|
||||||
|
export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 780,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
icon,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.setMinimumSize(1200, 780);
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||||
|
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
30
src/main/server.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import fs from 'fs';
|
||||||
|
import server from 'netease-cloud-music-api-alger/server';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { unblockMusic } from './unblockMusic';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
||||||
|
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理解锁音乐请求
|
||||||
|
ipcMain.handle('unblock-music', async (_, id, data) => {
|
||||||
|
return unblockMusic(id, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function startMusicApi(): Promise<void> {
|
||||||
|
console.log('MUSIC API STARTED');
|
||||||
|
|
||||||
|
const port = (store.get('set') as any).musicApiPort || 30488;
|
||||||
|
|
||||||
|
await server.serveNcmApi({
|
||||||
|
port
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { startMusicApi };
|
||||||
18
src/main/set.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"isProxy": false,
|
||||||
|
"proxyConfig": {
|
||||||
|
"enable": false,
|
||||||
|
"protocol": "http",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 7890
|
||||||
|
},
|
||||||
|
"enableRealIP": false,
|
||||||
|
"realIP": "",
|
||||||
|
"noAnimate": false,
|
||||||
|
"animationSpeed": 1,
|
||||||
|
"author": "Alger",
|
||||||
|
"authorUrl": "https://github.com/algerkong",
|
||||||
|
"musicApiPort": 30488,
|
||||||
|
"closeAction": "ask",
|
||||||
|
"musicQuality": "higher"
|
||||||
|
}
|
||||||
23
src/main/unblockMusic.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import match from '@unblockneteasemusic/server';
|
||||||
|
|
||||||
|
const unblockMusic = async (id: any, songData: any): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'], songData)
|
||||||
|
.then((data) => {
|
||||||
|
resolve({
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
type: 'song'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { unblockMusic };
|
||||||
19
src/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI;
|
||||||
|
api: {
|
||||||
|
sendLyric: (data: string) => void;
|
||||||
|
openLyric: () => void;
|
||||||
|
minimize: () => void;
|
||||||
|
maximize: () => void;
|
||||||
|
close: () => void;
|
||||||
|
dragStart: (data: string) => void;
|
||||||
|
miniTray: () => void;
|
||||||
|
restart: () => void;
|
||||||
|
unblockMusic: (id: number, data: any) => Promise<any>;
|
||||||
|
};
|
||||||
|
$message: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/preload/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { electronAPI } from '@electron-toolkit/preload';
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
minimize: () => ipcRenderer.send('minimize-window'),
|
||||||
|
maximize: () => ipcRenderer.send('maximize-window'),
|
||||||
|
close: () => ipcRenderer.send('close-window'),
|
||||||
|
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
||||||
|
miniTray: () => ipcRenderer.send('mini-tray'),
|
||||||
|
restart: () => ipcRenderer.send('restart'),
|
||||||
|
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||||
|
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||||
|
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI;
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.api = api;
|
||||||
|
}
|
||||||
54
src/renderer/App.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
|
||||||
|
<n-config-provider :theme="theme === 'dark' ? darkTheme : lightTheme">
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<router-view></router-view>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { darkTheme, lightTheme } from 'naive-ui';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import homeRouter from '@/router/home';
|
||||||
|
import store from '@/store';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
|
import { isMobile } from './utils';
|
||||||
|
|
||||||
|
const theme = computed(() => {
|
||||||
|
return store.state.theme;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.dispatch('initializeSettings');
|
||||||
|
store.dispatch('initializeTheme');
|
||||||
|
if (isMobile.value) {
|
||||||
|
store.commit(
|
||||||
|
'setMenus',
|
||||||
|
homeRouter.filter((item) => item.meta.isMobile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-container {
|
||||||
|
@apply h-full w-full;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.text-base {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.html:has(.mobile) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
src/renderer/api/home.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { IData } from '@/type';
|
||||||
|
import { IAlbumNew } from '@/type/album';
|
||||||
|
import { IDayRecommend } from '@/type/day_recommend';
|
||||||
|
import { IRecommendMusic } from '@/type/music';
|
||||||
|
import { IPlayListSort } from '@/type/playlist';
|
||||||
|
import { IHotSearch, ISearchKeyword } from '@/type/search';
|
||||||
|
import { IHotSinger } from '@/type/singer';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
interface IHotSingerParams {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRecommendMusicParams {
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取热门歌手
|
||||||
|
export const getHotSinger = (params: IHotSingerParams) => {
|
||||||
|
return request.get<IHotSinger>('/top/artists', { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取搜索推荐词
|
||||||
|
export const getSearchKeyword = () => {
|
||||||
|
return request.get<ISearchKeyword>('/search/default');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取热门搜索
|
||||||
|
export const getHotSearch = () => {
|
||||||
|
return request.get<IHotSearch>('/search/hot/detail');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取歌单分类
|
||||||
|
export const getPlaylistCategory = () => {
|
||||||
|
return request.get<IPlayListSort>('/playlist/catlist');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取推荐音乐
|
||||||
|
export const getRecommendMusic = (params: IRecommendMusicParams) => {
|
||||||
|
return request.get<IRecommendMusic>('/personalized/newsong', { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取每日推荐
|
||||||
|
export const getDayRecommend = () => {
|
||||||
|
return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取最新专辑推荐
|
||||||
|
export const getNewAlbum = () => {
|
||||||
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
58
src/renderer/api/music.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import store from '@/store';
|
||||||
|
import { ILyric } from '@/type/lyric';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import requestMusic from '@/utils/request_music';
|
||||||
|
|
||||||
|
// 获取音乐音质详情
|
||||||
|
export const getMusicQualityDetail = (id: number) => {
|
||||||
|
return request.get('/song/music/detail', { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据音乐Id获取音乐播放URl
|
||||||
|
export const getMusicUrl = async (id: number) => {
|
||||||
|
const res = await request.get('/song/download/url/v1', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
level: store.state.setData.musicQuality || 'higher'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.data.url) {
|
||||||
|
return { data: { data: [{ url: res.data.data.url }] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await request.get('/song/url/v1', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
level: store.state.setData.musicQuality || 'higher'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取歌曲详情
|
||||||
|
export const getMusicDetail = (ids: Array<number>) => {
|
||||||
|
return request.get('/song/detail', { params: { ids: ids.join(',') } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据音乐Id获取音乐歌词
|
||||||
|
export const getMusicLrc = (id: number) => {
|
||||||
|
return request.get<ILyric>('/lyric', { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getParsingMusicUrl = (id: number, data: any) => {
|
||||||
|
if (isElectron) {
|
||||||
|
return window.api.unblockMusic(id, data);
|
||||||
|
}
|
||||||
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 收藏歌曲
|
||||||
|
export const likeSong = (id: number, like: boolean = true) => {
|
||||||
|
return request.get('/like', { params: { id, like } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户喜欢的音乐列表
|
||||||
|
export const getLikedList = () => {
|
||||||
|
return request.get('/likelist');
|
||||||
|
};
|
||||||
45
src/renderer/api/mv.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { IData } from '@/type';
|
||||||
|
import { IMvUrlData } from '@/type/mv';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
interface MvParams {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
area?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 mv 排行
|
||||||
|
export const getTopMv = (params: MvParams) => {
|
||||||
|
return request({
|
||||||
|
url: '/mv/all',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有mv
|
||||||
|
export const getAllMv = (params: MvParams) => {
|
||||||
|
return request({
|
||||||
|
url: '/mv/all',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 mv 数据
|
||||||
|
export const getMvDetail = (mvid: string) => {
|
||||||
|
return request.get('/mv/detail', {
|
||||||
|
params: {
|
||||||
|
mvid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 mv 地址
|
||||||
|
export const getMvUrl = (id: Number) => {
|
||||||
|
return request.get<IData<IMvUrlData>>('/mv/url', {
|
||||||
|
params: {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
12
src/renderer/api/search.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
interface IParams {
|
||||||
|
keywords: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
// 搜索内容
|
||||||
|
export const getSearch = (params: IParams) => {
|
||||||
|
return request.get<any>('/cloudsearch', {
|
||||||
|
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 } });
|
||||||
}
|
}
|
||||||
BIN
src/renderer/assets/alipay.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
3687
src/renderer/assets/css/animate.css
vendored
Normal file
7
src/renderer/assets/css/base.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
body {
|
||||||
|
/* background-color: #000; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-popover:has(.music-play) {
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
|
}
|
||||||
BIN
src/renderer/assets/icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -1,12 +1,13 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 2685283 */
|
font-family: 'iconfont'; /* Project id 2685283 */
|
||||||
src: url('iconfont.woff2?t=1703643214551') format('woff2'),
|
src:
|
||||||
url('iconfont.woff?t=1703643214551') format('woff'),
|
url('iconfont.woff2?t=1703643214551') format('woff2'),
|
||||||
url('iconfont.ttf?t=1703643214551') format('truetype');
|
url('iconfont.woff?t=1703643214551') format('woff'),
|
||||||
|
url('iconfont.ttf?t=1703643214551') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
font-family: "iconfont" !important;
|
font-family: 'iconfont' !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -14,270 +15,269 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-list:before {
|
.icon-list:before {
|
||||||
content: "\e603";
|
content: '\e603';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-maxsize:before {
|
.icon-maxsize:before {
|
||||||
content: "\e692";
|
content: '\e692';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-close:before {
|
.icon-close:before {
|
||||||
content: "\e616";
|
content: '\e616';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-minisize:before {
|
.icon-minisize:before {
|
||||||
content: "\e602";
|
content: '\e602';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-shuaxin:before {
|
.icon-shuaxin:before {
|
||||||
content: "\e627";
|
content: '\e627';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-icon_error:before {
|
.icon-icon_error:before {
|
||||||
content: "\e615";
|
content: '\e615';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-3User:before {
|
.icon-a-3User:before {
|
||||||
content: "\e601";
|
content: '\e601';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Chat:before {
|
.icon-Chat:before {
|
||||||
content: "\e605";
|
content: '\e605';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Category:before {
|
.icon-Category:before {
|
||||||
content: "\e606";
|
content: '\e606';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Document:before {
|
.icon-Document:before {
|
||||||
content: "\e607";
|
content: '\e607';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Heart:before {
|
.icon-Heart:before {
|
||||||
content: "\e608";
|
content: '\e608';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Hide:before {
|
.icon-Hide:before {
|
||||||
content: "\e609";
|
content: '\e609';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Home:before {
|
.icon-Home:before {
|
||||||
content: "\e60a";
|
content: '\e60a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-Image2:before {
|
.icon-a-Image2:before {
|
||||||
content: "\e60b";
|
content: '\e60b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Profile:before {
|
.icon-Profile:before {
|
||||||
content: "\e60c";
|
content: '\e60c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Search:before {
|
.icon-Search:before {
|
||||||
content: "\e60d";
|
content: '\e60d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Paper:before {
|
.icon-Paper:before {
|
||||||
content: "\e60e";
|
content: '\e60e';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Play:before {
|
.icon-Play:before {
|
||||||
content: "\e60f";
|
content: '\e60f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Setting:before {
|
.icon-Setting:before {
|
||||||
content: "\e610";
|
content: '\e610';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-TicketStar:before {
|
.icon-a-TicketStar:before {
|
||||||
content: "\e611";
|
content: '\e611';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeOff:before {
|
.icon-a-VolumeOff:before {
|
||||||
content: "\e612";
|
content: '\e612';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeUp:before {
|
.icon-a-VolumeUp:before {
|
||||||
content: "\e613";
|
content: '\e613';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeDown:before {
|
.icon-a-VolumeDown:before {
|
||||||
content: "\e614";
|
content: '\e614';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-stop:before {
|
.icon-stop:before {
|
||||||
content: "\e600";
|
content: '\e600';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-next:before {
|
.icon-next:before {
|
||||||
content: "\e6a9";
|
content: '\e6a9';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-prev:before {
|
.icon-prev:before {
|
||||||
content: "\e6ac";
|
content: '\e6ac';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-play:before {
|
.icon-play:before {
|
||||||
content: "\e6aa";
|
content: '\e6aa';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-xiasanjiaoxing:before {
|
.icon-xiasanjiaoxing:before {
|
||||||
content: "\e642";
|
content: '\e642';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-videofill:before {
|
.icon-videofill:before {
|
||||||
content: "\e7c7";
|
content: '\e7c7';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favorfill:before {
|
.icon-favorfill:before {
|
||||||
content: "\e64b";
|
content: '\e64b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favor:before {
|
.icon-favor:before {
|
||||||
content: "\e64c";
|
content: '\e64c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-loading:before {
|
.icon-loading:before {
|
||||||
content: "\e64f";
|
content: '\e64f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-search:before {
|
.icon-search:before {
|
||||||
content: "\e65c";
|
content: '\e65c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-likefill:before {
|
.icon-likefill:before {
|
||||||
content: "\e668";
|
content: '\e668';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-like:before {
|
.icon-like:before {
|
||||||
content: "\e669";
|
content: '\e669';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-notificationfill:before {
|
.icon-notificationfill:before {
|
||||||
content: "\e66a";
|
content: '\e66a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-notification:before {
|
.icon-notification:before {
|
||||||
content: "\e66b";
|
content: '\e66b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-evaluate:before {
|
.icon-evaluate:before {
|
||||||
content: "\e672";
|
content: '\e672';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-homefill:before {
|
.icon-homefill:before {
|
||||||
content: "\e6bb";
|
content: '\e6bb';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-link:before {
|
.icon-link:before {
|
||||||
content: "\e6bf";
|
content: '\e6bf';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-roundaddfill:before {
|
.icon-roundaddfill:before {
|
||||||
content: "\e6d8";
|
content: '\e6d8';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-roundadd:before {
|
.icon-roundadd:before {
|
||||||
content: "\e6d9";
|
content: '\e6d9';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-add:before {
|
.icon-add:before {
|
||||||
content: "\e6da";
|
content: '\e6da';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-appreciatefill:before {
|
.icon-appreciatefill:before {
|
||||||
content: "\e6e3";
|
content: '\e6e3';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-forwardfill:before {
|
.icon-forwardfill:before {
|
||||||
content: "\e6ea";
|
content: '\e6ea';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-voicefill:before {
|
.icon-voicefill:before {
|
||||||
content: "\e6f0";
|
content: '\e6f0';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wefill:before {
|
.icon-wefill:before {
|
||||||
content: "\e6f4";
|
content: '\e6f4';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-keyboard:before {
|
.icon-keyboard:before {
|
||||||
content: "\e71b";
|
content: '\e71b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-picfill:before {
|
.icon-picfill:before {
|
||||||
content: "\e72c";
|
content: '\e72c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-markfill:before {
|
.icon-markfill:before {
|
||||||
content: "\e730";
|
content: '\e730';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-presentfill:before {
|
.icon-presentfill:before {
|
||||||
content: "\e732";
|
content: '\e732';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-peoplefill:before {
|
.icon-peoplefill:before {
|
||||||
content: "\e735";
|
content: '\e735';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-read:before {
|
.icon-read:before {
|
||||||
content: "\e742";
|
content: '\e742';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-backwardfill:before {
|
.icon-backwardfill:before {
|
||||||
content: "\e74d";
|
content: '\e74d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-playfill:before {
|
.icon-playfill:before {
|
||||||
content: "\e74f";
|
content: '\e74f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-all:before {
|
.icon-all:before {
|
||||||
content: "\e755";
|
content: '\e755';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-hotfill:before {
|
.icon-hotfill:before {
|
||||||
content: "\e757";
|
content: '\e757';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-recordfill:before {
|
.icon-recordfill:before {
|
||||||
content: "\e7a4";
|
content: '\e7a4';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-full:before {
|
.icon-full:before {
|
||||||
content: "\e7bc";
|
content: '\e7bc';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favor_fill_light:before {
|
.icon-favor_fill_light:before {
|
||||||
content: "\e7ec";
|
content: '\e7ec';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_favor_fill:before {
|
.icon-round_favor_fill:before {
|
||||||
content: "\e80a";
|
content: '\e80a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_location_fill:before {
|
.icon-round_location_fill:before {
|
||||||
content: "\e80b";
|
content: '\e80b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_like_fill:before {
|
.icon-round_like_fill:before {
|
||||||
content: "\e80c";
|
content: '\e80c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_people_fill:before {
|
.icon-round_people_fill:before {
|
||||||
content: "\e80d";
|
content: '\e80d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_skin_fill:before {
|
.icon-round_skin_fill:before {
|
||||||
content: "\e80e";
|
content: '\e80e';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-broadcast_fill:before {
|
.icon-broadcast_fill:before {
|
||||||
content: "\e81d";
|
content: '\e81d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-card_fill:before {
|
.icon-card_fill:before {
|
||||||
content: "\e81f";
|
content: '\e81f';
|
||||||
}
|
}
|
||||||
|
|
||||||