feat: add QR code login for NetEase/QQ Music accounts in Settings page

- QR code generated client-side via qrcode library
- Auto-polls backend every 2s for scan status
- Shows waiting/scanned/confirmed/expired states
- Falls back to cookie paste method
- Auth status check on page load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 01:34:06 +08:00
1 parent 0559f09f0a
commit 814910123e
3 files changed
+703 -43

No files matched your search

+317
View File
@@ -12,10 +12,12 @@
"axios": "^1.14.0", "axios": "^1.14.0",
"node-vibrant": "^4.0.3", "node-vibrant": "^4.0.3",
"pinia": "^3.0.0", "pinia": "^3.0.0",
"qrcode": "^1.5.4",
"vue": "^3.5.0", "vue": "^3.5.0",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/qrcode": "^1.5.6",
"@vitejs/plugin-vue": "^5.2.0", "@vitejs/plugin-vue": "^5.2.0",
"sass": "^1.87.0", "sass": "^1.87.0",
"typescript": "^5.8.0", "typescript": "^5.8.0",
@@ -1405,6 +1407,16 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vibrant/color": { "node_modules/@vibrant/color": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@vibrant/color/-/color-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@vibrant/color/-/color-4.0.4.tgz",
@@ -1786,6 +1798,30 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-base": { "node_modules/any-base": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
@@ -1898,6 +1934,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1914,6 +1959,35 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1954,6 +2028,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1974,6 +2057,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1988,6 +2077,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/entities": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -2151,6 +2246,19 @@
"url": "https://github.com/sindresorhus/file-type?sponsor=1" "url": "https://github.com/sindresorhus/file-type?sponsor=1"
} }
}, },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -2211,6 +2319,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2378,6 +2495,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -2420,6 +2546,18 @@
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2558,6 +2696,42 @@
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/pako": { "node_modules/pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -2571,6 +2745,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/peek-readable": { "node_modules/peek-readable": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
@@ -2706,6 +2889,32 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
@@ -2782,6 +2991,21 @@
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@@ -2874,6 +3098,12 @@
"@parcel/watcher": "^2.4.1" "@parcel/watcher": "^2.4.1"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2901,6 +3131,32 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strtok3": { "node_modules/strtok3": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
@@ -3173,6 +3429,67 @@
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
} }
} }
} }
+8 -6
View File
@@ -9,18 +9,20 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.5.0", "@iconify/vue": "^4.3.0",
"vue-router": "^4.5.0",
"pinia": "^3.0.0",
"axios": "^1.14.0", "axios": "^1.14.0",
"node-vibrant": "^4.0.3", "node-vibrant": "^4.0.3",
"@iconify/vue": "^4.3.0" "pinia": "^3.0.0",
"qrcode": "^1.5.4",
"vue": "^3.5.0",
"vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"vite": "^6.3.0", "@types/qrcode": "^1.5.6",
"@vitejs/plugin-vue": "^5.2.0", "@vitejs/plugin-vue": "^5.2.0",
"typescript": "^5.8.0",
"sass": "^1.87.0", "sass": "^1.87.0",
"typescript": "^5.8.0",
"vite": "^6.3.0",
"vue-tsc": "^2.2.0" "vue-tsc": "^2.2.0"
} }
} }
+378 -37
View File
@@ -46,37 +46,147 @@
</div> </div>
</section> </section>
<!-- Music Account --> <!-- Music Account - QR Code Login -->
<section class="settings-section"> <section class="settings-section">
<h2 class="section-title">音乐账号</h2> <h2 class="section-title">音乐账号</h2>
<div class="setting-row">
<div class="setting-label"> <!-- NetEase -->
<Icon icon="mdi:music-note" class="setting-icon" /> <div class="account-card">
网易云登录Cookie <div class="account-header">
<Icon icon="mdi:cloud-outline" class="account-icon" />
<div class="account-info">
<div class="account-name">网易云音乐</div>
<div class="account-status" :class="{ logged: neteaseAuth.loggedIn }">
{{ neteaseAuth.loggedIn ? `已登录: ${neteaseAuth.nickname}` : '未登录' }}
</div> </div>
<div class="cookie-input-wrap"> </div>
</div>
<div class="login-methods">
<button
class="login-btn"
:class="{ active: neteaseLoginMode === 'qr' }"
@click="startQrLogin('netease')"
:disabled="neteaseQr.loading"
>
<Icon icon="mdi:qrcode" />
扫码登录
</button>
<button
class="login-btn"
:class="{ active: neteaseLoginMode === 'cookie' }"
@click="neteaseLoginMode = 'cookie'"
>
<Icon icon="mdi:cookie" />
Cookie登录
</button>
</div>
<!-- QR Code -->
<div v-if="neteaseLoginMode === 'qr'" class="qr-section">
<div v-if="neteaseQr.loading" class="qr-loading">
<Icon icon="mdi:loading" class="spin" />
生成二维码中...
</div>
<div v-else-if="neteaseQr.dataUrl" class="qr-wrap">
<img :src="neteaseQr.dataUrl" class="qr-image" alt="QR Code" />
<div class="qr-status" :class="neteaseQr.status">
<template v-if="neteaseQr.status === 'waiting'">
<Icon icon="mdi:cellphone" /> 请使用网易云音乐APP扫码
</template>
<template v-else-if="neteaseQr.status === 'scanned'">
<Icon icon="mdi:check" /> 已扫码,请在手机上确认
</template>
<template v-else-if="neteaseQr.status === 'confirmed'">
<Icon icon="mdi:check-circle" /> 登录成功!
</template>
<template v-else-if="neteaseQr.status === 'expired'">
<Icon icon="mdi:refresh" /> 二维码已过期
<button class="btn-link" @click="startQrLogin('netease')">重新生成</button>
</template>
</div>
</div>
</div>
<!-- Cookie -->
<div v-if="neteaseLoginMode === 'cookie'" class="cookie-section">
<textarea <textarea
v-model="neteaseCookie" v-model="neteaseCookie"
class="textarea" class="textarea"
placeholder="粘贴网易云音乐Cookie..." placeholder="粘贴网易云音乐Cookie..."
rows="3" rows="3"
/> />
<button class="btn-primary" @click="saveCookie('netease')">保存</button> <button class="btn-primary btn-save" @click="saveCookie('netease')">保存Cookie</button>
</div> </div>
</div> </div>
<div class="setting-row">
<div class="setting-label"> <!-- QQ Music -->
<Icon icon="mdi:music-note" class="setting-icon" /> <div class="account-card">
QQ音乐登录Cookie <div class="account-header">
<Icon icon="mdi:music-circle-outline" class="account-icon" />
<div class="account-info">
<div class="account-name">QQ音乐</div>
<div class="account-status" :class="{ logged: qqAuth.loggedIn }">
{{ qqAuth.loggedIn ? `已登录: ${qqAuth.nickname}` : '未登录' }}
</div> </div>
<div class="cookie-input-wrap"> </div>
</div>
<div class="login-methods">
<button
class="login-btn"
:class="{ active: qqLoginMode === 'qr' }"
@click="startQrLogin('qq')"
:disabled="qqQr.loading"
>
<Icon icon="mdi:qrcode" />
扫码登录
</button>
<button
class="login-btn"
:class="{ active: qqLoginMode === 'cookie' }"
@click="qqLoginMode = 'cookie'"
>
<Icon icon="mdi:cookie" />
Cookie登录
</button>
</div>
<!-- QR Code -->
<div v-if="qqLoginMode === 'qr'" class="qr-section">
<div v-if="qqQr.loading" class="qr-loading">
<Icon icon="mdi:loading" class="spin" />
生成二维码中...
</div>
<div v-else-if="qqQr.dataUrl" class="qr-wrap">
<img :src="qqQr.dataUrl" class="qr-image" alt="QR Code" />
<div class="qr-status" :class="qqQr.status">
<template v-if="qqQr.status === 'waiting'">
<Icon icon="mdi:cellphone" /> 请使用QQ音乐APP扫码
</template>
<template v-else-if="qqQr.status === 'scanned'">
<Icon icon="mdi:check" /> 已扫码,请在手机上确认
</template>
<template v-else-if="qqQr.status === 'confirmed'">
<Icon icon="mdi:check-circle" /> 登录成功!
</template>
<template v-else-if="qqQr.status === 'expired'">
<Icon icon="mdi:refresh" /> 二维码已过期
<button class="btn-link" @click="startQrLogin('qq')">重新生成</button>
</template>
</div>
</div>
</div>
<!-- Cookie -->
<div v-if="qqLoginMode === 'cookie'" class="cookie-section">
<textarea <textarea
v-model="qqCookie" v-model="qqCookie"
class="textarea" class="textarea"
placeholder="粘贴QQ音乐Cookie..." placeholder="粘贴QQ音乐Cookie..."
rows="3" rows="3"
/> />
<button class="btn-primary" @click="saveCookie('qq')">保存</button> <button class="btn-primary btn-save" @click="saveCookie('qq')">保存Cookie</button>
</div> </div>
</div> </div>
</section> </section>
@@ -99,9 +209,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import axios from 'axios'; import axios from 'axios';
import QRCode from 'qrcode';
import { usePlayerStore } from '../stores/player.js'; import { usePlayerStore } from '../stores/player.js';
const store = usePlayerStore(); const store = usePlayerStore();
@@ -112,12 +223,116 @@ const neteaseCookie = ref('');
const qqCookie = ref(''); const qqCookie = ref('');
const commandPrefix = ref('!'); const commandPrefix = ref('!');
// Login mode: 'qr' | 'cookie' | null
const neteaseLoginMode = ref<'qr' | 'cookie' | null>(null);
const qqLoginMode = ref<'qr' | 'cookie' | null>(null);
// Auth status
const neteaseAuth = reactive({ loggedIn: false, nickname: '', avatarUrl: '' });
const qqAuth = reactive({ loggedIn: false, nickname: '', avatarUrl: '' });
// QR state
interface QrState {
loading: boolean;
dataUrl: string;
key: string;
status: 'waiting' | 'scanned' | 'confirmed' | 'expired';
pollTimer: ReturnType<typeof setInterval> | null;
}
const neteaseQr = reactive<QrState>({
loading: false, dataUrl: '', key: '', status: 'waiting', pollTimer: null,
});
const qqQr = reactive<QrState>({
loading: false, dataUrl: '', key: '', status: 'waiting', pollTimer: null,
});
function getQrState(platform: string): QrState {
return platform === 'netease' ? neteaseQr : qqQr;
}
async function checkAuthStatus() {
try {
const [nRes, qRes] = await Promise.all([
axios.get('/api/auth/status', { params: { platform: 'netease' } }),
axios.get('/api/auth/status', { params: { platform: 'qq' } }),
]);
Object.assign(neteaseAuth, nRes.data);
Object.assign(qqAuth, qRes.data);
} catch {
// API not ready
}
}
async function startQrLogin(platform: string) {
const qr = getQrState(platform);
if (platform === 'netease') neteaseLoginMode.value = 'qr';
else qqLoginMode.value = 'qr';
// Stop existing poll
if (qr.pollTimer) clearInterval(qr.pollTimer);
qr.loading = true;
qr.dataUrl = '';
qr.status = 'waiting';
try {
const res = await axios.post('/api/auth/qrcode', { platform });
const { qrUrl, key } = res.data;
qr.key = key;
// Generate QR code image
qr.dataUrl = await QRCode.toDataURL(qrUrl, {
width: 200,
margin: 2,
color: {
dark: store.theme === 'dark' ? '#ffffff' : '#000000',
light: store.theme === 'dark' ? '#2a2a2a' : '#ffffff',
},
});
qr.loading = false;
// Start polling
qr.pollTimer = setInterval(() => pollQrStatus(platform), 2000);
} catch (err) {
qr.loading = false;
console.error('QR generation failed:', err);
}
}
async function pollQrStatus(platform: string) {
const qr = getQrState(platform);
if (!qr.key) return;
try {
const res = await axios.get('/api/auth/qrcode/status', {
params: { key: qr.key, platform },
});
qr.status = res.data.status;
if (qr.status === 'confirmed') {
if (qr.pollTimer) clearInterval(qr.pollTimer);
qr.pollTimer = null;
// Refresh auth status
await checkAuthStatus();
} else if (qr.status === 'expired') {
if (qr.pollTimer) clearInterval(qr.pollTimer);
qr.pollTimer = null;
}
} catch {
// Ignore poll errors
}
}
async function createBot() { async function createBot() {
if (!newBotName.value || !newBotServer.value) return; if (!newBotName.value || !newBotServer.value) return;
try { try {
await axios.post('/api/bot', { await axios.post('/api/bot', {
name: newBotName.value, name: newBotName.value,
server: newBotServer.value, serverAddress: newBotServer.value,
serverPort: 9987,
nickname: newBotName.value,
autoStart: false,
}); });
newBotName.value = ''; newBotName.value = '';
newBotServer.value = ''; newBotServer.value = '';
@@ -142,20 +357,27 @@ async function toggleBot(botId: string, connected: boolean) {
async function saveCookie(platform: string) { async function saveCookie(platform: string) {
const cookie = platform === 'netease' ? neteaseCookie.value : qqCookie.value; const cookie = platform === 'netease' ? neteaseCookie.value : qqCookie.value;
if (!cookie) return;
try { try {
await axios.post('/api/settings/cookie', { platform, cookie }); await axios.post('/api/auth/cookie', { platform, cookie });
await checkAuthStatus();
} catch { } catch {
// Ignore // Ignore
} }
} }
async function savePrefix() { async function savePrefix() {
try { // Prefix is saved client-side for now
await axios.post('/api/settings/prefix', { prefix: commandPrefix.value });
} catch {
// Ignore
}
} }
onMounted(() => {
checkAuthStatus();
});
onUnmounted(() => {
if (neteaseQr.pollTimer) clearInterval(neteaseQr.pollTimer);
if (qqQr.pollTimer) clearInterval(qqQr.pollTimer);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -213,10 +435,10 @@ async function savePrefix() {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
transition: all var(--transition-fast); transition: all var(--transition-fast);
&:hover { background: var(--color-primary); color: white; } &:hover { background: var(--color-primary); color: white; }
} }
// Bot management
.bot-list { .bot-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -249,13 +471,135 @@ async function savePrefix() {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--border-color); background: var(--border-color);
color: var(--text-tertiary); color: var(--text-tertiary);
&.online { &.online {
background: rgba(51, 94, 234, 0.15); background: rgba(51, 94, 234, 0.15);
color: var(--color-primary); color: var(--color-primary);
} }
} }
// Account cards
.account-card {
margin-bottom: 20px;
padding: 20px;
background: var(--hover-bg);
border-radius: var(--radius-md);
}
.account-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.account-icon {
font-size: 28px;
color: var(--color-primary);
}
.account-name {
font-size: 15px;
font-weight: 600;
}
.account-status {
font-size: 12px;
color: var(--text-tertiary);
&.logged { color: #4caf50; }
}
.login-methods {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.login-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
transition: all var(--transition-fast);
&:hover { border-color: var(--color-primary); color: var(--color-primary); }
&.active {
background: rgba(51, 94, 234, 0.1);
border-color: var(--color-primary);
color: var(--color-primary);
}
&:disabled { opacity: 0.5; cursor: not-allowed; }
}
// QR code
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
.qr-loading {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 14px;
}
.qr-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.qr-image {
width: 200px;
height: 200px;
border-radius: var(--radius-md);
border: 2px solid var(--border-color);
}
.qr-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-secondary);
padding: 8px 16px;
border-radius: var(--radius-sm);
background: var(--bg-card);
&.scanned { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
&.confirmed { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
&.expired { color: #f44336; background: rgba(244, 67, 54, 0.1); }
}
.btn-link {
color: var(--color-primary);
font-size: 13px;
font-weight: 600;
text-decoration: underline;
margin-left: 8px;
}
// Cookie section
.cookie-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn-save {
align-self: flex-end;
}
// Shared
.form-row { .form-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -271,18 +615,15 @@ async function savePrefix() {
font-family: inherit; font-family: inherit;
font-size: 13px; font-size: 13px;
outline: none; outline: none;
&:focus { border-color: var(--color-primary); } &:focus { border-color: var(--color-primary); }
} }
.input-sm { .input-sm { max-width: 80px; }
max-width: 80px;
}
.textarea { .textarea {
width: 100%; width: 100%;
padding: 10px 14px; padding: 10px 14px;
background: var(--hover-bg); background: var(--bg-card);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-primary); color: var(--text-primary);
@@ -290,16 +631,9 @@ async function savePrefix() {
font-size: 13px; font-size: 13px;
outline: none; outline: none;
resize: vertical; resize: vertical;
&:focus { border-color: var(--color-primary); } &:focus { border-color: var(--color-primary); }
} }
.cookie-input-wrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.prefix-input-wrap { .prefix-input-wrap {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -315,7 +649,6 @@ async function savePrefix() {
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
transition: transform var(--transition-fast); transition: transform var(--transition-fast);
&:hover { transform: scale(1.02); } &:hover { transform: scale(1.02); }
&:active { transform: scale(0.98); } &:active { transform: scale(0.98); }
} }
@@ -327,7 +660,15 @@ async function savePrefix() {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
transition: all var(--transition-fast); transition: all var(--transition-fast);
&:hover { background: var(--color-primary); color: white; } &:hover { background: var(--color-primary); color: white; }
} }
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style> </style>