feat:播放列表拖拽排序;播放界面ui优化;插件页滚动问题;歌曲加载状提示;接入window系统的 AMTC 控制

This commit is contained in:
sqj
2025-09-15 20:43:44 +08:00
parent 65e876a2e9
commit 6692751c62
74 changed files with 1240 additions and 745 deletions

13
.eslintrc.backup.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": [
"@electron-toolkit/eslint-config-ts",
"@electron-toolkit/eslint-config-prettier"
],
"rules": {
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"no-console": "warn"
}
}

View File

@@ -2134,7 +2134,6 @@ function warn$1(msg, ...args) {
const trace = getComponentTrace() const trace = getComponentTrace()
if (appWarnHandler) { if (appWarnHandler) {
callWithErrorHandling(appWarnHandler, instance, 11, [ callWithErrorHandling(appWarnHandler, instance, 11, [
// eslint-disable-next-line no-restricted-syntax
msg + msg +
args args
.map((a) => { .map((a) => {
@@ -2700,7 +2699,6 @@ function setDevtoolsHook$1(hook, target) {
// (#4815) // (#4815)
typeof window !== 'undefined' && // some envs mock window but not fully typeof window !== 'undefined' && // some envs mock window but not fully
window.HTMLElement && // also exclude jsdom window.HTMLElement && // also exclude jsdom
// eslint-disable-next-line no-restricted-syntax
!((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null
? void 0 ? void 0
: _b.includes('jsdom')) : _b.includes('jsdom'))
@@ -6521,10 +6519,10 @@ function normalizePropsOptions(comp, appContext, asMixin = false) {
shouldCast = isFunction(propType) && propType.name === 'Boolean' shouldCast = isFunction(propType) && propType.name === 'Boolean'
} }
prop[0] = prop[0] =
/* shouldCast */ /* shouldCast */
shouldCast shouldCast
prop[1] = prop[1] =
/* shouldCastTrue */ /* shouldCastTrue */
shouldCastTrue shouldCastTrue
if (shouldCast || hasOwn(prop, 'default')) { if (shouldCast || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey) needCastKeys.push(normalizedKey)
@@ -9613,7 +9611,7 @@ function hydrateSuspense(
parentSuspense, parentSuspense,
parentComponent, parentComponent,
node.parentNode, node.parentNode,
// eslint-disable-next-line no-restricted-globals
document.createElement('div'), document.createElement('div'),
null, null,
namespace, namespace,

View File

@@ -3985,11 +3985,11 @@ var walker = (
const result = isEmptyObject(innerAnnotations) const result = isEmptyObject(innerAnnotations)
? { ? {
transformedValue, transformedValue,
annotations: !!transformationResult ? [transformationResult.type] : void 0 annotations: transformationResult ? [transformationResult.type] : void 0
} }
: { : {
transformedValue, transformedValue,
annotations: !!transformationResult annotations: transformationResult
? [transformationResult.type, innerAnnotations] ? [transformationResult.type, innerAnnotations]
: innerAnnotations : innerAnnotations
} }

235
eslint.config.js Normal file
View File

@@ -0,0 +1,235 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import vue from 'eslint-plugin-vue'
import prettier from '@electron-toolkit/eslint-config-prettier'
export default [
// 基础 JavaScript 推荐配置
js.configs.recommended,
// TypeScript 推荐配置
...tseslint.configs.recommended,
// Vue 3 推荐配置
...vue.configs['flat/recommended'],
// 忽略的文件和目录
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/out/**',
'**/build/**',
'**/.vitepress/**',
'**/docs/**',
'**/website/**',
'**/coverage/**',
'**/*.min.js',
'**/auto-imports.d.ts',
'**/components.d.ts',
'src/preload/index.d.ts', // 忽略类型定义文件
'src/renderer/src/assets/icon_font/**', // 忽略第三方图标字体文件
'src/main/utils/musicSdk/**', // 忽略第三方音乐 SDK
'src/main/utils/request.js', // 忽略第三方请求库
'scripts/**', // 忽略脚本文件
'src/common/utils/lyricUtils/**' // 忽略第三方歌词工具
]
},
// 全局配置
{
files: ['**/*.{js,ts,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// 代码质量 (放宽规则)
'no-unused-vars': 'off', // 由 TypeScript 处理
'no-undef': 'off', // 由 TypeScript 处理
'prefer-const': 'warn', // 降级为警告
'no-var': 'warn', // 降级为警告
'no-duplicate-imports': 'off', // 允许重复导入
'no-useless-return': 'off',
'no-useless-concat': 'off',
'no-useless-escape': 'off',
'no-unreachable': 'warn',
'no-debugger': 'off',
// 代码风格 (大幅放宽)
eqeqeq: 'off', // 允许 == 和 ===
curly: 'off', // 允许不使用大括号
'brace-style': 'off',
'comma-dangle': 'off',
quotes: 'off',
semi: 'off',
indent: 'off',
'object-curly-spacing': 'off',
'array-bracket-spacing': 'off',
'space-before-function-paren': 'off',
// 最佳实践 (放宽)
'no-eval': 'warn',
'no-implied-eval': 'warn',
'no-new-func': 'warn',
'no-alert': 'off',
'no-empty': 'off', // 允许空块
'no-extra-boolean-cast': 'off',
'no-extra-semi': 'off',
'no-irregular-whitespace': 'off',
'no-multiple-empty-lines': 'off',
'no-trailing-spaces': 'off',
'eol-last': 'off',
'no-fallthrough': 'off', // 允许 switch case 穿透
'no-case-declarations': 'off', // 允许 case 中声明变量
'no-empty-pattern': 'off', // 允许空对象模式
'no-prototype-builtins': 'off', // 允许直接调用 hasOwnProperty
'no-self-assign': 'off', // 允许自赋值
'no-async-promise-executor': 'off' // 允许异步 Promise 执行器
}
},
// 主进程 TypeScript 配置
{
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'src/common/**/*.ts', 'src/types/**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.node.json',
tsconfigRootDir: process.cwd()
}
},
rules: {
// TypeScript 特定规则 (大幅放宽)
'@typescript-eslint/no-unused-vars': 'off', // 完全关闭未使用变量检查
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off', // 允许 require
'@typescript-eslint/ban-ts-comment': 'off', // 允许 @ts-ignore
'@typescript-eslint/no-empty-function': 'off', // 允许空函数
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-expressions': 'off', // 允许未使用的表达式
'@typescript-eslint/no-require-imports': 'off', // 允许 require 导入
'@typescript-eslint/no-unsafe-function-type': 'off', // 允许 Function 类型
'@typescript-eslint/prefer-as-const': 'off' // 允许字面量类型
}
},
// 渲染进程 TypeScript 配置
{
files: ['src/renderer/**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.web.json',
tsconfigRootDir: process.cwd()
}
},
rules: {
// TypeScript 特定规则 (大幅放宽)
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/prefer-as-const': 'off'
}
},
// Vue 特定配置
{
files: ['src/renderer/**/*.vue'],
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.vue']
}
},
rules: {
// Vue 特定规则 (大幅放宽)
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off', // 允许 v-html
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off', // 不强制显式 emits
'vue/component-definition-name-casing': 'off',
'vue/component-name-in-template-casing': 'off',
'vue/custom-event-name-casing': 'off', // 允许任意事件命名
'vue/define-macros-order': 'off',
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/no-side-effects-in-computed-properties': 'off', // 允许计算属性中的副作用
'vue/no-required-prop-with-default': 'off', // 允许带默认值的必需属性
// TypeScript 在 Vue 中的规则 (放宽)
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off'
}
},
// 主进程文件配置 (Node.js 环境)
{
files: [
'src/main/**/*.{ts,js}',
'src/preload/**/*.{ts,js}',
'electron.vite.config.*',
'scripts/**/*.{js,ts}'
],
languageOptions: {
globals: {
__dirname: 'readonly',
__filename: 'readonly',
Buffer: 'readonly',
process: 'readonly',
global: 'readonly'
}
},
rules: {
// Node.js 特定规则 (放宽)
'no-console': 'off',
'no-process-exit': 'off' // 允许 process.exit()
}
},
// 渲染进程文件配置 (浏览器环境)
{
files: ['src/renderer/**/*.{ts,js,vue}'],
languageOptions: {
globals: {
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
console: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly'
}
},
rules: {
// 浏览器环境特定规则
'no-console': 'off'
}
},
// 配置文件特殊规则
{
files: ['*.config.{js,ts}', 'vite.config.*', 'electron.vite.config.*'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-var-requires': 'off'
}
},
// Prettier 配置 (必须放在最后)
prettier
]

View File

@@ -1,102 +0,0 @@
const baseRule = {
'no-new': 'off',
camelcase: 'off',
'no-return-assign': 'off',
'space-before-function-paren': ['error', 'never'],
'no-var': 'error',
'no-fallthrough': 'off',
eqeqeq: 'off',
'require-atomic-updates': ['error', { allowProperties: true }],
'no-multiple-empty-lines': [1, { max: 2 }],
'comma-dangle': [2, 'always-multiline'],
'standard/no-callback-literal': 'off',
'prefer-const': 'off',
'no-labels': 'off',
'node/no-callback-literal': 'off',
'multiline-ternary': 'off'
}
const typescriptRule = {
...baseRule,
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/restrict-template-expressions': [
1,
{
allowBoolean: true,
allowAny: true
}
],
'@typescript-eslint/restrict-plus-operands': [
1,
{
allowBoolean: true,
allowAny: true
}
],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
arguments: false,
attributes: false
}
}
],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/comma-dangle': 'off',
'@typescript-eslint/no-unsafe-argument': 'off'
}
const vueRule = {
...typescriptRule,
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/use-v-on-exact': 'off'
}
export const base = {
extends: ['standard'],
rules: baseRule,
parser: '@babel/eslint-parser'
}
export const html = {
files: ['*.html'],
plugins: ['html']
}
export const typescript = {
files: ['*.ts'],
rules: typescriptRule,
parser: '@typescript-eslint/parser',
extends: ['standard-with-typescript']
}
export const vue = {
files: ['*.vue'],
rules: vueRule,
parser: 'vue-eslint-parser',
extends: [
// 'plugin:vue/vue3-essential',
'plugin:vue/base',
'plugin:vue/vue3-recommended',
'plugin:vue-pug/vue3-recommended',
// "plugin:vue/strongly-recommended"
'standard-with-typescript'
],
parserOptions: {
sourceType: 'module',
parser: {
// Script parser for `<script>`
js: '@typescript-eslint/parser',
// Script parser for `<script lang="ts">`
ts: '@typescript-eslint/parser'
},
extraFileExtensions: ['.vue']
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "ceru-music", "name": "ceru-music",
"version": "1.2.8", "version": "1.3.0",
"description": "一款简洁优雅的音乐播放器", "description": "一款简洁优雅的音乐播放器",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "sqj,wldss,star", "author": "sqj,wldss,star",

View File

@@ -26,7 +26,7 @@ export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {
.replace(/[^0-9.]/g, fix) .replace(/[^0-9.]/g, fix)
.split('.') .split('.')
const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.') const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
let c = Math.max(currentVerArr.length, targetVerArr.length) const c = Math.max(currentVerArr.length, targetVerArr.length)
for (let i = 0; i < c; i++) { for (let i = 0; i < c; i++) {
// convert to integer the most efficient way // convert to integer the most efficient way
currentVerArr[i] = ~~currentVerArr[i] currentVerArr[i] = ~~currentVerArr[i]

View File

@@ -27,10 +27,10 @@ export const toDateObj = (date: any): Date | '' => {
switch (typeof date) { switch (typeof date) {
case 'string': case 'string':
if (!date.includes('T')) date = date.split('.')[0].replace(/-/g, '/') if (!date.includes('T')) date = date.split('.')[0].replace(/-/g, '/')
// eslint-disable-next-line no-fallthrough
case 'number': case 'number':
date = new Date(date) date = new Date(date)
// eslint-disable-next-line no-fallthrough
case 'object': case 'object':
break break
default: default:

View File

@@ -24,13 +24,13 @@ const headExp = /^.*\[id:\$\w+\]\n/
const parseLyric = (str) => { const parseLyric = (str) => {
str = str.replace(/\r/g, '') str = str.replace(/\r/g, '')
if (headExp.test(str)) str = str.replace(headExp, '') if (headExp.test(str)) str = str.replace(headExp, '')
let trans = str.match(/\[language:([\w=\\/+]+)\]/) const trans = str.match(/\[language:([\w=\\/+]+)\]/)
let lyric let lyric
let rlyric let rlyric
let tlyric let tlyric
if (trans) { if (trans) {
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '') str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
let json = JSON.parse(Buffer.from(trans[1], 'base64').toString()) const json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
for (const item of json.content) { for (const item of json.content) {
switch (item.type) { switch (item.type) {
case 0: case 0:
@@ -44,23 +44,23 @@ const parseLyric = (str) => {
} }
let i = 0 let i = 0
let crlyric = str.replace(/\[((\d+),\d+)\].*/g, (str) => { let crlyric = str.replace(/\[((\d+),\d+)\].*/g, (str) => {
let result = str.match(/\[((\d+),\d+)\].*/) const result = str.match(/\[((\d+),\d+)\].*/)
let lineStartTime = parseInt(result[2]) // 行开始时间 const lineStartTime = parseInt(result[2]) // 行开始时间
let time = lineStartTime let time = lineStartTime
let ms = time % 1000 const ms = time % 1000
time /= 1000 time /= 1000
let m = parseInt(time / 60) const m = parseInt(time / 60)
.toString() .toString()
.padStart(2, '0') .padStart(2, '0')
time %= 60 time %= 60
let s = parseInt(time).toString().padStart(2, '0') const s = parseInt(time).toString().padStart(2, '0')
time = `${m}:${s}.${ms}` time = `${m}:${s}.${ms}`
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}` if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}` if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
i++ i++
// 保持原始的 [start,duration] 格式,将相对时间戳转换为绝对时间戳 // 保持原始的 [start,duration] 格式,将相对时间戳转换为绝对时间戳
let processedStr = str.replace(/<(\d+),(\d+),(\d+)>/g, (match, start, duration, param) => { const processedStr = str.replace(/<(\d+),(\d+),(\d+)>/g, (match, start, duration, param) => {
const absoluteStart = lineStartTime + parseInt(start) const absoluteStart = lineStartTime + parseInt(start)
return `(${absoluteStart},${duration},${param})` return `(${absoluteStart},${duration},${param})`
}) })

View File

@@ -38,7 +38,7 @@ const handleScrollY = (
// @ts-expect-error // @ts-expect-error
const start = element.scrollTop ?? element.scrollY ?? 0 const start = element.scrollTop ?? element.scrollY ?? 0
if (to > start) { if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight const maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) { } else if (to < start) {
if (to < 0) to = 0 if (to < 0) to = 0
@@ -55,7 +55,7 @@ const handleScrollY = (
let currentTime = 0 let currentTime = 0
let val: number let val: number
let key = Math.random() const key = Math.random()
const animateScroll = () => { const animateScroll = () => {
element.lx_scrollTimeout = undefined element.lx_scrollTimeout = undefined
@@ -156,7 +156,7 @@ const handleScrollX = (
// @ts-expect-error // @ts-expect-error
const start = element.scrollLeft || element.scrollX || 0 const start = element.scrollLeft || element.scrollX || 0
if (to > start) { if (to > start) {
let maxScrollLeft = element.scrollWidth - element.clientWidth const maxScrollLeft = element.scrollWidth - element.clientWidth
if (to > maxScrollLeft) to = maxScrollLeft if (to > maxScrollLeft) to = maxScrollLeft
} else if (to < start) { } else if (to < start) {
if (to < 0) to = 0 if (to < 0) to = 0
@@ -173,7 +173,7 @@ const handleScrollX = (
let currentTime = 0 let currentTime = 0
let val: number let val: number
let key = Math.random() const key = Math.random()
const animateScroll = () => { const animateScroll = () => {
element.lx_scrollTimeout = undefined element.lx_scrollTimeout = undefined
@@ -272,7 +272,7 @@ const handleScrollXR = (
// @ts-expect-error // @ts-expect-error
const start = element.scrollLeft || (element.scrollX as number) || 0 const start = element.scrollLeft || (element.scrollX as number) || 0
if (to < start) { if (to < start) {
let maxScrollLeft = -element.scrollWidth + element.clientWidth const maxScrollLeft = -element.scrollWidth + element.clientWidth
if (to < maxScrollLeft) to = maxScrollLeft if (to < maxScrollLeft) to = maxScrollLeft
} else if (to > start) { } else if (to > start) {
if (to > 0) to = 0 if (to > 0) to = 0
@@ -290,7 +290,7 @@ const handleScrollXR = (
let currentTime = 0 let currentTime = 0
let val: number let val: number
let key = Math.random() const key = Math.random()
const animateScroll = () => { const animateScroll = () => {
element.lx_scrollTimeout = undefined element.lx_scrollTimeout = undefined
@@ -371,7 +371,7 @@ export const scrollXRTo = (
/** /**
* 设置标题 * 设置标题
*/ */
let dom_title = document.getElementsByTagName('title')[0] const dom_title = document.getElementsByTagName('title')[0]
export const setTitle = (title: string | null) => { export const setTitle = (title: string | null) => {
title ||= 'LX Music' title ||= 'LX Music'
dom_title.innerText = title dom_title.innerText = title

View File

@@ -51,14 +51,14 @@ export default {
...sources, ...sources,
init() { init() {
const tasks = [] const tasks = []
for (let source of sources.sources) { for (const source of sources.sources) {
let sm = sources[source.id] const sm = sources[source.id]
sm && sm.init && tasks.push(sm.init()) sm && sm.init && tasks.push(sm.init())
} }
return Promise.all(tasks) return Promise.all(tasks)
}, },
async searchMusic({ name, singer, source: s, limit = 25 }) { async searchMusic({ name, singer, source: s, limit = 25 }) {
const trimStr = (str) => (typeof str == 'string' ? str.trim() : str) const trimStr = (str) => (typeof str === 'string' ? str.trim() : str)
const musicName = trimStr(name) const musicName = trimStr(name)
const tasks = [] const tasks = []
const excludeSource = ['xm'] const excludeSource = ['xm']
@@ -106,7 +106,7 @@ export default {
const getIntv = (interval) => { const getIntv = (interval) => {
if (!interval) return 0 if (!interval) return 0
// if (musicInfo._interval) return musicInfo._interval // if (musicInfo._interval) return musicInfo._interval
let intvArr = interval.split(':') const intvArr = interval.split(':')
let intv = 0 let intv = 0
let unit = 1 let unit = 1
while (intvArr.length) { while (intvArr.length) {
@@ -115,9 +115,9 @@ export default {
} }
return intv return intv
} }
const trimStr = (str) => (typeof str == 'string' ? str.trim() : str || '') const trimStr = (str) => (typeof str === 'string' ? str.trim() : str || '')
const filterStr = (str) => const filterStr = (str) =>
typeof str == 'string' typeof str === 'string'
? str.replace(/\s|'|\.|,||&|"|、|\(|\)|||`|~|-|<|>|\||\/|\]|\[|!|/g, '') ? str.replace(/\s|'|\.|,||&|"|、|\(|\)|||`|~|-|<|>|\||\/|\]|\[|!|/g, '')
: String(str || '') : String(str || '')
const fMusicName = filterStr(name).toLowerCase() const fMusicName = filterStr(name).toLowerCase()

View File

@@ -47,7 +47,7 @@ export default {
) )
if (!albumList.info) return Promise.reject(new Error('Get album list failed.')) if (!albumList.info) return Promise.reject(new Error('Get album list failed.'))
let result = await getMusicInfosByList(albumList.info) const result = await getMusicInfosByList(albumList.info)
const info = await this.getAlbumInfo(id) const info = await this.getAlbumInfo(id)

View File

@@ -12,7 +12,7 @@ export default {
// const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id // const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id
// if (!res_id) throw new Error('获取评论失败') // if (!res_id) throw new Error('获取评论失败')
let timestamp = Date.now() const timestamp = Date.now()
const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0`
// const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=${res_id}&p=${page}&pagesize=${limit}&uuid=0&ver=10` // const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=${res_id}&p=${page}&pagesize=${limit}&uuid=0&ver=10`
const _requestObj = httpFetch( const _requestObj = httpFetch(
@@ -40,7 +40,7 @@ export default {
async getHotComment({ hash }, page = 1, limit = 20) { async getHotComment({ hash }, page = 1, limit = 20) {
// console.log(songmid) // console.log(songmid)
if (this._requestObj2) this._requestObj2.cancelHttp() if (this._requestObj2) this._requestObj2.cancelHttp()
let timestamp = Date.now() const timestamp = Date.now()
const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0`
// https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53 // https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53
const _requestObj2 = httpFetch( const _requestObj2 = httpFetch(
@@ -94,7 +94,7 @@ export default {
}, },
filterComment(rawList) { filterComment(rawList) {
return rawList.map((item) => { return rawList.map((item) => {
let data = { const data = {
id: item.id, id: item.id,
text: decodeName( text: decodeName(
(item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || '' (item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || ''

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../index' import { decodeName, formatPlayTime, sizeFormate } from '../index'
import { formatSingerName } from '../utils' import { formatSingerName } from '../utils'
let boardList = [ const boardList = [
{ id: 'kg__8888', name: 'TOP500', bangid: '8888' }, { id: 'kg__8888', name: 'TOP500', bangid: '8888' },
{ id: 'kg__6666', name: '飙升榜', bangid: '6666' }, { id: 'kg__6666', name: '飙升榜', bangid: '6666' },
{ id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' }, { id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' },
@@ -137,7 +137,7 @@ export default {
return requestDataObj.promise return requestDataObj.promise
}, },
getSinger(singers) { getSinger(singers) {
let arr = [] const arr = []
singers.forEach((singer) => { singers.forEach((singer) => {
arr.push(singer.author_name) arr.push(singer.author_name)
}) })
@@ -149,7 +149,7 @@ export default {
const types = [] const types = []
const _types = {} const _types = {}
if (item.filesize !== 0) { if (item.filesize !== 0) {
let size = sizeFormate(item.filesize) const size = sizeFormate(item.filesize)
types.push({ type: '128k', size, hash: item.hash }) types.push({ type: '128k', size, hash: item.hash })
_types['128k'] = { _types['128k'] = {
size, size,
@@ -157,7 +157,7 @@ export default {
} }
} }
if (item['320filesize'] !== 0) { if (item['320filesize'] !== 0) {
let size = sizeFormate(item['320filesize']) const size = sizeFormate(item['320filesize'])
types.push({ type: '320k', size, hash: item['320hash'] }) types.push({ type: '320k', size, hash: item['320hash'] })
_types['320k'] = { _types['320k'] = {
size, size,
@@ -165,7 +165,7 @@ export default {
} }
} }
if (item.sqfilesize !== 0) { if (item.sqfilesize !== 0) {
let size = sizeFormate(item.sqfilesize) const size = sizeFormate(item.sqfilesize)
types.push({ type: 'flac', size, hash: item.sqhash }) types.push({ type: 'flac', size, hash: item.sqhash })
_types.flac = { _types.flac = {
size, size,
@@ -173,7 +173,7 @@ export default {
} }
} }
if (item.filesize_high !== 0) { if (item.filesize_high !== 0) {
let size = sizeFormate(item.filesize_high) const size = sizeFormate(item.filesize_high)
types.push({ type: 'flac24bit', size, hash: item.hash_high }) types.push({ type: 'flac24bit', size, hash: item.hash_high })
_types.flac24bit = { _types.flac24bit = {
size, size,
@@ -201,7 +201,7 @@ export default {
filterBoardsData(rawList) { filterBoardsData(rawList) {
// console.log(rawList) // console.log(rawList)
let list = [] const list = []
for (const board of rawList) { for (const board of rawList) {
if (board.isvol != 1) continue if (board.isvol != 1) continue
list.push({ list.push({
@@ -243,9 +243,9 @@ export default {
if (body.errcode != 0) return this.getList(bangid, page, retryNum) if (body.errcode != 0) return this.getList(bangid, page, retryNum)
// console.log(body) // console.log(body)
let total = body.data.total const total = body.data.total
let limit = 100 const limit = 100
let listData = this.filterData(body.data.info) const listData = this.filterData(body.data.info)
// console.log(listData) // console.log(listData)
return { return {
total, total,
@@ -256,7 +256,7 @@ export default {
} }
}, },
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('kg__', '') if (typeof id === 'string') id = id.replace('kg__', '')
return `https://www.kugou.com/yy/rank/home/1-${id}.html` return `https://www.kugou.com/yy/rank/home/1-${id}.html`
} }
} }

View File

@@ -4,7 +4,7 @@ import { decodeKrc } from '../../../../common/utils/lyricUtils/kg'
export default { export default {
getIntv(interval) { getIntv(interval) {
if (!interval) return 0 if (!interval) return 0
let intvArr = interval.split(':') const intvArr = interval.split(':')
let intv = 0 let intv = 0
let unit = 1 let unit = 1
while (intvArr.length) { while (intvArr.length) {
@@ -36,7 +36,7 @@ export default {
// return requestObj // return requestObj
// }, // },
searchLyric(name, hash, time, tryNum = 0) { searchLyric(name, hash, time, tryNum = 0) {
let requestObj = httpFetch( const requestObj = httpFetch(
`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`, `http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`,
{ {
headers: { headers: {
@@ -49,12 +49,12 @@ export default {
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) { if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.searchLyric(name, hash, time, ++tryNum) const tryRequestObj = this.searchLyric(name, hash, time, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise return tryRequestObj.promise
} }
if (body.candidates.length) { if (body.candidates.length) {
let info = body.candidates[0] const info = body.candidates[0]
return { return {
id: info.id, id: info.id,
accessKey: info.accesskey, accessKey: info.accesskey,
@@ -66,7 +66,7 @@ export default {
return requestObj return requestObj
}, },
getLyricDownload(id, accessKey, fmt, tryNum = 0) { getLyricDownload(id, accessKey, fmt, tryNum = 0) {
let requestObj = httpFetch( const requestObj = httpFetch(
`http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`, `http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`,
{ {
headers: { headers: {
@@ -79,7 +79,7 @@ export default {
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) { if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum) const tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise return tryRequestObj.promise
} }
@@ -102,7 +102,7 @@ export default {
return requestObj return requestObj
}, },
getLyric(songInfo, tryNum = 0) { getLyric(songInfo, tryNum = 0) {
let requestObj = this.searchLyric( const requestObj = this.searchLyric(
songInfo.name, songInfo.name,
songInfo.hash, songInfo.hash,
songInfo._interval || this.getIntv(songInfo.interval) songInfo._interval || this.getIntv(songInfo.interval)
@@ -111,7 +111,7 @@ export default {
requestObj.promise = requestObj.promise.then((result) => { requestObj.promise = requestObj.promise.then((result) => {
if (!result) return Promise.reject(new Error('Get lyric failed')) if (!result) return Promise.reject(new Error('Get lyric failed'))
let requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt) const requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt)
requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2) requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2)

View File

@@ -2,7 +2,7 @@ import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { createHttpFetch } from './util' import { createHttpFetch } from './util'
const createGetMusicInfosTask = (hashs) => { const createGetMusicInfosTask = (hashs) => {
let data = { const data = {
area_code: '1', area_code: '1',
show_privilege: 1, show_privilege: 1,
show_album_info: '1', show_album_info: '1',
@@ -16,13 +16,13 @@ const createGetMusicInfosTask = (hashs) => {
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification' fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification'
} }
let list = hashs let list = hashs
let tasks = [] const tasks = []
while (list.length) { while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break if (list.length < 100) break
list = list.slice(100) list = list.slice(100)
} }
let url = 'http://gateway.kugou.com/v3/album_audio/audio' const url = 'http://gateway.kugou.com/v3/album_audio/audio'
return tasks.map((task) => return tasks.map((task) =>
createHttpFetch(url, { createHttpFetch(url, {
method: 'POST', method: 'POST',
@@ -41,8 +41,8 @@ const createGetMusicInfosTask = (hashs) => {
export const filterMusicInfoList = (rawList) => { export const filterMusicInfoList = (rawList) => {
// console.log(rawList) // console.log(rawList)
let ids = new Set() const ids = new Set()
let list = [] const list = []
rawList.forEach((item) => { rawList.forEach((item) => {
if (!item) return if (!item) return
if (ids.has(item.audio_info.audio_id)) return if (ids.has(item.audio_info.audio_id)) return
@@ -50,7 +50,7 @@ export const filterMusicInfoList = (rawList) => {
const types = [] const types = []
const _types = {} const _types = {}
if (item.audio_info.filesize !== '0') { if (item.audio_info.filesize !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize)) const size = sizeFormate(parseInt(item.audio_info.filesize))
types.push({ type: '128k', size, hash: item.audio_info.hash }) types.push({ type: '128k', size, hash: item.audio_info.hash })
_types['128k'] = { _types['128k'] = {
size, size,
@@ -58,7 +58,7 @@ export const filterMusicInfoList = (rawList) => {
} }
} }
if (item.audio_info.filesize_320 !== '0') { if (item.audio_info.filesize_320 !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_320)) const size = sizeFormate(parseInt(item.audio_info.filesize_320))
types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) types.push({ type: '320k', size, hash: item.audio_info.hash_320 })
_types['320k'] = { _types['320k'] = {
size, size,
@@ -66,7 +66,7 @@ export const filterMusicInfoList = (rawList) => {
} }
} }
if (item.audio_info.filesize_flac !== '0') { if (item.audio_info.filesize_flac !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) const size = sizeFormate(parseInt(item.audio_info.filesize_flac))
types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })
_types.flac = { _types.flac = {
size, size,
@@ -74,7 +74,7 @@ export const filterMusicInfoList = (rawList) => {
} }
} }
if (item.audio_info.filesize_high !== '0') { if (item.audio_info.filesize_high !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_high)) const size = sizeFormate(parseInt(item.audio_info.filesize_high))
types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })
_types.flac24bit = { _types.flac24bit = {
size, size,

View File

@@ -17,7 +17,7 @@ export default {
const types = [] const types = []
const _types = {} const _types = {}
if (rawData.FileSize !== 0) { if (rawData.FileSize !== 0) {
let size = sizeFormate(rawData.FileSize) const size = sizeFormate(rawData.FileSize)
types.push({ type: '128k', size, hash: rawData.FileHash }) types.push({ type: '128k', size, hash: rawData.FileHash })
_types['128k'] = { _types['128k'] = {
size, size,
@@ -25,7 +25,7 @@ export default {
} }
} }
if (rawData.HQFileSize !== 0) { if (rawData.HQFileSize !== 0) {
let size = sizeFormate(rawData.HQFileSize) const size = sizeFormate(rawData.HQFileSize)
types.push({ type: '320k', size, hash: rawData.HQFileHash }) types.push({ type: '320k', size, hash: rawData.HQFileHash })
_types['320k'] = { _types['320k'] = {
size, size,
@@ -33,7 +33,7 @@ export default {
} }
} }
if (rawData.SQFileSize !== 0) { if (rawData.SQFileSize !== 0) {
let size = sizeFormate(rawData.SQFileSize) const size = sizeFormate(rawData.SQFileSize)
types.push({ type: 'flac', size, hash: rawData.SQFileHash }) types.push({ type: 'flac', size, hash: rawData.SQFileHash })
_types.flac = { _types.flac = {
size, size,
@@ -41,7 +41,7 @@ export default {
} }
} }
if (rawData.ResFileSize !== 0) { if (rawData.ResFileSize !== 0) {
let size = sizeFormate(rawData.ResFileSize) const size = sizeFormate(rawData.ResFileSize)
types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash }) types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash })
_types.flac24bit = { _types.flac24bit = {
size, size,
@@ -67,7 +67,7 @@ export default {
} }
}, },
handleResult(rawData) { handleResult(rawData) {
let ids = new Set() const ids = new Set()
const list = [] const list = []
rawData.forEach((item) => { rawData.forEach((item) => {
const key = item.Audioid + item.FileHash const key = item.Audioid + item.FileHash
@@ -89,7 +89,7 @@ export default {
// http://newlyric.kuwo.cn/newlyric.lrc?62355680 // http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page, limit).then((result) => { return this.musicSearch(str, page, limit).then((result) => {
if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum) if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.data.lists) const list = this.handleResult(result.data.lists)
if (list == null) return this.search(str, page, limit, retryNum) if (list == null) return this.search(str, page, limit, retryNum)

View File

@@ -36,7 +36,7 @@ export default {
}) })
return requestObj.promise.then(({ body }) => { return requestObj.promise.then(({ body }) => {
if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败')) if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败'))
let info = body.data[0].info const info = body.data[0].info
const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
if (!img) return Promise.reject(new Error('Pic get failed')) if (!img) return Promise.reject(new Error('Pic get failed'))
return img return img

View File

@@ -71,10 +71,10 @@ export default {
if (tryNum > 2) throw new Error('try max num') if (tryNum > 2) throw new Error('try max num')
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
let listData = body.match(this.regExps.listData) const listData = body.match(this.regExps.listData)
let listInfo = body.match(this.regExps.listInfo) const listInfo = body.match(this.regExps.listInfo)
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum) if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
let list = await this.getMusicInfos(JSON.parse(listData[1])) const list = await this.getMusicInfos(JSON.parse(listData[1]))
// listData = this.filterData(JSON.parse(listData[1])) // listData = this.filterData(JSON.parse(listData[1]))
let name let name
let pic let pic
@@ -82,7 +82,7 @@ export default {
name = listInfo[1] name = listInfo[1]
pic = listInfo[2] pic = listInfo[2]
} }
let desc = this.parseHtmlDesc(body) const desc = this.parseHtmlDesc(body)
return { return {
list, list,
@@ -116,7 +116,7 @@ export default {
const result = [] const result = []
if (rawData.status !== 1) return result if (rawData.status !== 1) return result
for (const key of Object.keys(rawData.data)) { for (const key of Object.keys(rawData.data)) {
let tag = rawData.data[key] const tag = rawData.data[key]
result.push({ result.push({
id: tag.special_id, id: tag.special_id,
name: tag.special_name, name: tag.special_name,
@@ -219,7 +219,7 @@ export default {
}, },
createTask(hashs) { createTask(hashs) {
let data = { const data = {
area_code: '1', area_code: '1',
show_privilege: 1, show_privilege: 1,
show_album_info: '1', show_album_info: '1',
@@ -233,13 +233,13 @@ export default {
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname' fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname'
} }
let list = hashs let list = hashs
let tasks = [] const tasks = []
while (list.length) { while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break if (list.length < 100) break
list = list.slice(100) list = list.slice(100)
} }
let url = 'http://gateway.kugou.com/v2/album_audio/audio' const url = 'http://gateway.kugou.com/v2/album_audio/audio'
return tasks.map((task) => return tasks.map((task) =>
this.createHttp(url, { this.createHttp(url, {
method: 'POST', method: 'POST',
@@ -283,7 +283,7 @@ export default {
// console.log(songInfo) // console.log(songInfo)
// type 1单曲2歌单3电台4酷狗码5别人的播放队列 // type 1单曲2歌单3电台4酷狗码5别人的播放队列
let songList let songList
let info = songInfo.info const info = songInfo.info
switch (info.type) { switch (info.type) {
case 2: case 2:
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id) if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
@@ -319,7 +319,7 @@ export default {
}) })
// console.log(songList) // console.log(songList)
} }
let list = await this.getMusicInfos(songList || songInfo.list) const list = await this.getMusicInfos(songList || songInfo.list)
return { return {
list, list,
page: 1, page: 1,
@@ -354,7 +354,7 @@ export default {
this.getUserListDetail5(chain) this.getUserListDetail5(chain)
) )
} }
let list = await this.getMusicInfos(songInfo.list) const list = await this.getMusicInfos(songInfo.list)
// console.log(info, songInfo) // console.log(info, songInfo)
return { return {
list, list,
@@ -373,7 +373,7 @@ export default {
}, },
deDuplication(datas) { deDuplication(datas) {
let ids = new Set() const ids = new Set()
return datas.filter(({ hash }) => { return datas.filter(({ hash }) => {
if (ids.has(hash)) return false if (ids.has(hash)) return false
ids.add(hash) ids.add(hash)
@@ -408,9 +408,9 @@ export default {
}, },
async getUserListDetailByLink({ info }, link) { async getUserListDetailByLink({ info }, link) {
let listInfo = info['0'] const listInfo = info['0']
let total = listInfo.count let total = listInfo.count
let tasks = [] const tasks = []
let page = 0 let page = 0
while (total) { while (total) {
const limit = total > 90 ? 90 : total const limit = total > 90 ? 90 : total
@@ -448,7 +448,7 @@ export default {
} }
}, },
createGetListDetail2Task(id, total) { createGetListDetail2Task(id, total) {
let tasks = [] const tasks = []
let page = 0 let page = 0
while (total) { while (total) {
const limit = total > 300 ? 300 : total const limit = total > 300 ? 300 : total
@@ -481,13 +481,13 @@ export default {
return Promise.all(tasks).then(([...datas]) => datas.flat()) return Promise.all(tasks).then(([...datas]) => datas.flat())
}, },
async getUserListDetail2(global_collection_id) { async getUserListDetail2(global_collection_id) {
let id = global_collection_id const id = global_collection_id
if (id.length > 1000) throw new Error('get list error') if (id.length > 1000) throw new Error('get list error')
const params = const params =
'appid=1058&specialid=0&global_specialid=' + 'appid=1058&specialid=0&global_specialid=' +
id + id +
'&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-' '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
let info = await this.createHttp( const info = await this.createHttp(
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, `https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`,
{ {
headers: { headers: {
@@ -501,7 +501,7 @@ export default {
} }
) )
const songInfo = await this.createGetListDetail2Task(id, info.songcount) const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let list = await this.getMusicInfos(songInfo) const list = await this.getMusicInfos(songInfo)
// console.log(info, songInfo, list) // console.log(info, songInfo, list)
return { return {
list, list,
@@ -534,7 +534,7 @@ export default {
}, },
async getUserListDetailByPcChain(chain) { async getUserListDetailByPcChain(chain) {
let key = `${chain}_pc_list` const key = `${chain}_pc_list`
if (this.cache.has(key)) return this.cache.get(key) if (this.cache.has(key)) return this.cache.get(key)
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, { const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
headers: { headers: {
@@ -595,7 +595,7 @@ export default {
async getUserListDetailById(id, page, limit) { async getUserListDetailById(id, page, limit) {
const signature = await handleSignature(id, page, limit) const signature = await handleSignature(id, page, limit)
let info = await this.createHttp( const info = await this.createHttp(
`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`, `https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`,
{ {
headers: { headers: {
@@ -608,7 +608,7 @@ export default {
) )
// console.log(info) // console.log(info)
let result = await this.getMusicInfos(info.info) const result = await this.getMusicInfos(info.info)
// console.log(info, songInfo) // console.log(info, songInfo)
return result return result
}, },
@@ -621,7 +621,7 @@ export default {
link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1') link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
) )
if (link.includes('gcid_')) { if (link.includes('gcid_')) {
let gcid = link.match(/gcid_\w+/)?.[0] const gcid = link.match(/gcid_\w+/)?.[0]
if (gcid) { if (gcid) {
const global_collection_id = await this.decodeGcid(gcid) const global_collection_id = await this.decodeGcid(gcid)
if (global_collection_id) return this.getUserListDetail2(global_collection_id) if (global_collection_id) return this.getUserListDetail2(global_collection_id)
@@ -667,7 +667,7 @@ export default {
location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1') location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
) )
if (location.includes('gcid_')) { if (location.includes('gcid_')) {
let gcid = link.match(/gcid_\w+/)?.[0] const gcid = link.match(/gcid_\w+/)?.[0]
if (gcid) { if (gcid) {
const global_collection_id = await this.decodeGcid(gcid) const global_collection_id = await this.decodeGcid(gcid)
if (global_collection_id) return this.getUserListDetail2(global_collection_id) if (global_collection_id) return this.getUserListDetail2(global_collection_id)
@@ -698,7 +698,7 @@ export default {
// console.log('location', location) // console.log('location', location)
return this.getUserListDetail(location, page, ++retryNum) return this.getUserListDetail(location, page, ++retryNum)
} }
if (typeof body == 'string') { if (typeof body === 'string') {
let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1] let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1]
if (!global_collection_id) { if (!global_collection_id) {
let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1] let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1]
@@ -735,7 +735,7 @@ export default {
const types = [] const types = []
const _types = {} const _types = {}
if (item.filesize !== 0) { if (item.filesize !== 0) {
let size = sizeFormate(item.filesize) const size = sizeFormate(item.filesize)
types.push({ type: '128k', size, hash: item.hash }) types.push({ type: '128k', size, hash: item.hash })
_types['128k'] = { _types['128k'] = {
size, size,
@@ -743,7 +743,7 @@ export default {
} }
} }
if (item.filesize_320 !== 0) { if (item.filesize_320 !== 0) {
let size = sizeFormate(item.filesize_320) const size = sizeFormate(item.filesize_320)
types.push({ type: '320k', size, hash: item.hash_320 }) types.push({ type: '320k', size, hash: item.hash_320 })
_types['320k'] = { _types['320k'] = {
size, size,
@@ -751,7 +751,7 @@ export default {
} }
} }
if (item.filesize_ape !== 0) { if (item.filesize_ape !== 0) {
let size = sizeFormate(item.filesize_ape) const size = sizeFormate(item.filesize_ape)
types.push({ type: 'ape', size, hash: item.hash_ape }) types.push({ type: 'ape', size, hash: item.hash_ape })
_types.ape = { _types.ape = {
size, size,
@@ -759,7 +759,7 @@ export default {
} }
} }
if (item.filesize_flac !== 0) { if (item.filesize_flac !== 0) {
let size = sizeFormate(item.filesize_flac) const size = sizeFormate(item.filesize_flac)
types.push({ type: 'flac', size, hash: item.hash_flac }) types.push({ type: 'flac', size, hash: item.hash_flac })
_types.flac = { _types.flac = {
size, size,
@@ -849,8 +849,8 @@ export default {
// hash list filter // hash list filter
filterData2(rawList) { filterData2(rawList) {
// console.log(rawList) // console.log(rawList)
let ids = new Set() const ids = new Set()
let list = [] const list = []
rawList.forEach((item) => { rawList.forEach((item) => {
if (!item) return if (!item) return
if (ids.has(item.audio_info.audio_id)) return if (ids.has(item.audio_info.audio_id)) return
@@ -858,7 +858,7 @@ export default {
const types = [] const types = []
const _types = {} const _types = {}
if (item.audio_info.filesize !== '0') { if (item.audio_info.filesize !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize)) const size = sizeFormate(parseInt(item.audio_info.filesize))
types.push({ type: '128k', size, hash: item.audio_info.hash }) types.push({ type: '128k', size, hash: item.audio_info.hash })
_types['128k'] = { _types['128k'] = {
size, size,
@@ -866,7 +866,7 @@ export default {
} }
} }
if (item.audio_info.filesize_320 !== '0') { if (item.audio_info.filesize_320 !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_320)) const size = sizeFormate(parseInt(item.audio_info.filesize_320))
types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) types.push({ type: '320k', size, hash: item.audio_info.hash_320 })
_types['320k'] = { _types['320k'] = {
size, size,
@@ -874,7 +874,7 @@ export default {
} }
} }
if (item.audio_info.filesize_flac !== '0') { if (item.audio_info.filesize_flac !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) const size = sizeFormate(parseInt(item.audio_info.filesize_flac))
types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })
_types.flac = { _types.flac = {
size, size,
@@ -882,7 +882,7 @@ export default {
} }
} }
if (item.audio_info.filesize_high !== '0') { if (item.audio_info.filesize_high !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_high)) const size = sizeFormate(parseInt(item.audio_info.filesize_high))
types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })
_types.flac24bit = { _types.flac24bit = {
size, size,
@@ -927,7 +927,7 @@ export default {
// 获取列表数据 // 获取列表数据
getList(sortId, tagId, page) { getList(sortId, tagId, page) {
let tasks = [this.getSongList(sortId, tagId, page)] const tasks = [this.getSongList(sortId, tagId, page)]
tasks.push( tasks.push(
this.currentTagInfo.id === tagId this.currentTagInfo.id === tagId
? Promise.resolve(this.currentTagInfo.info) ? Promise.resolve(this.currentTagInfo.info)
@@ -964,7 +964,7 @@ export default {
}, },
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (typeof id == 'string') { if (typeof id === 'string') {
if (/^https?:\/\//.test(id)) return id if (/^https?:\/\//.test(id)) return id
id = id.replace('id_', '') id = id.replace('id_', '')
} }

View File

@@ -13,9 +13,9 @@ import { httpFetch } from '../../request'
export const signatureParams = (params, platform = 'android', body = '') => { export const signatureParams = (params, platform = 'android', body = '') => {
let keyparam = 'OIlwieks28dk2k092lksi2UIkp' let keyparam = 'OIlwieks28dk2k092lksi2UIkp'
if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt' if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'
let param_list = params.split('&') const param_list = params.split('&')
param_list.sort() param_list.sort()
let sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}` const sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}`
return toMD5(sign_params) return toMD5(sign_params)
} }

View File

@@ -10,9 +10,9 @@ export default {
// console.log(rawList) // console.log(rawList)
// console.log(rawList.length, rawList2.length) // console.log(rawList.length, rawList2.length)
return rawList.map((item, inedx) => { return rawList.map((item, inedx) => {
let formats = item.formats.split('|') const formats = item.formats.split('|')
let types = [] const types = []
let _types = {} const _types = {}
if (formats.includes('MP3128')) { if (formats.includes('MP3128')) {
types.push({ type: '128k', size: null }) types.push({ type: '128k', size: null })
_types['128k'] = { _types['128k'] = {

View File

@@ -68,13 +68,13 @@ const kw = {
}, },
getMusicUrls(musicInfo, cb) { getMusicUrls(musicInfo, cb) {
let tasks = [] const tasks = []
let songId = musicInfo.songmid const songId = musicInfo.songmid
musicInfo.types.forEach((type) => { musicInfo.types.forEach((type) => {
tasks.push(kw.getMusicUrl(songId, type.type).promise) tasks.push(kw.getMusicUrl(songId, type.type).promise)
}) })
Promise.all(tasks).then((urlInfo) => { Promise.all(tasks).then((urlInfo) => {
let typeUrl = {} const typeUrl = {}
urlInfo.forEach((info) => { urlInfo.forEach((info) => {
typeUrl[info.type] = info.url typeUrl[info.type] = info.url
}) })

View File

@@ -206,7 +206,7 @@ export default {
filterBoardsData(rawList) { filterBoardsData(rawList) {
// console.log(rawList) // console.log(rawList)
let list = [] const list = []
for (const board of rawList) { for (const board of rawList) {
if (board.source != '1') continue if (board.source != '1') continue
list.push({ list.push({

View File

@@ -148,8 +148,8 @@ export default {
}, */ }, */
sortLrcArr(arr) { sortLrcArr(arr) {
const lrcSet = new Set() const lrcSet = new Set()
let lrc = [] const lrc = []
let lrcT = [] const lrcT = []
let isLyricx = false let isLyricx = false
for (const item of arr) { for (const item of arr) {
@@ -192,11 +192,11 @@ export default {
}, },
parseLrc(lrc) { parseLrc(lrc) {
const lines = lrc.split(/\r\n|\r|\n/) const lines = lrc.split(/\r\n|\r|\n/)
let tags = [] const tags = []
let lrcArr = [] const lrcArr = []
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim() const line = lines[i].trim()
let result = timeExp.exec(line) const result = timeExp.exec(line)
if (result) { if (result) {
const text = line.replace(timeExp, '').trim() const text = line.replace(timeExp, '').trim()
let time = RegExp.$1 let time = RegExp.$1

View File

@@ -32,7 +32,7 @@ export default {
// console.log(rawData) // console.log(rawData)
for (let i = 0; i < rawData.length; i++) { for (let i = 0; i < rawData.length; i++) {
const info = rawData[i] const info = rawData[i]
let songId = info.MUSICRID.replace('MUSIC_', '') const songId = info.MUSICRID.replace('MUSIC_', '')
// const format = (info.FORMATS || info.formats).split('|') // const format = (info.FORMATS || info.formats).split('|')
if (!info.N_MINFO) { if (!info.N_MINFO) {
@@ -43,7 +43,7 @@ export default {
const types = [] const types = []
const _types = {} const _types = {}
let infoArr = info.N_MINFO.split(';') const infoArr = info.N_MINFO.split(';')
for (let info of infoArr) { for (let info of infoArr) {
info = info.match(this.regExps.mInfo) info = info.match(this.regExps.mInfo)
if (info) { if (info) {
@@ -77,7 +77,7 @@ export default {
} }
types.reverse() types.reverse()
let interval = parseInt(info.DURATION) const interval = parseInt(info.DURATION)
result.push({ result.push({
name: decodeName(info.SONGNAME), name: decodeName(info.SONGNAME),
@@ -109,7 +109,7 @@ export default {
// console.log(result) // console.log(result)
if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) if (!result || (result.TOTAL !== '0' && result.SHOW === '0'))
return this.search(str, page, limit, ++retryNum) return this.search(str, page, limit, ++retryNum)
let list = this.handleResult(result.abslist) const list = this.handleResult(result.abslist)
if (list == null) return this.search(str, page, limit, ++retryNum) if (list == null) return this.search(str, page, limit, ++retryNum)

View File

@@ -95,7 +95,7 @@ export default {
let id let id
let type let type
if (tagId) { if (tagId) {
let arr = tagId.split('-') const arr = tagId.split('-')
id = arr[0] id = arr[0]
type = arr[1] type = arr[1]
} else { } else {
@@ -235,9 +235,9 @@ export default {
filterBDListDetail(rawList) { filterBDListDetail(rawList) {
return rawList.map((item) => { return rawList.map((item) => {
let types = [] const types = []
let _types = {} const _types = {}
for (let info of item.audios) { for (const info of item.audios) {
info.size = info.size?.toLocaleUpperCase() info.size = info.size?.toLocaleUpperCase()
switch (info.bitrate) { switch (info.bitrate) {
case '4000': case '4000':
@@ -415,9 +415,9 @@ export default {
filterListDetail(rawData) { filterListDetail(rawData) {
// console.log(rawData) // console.log(rawData)
return rawData.map((item) => { return rawData.map((item) => {
let infoArr = item.N_MINFO.split(';') const infoArr = item.N_MINFO.split(';')
let types = [] const types = []
let _types = {} const _types = {}
for (let info of infoArr) { for (let info of infoArr) {
info = info.match(this.regExps.mInfo) info = info.match(this.regExps.mInfo)
if (info) { if (info) {
@@ -478,7 +478,7 @@ export default {
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1') if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
else if (/^digest-/.test(id)) { else if (/^digest-/.test(id)) {
let result = id.split('__') const result = id.split('__')
id = result[1] id = result[1]
} }
return `http://www.kuwo.cn/playlist_detail/${id}` return `http://www.kuwo.cn/playlist_detail/${id}`

View File

@@ -111,8 +111,8 @@ export const lrcTools = {
// 使用原始的酷我音乐时间计算逻辑,但输出绝对时间戳 // 使用原始的酷我音乐时间计算逻辑,但输出绝对时间戳
const offset = parseInt(str) const offset = parseInt(str)
const offset2 = parseInt(str2) const offset2 = parseInt(str2)
let startTime = Math.abs((offset + offset2) / (this.offset * 2)) const startTime = Math.abs((offset + offset2) / (this.offset * 2))
let duration = Math.abs((offset - offset2) / (this.offset2 * 2)) const duration = Math.abs((offset - offset2) / (this.offset2 * 2))
// 转换为基于行开始时间的绝对时间戳 // 转换为基于行开始时间的绝对时间戳
const absoluteStartTime = lineStartTime + startTime const absoluteStartTime = lineStartTime + startTime

View File

@@ -9,7 +9,7 @@ export default {
async getComment(musicInfo, page = 1, limit = 10) { async getComment(musicInfo, page = 1, limit = 10) {
if (this._requestObj) this._requestObj.cancelHttp() if (this._requestObj) this._requestObj.cancelHttp()
if (!musicInfo.songId) { if (!musicInfo.songId) {
let id = await getSongId(musicInfo) const id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败') if (!id) throw new Error('获取评论失败')
musicInfo.songId = id musicInfo.songId = id
} }
@@ -40,7 +40,7 @@ export default {
if (this._requestObj2) this._requestObj2.cancelHttp() if (this._requestObj2) this._requestObj2.cancelHttp()
if (!musicInfo.songId) { if (!musicInfo.songId) {
let id = await getSongId(musicInfo) const id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败') if (!id) throw new Error('获取评论失败')
musicInfo.songId = id musicInfo.songId = id
} }

View File

@@ -102,7 +102,7 @@ export default {
}, },
filterBoardsData(rawList) { filterBoardsData(rawList) {
// console.log(rawList) // console.log(rawList)
let list = [] const list = []
for (const board of rawList) { for (const board of rawList) {
if (board.template != 'group1') continue if (board.template != 'group1') continue
for (const item of board.itemList) { for (const item of board.itemList) {
@@ -112,7 +112,7 @@ export default {
) )
continue continue
let data = item.displayLogId.param const data = item.displayLogId.param
list.push({ list.push({
id: 'mg__' + data.rankId, id: 'mg__' + data.rankId,
name: data.rankName, name: data.rankName,
@@ -164,7 +164,7 @@ export default {
}, },
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('mg__', '') if (typeof id === 'string') id = id.replace('mg__', '')
for (const item of boardList) { for (const item of boardList) {
if (item.bangid == id) { if (item.bangid == id) {
return `https://music.migu.cn/v3/music/top/${item.webId}` return `https://music.migu.cn/v3/music/top/${item.webId}`

View File

@@ -16,21 +16,21 @@ const mrcTools = {
for (const line of lines) { for (const line of lines) {
if (line.length < 6) continue if (line.length < 6) continue
let result = this.rxps.lineTime.exec(line) const result = this.rxps.lineTime.exec(line)
if (!result) continue if (!result) continue
const startTime = parseInt(result[1]) const startTime = parseInt(result[1])
let time = startTime let time = startTime
let ms = time % 1000 const ms = time % 1000
time /= 1000 time /= 1000
let m = parseInt(time / 60) const m = parseInt(time / 60)
.toString() .toString()
.padStart(2, '0') .padStart(2, '0')
time %= 60 time %= 60
let s = parseInt(time).toString().padStart(2, '0') const s = parseInt(time).toString().padStart(2, '0')
time = `${m}:${s}.${ms}` time = `${m}:${s}.${ms}`
let words = line.replace(this.rxps.lineTime, '') const words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`) lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`)
@@ -100,11 +100,11 @@ export default {
getLyricWeb(songInfo, tryNum = 0) { getLyricWeb(songInfo, tryNum = 0) {
// console.log(songInfo.copyrightId) // console.log(songInfo.copyrightId)
if (songInfo.lrcUrl) { if (songInfo.lrcUrl) {
let requestObj = httpFetch(songInfo.lrcUrl) const requestObj = httpFetch(songInfo.lrcUrl)
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) { if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum) const tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise return tryRequestObj.promise
} }
@@ -115,7 +115,7 @@ export default {
}) })
return requestObj return requestObj
} else { } else {
let requestObj = httpFetch( const requestObj = httpFetch(
`https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`, `https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`,
{ {
headers: { headers: {
@@ -126,7 +126,7 @@ export default {
requestObj.promise = requestObj.promise.then(({ body }) => { requestObj.promise = requestObj.promise.then(({ body }) => {
if (body.returnCode !== '000000' || !body.lyric) { if (body.returnCode !== '000000' || !body.lyric) {
if (tryNum > 5) return Promise.reject(new Error('Get lyric failed')) if (tryNum > 5) return Promise.reject(new Error('Get lyric failed'))
let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum) const tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise return tryRequestObj.promise
} }
@@ -140,9 +140,9 @@ export default {
}, },
getLyric(songInfo) { getLyric(songInfo) {
let requestObj = mrcTools.getLyric(songInfo) const requestObj = mrcTools.getLyric(songInfo)
requestObj.promise = requestObj.promise.catch(() => { requestObj.promise = requestObj.promise.catch(() => {
let webRequestObj = this.getLyricWeb(songInfo) const webRequestObj = this.getLyricWeb(songInfo)
requestObj.cancelHttp = webRequestObj.cancelHttp.bind(webRequestObj) requestObj.cancelHttp = webRequestObj.cancelHttp.bind(webRequestObj)
return webRequestObj.promise return webRequestObj.promise
}) })

View File

@@ -4,13 +4,13 @@ import { formatSingerName } from '../utils'
const createGetMusicInfosTask = (ids) => { const createGetMusicInfosTask = (ids) => {
let list = ids let list = ids
let tasks = [] const tasks = []
while (list.length) { while (list.length) {
tasks.push(list.slice(0, 100)) tasks.push(list.slice(0, 100))
if (list.length < 100) break if (list.length < 100) break
list = list.slice(100) list = list.slice(100)
} }
let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2' const url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2'
return Promise.all( return Promise.all(
tasks.map((task) => tasks.map((task) =>
createHttpFetch(url, { createHttpFetch(url, {
@@ -25,7 +25,7 @@ const createGetMusicInfosTask = (ids) => {
export const filterMusicInfoList = (rawList) => { export const filterMusicInfoList = (rawList) => {
// console.log(rawList) // console.log(rawList)
let ids = new Set() const ids = new Set()
const list = [] const list = []
rawList.forEach((item) => { rawList.forEach((item) => {
if (!item.songId || ids.has(item.songId)) return if (!item.songId || ids.has(item.songId)) return

View File

@@ -212,7 +212,7 @@ export default {
return Promise.reject(new Error(result ? result.info : '搜索失败')) return Promise.reject(new Error(result ? result.info : '搜索失败'))
const songResultData = result.songResultData || { resultList: [], totalCount: 0 } const songResultData = result.songResultData || { resultList: [], totalCount: 0 }
let list = this.filterData(songResultData.resultList) const list = this.filterData(songResultData.resultList)
if (list == null) return this.search(str, page, limit, retryNum) if (list == null) return this.search(str, page, limit, retryNum)
this.total = parseInt(songResultData.totalCount) this.total = parseInt(songResultData.totalCount)

View File

@@ -3,7 +3,7 @@ import getSongId from './songId'
export default { export default {
async getPicUrl(songId, tryNum = 0) { async getPicUrl(songId, tryNum = 0) {
let requestObj = httpFetch( const requestObj = httpFetch(
`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`, `http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`,
{ {
headers: { headers: {
@@ -14,7 +14,7 @@ export default {
requestObj.promise.then(({ body }) => { requestObj.promise.then(({ body }) => {
if (body.returnCode !== '000000') { if (body.returnCode !== '000000') {
if (tryNum > 5) return Promise.reject(new Error('图片获取失败')) if (tryNum > 5) return Promise.reject(new Error('图片获取失败'))
let tryRequestObj = this.getPic(songId, ++tryNum) const tryRequestObj = this.getPic(songId, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise return tryRequestObj.promise
} }

View File

@@ -22,15 +22,15 @@ const teaDecrypt = (data, key) => {
let j2 = data[0] let j2 = data[0]
let j3 = toLong((6n + 52n / lengthBitint) * DELTA) let j3 = toLong((6n + 52n / lengthBitint) * DELTA)
while (true) { while (true) {
let j4 = j3 const j4 = j3
if (j4 == 0n) break if (j4 == 0n) break
let j5 = toLong(3n & toLong(j4 >> 2n)) const j5 = toLong(3n & toLong(j4 >> 2n))
let j6 = lengthBitint let j6 = lengthBitint
while (true) { while (true) {
j6-- j6--
if (j6 > 0n) { if (j6 > 0n) {
let j7 = data[j6 - 1n] const j7 = data[j6 - 1n]
let i = j6 const i = j6
j2 = toLong( j2 = toLong(
data[i] - data[i] -
(toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^ (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^
@@ -42,7 +42,7 @@ const teaDecrypt = (data, key) => {
data[i] = j2 data[i] = j2
} else break } else break
} }
let j8 = data[lengthBitint - 1n] const j8 = data[lengthBitint - 1n]
j2 = toLong( j2 = toLong(
data[0n] - data[0n] -
toLong( toLong(
@@ -89,7 +89,7 @@ const toBigintArray = (data) => {
const MAX = 9223372036854775807n const MAX = 9223372036854775807n
const MIN = -9223372036854775808n const MIN = -9223372036854775808n
const toLong = (str) => { const toLong = (str) => {
const num = typeof str == 'string' ? BigInt('0x' + str) : str const num = typeof str === 'string' ? BigInt('0x' + str) : str
if (num > MAX) return toLong(num - (1n << 64n)) if (num > MAX) return toLong(num - (1n << 64n))
else if (num < MIN) return toLong(num + (1n << 64n)) else if (num < MIN) return toLong(num + (1n << 64n))
return num return num

View File

@@ -197,10 +197,10 @@ export default {
}, },
filterNewComment(rawList) { filterNewComment(rawList) {
return rawList.map((item) => { return rawList.map((item) => {
let time = this.formatTime(item.time) const time = this.formatTime(item.time)
let timeStr = time ? dateFormat2(time) : null const timeStr = time ? dateFormat2(time) : null
if (item.middlecommentcontent) { if (item.middlecommentcontent) {
let firstItem = item.middlecommentcontent[0] const firstItem = item.middlecommentcontent[0]
firstItem.avatarurl = item.avatarurl firstItem.avatarurl = item.avatarurl
firstItem.praisenum = item.praisenum firstItem.praisenum = item.praisenum
item.avatarurl = null item.avatarurl = null
@@ -270,12 +270,12 @@ export default {
}) })
}, },
replaceEmoji(msg) { replaceEmoji(msg) {
let rxp = /^\[em\](e\d+)\[\/em\]$/ const rxp = /^\[em\](e\d+)\[\/em\]$/
let result = msg.match(/\[em\]e\d+\[\/em\]/g) let result = msg.match(/\[em\]e\d+\[\/em\]/g)
if (!result) return msg if (!result) return msg
result = Array.from(new Set(result)) result = Array.from(new Set(result))
for (let item of result) { for (const item of result) {
let code = item.replace(rxp, '$1') const code = item.replace(rxp, '$1')
msg = msg.replace( msg = msg.replace(
new RegExp(item.replace('[em]', '\\[em\\]').replace('[/em]', '\\[\\/em\\]'), 'g'), new RegExp(item.replace('[em]', '\\[em\\]').replace('[/em]', '\\[\\/em\\]'), 'g'),
emojis[code] || '' emojis[code] || ''

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../index' import { formatPlayTime, sizeFormate } from '../index'
import { formatSingerName } from '../utils' import { formatSingerName } from '../utils'
let boardList = [ const boardList = [
{ id: 'tx__4', name: '流行指数榜', bangid: '4' }, { id: 'tx__4', name: '流行指数榜', bangid: '4' },
{ id: 'tx__26', name: '热歌榜', bangid: '26' }, { id: 'tx__26', name: '热歌榜', bangid: '26' },
{ id: 'tx__27', name: '新歌榜', bangid: '27' }, { id: 'tx__27', name: '新歌榜', bangid: '27' },
@@ -137,31 +137,31 @@ export default {
filterData(rawList) { filterData(rawList) {
// console.log(rawList) // console.log(rawList)
return rawList.map((item) => { return rawList.map((item) => {
let types = [] const types = []
let _types = {} const _types = {}
if (item.file.size_128mp3 !== 0) { if (item.file.size_128mp3 !== 0) {
let size = sizeFormate(item.file.size_128mp3) const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size }) types.push({ type: '128k', size })
_types['128k'] = { _types['128k'] = {
size size
} }
} }
if (item.file.size_320mp3 !== 0) { if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3) const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size }) types.push({ type: '320k', size })
_types['320k'] = { _types['320k'] = {
size size
} }
} }
if (item.file.size_flac !== 0) { if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac) const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size }) types.push({ type: 'flac', size })
_types.flac = { _types.flac = {
size size
} }
} }
if (item.file.size_hires !== 0) { if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires) const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size }) types.push({ type: 'flac24bit', size })
_types.flac24bit = { _types.flac24bit = {
size size
@@ -195,10 +195,10 @@ export default {
}, },
getPeriods(bangid) { getPeriods(bangid) {
return this.getData(this.periodUrl).then(({ body: html }) => { return this.getData(this.periodUrl).then(({ body: html }) => {
let result = html.match(this.regExps.periodList) const result = html.match(this.regExps.periodList)
if (!result) return Promise.reject(new Error('get data failed')) if (!result) return Promise.reject(new Error('get data failed'))
result.forEach((item) => { result.forEach((item) => {
let result = item.match(this.regExps.period) const result = item.match(this.regExps.period)
if (!result) return if (!result) return
this.periods[result[2]] = { this.periods[result[2]] = {
name: result[1], name: result[1],
@@ -212,7 +212,7 @@ export default {
}, },
filterBoardsData(rawList) { filterBoardsData(rawList) {
// console.log(rawList) // console.log(rawList)
let list = [] const list = []
for (const board of rawList) { for (const board of rawList) {
// 排除 MV榜 // 排除 MV榜
if (board.id == 201) continue if (board.id == 201) continue
@@ -256,8 +256,8 @@ export default {
getList(bangid, page, retryNum = 0) { getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num')) if (++retryNum > 3) return Promise.reject(new Error('try max num'))
bangid = parseInt(bangid) bangid = parseInt(bangid)
let info = this.periods[bangid] const info = this.periods[bangid]
let p = info ? Promise.resolve(info.period) : this.getPeriods(bangid) const p = info ? Promise.resolve(info.period) : this.getPeriods(bangid)
return p.then((period) => { return p.then((period) => {
return this.listDetailRequest(bangid, period, this.limit).then((resp) => { return this.listDetailRequest(bangid, period, this.limit).then((resp) => {
if (resp.body.code !== 0) return this.getList(bangid, page, retryNum) if (resp.body.code !== 0) return this.getList(bangid, page, retryNum)
@@ -273,7 +273,7 @@ export default {
}, },
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('tx__', '') if (typeof id === 'string') id = id.replace('tx__', '')
return `https://y.qq.com/n/ryqq/toplist/${id}` return `https://y.qq.com/n/ryqq/toplist/${id}`
} }
} }

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../index' import { formatPlayTime, sizeFormate } from '../index'
const getSinger = (singers) => { const getSinger = (singers) => {
let arr = [] const arr = []
singers.forEach((singer) => { singers.forEach((singer) => {
arr.push(singer.name) arr.push(singer.name)
}) })
@@ -37,32 +37,32 @@ export default (songmid) => {
const item = body.req.data.track_info const item = body.req.data.track_info
if (!item.file?.media_mid) return null if (!item.file?.media_mid) return null
let types = [] const types = []
let _types = {} const _types = {}
const file = item.file const file = item.file
if (file.size_128mp3 != 0) { if (file.size_128mp3 != 0) {
let size = sizeFormate(file.size_128mp3) const size = sizeFormate(file.size_128mp3)
types.push({ type: '128k', size }) types.push({ type: '128k', size })
_types['128k'] = { _types['128k'] = {
size size
} }
} }
if (file.size_320mp3 !== 0) { if (file.size_320mp3 !== 0) {
let size = sizeFormate(file.size_320mp3) const size = sizeFormate(file.size_320mp3)
types.push({ type: '320k', size }) types.push({ type: '320k', size })
_types['320k'] = { _types['320k'] = {
size size
} }
} }
if (file.size_flac !== 0) { if (file.size_flac !== 0) {
let size = sizeFormate(file.size_flac) const size = sizeFormate(file.size_flac)
types.push({ type: 'flac', size }) types.push({ type: 'flac', size })
_types.flac = { _types.flac = {
size size
} }
} }
if (file.size_hires !== 0) { if (file.size_hires !== 0) {
let size = sizeFormate(file.size_hires) const size = sizeFormate(file.size_hires)
types.push({ type: 'flac24bit', size }) types.push({ type: 'flac24bit', size })
_types.flac24bit = { _types.flac24bit = {
size size

View File

@@ -56,32 +56,32 @@ export default {
rawList.forEach((item) => { rawList.forEach((item) => {
if (!item.file?.media_mid) return if (!item.file?.media_mid) return
let types = [] const types = []
let _types = {} const _types = {}
const file = item.file const file = item.file
if (file.size_128mp3 != 0) { if (file.size_128mp3 != 0) {
let size = sizeFormate(file.size_128mp3) const size = sizeFormate(file.size_128mp3)
types.push({ type: '128k', size }) types.push({ type: '128k', size })
_types['128k'] = { _types['128k'] = {
size size
} }
} }
if (file.size_320mp3 !== 0) { if (file.size_320mp3 !== 0) {
let size = sizeFormate(file.size_320mp3) const size = sizeFormate(file.size_320mp3)
types.push({ type: '320k', size }) types.push({ type: '320k', size })
_types['320k'] = { _types['320k'] = {
size size
} }
} }
if (file.size_flac !== 0) { if (file.size_flac !== 0) {
let size = sizeFormate(file.size_flac) const size = sizeFormate(file.size_flac)
types.push({ type: 'flac', size }) types.push({ type: 'flac', size })
_types.flac = { _types.flac = {
size size
} }
} }
if (file.size_hires !== 0) { if (file.size_hires !== 0) {
let size = sizeFormate(file.size_hires) const size = sizeFormate(file.size_hires)
types.push({ type: 'flac24bit', size }) types.push({ type: 'flac24bit', size })
_types.flac24bit = { _types.flac24bit = {
size size
@@ -123,7 +123,7 @@ export default {
if (limit == null) limit = this.limit if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680 // http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page, limit).then(({ body, meta }) => { return this.musicSearch(str, page, limit).then(({ body, meta }) => {
let list = this.handleResult(body.item_song) const list = this.handleResult(body.item_song)
this.total = meta.estimate_sum this.total = meta.estimate_sum
this.page = page this.page = page

View File

@@ -7,28 +7,28 @@ export const filterMusicInfoItem = (item) => {
const types = [] const types = []
const _types = {} const _types = {}
if (item.file.size_128mp3 != 0) { if (item.file.size_128mp3 != 0) {
let size = sizeFormate(item.file.size_128mp3) const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size }) types.push({ type: '128k', size })
_types['128k'] = { _types['128k'] = {
size size
} }
} }
if (item.file.size_320mp3 !== 0) { if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3) const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size }) types.push({ type: '320k', size })
_types['320k'] = { _types['320k'] = {
size size
} }
} }
if (item.file.size_flac !== 0) { if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac) const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size }) types.push({ type: 'flac', size })
_types.flac = { _types.flac = {
size size
} }
} }
if (item.file.size_hires !== 0) { if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires) const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size }) types.push({ type: 'flac24bit', size })
_types.flac24bit = { _types.flac24bit = {
size size

View File

@@ -95,12 +95,12 @@ export default {
}) })
}, },
filterInfoHotTag(html) { filterInfoHotTag(html) {
let hotTag = html.match(this.regExps.hotTagHtml) const hotTag = html.match(this.regExps.hotTagHtml)
const hotTags = [] const hotTags = []
if (!hotTag) return hotTags if (!hotTag) return hotTags
hotTag.forEach((tagHtml) => { hotTag.forEach((tagHtml) => {
let result = tagHtml.match(this.regExps.hotTag) const result = tagHtml.match(this.regExps.hotTag)
if (!result) return if (!result) return
hotTags.push({ hotTags.push({
id: parseInt(result[1]), id: parseInt(result[1]),
@@ -240,31 +240,31 @@ export default {
filterListDetail(rawList) { filterListDetail(rawList) {
// console.log(rawList) // console.log(rawList)
return rawList.map((item) => { return rawList.map((item) => {
let types = [] const types = []
let _types = {} const _types = {}
if (item.file.size_128mp3 !== 0) { if (item.file.size_128mp3 !== 0) {
let size = sizeFormate(item.file.size_128mp3) const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size }) types.push({ type: '128k', size })
_types['128k'] = { _types['128k'] = {
size size
} }
} }
if (item.file.size_320mp3 !== 0) { if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3) const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size }) types.push({ type: '320k', size })
_types['320k'] = { _types['320k'] = {
size size
} }
} }
if (item.file.size_flac !== 0) { if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac) const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size }) types.push({ type: 'flac', size })
_types.flac = { _types.flac = {
size size
} }
} }
if (item.file.size_hires !== 0) { if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires) const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size }) types.push({ type: 'flac24bit', size })
_types.flac24bit = { _types.flac24bit = {
size size

View File

@@ -42,7 +42,7 @@ export const formatSingerName = (singers, nameKey = 'name', join = '、') => {
if (Array.isArray(singers)) { if (Array.isArray(singers)) {
const singer = [] const singer = []
singers.forEach((item) => { singers.forEach((item) => {
let name = item[nameKey] const name = item[nameKey]
if (!name) return if (!name) return
singer.push(name) singer.push(name)
}) })

View File

@@ -70,7 +70,7 @@ const applyEmoji = (text) => {
return text return text
} }
let cursorTools = { const cursorTools = {
cache: {}, cache: {},
getCursor(id, page, limit) { getCursor(id, page, limit) {
let cacheData = this.cache[id] let cacheData = this.cache[id]
@@ -190,7 +190,7 @@ export default {
}, },
filterComment(rawList) { filterComment(rawList) {
return rawList.map((item) => { return rawList.map((item) => {
let data = { const data = {
id: item.commentId, id: item.commentId,
text: item.content ? applyEmoji(item.content) : '', text: item.content ? applyEmoji(item.content) : '',
time: item.time ? item.time : '', time: item.time ? item.time : '',
@@ -203,7 +203,7 @@ export default {
reply: [] reply: []
} }
let replyData = item.beReplied && item.beReplied[0] const replyData = item.beReplied && item.beReplied[0]
return replyData return replyData
? { ? {
id: item.commentId, id: item.commentId,

View File

@@ -134,7 +134,7 @@ export default {
filterBoardsData(rawList) { filterBoardsData(rawList) {
// console.log(rawList) // console.log(rawList)
let list = [] const list = []
for (const board of rawList) { for (const board of rawList) {
// 排除 MV榜 // 排除 MV榜
// if (board.id == 201) continue // if (board.id == 201) continue
@@ -210,7 +210,7 @@ export default {
}, },
getDetailPageUrl(id) { getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('wy__', '') if (typeof id === 'string') id = id.replace('wy__', '')
return `https://music.163.com/#/discover/toplist?id=${id}` return `https://music.163.com/#/discover/toplist?id=${id}`
} }
} }

View File

@@ -64,13 +64,13 @@ const parseTools = {
}, },
msFormat(timeMs) { msFormat(timeMs) {
if (Number.isNaN(timeMs)) return '' if (Number.isNaN(timeMs)) return ''
let ms = timeMs % 1000 const ms = timeMs % 1000
timeMs /= 1000 timeMs /= 1000
let m = parseInt(timeMs / 60) const m = parseInt(timeMs / 60)
.toString() .toString()
.padStart(2, '0') .padStart(2, '0')
timeMs %= 60 timeMs %= 60
let s = parseInt(timeMs).toString().padStart(2, '0') const s = parseInt(timeMs).toString().padStart(2, '0')
return `[${m}:${s}.${ms}]` return `[${m}:${s}.${ms}]`
}, },
parseLyric(lines) { parseLyric(lines) {
@@ -79,7 +79,7 @@ const parseTools = {
for (let line of lines) { for (let line of lines) {
line = line.trim() line = line.trim()
let result = this.rxps.lineTime.exec(line) const result = this.rxps.lineTime.exec(line)
if (!result) { if (!result) {
if (line.startsWith('[offset')) { if (line.startsWith('[offset')) {
lxlrcLines.push(line) lxlrcLines.push(line)
@@ -92,7 +92,7 @@ const parseTools = {
const startTimeStr = this.msFormat(startMsTime) const startTimeStr = this.msFormat(startMsTime)
if (!startTimeStr) continue if (!startTimeStr) continue
let words = line.replace(this.rxps.lineTime, '') const words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`) lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)
@@ -124,7 +124,7 @@ const parseTools = {
getIntv(interval) { getIntv(interval) {
if (!interval) return 0 if (!interval) return 0
if (!interval.includes('.')) interval += '.0' if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./) const arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0') while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms) return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
@@ -134,7 +134,7 @@ const parseTools = {
const targetlrcLines = targetlrc.split('\n') const targetlrcLines = targetlrc.split('\n')
const timeRxp = /^\[([\d:.]+)\]/ const timeRxp = /^\[([\d:.]+)\]/
let temp = [] let temp = []
let newLrc = [] const newLrc = []
targetlrcLines.forEach((line) => { targetlrcLines.forEach((line) => {
const result = timeRxp.exec(line) const result = timeRxp.exec(line)
if (!result) return if (!result) return
@@ -168,7 +168,7 @@ const parseTools = {
crlyric: '' crlyric: ''
} }
if (ylrc) { if (ylrc) {
let lines = this.parseHeaderInfo(ylrc) const lines = this.parseHeaderInfo(ylrc)
if (lines) { if (lines) {
const result = this.parseLyric(lines) const result = this.parseLyric(lines)
if (ytlrc) { if (ytlrc) {
@@ -245,8 +245,8 @@ const parseTools = {
// https://github.com/lyswhut/lx-music-mobile/issues/370 // https://github.com/lyswhut/lx-music-mobile/issues/370
const fixTimeLabel = (lrc, tlrc, romalrc) => { const fixTimeLabel = (lrc, tlrc, romalrc) => {
if (lrc) { if (lrc) {
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') const newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
let newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc const newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
if (newLrc != lrc || newTlrc != tlrc) { if (newLrc != lrc || newTlrc != tlrc) {
lrc = newLrc lrc = newLrc
tlrc = newTlrc tlrc = newTlrc

View File

@@ -5,7 +5,7 @@ import { formatPlayTime, sizeFormate } from '../index'
export default { export default {
getSinger(singers) { getSinger(singers) {
let arr = [] const arr = []
singers?.forEach((singer) => { singers?.forEach((singer) => {
arr.push(singer.name) arr.push(singer.name)
}) })

View File

@@ -20,7 +20,7 @@ export default {
return searchRequest.promise.then(({ body }) => body) return searchRequest.promise.then(({ body }) => body)
}, },
getSinger(singers) { getSinger(singers) {
let arr = [] const arr = []
singers.forEach((singer) => { singers.forEach((singer) => {
arr.push(singer.name) arr.push(singer.name)
}) })
@@ -87,7 +87,7 @@ export default {
return this.musicSearch(str, page, limit).then((result) => { return this.musicSearch(str, page, limit).then((result) => {
// console.log(result) // console.log(result)
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum) if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.result.songs || []) const list = this.handleResult(result.result.songs || [])
// console.log(list) // console.log(list)
if (list == null) return this.search(str, page, limit, retryNum) if (list == null) return this.search(str, page, limit, retryNum)

View File

@@ -90,8 +90,8 @@ export default {
const { statusCode, body } = await requestObj_listDetail.promise const { statusCode, body } = await requestObj_listDetail.promise
if (statusCode !== 200 || body.code !== this.successCode) if (statusCode !== 200 || body.code !== this.successCode)
return this.getListDetail(id, page, ++tryNum) return this.getListDetail(id, page, ++tryNum)
let limit = 1000 const limit = 1000
let rangeStart = (page - 1) * limit const rangeStart = (page - 1) * limit
// console.log(body) // console.log(body)
let list let list
if (body.playlist.trackIds.length == body.privileges.length) { if (body.playlist.trackIds.length == body.privileges.length) {

View File

@@ -21,7 +21,7 @@ const aesEncrypt = (buffer, mode, key, iv) => {
} }
const aesDecrypt = function (cipherBuffer, mode, key, iv) { const aesDecrypt = function (cipherBuffer, mode, key, iv) {
let decipher = createDecipheriv(mode, key, iv) const decipher = createDecipheriv(mode, key, iv)
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]) return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
} }

View File

@@ -20,7 +20,7 @@ function getAppDirPath(
| 'logs' | 'logs'
| 'crashDumps' | 'crashDumps'
) { ) {
let dirPath: string = electron.app.getPath(name ?? 'userData') const dirPath: string = electron.app.getPath(name ?? 'userData')
return dirPath return dirPath
} }

View File

@@ -85,7 +85,7 @@ const defaultHeaders = {
* @param {Object} options - 请求选项 * @param {Object} options - 请求选项
*/ */
const buildHttpPromise = (url, options) => { const buildHttpPromise = (url, options) => {
let obj = { const obj = {
isCancelled: false, isCancelled: false,
cancelToken: axios.CancelToken.source(), cancelToken: axios.CancelToken.source(),
cancelHttp: () => { cancelHttp: () => {
@@ -190,12 +190,12 @@ const fetchData = async (url, method = 'get', options = {}) => {
let s = Buffer.from(bHh, 'hex').toString() let s = Buffer.from(bHh, 'hex').toString()
s = s.replace(s.substr(-1), '') s = s.replace(s.substr(-1), '')
s = Buffer.from(s, 'base64').toString() s = Buffer.from(s, 'base64').toString()
let v = process.versions.app const v = process.versions.app
.split('-')[0] .split('-')[0]
.split('.') .split('.')
.map((n) => (n.length < 3 ? n.padStart(3, '0') : n)) .map((n) => (n.length < 3 ? n.padStart(3, '0') : n))
.join('') .join('')
let v2 = process.versions.app.split('-')[1] || '' const v2 = process.versions.app.split('-')[1] || ''
requestHeaders[s] = requestHeaders[s] =
!s || !s ||
`${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}` `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`
@@ -385,7 +385,7 @@ export const http_jsonp = (url, options, callback) => {
options = {} options = {}
} }
let jsonpCallback = 'jsonpCallback' const jsonpCallback = 'jsonpCallback'
if (url.indexOf('?') < 0) url += '?' if (url.indexOf('?') < 0) url += '?'
url += `&${options.jsonpCallback}=${jsonpCallback}` url += `&${options.jsonpCallback}=${jsonpCallback}`

View File

@@ -1,54 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
MusicCache: typeof import('./src/components/Settings/MusicCache.vue')['default']
PlaylistActions: typeof import('./src/components/Play/PlaylistActions.vue')['default']
PlaylistDrawer: typeof import('./src/components/Play/PlaylistDrawer.vue')['default']
PlaylistSettings: typeof import('./src/components/Settings/PlaylistSettings.vue')['default']
PlayMusic: typeof import('./src/components/Play/PlayMusic.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
TAlert: typeof import('tdesign-vue-next')['Alert']
TAside: typeof import('tdesign-vue-next')['Aside']
TBadge: typeof import('tdesign-vue-next')['Badge']
TButton: typeof import('tdesign-vue-next')['Button']
TCard: typeof import('tdesign-vue-next')['Card']
TContent: typeof import('tdesign-vue-next')['Content']
TDialog: typeof import('tdesign-vue-next')['Dialog']
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
TForm: typeof import('tdesign-vue-next')['Form']
TFormItem: typeof import('tdesign-vue-next')['FormItem']
ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default']
TIcon: typeof import('tdesign-vue-next')['Icon']
TImage: typeof import('tdesign-vue-next')['Image']
TInput: typeof import('tdesign-vue-next')['Input']
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
TLayout: typeof import('tdesign-vue-next')['Layout']
TLoading: typeof import('tdesign-vue-next')['Loading']
TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
TSlider: typeof import('tdesign-vue-next')['Slider']
TSwitch: typeof import('tdesign-vue-next')['Switch']
TTextarea: typeof import('tdesign-vue-next')['Textarea']
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default']
UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default']
Versions: typeof import('./src/components/Versions.vue')['default']
}
}

View File

@@ -25,7 +25,7 @@
} }
} }
;((i = function () { ;((i = function () {
var a, let a,
l = document.createElement('div') l = document.createElement('div')
;((l.innerHTML = c._iconfont_svg_string_4997692), ;((l.innerHTML = c._iconfont_svg_string_4997692),
(l = l.getElementsByTagName('svg')[0]) && (l = l.getElementsByTagName('svg')[0]) &&

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -481,7 +481,7 @@ onBeforeUnmount(() => {
:class="message.type" :class="message.type"
> >
<div v-if="message.type === 'loading'" class="message-content loading-content"> <div v-if="message.type === 'loading'" class="message-content loading-content">
<t-loading size="small" /> <TLoading size="small" />
<span class="loading-text">{{ message.content }}</span> <span class="loading-text">{{ message.content }}</span>
</div> </div>
<div v-else class="message-content" v-html="message.html || message.content"></div> <div v-else class="message-content" v-html="message.html || message.content"></div>

View File

@@ -2,11 +2,11 @@
<div class="song-virtual-list"> <div class="song-virtual-list">
<!-- 表头 --> <!-- 表头 -->
<div class="list-header"> <div class="list-header">
<div class="col-index" v-if="showIndex"></div> <div v-if="showIndex" class="col-index"></div>
<div class="col-title">标题</div> <div class="col-title">标题</div>
<div class="col-album" v-if="showAlbum">专辑</div> <div v-if="showAlbum" class="col-album">专辑</div>
<div class="col-like">喜欢</div> <div class="col-like">喜欢</div>
<div class="col-duration" v-if="showDuration">时长</div> <div v-if="showDuration" class="col-duration">时长</div>
</div> </div>
<!-- 虚拟滚动容器 --> <!-- 虚拟滚动容器 -->
@@ -21,13 +21,15 @@
@mouseleave="hoveredSong = null" @mouseleave="hoveredSong = null"
> >
<!-- 序号或播放状态图标 --> <!-- 序号或播放状态图标 -->
<div class="col-index" v-if="showIndex"> <div v-if="showIndex" class="col-index">
<span v-if="hoveredSong !== (song.id || song.songmid)" class="track-number"> <Transition name="playSong" mode="out-in">
{{ String(visibleStartIndex + index + 1).padStart(2, '0') }} <span v-if="hoveredSong !== (song.id || song.songmid)" class="track-number">
</span> {{ String(visibleStartIndex + index + 1).padStart(2, '0') }}
<button v-else class="play-btn" title="播放" @click.stop="handlePlay(song)"> </span>
<i class="icon-play"></i> <button v-else class="play-btn" title="播放" @click.stop="handlePlay(song)">
</button> <i class="icon-play"></i>
</button>
</Transition>
</div> </div>
<!-- 歌曲信息 --> <!-- 歌曲信息 -->
@@ -47,7 +49,7 @@
</div> </div>
<!-- 专辑信息 --> <!-- 专辑信息 -->
<div class="col-album" v-if="showAlbum"> <div v-if="showAlbum" class="col-album">
<span class="album-name" :title="song.albumName"> <span class="album-name" :title="song.albumName">
{{ song.albumName || '-' }} {{ song.albumName || '-' }}
</span> </span>
@@ -61,7 +63,7 @@
</div> </div>
<!-- 时长 --> <!-- 时长 -->
<div class="col-duration" v-if="showDuration"> <div v-if="showDuration" class="col-duration">
<div class="duration-wrapper"> <div class="duration-wrapper">
<span v-if="hoveredSong !== (song.id || song.songmid)" class="duration"> <span v-if="hoveredSong !== (song.id || song.songmid)" class="duration">
{{ formatDuration(song.interval) }} {{ formatDuration(song.interval) }}
@@ -247,6 +249,15 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.playSong-enter-active,
.playSong-leave-active {
transition: all 0.2s ease-in-out;
}
.playSong-enter-from,
.playSong-leave-to {
opacity: 0;
transform: scale(0.9);
}
.song-virtual-list { .song-virtual-list {
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -373,6 +384,7 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-style: none; font-style: none;
&:hover { &:hover {
background: rgba(80, 125, 175, 0.1); background: rgba(80, 125, 175, 0.1);
color: #3a5d8f; color: #3a5d8f;
@@ -568,14 +580,17 @@ onMounted(() => {
content: '▶'; content: '▶';
font-style: normal; font-style: normal;
} }
.icon-pause::before { .icon-pause::before {
content: '⏸'; content: '⏸';
font-style: normal; font-style: normal;
} }
.icon-download::before { .icon-download::before {
content: '⬇'; content: '⬇';
font-style: normal; font-style: normal;
} }
.icon-heart::before { .icon-heart::before {
content: '♡'; content: '♡';
font-style: normal; font-style: normal;

View File

@@ -12,11 +12,6 @@ interface Props {
backgroundColor?: string backgroundColor?: string
} }
// 定义事件
const emit = defineEmits<{
lowFreqUpdate: [volume: number]
}>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
show: true, show: true,
height: 80, height: 80,
@@ -25,6 +20,11 @@ const props = withDefaults(defineProps<Props>(), {
backgroundColor: 'transparent' backgroundColor: 'transparent'
}) })
// 定义事件
const emit = defineEmits<{
lowFreqUpdate: [volume: number]
}>()
const canvasRef = ref<HTMLCanvasElement>() const canvasRef = ref<HTMLCanvasElement>()
const animationId = ref<number>() const animationId = ref<number>()
const analyser = ref<AnalyserNode>() const analyser = ref<AnalyserNode>()

View File

@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
show: false, show: false,
coverImage: '@assets/images/Default.jpg', coverImage: '@assets/images/Default.jpg',
songId: '', songId: '',
mainColor: '#fff' mainColor: '#rgb(0,0,0)'
}) })
// 定义事件 // 定义事件
const emit = defineEmits(['toggle-fullscreen']) const emit = defineEmits(['toggle-fullscreen'])
@@ -251,6 +251,27 @@ watch(
const handleLowFreqUpdate = (volume: number) => { const handleLowFreqUpdate = (volume: number) => {
state.lowFreqVolume = volume state.lowFreqVolume = volume
} }
// 计算偏白的主题色
const lightMainColor = computed(() => {
const color = props.mainColor
// 解析rgb颜色值
const rgbMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*\d+\)/)
if (rgbMatch) {
let r = parseInt(rgbMatch[1])
let g = parseInt(rgbMatch[2])
let b = parseInt(rgbMatch[3])
// 适度向白色偏移,保持主题色特征
r = Math.min(255, r + (255 - r) * 0.8)
g = Math.min(255, g + (255 - g) * 0.8)
b = Math.min(255, b + (255 - b) * 0.8)
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, 0.9)`
}
// 如果解析失败,返回默认的偏白色
return 'rgba(255, 255, 255, 0.9)'
})
</script> </script>
<template> <template>
@@ -291,6 +312,12 @@ const handleLowFreqUpdate = (volume: number) => {
</Transition> </Transition>
<div class="playbox"> <div class="playbox">
<div class="left" :style="state.lyricLines.length <= 0 && 'width:100vw'"> <div class="left" :style="state.lyricLines.length <= 0 && 'width:100vw'">
<img
class="pointer"
:class="{ playing: Audio.isPlay }"
src="@renderer/assets/pointer.png"
alt="pointer"
/>
<div <div
class="cd-container" class="cd-container"
:class="{ playing: Audio.isPlay }" :class="{ playing: Audio.isPlay }"
@@ -331,7 +358,7 @@ const handleLowFreqUpdate = (volume: number) => {
</div> </div>
</div> </div>
<!-- 音频可视化组件 --> <!-- 音频可视化组件 -->
<div class="audio-visualizer-container" v-if="props.show && coverImage"> <div v-if="props.show && coverImage" class="audio-visualizer-container">
<AudioVisualizer <AudioVisualizer
:show="props.show && Audio.isPlay" :show="props.show && Audio.isPlay"
:height="70" :height="70"
@@ -441,12 +468,13 @@ const handleLowFreqUpdate = (volume: number) => {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.256); background-color: rgba(0, 0, 0, 0.256);
-webkit-drop-filter: blur(10px); -webkit-drop-filter: blur(80px);
padding: 0 10vw; padding: 0 10vw;
-webkit-drop-filter: blur(10px); -webkit-drop-filter: blur(80px);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
position: relative; position: relative;
--cd-width-auto: max(200px, min(30vw, 700px, calc(100vh - var(--play-bottom-height) - 250px)));
.left { .left {
width: 40%; width: 40%;
@@ -464,9 +492,24 @@ const handleLowFreqUpdate = (volume: number) => {
margin: 0 0 var(--play-bottom-height) 0; margin: 0 0 var(--play-bottom-height) 0;
perspective: 1000px; perspective: 1000px;
.pointer {
position: absolute;
width: calc(var(--cd-width-auto) / 3.5);
left: calc(50% - 1.8vh);
top: calc(50% - var(--cd-width-auto) / 2 - calc(var(--cd-width-auto) / 3.5) - 1vh);
transform: rotate(-20deg);
transform-origin: 1.8vh 1.8vh;
z-index: 2;
transition: transform 0.3s;
&.playing {
transform: rotate(0deg);
}
}
.cd-container { .cd-container {
width: min(30vw, 700px); width: var(--cd-width-auto);
height: min(30vw, 700px); height: var(--cd-width-auto);
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -620,14 +663,33 @@ const handleLowFreqUpdate = (volume: number) => {
} }
.right { .right {
mask: linear-gradient(
rgba(255, 255, 255, 0) 0px,
rgba(255, 255, 255, 0.6) 5%,
rgb(255, 255, 255) 10%,
rgb(255, 255, 255) 75%,
rgba(255, 255, 255, 0.6) 85%,
rgba(255, 255, 255, 0)
);
:deep(.lyric-player) { :deep(.lyric-player) {
--amll-lyric-view-color: v-bind(lightMainColor);
font-family: lyricfont; font-family: lyricfont;
--amll-lyric-player-font-size: min(2.6vw, 32px); --amll-lyric-player-font-size: min(2.6vw, 39px);
// bottom: max(2vw, 29px); // bottom: max(2vw, 29px);
height: 200%; height: 200%;
transform: translateY(-25%); transform: translateY(-25%);
* [class^='lyricMainLine'] {
font-weight: 600 !important;
* {
font-weight: 600 !important;
}
}
& > div { & > div {
padding-bottom: 0; padding-bottom: 0;
overflow: hidden; overflow: hidden;
@@ -665,6 +727,7 @@ const handleLowFreqUpdate = (volume: number) => {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
@@ -675,10 +738,12 @@ const handleLowFreqUpdate = (volume: number) => {
opacity: 0.1; opacity: 0.1;
transform: rotate(0deg) scale(1); transform: rotate(0deg) scale(1);
} }
50% { 50% {
opacity: 0.2; opacity: 0.2;
transform: rotate(180deg) scale(1.1); transform: rotate(180deg) scale(1.1);
} }
100% { 100% {
opacity: 0.1; opacity: 0.1;
transform: rotate(360deg) scale(1); transform: rotate(360deg) scale(1);
@@ -690,16 +755,20 @@ const handleLowFreqUpdate = (volume: number) => {
opacity: 0.05; opacity: 0.05;
transform: rotate(0deg); transform: rotate(0deg);
} }
25% { 25% {
opacity: 0.15; opacity: 0.15;
} }
50% { 50% {
opacity: 0.1; opacity: 0.1;
transform: rotate(180deg); transform: rotate(180deg);
} }
75% { 75% {
opacity: 0.15; opacity: 0.15;
} }
100% { 100% {
opacity: 0.05; opacity: 0.05;
transform: rotate(360deg); transform: rotate(360deg);

View File

@@ -121,6 +121,7 @@ onUnmounted(() => {
<template> <template>
<div> <div>
<audio <audio
id="globaAudio"
ref="audioMeta" ref="audioMeta"
preload="auto" preload="auto"
:src="audioStore.Audio.url" :src="audioStore.Audio.url"
@@ -131,7 +132,6 @@ onUnmounted(() => {
@loadeddata="handleLoadedData" @loadeddata="handleLoadedData"
@ended="handleEnded" @ended="handleEnded"
@canplay="handleCanPlay" @canplay="handleCanPlay"
id="globaAudio"
></audio> ></audio>
</div> </div>
</template> </template>

View File

@@ -27,6 +27,7 @@ import {
destroyPlaylistEventListeners, destroyPlaylistEventListeners,
getSongRealUrl getSongRealUrl
} from '@renderer/utils/playlistManager' } from '@renderer/utils/playlistManager'
import mediaSessionController from '@renderer/utils/useAmtc'
import defaultCoverImg from '/default-cover.png' import defaultCoverImg from '/default-cover.png'
const controlAudio = ControlAudioStore() const controlAudio = ControlAudioStore()
@@ -41,7 +42,7 @@ const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
togglePlayPause() togglePlayPause()
}) })
let timer: any = null let timer: any = null
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function throttle(callback: Function, delay: number) { function throttle(callback: Function, delay: number) {
if (timer) return if (timer) return
timer = setTimeout(() => { timer = setTimeout(() => {
@@ -132,6 +133,9 @@ let isFull = false
// 播放指定歌曲 // 播放指定歌曲
const playSong = async (song: SongList) => { const playSong = async (song: SongList) => {
try { try {
// 设置加载状态
isLoadingSong.value = true
// 检查是否需要恢复播放位置(历史播放) // 检查是否需要恢复播放位置(历史播放)
const isHistoryPlay = const isHistoryPlay =
song.songmid === userInfo.value.lastPlaySongId && song.songmid === userInfo.value.lastPlaySongId &&
@@ -163,6 +167,14 @@ const playSong = async (song: SongList) => {
...song ...song
} }
// 更新媒体会话元数据
mediaSessionController.updateMetadata({
title: song.name,
artist: song.singer,
album: song.albumName || '未知专辑',
artworkUrl: song.img || defaultCoverImg
})
// 确保主题色更新 // 确保主题色更新
await setColor() await setColor()
@@ -198,8 +210,8 @@ const playSong = async (song: SongList) => {
// 等待音频准备就绪 // 等待音频准备就绪
await waitForAudioReady() await waitForAudioReady()
// 短暂延迟确保音频状态稳定 // // 短暂延迟确保音频状态稳定
await new Promise((resolve) => setTimeout(resolve, 100)) // await new Promise((resolve) => setTimeout(resolve, 100))
// 开始播放 // 开始播放
try { try {
@@ -229,6 +241,9 @@ const playSong = async (song: SongList) => {
} catch (error: any) { } catch (error: any) {
console.error('播放歌曲失败:', error) console.error('播放歌曲失败:', error)
MessagePlugin.error('播放失败,原因:' + error.message) MessagePlugin.error('播放失败,原因:' + error.message)
} finally {
// 无论成功还是失败,都清除加载状态
isLoadingSong.value = false
} }
} }
provide('PlaySong', playSong) provide('PlaySong', playSong)
@@ -236,6 +251,9 @@ provide('PlaySong', playSong)
// const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE) // const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
const playMode = ref(PlayMode.SEQUENCE) const playMode = ref(PlayMode.SEQUENCE)
// 歌曲加载状态
const isLoadingSong = ref(false)
// 更新播放模式 // 更新播放模式
const updatePlayMode = () => { const updatePlayMode = () => {
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE] const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]
@@ -270,8 +288,6 @@ const showVolumeSlider = ref(false)
const volumeBarRef = ref<HTMLDivElement | null>(null) const volumeBarRef = ref<HTMLDivElement | null>(null)
const isDraggingVolume = ref(false) const isDraggingVolume = ref(false)
const volumeValue = computed({ const volumeValue = computed({
get: () => Audio.value.volume, get: () => Audio.value.volume,
set: (val) => { set: (val) => {
@@ -415,6 +431,26 @@ onMounted(async () => {
// 初始化播放列表事件监听器 // 初始化播放列表事件监听器
initPlaylistEventListeners(localUserStore, playSong) initPlaylistEventListeners(localUserStore, playSong)
// 初始化媒体会话控制器
if (Audio.value.audio) {
mediaSessionController.init(Audio.value.audio, {
play: async () => {
// 专门的播放函数,只处理播放逻辑
if (!Audio.value.isPlay) {
await handlePlay()
}
},
pause: async () => {
// 专门的暂停函数,只处理暂停逻辑
if (Audio.value.isPlay) {
await handlePause()
}
},
playPrevious: () => playPrevious(),
playNext: () => playNext()
})
}
// 监听音频结束事件,根据播放模式播放下一首 // 监听音频结束事件,根据播放模式播放下一首
unEnded = controlAudio.subscribe('ended', () => { unEnded = controlAudio.subscribe('ended', () => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@@ -432,6 +468,14 @@ onMounted(async () => {
...lastPlayedSong ...lastPlayedSong
} }
// 立即更新媒体会话元数据,让系统显示当前歌曲信息
mediaSessionController.updateMetadata({
title: lastPlayedSong.name,
artist: lastPlayedSong.singer,
album: lastPlayedSong.albumName || '未知专辑',
artworkUrl: lastPlayedSong.img || defaultCoverImg
})
// 如果有历史播放位置,设置为待恢复状态 // 如果有历史播放位置,设置为待恢复状态
if (!Audio.value.isPlay) { if (!Audio.value.isPlay) {
if (userInfo.value.currentTime && userInfo.value.currentTime > 0) { if (userInfo.value.currentTime && userInfo.value.currentTime > 0) {
@@ -451,6 +495,9 @@ onMounted(async () => {
} catch (error) { } catch (error) {
console.error('获取上次播放歌曲URL失败:', error) console.error('获取上次播放歌曲URL失败:', error)
} }
} else {
// 如果当前正在播放,设置状态为播放中
mediaSessionController.updatePlaybackState('playing')
} }
} }
} }
@@ -463,8 +510,6 @@ onMounted(async () => {
}, 1000) // 每1秒保存一次 }, 1000) // 每1秒保存一次
}) })
// 组件卸载时清理 // 组件卸载时清理
onUnmounted(() => { onUnmounted(() => {
destroyPlaylistEventListeners() destroyPlaylistEventListeners()
@@ -475,6 +520,8 @@ onUnmounted(() => {
if (removeMusicCtrlListener) { if (removeMusicCtrlListener) {
removeMusicCtrlListener() removeMusicCtrlListener()
} }
// 清理媒体会话控制器
mediaSessionController.cleanup()
unEnded() unEnded()
}) })
@@ -559,50 +606,63 @@ const formatTime = (seconds: number) => {
const currentTimeFormatted = computed(() => formatTime(Audio.value.currentTime)) const currentTimeFormatted = computed(() => formatTime(Audio.value.currentTime))
const durationFormatted = computed(() => formatTime(Audio.value.duration)) const durationFormatted = computed(() => formatTime(Audio.value.duration))
// 播放/暂停切换 // 专门的播放函数
const togglePlayPause = async () => { const handlePlay = async () => {
if (Audio.value.url) { if (!Audio.value.url) {
if (Audio.value.isPlay) {
const stopResult = stop()
if (stopResult && typeof stopResult.then === 'function') {
await stopResult
}
} else {
try {
// 检查是否需要恢复历史播放位置
if (pendingRestorePosition > 0 && pendingRestoreSongId === userInfo.value.lastPlaySongId) {
console.log(`恢复播放位置: ${pendingRestorePosition}`)
// 等待音频准备就绪
await waitForAudioReady()
// 设置播放位置
setCurrentTime(pendingRestorePosition)
if (Audio.value.audio) {
Audio.value.audio.currentTime = pendingRestorePosition
}
// 清除待恢复的位置
pendingRestorePosition = 0
pendingRestoreSongId = null
}
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
} catch (error) {
console.error('播放失败:', error)
MessagePlugin.error('播放失败,请重试')
}
}
} else {
// 如果没有URL但有播放列表尝试播放第一首歌 // 如果没有URL但有播放列表尝试播放第一首歌
if (list.value.length > 0) { if (list.value.length > 0) {
await playSong(list.value[0]) await playSong(list.value[0])
} else { } else {
MessagePlugin.warning('播放列表为空,请先添加歌曲') MessagePlugin.warning('播放列表为空,请先添加歌曲')
} }
return
}
try {
// 检查是否需要恢复历史播放位置
if (pendingRestorePosition > 0 && pendingRestoreSongId === userInfo.value.lastPlaySongId) {
console.log(`恢复播放位置: ${pendingRestorePosition}`)
// 等待音频准备就绪
await waitForAudioReady()
// 设置播放位置
setCurrentTime(pendingRestorePosition)
if (Audio.value.audio) {
Audio.value.audio.currentTime = pendingRestorePosition
}
// 清除待恢复的位置
pendingRestorePosition = 0
pendingRestoreSongId = null
}
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
} catch (error) {
console.error('播放失败:', error)
MessagePlugin.error('播放失败,请重试')
}
}
// 专门的暂停函数
const handlePause = async () => {
if (Audio.value.url && Audio.value.isPlay) {
const stopResult = stop()
if (stopResult && typeof stopResult.then === 'function') {
await stopResult
}
}
}
// 播放/暂停切换
const togglePlayPause = async () => {
if (Audio.value.isPlay) {
await handlePause()
} else {
await handlePlay()
} }
} }
@@ -754,8 +814,8 @@ watch(showFullPlay, (val) => {
<div class="player-content"> <div class="player-content">
<!-- 左侧封面和歌曲信息 --> <!-- 左侧封面和歌曲信息 -->
<div class="left-section"> <div class="left-section">
<div class="album-cover" v-if="songInfo.songmid"> <div v-if="songInfo.songmid" class="album-cover">
<img :src="songInfo.img" alt="专辑封面" v-if="songInfo.img" /> <img v-if="songInfo.img" :src="songInfo.img" alt="专辑封面" />
<img :src="defaultCoverImg" alt="默认封面" /> <img :src="defaultCoverImg" alt="默认封面" />
</div> </div>
@@ -770,11 +830,18 @@ watch(showFullPlay, (val) => {
<t-button class="control-btn" variant="text" shape="circle" @click.stop="playPrevious"> <t-button class="control-btn" variant="text" shape="circle" @click.stop="playPrevious">
<span class="iconfont icon-shangyishou"></span> <span class="iconfont icon-shangyishou"></span>
</t-button> </t-button>
<button class="control-btn play-btn" @click.stop="togglePlayPause"> <button
<transition name="fade" mode="out-in"> class="control-btn play-btn"
<span v-if="Audio.isPlay" key="play" class="iconfont icon-zanting"></span> :disabled="isLoadingSong"
<span v-else key="pause" class="iconfont icon-bofang"></span> @click.stop="() => !isLoadingSong && togglePlayPause()"
</transition> >
<Transition name="loadSong" mode="out-in">
<div v-if="isLoadingSong" key="loading" class="loading-spinner play-loading"></div>
<transition v-else name="fade" mode="out-in">
<span v-if="Audio.isPlay" key="play" class="iconfont icon-zanting"></span>
<span v-else key="pause" class="iconfont icon-bofang"></span>
</transition>
</Transition>
</button> </button>
<t-button class="control-btn" shape="circle" variant="text" @click.stop="playNext"> <t-button class="control-btn" shape="circle" variant="text" @click.stop="playNext">
<span class="iconfont icon-xiayishou"></span> <span class="iconfont icon-xiayishou"></span>
@@ -830,7 +897,7 @@ watch(showFullPlay, (val) => {
<!-- 播放列表按钮 --> <!-- 播放列表按钮 -->
<t-tooltip content="播放列表"> <t-tooltip content="播放列表">
<t-badge :count="list.length" :maxCount="99" color="#aaa"> <t-badge :count="list.length" :max-count="99" color="#aaa">
<t-button <t-button
class="control-btn" class="control-btn"
shape="circle" shape="circle"
@@ -850,9 +917,9 @@ watch(showFullPlay, (val) => {
:song-id="songInfo.songmid ? songInfo.songmid.toString() : null" :song-id="songInfo.songmid ? songInfo.songmid.toString() : null"
:show="showFullPlay" :show="showFullPlay"
:cover-image="songInfo.img" :cover-image="songInfo.img"
@toggle-fullscreen="toggleFullPlay"
:song-info="songInfo" :song-info="songInfo"
:main-color="maincolor" :main-color="maincolor"
@toggle-fullscreen="toggleFullPlay"
/> />
</div> </div>
@@ -886,6 +953,55 @@ watch(showFullPlay, (val) => {
transform: rotate(-180deg); transform: rotate(-180deg);
} }
/* 加载动画 */
.loading-spinner {
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid v-bind(hoverColor);
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
width: 1em;
height: 1em;
}
/* 播放按钮中的加载动画 */
.play-loading {
width: 20px !important;
height: 20px !important;
margin: 4px;
border-width: 3px;
border-color: rgba(255, 255, 255, 0.3);
border-top-color: v-bind(hoverColor);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 加载歌曲过渡动画 - 缩小透明效果 */
.loadSong-enter-active,
.loadSong-leave-active {
transition: all 0.2s ease-in-out;
}
.loadSong-enter-from,
.loadSong-leave-to {
opacity: 0;
transform: scale(0.8);
}
.loadSong-enter-to,
.loadSong-leave-from {
opacity: 1;
transform: scale(1);
}
.player-container { .player-container {
box-shadow: 0px -2px 20px 0px #00000039; box-shadow: 0px -2px 20px 0px #00000039;
position: fixed; position: fixed;
@@ -1211,8 +1327,6 @@ watch(showFullPlay, (val) => {
transform: translateY(10px) scale(0.95); transform: translateY(10px) scale(0.95);
} }
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.right-section .time-display { .right-section .time-display {

View File

@@ -76,8 +76,6 @@ const scrollToCurrentSong = () => {
}) })
} }
// 关闭播放列表 // 关闭播放列表
const handleClose = () => { const handleClose = () => {
emit('close') emit('close')
@@ -87,12 +85,12 @@ const handleClose = () => {
const handleMouseEnter = (index: number) => { const handleMouseEnter = (index: number) => {
// 如果正在拖拽,不显示提示 // 如果正在拖拽,不显示提示
if (isDragSorting.value) return if (isDragSorting.value) return
// 清除之前的定时器 // 清除之前的定时器
if (hoverTimer.value) { if (hoverTimer.value) {
clearTimeout(hoverTimer.value) clearTimeout(hoverTimer.value)
} }
// 设置新的定时器 // 设置新的定时器
hoverTimer.value = window.setTimeout(() => { hoverTimer.value = window.setTimeout(() => {
hoverTipVisible.value = true hoverTipVisible.value = true
@@ -106,7 +104,7 @@ const handleMouseLeave = () => {
clearTimeout(hoverTimer.value) clearTimeout(hoverTimer.value)
hoverTimer.value = null hoverTimer.value = null
} }
// 隐藏提示 // 隐藏提示
hoverTipVisible.value = false hoverTipVisible.value = false
hoverTipIndex.value = -1 hoverTipIndex.value = -1
@@ -134,65 +132,75 @@ const handleTouchStart = (event: TouchEvent, index: number, song: any) => {
} }
// 拖拽排序相关方法 // 拖拽排序相关方法
const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song: any, isTouch: boolean) => { const handlePointerStart = (
event: MouseEvent | TouchEvent,
index: number,
song: any,
isTouch: boolean
) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
// 重置标记 // 重置标记
isDragStarted.value = false isDragStarted.value = false
wasLongPressed.value = false wasLongPressed.value = false
currentOperatingSong.value = song currentOperatingSong.value = song
// 清除之前的定时器 // 清除之前的定时器
if (longPressTimer.value) { if (longPressTimer.value) {
clearTimeout(longPressTimer.value) clearTimeout(longPressTimer.value)
} }
// 记录初始位置 // 记录初始位置
const clientY = isTouch ? (event as TouchEvent).touches[0].clientY : (event as MouseEvent).clientY const clientY = isTouch ? (event as TouchEvent).touches[0].clientY : (event as MouseEvent).clientY
dragStartY.value = clientY dragStartY.value = clientY
dragCurrentY.value = clientY dragCurrentY.value = clientY
// 设置长按定时器 // 设置长按定时器
longPressTimer.value = window.setTimeout(() => { longPressTimer.value = window.setTimeout(() => {
wasLongPressed.value = true // 标记发生了长按 wasLongPressed.value = true // 标记发生了长按
startDragSort(index, song) startDragSort(index, song)
isDragStarted.value = true // 标记已开始拖动 isDragStarted.value = true // 标记已开始拖动
}, longPressDelay) }, longPressDelay)
// 添加移动和结束事件监听 // 添加移动和结束事件监听
const handleMove = (e: MouseEvent | TouchEvent) => { const handleMove = (e: MouseEvent | TouchEvent) => {
const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY
const deltaY = Math.abs(currentY - dragStartY.value) const deltaY = Math.abs(currentY - dragStartY.value)
// 如果移动距离超过阈值,取消长按 // 如果移动距离超过阈值,取消长按
if (deltaY > dragThreshold && longPressTimer.value) { if (deltaY > dragThreshold && longPressTimer.value) {
clearTimeout(longPressTimer.value) clearTimeout(longPressTimer.value)
longPressTimer.value = null longPressTimer.value = null
} }
// 如果已经开始拖拽,更新位置 // 如果已经开始拖拽,更新位置
if (isDragSorting.value) { if (isDragSorting.value) {
dragCurrentY.value = currentY dragCurrentY.value = currentY
updateDragOverIndex(currentY) updateDragOverIndex(currentY)
} }
} }
const handleEnd = () => { const handleEnd = () => {
const hadLongPressTimer = !!longPressTimer.value const hadLongPressTimer = !!longPressTimer.value
const wasInDragMode = isDragSorting.value const wasInDragMode = isDragSorting.value
if (longPressTimer.value) { if (longPressTimer.value) {
clearTimeout(longPressTimer.value) clearTimeout(longPressTimer.value)
longPressTimer.value = null longPressTimer.value = null
} }
if (isDragSorting.value) { if (isDragSorting.value) {
endDragSort() endDragSort()
} }
// 如果没有发生长按且没有在拖拽模式,说明是正常点击,触发播放 // 如果没有发生长按且没有在拖拽模式,说明是正常点击,触发播放
if (!wasLongPressed.value && !wasInDragMode && hadLongPressTimer && currentOperatingSong.value) { if (
!wasLongPressed.value &&
!wasInDragMode &&
hadLongPressTimer &&
currentOperatingSong.value
) {
// 短暂延迟后播放,确保状态已经稳定 // 短暂延迟后播放,确保状态已经稳定
setTimeout(() => { setTimeout(() => {
emit('playSong', currentOperatingSong.value) emit('playSong', currentOperatingSong.value)
@@ -208,14 +216,14 @@ const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song:
currentOperatingSong.value = null currentOperatingSong.value = null
}, 200) }, 200)
} }
// 移除事件监听 // 移除事件监听
document.removeEventListener('mousemove', handleMove) document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleEnd) document.removeEventListener('mouseup', handleEnd)
document.removeEventListener('touchmove', handleMove) document.removeEventListener('touchmove', handleMove)
document.removeEventListener('touchend', handleEnd) document.removeEventListener('touchend', handleEnd)
} }
// 添加事件监听 // 添加事件监听
document.addEventListener('mousemove', handleMove) document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleEnd) document.addEventListener('mouseup', handleEnd)
@@ -226,18 +234,18 @@ const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song:
const startDragSort = (index: number, song: any) => { const startDragSort = (index: number, song: any) => {
// 隐藏悬停提示 // 隐藏悬停提示
hideTip() hideTip()
isDragSorting.value = true isDragSorting.value = true
draggedIndex.value = index draggedIndex.value = index
draggedSong.value = song draggedSong.value = song
dragOverIndex.value = index dragOverIndex.value = index
// 保存原始列表用于实时预览 // 保存原始列表用于实时预览
originalList.value = [...list.value] originalList.value = [...list.value]
// 添加拖拽样式 // 添加拖拽样式
document.body.style.userSelect = 'none' document.body.style.userSelect = 'none'
// 触觉反馈(如果支持) // 触觉反馈(如果支持)
if ('vibrate' in navigator) { if ('vibrate' in navigator) {
navigator.vibrate(50) navigator.vibrate(50)
@@ -247,19 +255,20 @@ const startDragSort = (index: number, song: any) => {
const updateDragOverIndex = (clientY: number) => { const updateDragOverIndex = (clientY: number) => {
const playlistContainer = document.querySelector('.playlist-content') const playlistContainer = document.querySelector('.playlist-content')
if (!playlistContainer) return if (!playlistContainer) return
const containerRect = playlistContainer.getBoundingClientRect() const containerRect = playlistContainer.getBoundingClientRect()
const scrollThreshold = 80 // 增加边缘滚动触发区域 const scrollThreshold = 80 // 增加边缘滚动触发区域
const maxScrollSpeed = 15 // 增加最大滚动速度 const maxScrollSpeed = 15 // 增加最大滚动速度
// 检查是否需要自动滚动 // 检查是否需要自动滚动
const distanceFromTop = clientY - containerRect.top const distanceFromTop = clientY - containerRect.top
const distanceFromBottom = containerRect.bottom - clientY const distanceFromBottom = containerRect.bottom - clientY
// 检查是否可以滚动 // 检查是否可以滚动
const canScrollUp = playlistContainer.scrollTop > 0 const canScrollUp = playlistContainer.scrollTop > 0
const canScrollDown = playlistContainer.scrollTop < (playlistContainer.scrollHeight - playlistContainer.clientHeight) const canScrollDown =
playlistContainer.scrollTop < playlistContainer.scrollHeight - playlistContainer.clientHeight
if (distanceFromTop < scrollThreshold && distanceFromTop > 0 && canScrollUp) { if (distanceFromTop < scrollThreshold && distanceFromTop > 0 && canScrollUp) {
// 向上滚动 // 向上滚动
const intensity = (scrollThreshold - distanceFromTop) / scrollThreshold const intensity = (scrollThreshold - distanceFromTop) / scrollThreshold
@@ -274,18 +283,18 @@ const updateDragOverIndex = (clientY: number) => {
// 停止自动滚动 // 停止自动滚动
stopAutoScroll() stopAutoScroll()
} }
// 计算拖拽位置,考虑容器滚动偏移 // 计算拖拽位置,考虑容器滚动偏移
const playlistSongs = document.querySelectorAll('.playlist-song') const playlistSongs = document.querySelectorAll('.playlist-song')
let newOverIndex = draggedIndex.value let newOverIndex = draggedIndex.value
// 如果在容器范围内,计算最接近的插入位置 // 如果在容器范围内,计算最接近的插入位置
if (clientY >= containerRect.top && clientY <= containerRect.bottom) { if (clientY >= containerRect.top && clientY <= containerRect.bottom) {
for (let i = 0; i < playlistSongs.length; i++) { for (let i = 0; i < playlistSongs.length; i++) {
const songElement = playlistSongs[i] as HTMLElement const songElement = playlistSongs[i] as HTMLElement
const rect = songElement.getBoundingClientRect() const rect = songElement.getBoundingClientRect()
const centerY = rect.top + rect.height / 2 const centerY = rect.top + rect.height / 2
if (clientY < centerY) { if (clientY < centerY) {
newOverIndex = i newOverIndex = i
break break
@@ -300,9 +309,13 @@ const updateDragOverIndex = (clientY: number) => {
// 在容器下方,插入到末尾 // 在容器下方,插入到末尾
newOverIndex = playlistSongs.length newOverIndex = playlistSongs.length
} }
// 实时更新列表顺序进行预览 // 实时更新列表顺序进行预览
if (newOverIndex !== dragOverIndex.value && newOverIndex >= 0 && newOverIndex <= list.value.length) { if (
newOverIndex !== dragOverIndex.value &&
newOverIndex >= 0 &&
newOverIndex <= list.value.length
) {
dragOverIndex.value = newOverIndex dragOverIndex.value = newOverIndex
updatePreviewList() updatePreviewList()
} }
@@ -311,21 +324,21 @@ const updateDragOverIndex = (clientY: number) => {
// 实时预览列表更新 // 实时预览列表更新
const updatePreviewList = () => { const updatePreviewList = () => {
if (draggedIndex.value === -1 || dragOverIndex.value === -1) return if (draggedIndex.value === -1 || dragOverIndex.value === -1) return
const newList = [...list.value] const newList = [...list.value]
const draggedItem = newList.splice(draggedIndex.value, 1)[0] const draggedItem = newList.splice(draggedIndex.value, 1)[0]
// 计算实际插入位置 // 计算实际插入位置
let insertIndex = dragOverIndex.value let insertIndex = dragOverIndex.value
if (dragOverIndex.value > draggedIndex.value) { if (dragOverIndex.value > draggedIndex.value) {
insertIndex = dragOverIndex.value - 1 insertIndex = dragOverIndex.value - 1
} }
newList.splice(insertIndex, 0, draggedItem) newList.splice(insertIndex, 0, draggedItem)
// 更新预览列表不保存到localStorage // 更新预览列表不保存到localStorage
list.value = newList list.value = newList
// 更新拖拽索引 // 更新拖拽索引
draggedIndex.value = insertIndex draggedIndex.value = insertIndex
} }
@@ -333,12 +346,12 @@ const updatePreviewList = () => {
// 自动滚动相关方法 // 自动滚动相关方法
const startAutoScroll = () => { const startAutoScroll = () => {
if (autoScrollTimer.value) return if (autoScrollTimer.value) return
autoScrollTimer.value = window.setInterval(() => { autoScrollTimer.value = window.setInterval(() => {
const playlistContainer = document.querySelector('.playlist-content') const playlistContainer = document.querySelector('.playlist-content')
if (playlistContainer && scrollSpeed.value !== 0) { if (playlistContainer && scrollSpeed.value !== 0) {
playlistContainer.scrollTop += scrollSpeed.value playlistContainer.scrollTop += scrollSpeed.value
// 在自动滚动过程中持续更新拖拽位置预览 // 在自动滚动过程中持续更新拖拽位置预览
if (isDragSorting.value) { if (isDragSorting.value) {
updateDragOverIndex(dragCurrentY.value) updateDragOverIndex(dragCurrentY.value)
@@ -358,10 +371,10 @@ const stopAutoScroll = () => {
const endDragSort = () => { const endDragSort = () => {
// 停止自动滚动 // 停止自动滚动
stopAutoScroll() stopAutoScroll()
// 由于实时预览已经更新了列表,这里只需要确保保存 // 由于实时预览已经更新了列表,这里只需要确保保存
// list.value 的变化会被 watch 监听器自动保存到 localStorage // list.value 的变化会被 watch 监听器自动保存到 localStorage
// 重置状态 // 重置状态
isDragSorting.value = false isDragSorting.value = false
draggedIndex.value = -1 draggedIndex.value = -1
@@ -369,7 +382,7 @@ const endDragSort = () => {
draggedSong.value = null draggedSong.value = null
isDragStarted.value = false isDragStarted.value = false
wasLongPressed.value = false wasLongPressed.value = false
// 移除拖拽样式 // 移除拖拽样式
document.body.style.userSelect = '' document.body.style.userSelect = ''
} }
@@ -380,12 +393,12 @@ onUnmounted(() => {
if (hoverTimer.value) { if (hoverTimer.value) {
clearTimeout(hoverTimer.value) clearTimeout(hoverTimer.value)
} }
// 清理拖拽相关定时器 // 清理拖拽相关定时器
if (longPressTimer.value) { if (longPressTimer.value) {
clearTimeout(longPressTimer.value) clearTimeout(longPressTimer.value)
} }
// 清理自动滚动定时器 // 清理自动滚动定时器
stopAutoScroll() stopAutoScroll()
}) })
@@ -418,19 +431,14 @@ defineExpose({
<p>播放列表为空</p> <p>播放列表为空</p>
<p>请添加歌曲到播放列表也可在设置中导入歌曲列表</p> <p>请添加歌曲到播放列表也可在设置中导入歌曲列表</p>
</div> </div>
<TransitionGroup <TransitionGroup v-else :class="playlistSongsClass" name="list-item" tag="div">
v-else
:class="playlistSongsClass"
name="list-item"
tag="div"
>
<div <div
v-for="(song, index) in list" v-for="(song, index) in list"
:key="song.songmid" :key="song.songmid"
class="playlist-song" class="playlist-song"
:class="{ :class="{
active: song.songmid === currentSongId, active: song.songmid === currentSongId,
'dragging': isDragSorting && index === draggedIndex dragging: isDragSorting && index === draggedIndex
}" }"
@mousedown="handleMouseDown($event, index, song)" @mousedown="handleMouseDown($event, index, song)"
@touchstart="handleTouchStart($event, index, song)" @touchstart="handleTouchStart($event, index, song)"
@@ -438,10 +446,10 @@ defineExpose({
@mouseleave="handleMouseLeave" @mouseleave="handleMouseLeave"
> >
<!-- 拖拽手柄 --> <!-- 拖拽手柄 -->
<div class="drag-handle" v-if="isDragSorting && index === draggedIndex"> <div v-if="isDragSorting && index === draggedIndex" class="drag-handle">
<span class="drag-dots"></span> <span class="drag-dots"></span>
</div> </div>
<div class="song-info"> <div class="song-info">
<div class="song-name">{{ song.name }}</div> <div class="song-name">{{ song.name }}</div>
<div class="song-artist">{{ song.singer }}</div> <div class="song-artist">{{ song.singer }}</div>
@@ -456,14 +464,10 @@ defineExpose({
<button class="song-remove" @click.stop="localUserStore.removeSong(song.songmid)"> <button class="song-remove" @click.stop="localUserStore.removeSong(song.songmid)">
<span class="iconfont icon-xuanxiangshanchu"></span> <span class="iconfont icon-xuanxiangshanchu"></span>
</button> </button>
<!-- 悬停提示 --> <!-- 悬停提示 -->
<transition name="hover-tip"> <transition name="hover-tip">
<div <div v-if="hoverTipVisible && hoverTipIndex === index" class="hover-tip" @click.stop>
v-if="hoverTipVisible && hoverTipIndex === index"
class="hover-tip"
@click.stop
>
长按可拖动排序 长按可拖动排序
</div> </div>
</transition> </transition>
@@ -801,4 +805,4 @@ defineExpose({
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
} }
} }
</style> </style>

View File

@@ -12,9 +12,9 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
let gl: WebGLRenderingContext | null = null let gl: WebGLRenderingContext | null = null
let program: WebGLProgram | null = null let program: WebGLProgram | null = null
let animationFrameId: number | null = null let animationFrameId: number | null = null
let startTime = Date.now() const startTime = Date.now()
let dominantColor = ref({ r: 0.3, g: 0.3, b: 0.5 }) const dominantColor = ref({ r: 0.3, g: 0.3, b: 0.5 })
let colorPalette = ref<Color[]>([ const colorPalette = ref<Color[]>([
{ r: 76, g: 116, b: 206 }, // 蓝色 { r: 76, g: 116, b: 206 }, // 蓝色
{ r: 120, g: 80, b: 180 }, // 紫色 { r: 120, g: 80, b: 180 }, // 紫色
{ r: 60, g: 160, b: 160 } // 青色 { r: 60, g: 160, b: 160 } // 青色

View File

@@ -4,9 +4,9 @@
<h3>自动更新</h3> <h3>自动更新</h3>
<div class="update-info"> <div class="update-info">
<p>当前版本: {{ currentVersion }}</p> <p>当前版本: {{ currentVersion }}</p>
<t-button theme="primary" :loading="isChecking" @click="handleCheckUpdate"> <TButton theme="primary" :loading="isChecking" @click="handleCheckUpdate">
{{ isChecking ? '检查中...' : '检查更新' }} {{ isChecking ? '检查中...' : '检查更新' }}
</t-button> </TButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,13 @@ import { useRouter } from 'vue-router'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
const props = withDefaults(defineProps<Props>(), {
controlStyle: false,
showSettings: true,
showBack: false,
title: '',
color: 'black'
})
const Store = LocalUserDetailStore() const Store = LocalUserDetailStore()
const { userInfo } = storeToRefs(Store) const { userInfo } = storeToRefs(Store)
@@ -18,14 +25,6 @@ interface Props {
color?: string color?: string
} }
const props = withDefaults(defineProps<Props>(), {
controlStyle: false,
showSettings: true,
showBack: false,
title: '',
color: 'black'
})
// Mini 模式现在是直接隐藏到系统托盘,不需要状态跟踪 // Mini 模式现在是直接隐藏到系统托盘,不需要状态跟踪
// 计算样式类名 // 计算样式类名

View File

@@ -22,7 +22,7 @@
<span>已下载: {{ formatBytes(downloadState.progress.transferred) }}</span> <span>已下载: {{ formatBytes(downloadState.progress.transferred) }}</span>
<span>总大小: {{ formatBytes(downloadState.progress.total) }}</span> <span>总大小: {{ formatBytes(downloadState.progress.total) }}</span>
</div> </div>
<div class="download-speed" v-if="downloadSpeed > 0"> <div v-if="downloadSpeed > 0" class="download-speed">
下载速度: {{ formatBytes(downloadSpeed) }}/s 下载速度: {{ formatBytes(downloadSpeed) }}/s
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { createWebHashHistory, createRouter, RouteRecordRaw, RouterOptions } from 'vue-router' import { createWebHashHistory, createRouter, RouteRecordRaw, RouterOptions } from 'vue-router'
let routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
name: 'welcome', name: 'welcome',
@@ -55,7 +55,7 @@ let routes: RouteRecordRaw[] = [
] ]
function setAnimate(routerObj: RouteRecordRaw[]) { function setAnimate(routerObj: RouteRecordRaw[]) {
for (let i = 0; i < routerObj.length; i++) { for (let i = 0; i < routerObj.length; i++) {
let item = routerObj[i] const item = routerObj[i]
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
setAnimate(item.children) setAnimate(item.children)
} else { } else {

View File

@@ -180,7 +180,6 @@ export const ControlAudioStore = defineStore('controlAudio', () => {
} }
const start = async () => { const start = async () => {
const volume = Audio.volume const volume = Audio.volume
console.log('开始播放音频111', volume)
if (Audio.audio) { if (Audio.audio) {
Audio.audio.volume = 0 Audio.audio.volume = 0
try { try {

View File

@@ -1,7 +1,6 @@
import { NotifyPlugin } from 'tdesign-vue-next' import { NotifyPlugin, MessagePlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { toRaw } from 'vue' import { toRaw } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next'
interface MusicItem { interface MusicItem {
singer: string singer: string

View File

@@ -0,0 +1,161 @@
interface MediaSessionCallbacks {
play: () => void
pause: () => void
playPrevious: () => void
playNext: () => void
}
interface TrackMetadata {
title: string
artist: string
album: string
artworkUrl: string
}
/**
* Media Session API 控制器
* 用于管理浏览器的媒体会话,支持系统级媒体控制
*/
class MediaSessionController {
private audioElement: HTMLAudioElement | null = null
private callbacks: MediaSessionCallbacks | null = null
private eventListeners: Array<{
element: HTMLAudioElement
event: string
handler: EventListener
}> = []
/**
* 检查浏览器是否支持 Media Session API
*/
private get isSupported(): boolean {
return 'mediaSession' in navigator
}
/**
* 更新媒体会话元数据
*/
updateMetadata(metadata: TrackMetadata): void {
if (!this.isSupported) return
try {
navigator.mediaSession.metadata = new MediaMetadata({
title: metadata.title,
artist: metadata.artist,
album: metadata.album,
artwork: this.generateArtworkSizes(metadata.artworkUrl)
})
} catch (error) {
console.warn('Failed to update media session metadata:', error)
}
}
/**
* 生成不同尺寸的封面图片配置
*/
private generateArtworkSizes(artworkUrl: string): MediaImage[] {
const sizes = ['96x96', '128x128', '192x192', '256x256', '384x384', '512x512']
return sizes.map((size) => ({
src: artworkUrl,
sizes: size,
type: 'image/png'
}))
}
/**
* 初始化媒体会话控制器
*/
init(audioElement: HTMLAudioElement, callbacks: MediaSessionCallbacks): void {
if (!this.isSupported) {
console.warn('Media Session API is not supported in this browser')
return
}
// 清理之前的监听器
this.cleanup()
this.audioElement = audioElement
this.callbacks = callbacks
// 只设置媒体会话动作处理器,不自动监听音频事件
// 让应用层手动控制播放状态更新,避免循环调用
this.setupMediaSessionActionHandlers()
}
/**
* 设置媒体会话动作处理器
*/
private setupMediaSessionActionHandlers(): void {
if (!this.callbacks) return
const actionHandlers: Array<[MediaSessionAction, () => void]> = [
['play', this.callbacks.play],
['pause', this.callbacks.pause],
['previoustrack', this.callbacks.playPrevious],
['nexttrack', this.callbacks.playNext]
]
actionHandlers.forEach(([action, handler]) => {
navigator.mediaSession.setActionHandler(action, handler)
})
// 设置 seekto 处理器
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (!this.audioElement || !details.seekTime) return
try {
if (details.fastSeek && 'fastSeek' in this.audioElement) {
this.audioElement.fastSeek(details.seekTime)
} else {
this.audioElement.currentTime = details.seekTime
}
} catch (error) {
console.warn('Failed to seek audio:', error)
}
})
}
/**
* 更新播放状态
*/
updatePlaybackState(state: MediaSessionPlaybackState): void {
if (!this.isSupported) return
try {
navigator.mediaSession.playbackState = state
} catch (error) {
console.warn('Failed to update playback state:', error)
}
}
/**
* 清理事件监听器和媒体会话
*/
cleanup(): void {
// 移除音频事件监听器
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler)
})
this.eventListeners = []
// 清理媒体会话动作处理器
if (this.isSupported) {
const actions: MediaSessionAction[] = [
'play',
'pause',
'previoustrack',
'nexttrack',
'seekto'
]
actions.forEach((action) => {
navigator.mediaSession.setActionHandler(action, null)
})
}
this.audioElement = null
this.callbacks = null
}
}
// 导出单例实例
export default new MediaSessionController()

View File

@@ -142,7 +142,7 @@ onUnmounted(() => {
<!-- 错误状态 --> <!-- 错误状态 -->
<div v-else-if="error" class="error-container"> <div v-else-if="error" class="error-container">
<t-alert theme="error" :message="error" /> <t-alert theme="error" :message="error" />
<t-button theme="primary" @click="fetchHotSonglist" style="margin-top: 1rem"> <t-button theme="primary" style="margin-top: 1rem" @click="fetchHotSonglist">
重新加载 重新加载
</t-button> </t-button>
</div> </div>
@@ -161,24 +161,22 @@ onUnmounted(() => {
<div <div
class="playlist-info" class="playlist-info"
:style="{ :style="{
'background-color': mainColors[index], '--hover-bg-color': mainColors[index],
color: textColors[index] '--hover-text-color': textColors[index]
}" }"
> >
<h4 class="playlist-title" :style="{ color: textColors[index] }"> <h4 class="playlist-title">
{{ playlist.title }} {{ playlist.title }}
</h4> </h4>
<p class="playlist-desc" :style="{ color: textColors[index] }"> <p class="playlist-desc">
{{ playlist.description }} {{ playlist.description }}
</p> </p>
<div class="playlist-meta"> <div class="playlist-meta">
<span class="play-count" :style="{ color: textColors[index] }"> <span class="play-count">
<i class="iconfont icon-bofang"></i> <i class="iconfont icon-bofang"></i>
{{ playlist.playCount }} {{ playlist.playCount }}
</span> </span>
<span class="song-count" v-if="playlist.total" :style="{ color: textColors[index] }" <span v-if="playlist.total" class="song-count">{{ playlist.total }}</span>
>{{ playlist.total }}</span
>
</div> </div>
<!-- <div class="playlist-author">by {{ playlist.author }}</div> --> <!-- <div class="playlist-author">by {{ playlist.author }}</div> -->
</div> </div>
@@ -312,6 +310,23 @@ onUnmounted(() => {
.playlist-info { .playlist-info {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
background-color: var(--hover-bg-color);
color: #111827;
.playlist-title {
color: var(--hover-text-color);
}
.playlist-desc {
color: var(--hover-text-color);
}
.playlist-meta {
color: var(--hover-text-color);
* {
color: var(--hover-text-color);
}
}
.playlist-author {
color: var(--hover-text-color);
}
} }
} }
@@ -357,8 +372,9 @@ onUnmounted(() => {
padding: 1.25rem 1rem; padding: 1.25rem 1rem;
position: relative; position: relative;
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
transition: backdrop-filter 0.3s ease; transition: all 0.3s ease;
.playlist-title { .playlist-title {
font-size: 1rem; font-size: 1rem;
@@ -384,6 +400,7 @@ onUnmounted(() => {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
min-height: 2.625rem; // 确保描述区域高度一致 min-height: 2.625rem; // 确保描述区域高度一致
transition: color 0.3s ease;
} }
.playlist-meta { .playlist-meta {
@@ -394,6 +411,7 @@ onUnmounted(() => {
margin-top: auto; // 推到底部 margin-top: auto; // 推到底部
padding-top: 0.5rem; padding-top: 0.5rem;
border-top: 1px solid rgba(229, 231, 235, 0.5); border-top: 1px solid rgba(229, 231, 235, 0.5);
transition: color 0.3s ease;
} }
.play-count { .play-count {
@@ -403,6 +421,7 @@ onUnmounted(() => {
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
font-weight: 500; font-weight: 500;
transition: color 0.3s ease;
.iconfont { .iconfont {
font-size: 0.875rem; font-size: 0.875rem;
@@ -417,6 +436,7 @@ onUnmounted(() => {
background: rgba(156, 163, 175, 0.1); background: rgba(156, 163, 175, 0.1);
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
transition: color 0.3s ease;
} }
.playlist-author { .playlist-author {
@@ -425,6 +445,7 @@ onUnmounted(() => {
font-style: italic; font-style: italic;
margin-top: 0.25rem; margin-top: 0.25rem;
opacity: 0.8; opacity: 0.8;
transition: color 0.3s ease;
} }
} }
} }

View File

@@ -312,9 +312,9 @@ onMounted(() => {
<t-button <t-button
theme="primary" theme="primary"
size="medium" size="medium"
@click="handlePlayPlaylist"
:disabled="songs.length === 0 || loading" :disabled="songs.length === 0 || loading"
class="play-btn" class="play-btn"
@click="handlePlayPlaylist"
> >
<template #icon> <template #icon>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"> <svg class="play-icon" viewBox="0 0 24 24" fill="currentColor">
@@ -327,9 +327,9 @@ onMounted(() => {
<t-button <t-button
variant="outline" variant="outline"
size="medium" size="medium"
@click="handleShufflePlaylist"
:disabled="songs.length === 0 || loading" :disabled="songs.length === 0 || loading"
class="shuffle-btn" class="shuffle-btn"
@click="handleShufflePlaylist"
> >
<template #icon> <template #icon>
<svg class="shuffle-icon" viewBox="0 0 24 24" fill="currentColor"> <svg class="shuffle-icon" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -684,12 +684,12 @@ onMounted(() => {
</div> </div>
</div> </div>
<div class="playlist-info"> <div class="playlist-info">
<div class="playlist-name" @click="viewPlaylist(playlist)" :title="playlist.name"> <div class="playlist-name" :title="playlist.name" @click="viewPlaylist(playlist)">
{{ playlist.name }} {{ playlist.name }}
</div> </div>
<div <div
class="playlist-description"
v-if="playlist.description" v-if="playlist.description"
class="playlist-description"
:title="playlist.description" :title="playlist.description"
> >
{{ playlist.description }} {{ playlist.description }}
@@ -860,8 +860,8 @@ onMounted(() => {
<!-- 创建歌单对话框 --> <!-- 创建歌单对话框 -->
<t-dialog <t-dialog
placement="center"
v-model:visible="showCreatePlaylistDialog" v-model:visible="showCreatePlaylistDialog"
placement="center"
header="创建新歌单" header="创建新歌单"
width="500px" width="500px"
:confirm-btn="{ content: '创建', theme: 'primary' }" :confirm-btn="{ content: '创建', theme: 'primary' }"
@@ -892,8 +892,8 @@ onMounted(() => {
<!-- 导入选择对话框 --> <!-- 导入选择对话框 -->
<t-dialog <t-dialog
placement="center"
v-model:visible="showImportDialog" v-model:visible="showImportDialog"
placement="center"
header="选择导入方式" header="选择导入方式"
width="400px" width="400px"
:footer="false" :footer="false"
@@ -928,14 +928,14 @@ onMounted(() => {
</t-dialog> </t-dialog>
<!-- 网络歌单导入对话框 --> <!-- 网络歌单导入对话框 -->
<t-dialog <t-dialog
placement="center"
v-model:visible="showNetworkImportDialog" v-model:visible="showNetworkImportDialog"
placement="center"
header="导入网易云音乐歌单" header="导入网易云音乐歌单"
:confirm-btn="{ content: '开始导入', theme: 'primary' }" :confirm-btn="{ content: '开始导入', theme: 'primary' }"
:cancel-btn="{ content: '取消', variant: 'outline' }" :cancel-btn="{ content: '取消', variant: 'outline' }"
width="500px"
@confirm="confirmNetworkImport" @confirm="confirmNetworkImport"
@cancel="cancelNetworkImport" @cancel="cancelNetworkImport"
width="500px"
> >
<div class="network-import-content"> <div class="network-import-content">
<p class="import-description"> <p class="import-description">
@@ -967,14 +967,14 @@ onMounted(() => {
<!-- 编辑歌单对话框 --> <!-- 编辑歌单对话框 -->
<t-dialog <t-dialog
placement="center"
v-model:visible="showEditPlaylistDialog" v-model:visible="showEditPlaylistDialog"
placement="center"
header="编辑歌单信息" header="编辑歌单信息"
:confirm-btn="{ content: '保存', theme: 'primary' }" :confirm-btn="{ content: '保存', theme: 'primary' }"
:cancel-btn="{ content: '取消', variant: 'outline' }" :cancel-btn="{ content: '取消', variant: 'outline' }"
width="500px"
@confirm="savePlaylistEdit" @confirm="savePlaylistEdit"
@cancel="cancelPlaylistEdit" @cancel="cancelPlaylistEdit"
width="500px"
> >
<div class="edit-playlist-content"> <div class="edit-playlist-content">
<div class="form-item"> <div class="form-item">

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import TitleBarControls from '@renderer/components/TitleBarControls.vue' import TitleBarControls from '@renderer/components/TitleBarControls.vue'
import PlaylistSettings from '@renderer/components/Settings/PlaylistSettings.vue' import PlaylistSettings from '@renderer/components/Settings/PlaylistSettings.vue'
import { ref } from 'vue' import { ref, computed, watch } from 'vue'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { import {
@@ -15,7 +15,6 @@ import {
} from 'tdesign-icons-vue-next' } from 'tdesign-icons-vue-next'
import fonts from '@renderer/assets/icon_font/icons' import fonts from '@renderer/assets/icon_font/icons'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, watch } from 'vue'
import MusicCache from '@renderer/components/Settings/MusicCache.vue' import MusicCache from '@renderer/components/Settings/MusicCache.vue'
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue' import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
import ThemeSelector from '@renderer/components/ThemeSelector.vue' import ThemeSelector from '@renderer/components/ThemeSelector.vue'
@@ -26,7 +25,6 @@ const { userInfo } = storeToRefs(Store)
// 当前选择的设置分类 // 当前选择的设置分类
const activeCategory = ref<string>('appearance') const activeCategory = ref<string>('appearance')
// 应用版本号 // 应用版本号
const appVersion = ref('1.0.0') const appVersion = ref('1.0.0')
@@ -477,7 +475,7 @@ const openLink = (url: string) => {
<div class="source-name">{{ source.name }}</div> <div class="source-name">{{ source.name }}</div>
<div class="source-type">{{ source.type || '音乐源' }}</div> <div class="source-type">{{ source.type || '音乐源' }}</div>
</div> </div>
<div class="source-check" v-if="userInfo.selectSources === key"> <div v-if="userInfo.selectSources === key" class="source-check">
<i class="iconfont icon-check"></i> <i class="iconfont icon-check"></i>
</div> </div>
</div> </div>
@@ -494,8 +492,8 @@ const openLink = (url: string) => {
:step="1" :step="1"
:marks="qualityMarks" :marks="qualityMarks"
:label="qualityMarks[qualitySliderValue]" :label="qualityMarks[qualitySliderValue]"
@change="onQualityChange"
class="quality-slider" class="quality-slider"
@change="onQualityChange"
/> />
</div> </div>
<div class="quality-description"> <div class="quality-description">

View File

@@ -1,178 +1,180 @@
<template> <template>
<TitleBarControls title="插件管理" :show-back="true" class="header"></TitleBarControls> <div class="page">
<div class="plugins-container"> <TitleBarControls title="插件管理" :show-back="true" class="header"></TitleBarControls>
<h2>插件管理</h2> <div class="plugins-container">
<h2>插件管理</h2>
<div class="plugin-actions"> <div class="plugin-actions">
<t-button theme="primary" @click="plugTypeDialog = true"> <t-button theme="primary" @click="plugTypeDialog = true">
<template #icon><t-icon name="add" /></template> 添加插件 <template #icon><t-icon name="add" /></template> 添加插件
</t-button> </t-button>
<t-dialog <t-dialog
:visible="plugTypeDialog" :visible="plugTypeDialog"
:close-btn="true" :close-btn="true"
confirm-btn="确定" confirm-btn="确定"
cancel-btn="取消" cancel-btn="取消"
:on-confirm="addPlug" :on-confirm="addPlug"
:on-close="() => (plugTypeDialog = false)" :on-close="() => (plugTypeDialog = false)"
> >
<template #header>请选择你的插件类别</template> <template #header>请选择你的插件类别</template>
<template #body> <template #body>
<t-radio-group v-model="type" variant="primary-filled" default-value="cr"> <t-radio-group v-model="type" variant="primary-filled" default-value="cr">
<t-radio-button value="cr">澜音插件</t-radio-button> <t-radio-button value="cr">澜音插件</t-radio-button>
<t-radio-button value="lx">洛雪插件</t-radio-button> <t-radio-button value="lx">洛雪插件</t-radio-button>
</t-radio-group> </t-radio-group>
</template> </template>
</t-dialog> </t-dialog>
<t-button theme="default" @click="refreshPlugins"> <t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 刷新 <template #icon><t-icon name="refresh" /></template> 刷新
</t-button> </t-button>
</div>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else-if="error" class="error-state">
<t-icon name="error-circle" style="font-size: 48px; color: #dc3545" />
<p>加载插件时出错</p>
<p class="error-message">{{ error }}</p>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 重试
</t-button>
</div>
<div v-else-if="plugins.length === 0" class="empty-state">
<t-icon name="app" style="font-size: 48px" />
<p>暂无已安装的插件</p>
<p class="hint">点击"添加插件"按钮来安装新插件</p>
</div>
<div v-else class="plugin-list">
<div
v-for="plugin in plugins"
:key="plugin.pluginId"
class="plugin-item"
:class="{ selected: isPluginSelected(plugin.pluginId) }"
>
<div class="plugin-info">
<h3>
{{ plugin.pluginInfo.name }}
<span class="version">{{ plugin.pluginInfo.version }}</span>
<span v-if="isPluginSelected(plugin.pluginId)" class="current-tag">当前使用</span>
</h3>
<p class="author">作者: {{ plugin.pluginInfo.author }}</p>
<p class="description">{{ plugin.pluginInfo.description || '无描述' }}</p>
<div
v-if="plugin.supportedSources && Object.keys(plugin.supportedSources).length > 0"
class="plugin-sources"
>
<span class="source-label">支持的音源:</span>
<span v-for="source in plugin.supportedSources" :key="source.name" class="source-tag">
{{ source.name }}
</span>
</div>
</div>
<div class="plugin-actions">
<t-button
theme="default"
size="small"
@click.stop="viewPluginLogs(plugin.pluginId, plugin.pluginInfo.name)"
:disabled="loading"
>
<template #icon><t-icon name="view-list" /></template> 日志
</t-button>
<t-button
v-if="!isPluginSelected(plugin.pluginId)"
theme="primary"
size="small"
@click="selectPlugin(plugin)"
>
<template #icon><t-icon name="check" /></template> 使用
</t-button>
<t-button
theme="danger"
size="small"
@click="uninstallPlugin(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="delete" /></template> 卸载
</t-button>
</div>
</div> </div>
</div>
<!-- 插件日志弹窗 --> <div v-if="loading" class="loading">
<t-dialog <div class="spinner"></div>
v-model:visible="logDialogVisible" <span>加载中...</span>
top="10vh" </div>
:close-btn="false"
:footer="false" <div v-else-if="error" class="error-state">
width="80%" <t-icon name="error-circle" style="font-size: 48px; color: #dc3545" />
:style="{ maxWidth: '900px', maxHeight: '80vh' }" <p>加载插件时出错</p>
class="log-dialog" <p class="error-message">{{ error }}</p>
> <t-button theme="default" @click="refreshPlugins">
<template #header> <template #icon><t-icon name="refresh" /></template> 重试
<div class="log-dialog-header"> </t-button>
<div class="log-title"> </div>
<i class="iconfont icon-terminal"></i>
{{ currentLogPluginName }} - 插件日志 <div v-else-if="plugins.length === 0" class="empty-state">
</div> <t-icon name="app" style="font-size: 48px" />
<div class="log-actions"> <p>暂无已安装的插件</p>
<t-button <p class="hint">点击"添加插件"按钮来安装新插件</p>
size="small" </div>
variant="outline"
theme="default" <div v-else class="plugin-list">
ghost <div
:disabled="logsLoading" v-for="plugin in plugins"
@click.stop="refreshLogs" :key="plugin.pluginId"
class="plugin-item"
:class="{ selected: isPluginSelected(plugin.pluginId) }"
>
<div class="plugin-info">
<h3>
{{ plugin.pluginInfo.name }}
<span class="version">{{ plugin.pluginInfo.version }}</span>
<span v-if="isPluginSelected(plugin.pluginId)" class="current-tag">当前使用</span>
</h3>
<p class="author">作者: {{ plugin.pluginInfo.author }}</p>
<p class="description">{{ plugin.pluginInfo.description || '无描述' }}</p>
<div
v-if="plugin.supportedSources && Object.keys(plugin.supportedSources).length > 0"
class="plugin-sources"
> >
刷新 <span class="source-label">支持的音源:</span>
<span v-for="source in plugin.supportedSources" :key="source.name" class="source-tag">
{{ source.name }}
</span>
</div>
</div>
<div class="plugin-actions">
<t-button
theme="default"
size="small"
:disabled="loading"
@click.stop="viewPluginLogs(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="view-list" /></template> 日志
</t-button>
<t-button
v-if="!isPluginSelected(plugin.pluginId)"
theme="primary"
size="small"
@click="selectPlugin(plugin)"
>
<template #icon><t-icon name="check" /></template> 使用
</t-button>
<t-button
theme="danger"
size="small"
@click="uninstallPlugin(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="delete" /></template> 卸载
</t-button> </t-button>
</div> </div>
<div class="mac-controls">
<div class="mac-button close" @click="logDialogVisible = false"></div>
<div class="mac-button minimize"></div>
<div class="mac-button maximize"></div>
</div>
</div> </div>
</template> </div>
<template #body>
<div class="console-container"> <!-- 插件日志弹窗 -->
<div class="console-header"> <t-dialog
<div class="console-info"> v-model:visible="logDialogVisible"
<span class="console-prompt">$</span> top="10vh"
<span class="console-path">~/plugins/{{ currentLogPluginName }}</span> :close-btn="false"
<span class="console-time">{{ formatTime(new Date()) }}</span> :footer="false"
width="80%"
:style="{ maxWidth: '900px', maxHeight: '80vh' }"
class="log-dialog"
>
<template #header>
<div class="log-dialog-header">
<div class="log-title">
<i class="iconfont icon-terminal"></i>
{{ currentLogPluginName }} - 插件日志
</div>
<div class="log-actions">
<t-button
size="small"
variant="outline"
theme="default"
ghost
:disabled="logsLoading"
@click.stop="refreshLogs"
>
刷新
</t-button>
</div>
<div class="mac-controls">
<div class="mac-button close" @click="logDialogVisible = false"></div>
<div class="mac-button minimize"></div>
<div class="mac-button maximize"></div>
</div> </div>
</div> </div>
<div ref="logContentRef" class="console-content" :class="{ loading: logsLoading }"> </template>
<div v-if="logsLoading" class="console-loading"> <template #body>
<div class="loading-spinner"></div> <div class="console-container">
<span>正在加载日志...</span> <div class="console-header">
<div class="console-info">
<span class="console-prompt">$</span>
<span class="console-path">~/plugins/{{ currentLogPluginName }}</span>
<span class="console-time">{{ formatTime(new Date()) }}</span>
</div>
</div> </div>
<div v-else-if="logsError" class="console-error"> <div ref="logContentRef" class="console-content" :class="{ loading: logsLoading }">
<span class="error-icon">❌</span> <div v-if="logsLoading" class="console-loading">
<span>加载日志失败: {{ logsError }}</span> <div class="loading-spinner"></div>
</div> <span>正在加载日志...</span>
<div v-else-if="logs.length === 0" class="console-empty"> </div>
<span class="empty-icon">📝</span> <div v-else-if="logsError" class="console-error">
<span>暂无日志记录</span> <span class="error-icon">❌</span>
</div> <span>加载日志失败: {{ logsError }}</span>
<div v-else class="log-entries"> </div>
<div <div v-else-if="logs.length === 0" class="console-empty">
v-for="(log, index) in logs" <span class="empty-icon">📝</span>
:key="index" <span>暂无日志记录</span>
class="log-entry" </div>
:class="getLogLevel(log)" <div v-else class="log-entries">
> <div
<span class="log-timestamp">{{ formatLogTime(index) }}</span> v-for="(log, index) in logs"
<span class="log-content">{{ log }}</span> :key="index"
class="log-entry"
:class="getLogLevel(log)"
>
<span class="log-timestamp">{{ formatLogTime(index) }}</span>
<span class="log-content">{{ log }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </t-dialog>
</t-dialog> </div>
</div> </div>
</template> </template>
@@ -213,7 +215,7 @@ const plugins = ref<Plugin[]>([])
const loading = ref(true) const loading = ref(true)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const plugTypeDialog = ref(false) const plugTypeDialog = ref(false)
let type = ref<'lx' | 'cr'>('cr') const type = ref<'lx' | 'cr'>('cr')
// 日志相关状态 // 日志相关状态
const logDialogVisible = ref(false) const logDialogVisible = ref(false)
@@ -381,7 +383,7 @@ async function uninstallPlugin(pluginId: string, pluginName: string) {
// 卸载成功才刷新插件列表 // 卸载成功才刷新插件列表
await getPlugins() await getPlugins()
// 显示成功消息 // 显示成功消息
if (pluginId == localUserStore.userInfo.pluginId) { if (pluginId === localUserStore.userInfo.pluginId) {
localUserStore.userInfo.pluginId = '' localUserStore.userInfo.pluginId = ''
localUserStore.userInfo.supportedSources = {} localUserStore.userInfo.supportedSources = {}
localUserStore.userInfo.selectSources = '' localUserStore.userInfo.selectSources = ''
@@ -508,6 +510,11 @@ onMounted(async () => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.page {
display: flex;
flex-direction: column;
height: 100vh;
}
.header { .header {
-webkit-app-region: drag; -webkit-app-region: drag;
display: flex; display: flex;
@@ -522,8 +529,13 @@ onMounted(async () => {
} }
.plugins-container { .plugins-container {
flex: 1;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
} }
.plugin-actions { .plugin-actions {
@@ -590,6 +602,9 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
flex: 1;
overflow-y: auto;
min-height: 0;
} }
.plugin-item { .plugin-item {
@@ -676,6 +691,7 @@ onMounted(async () => {
/* 日志弹窗样式 */ /* 日志弹窗样式 */
:deep(.log-dialog) { :deep(.log-dialog) {
height: 80vh; height: 80vh;
.t-dialog { .t-dialog {
background: #1e1e1e; background: #1e1e1e;
border-radius: 12px; border-radius: 12px;
@@ -710,6 +726,7 @@ onMounted(async () => {
background: linear-gradient(135deg, #2d2d2d 0%, #1e1e1e 100%); background: linear-gradient(135deg, #2d2d2d 0%, #1e1e1e 100%);
min-height: 48px; min-height: 48px;
width: 100%; width: 100%;
.log-title { .log-title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -753,6 +770,7 @@ onMounted(async () => {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-direction: row-reverse; flex-direction: row-reverse;
.mac-button { .mac-button {
width: 12px; width: 12px;
height: 12px; height: 12px;
@@ -762,6 +780,7 @@ onMounted(async () => {
&.close { &.close {
background: #ff5f57; background: #ff5f57;
&:hover { &:hover {
background: #ff3b30; background: #ff3b30;
} }
@@ -769,6 +788,7 @@ onMounted(async () => {
&.minimize { &.minimize {
background: #ffbd2e; background: #ffbd2e;
&:hover { &:hover {
background: #ff9500; background: #ff9500;
} }
@@ -776,6 +796,7 @@ onMounted(async () => {
&.maximize { &.maximize {
background: #28ca42; background: #28ca42;
&:hover { &:hover {
background: #30d158; background: #30d158;
} }
@@ -938,6 +959,7 @@ onMounted(async () => {
.log-content { .log-content {
color: #ff6b6b; color: #ff6b6b;
} }
.log-timestamp { .log-timestamp {
color: #ff6b6b; color: #ff6b6b;
} }
@@ -947,6 +969,7 @@ onMounted(async () => {
.log-content { .log-content {
color: #ffd93d; color: #ffd93d;
} }
.log-timestamp { .log-timestamp {
color: #ffd93d; color: #ffd93d;
} }
@@ -956,6 +979,7 @@ onMounted(async () => {
.log-content { .log-content {
color: #74b9ff; color: #74b9ff;
} }
.log-timestamp { .log-timestamp {
color: #74b9ff; color: #74b9ff;
} }
@@ -965,6 +989,7 @@ onMounted(async () => {
.log-content { .log-content {
color: #a29bfe; color: #a29bfe;
} }
.log-timestamp { .log-timestamp {
color: #a29bfe; color: #a29bfe;
} }
@@ -982,6 +1007,7 @@ onMounted(async () => {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }

View File

@@ -24,7 +24,7 @@
<!-- 特性标签 --> <!-- 特性标签 -->
<div class="feature-tags"> <div class="feature-tags">
<span class="tag" v-for="(feature, index) in features" :key="index"> <span v-for="(feature, index) in features" :key="index" class="tag">
{{ feature }} {{ feature }}
</span> </span>
</div> </div>