feat: 添加二维码登录功能并优化全屏播放器界面

添加了 qrcode 库支持扫码登录功能,包括相关API接口类型定义
和扫码登录流程实现。同时调整全屏播放器组件的样式布局,
优化歌词显示效果和封面旋转动画尺寸。
This commit is contained in:
Yuhang Wu 2026-08-12 14:29:00 +08:00
parent 2db45f95c0
commit 6e8c97ff64
7 changed files with 625 additions and 13 deletions

View File

@ -15,12 +15,14 @@
"axios": "^1.19.0", "axios": "^1.19.0",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"naive-ui": "^2.44.1", "naive-ui": "^2.44.1",
"qrcode": "^1.5.4",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^5.2.0" "vue-router": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"@types/qrcode": "^1.5.6",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"unplugin-auto-import": "^21.1.0", "unplugin-auto-import": "^21.1.0",

View File

@ -23,6 +23,9 @@ importers:
naive-ui: naive-ui:
specifier: ^2.44.1 specifier: ^2.44.1
version: 2.44.1(vue@3.5.41(typescript@5.6.2)) version: 2.44.1(vue@3.5.41(typescript@5.6.2))
qrcode:
specifier: ^1.5.4
version: 1.5.4
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.41(typescript@5.6.2) version: 3.5.41(typescript@5.6.2)
@ -36,6 +39,9 @@ importers:
'@types/node': '@types/node':
specifier: ^26.2.0 specifier: ^26.2.0
version: 26.2.0 version: 26.2.0
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(vite@6.4.3(@types/node@26.2.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.6.2)) version: 5.2.1(vite@6.4.3(@types/node@26.2.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.6.2))
@ -527,6 +533,9 @@ packages:
'@types/node@26.2.0': '@types/node@26.2.0':
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@vitejs/plugin-vue@5.2.1': '@vitejs/plugin-vue@5.2.1':
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@ -611,6 +620,14 @@ packages:
alien-signals@0.2.0: alien-signals@0.2.0:
resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==} resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.0.0:
resolution: {integrity: sha512-8zjUtFJ3db/QoPXuuEMloS2AUf79/yeyttJ7Abr3hteopJu9HK8vsgGviGUMq+zyA6cZZO6gAyZoMTF6TgaEjA==}
engines: {node: '>=8'}
ast-kit@2.1.2: ast-kit@2.1.2:
resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==} resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==}
engines: {node: '>=20.18.0'} engines: {node: '>=20.18.0'}
@ -645,10 +662,24 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
chokidar@5.0.0: chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8: combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -691,14 +722,24 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
delayed-stream@1.0.0: delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
entities@7.0.1: entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -749,6 +790,10 @@ packages:
picomatch: picomatch:
optional: true optional: true
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
follow-redirects@1.16.0: follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -770,6 +815,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -809,6 +858,10 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
js-tokens@10.0.0: js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
@ -826,6 +879,10 @@ packages:
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash-es@4.18.1: lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
@ -885,9 +942,25 @@ packages:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
p-limit@2.2.0:
resolution: {integrity: sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
pathe@2.0.3: pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@ -907,6 +980,10 @@ packages:
pkg-types@2.3.1: pkg-types@2.3.1:
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.26: postcss@8.5.26:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -915,6 +992,11 @@ packages:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'} engines: {node: '>=10'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
quansync@0.2.11: quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
@ -922,6 +1004,13 @@ packages:
resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
rollup@4.62.4: rollup@4.62.4:
resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -938,10 +1027,21 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
string-width@4.2.0:
resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==}
engines: {node: '>=8'}
strip-ansi@6.0.0:
resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==}
engines: {node: '>=8'}
strip-literal@4.0.0: strip-literal@4.0.0:
resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==} resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==}
@ -1127,11 +1227,29 @@ packages:
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yaml@2.9.0: yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'} engines: {node: '>= 14.6'}
hasBin: true hasBin: true
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.3.1:
resolution: {integrity: sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==}
engines: {node: '>=8'}
snapshots: snapshots:
'@babel/generator@8.0.0': '@babel/generator@8.0.0':
@ -1428,6 +1546,10 @@ snapshots:
dependencies: dependencies:
undici-types: 8.3.0 undici-types: 8.3.0
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 26.2.0
'@vitejs/plugin-vue@5.2.1(vite@6.4.3(@types/node@26.2.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.6.2))': '@vitejs/plugin-vue@5.2.1(vite@6.4.3(@types/node@26.2.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.6.2))':
dependencies: dependencies:
vite: 6.4.3(@types/node@26.2.0)(yaml@2.9.0) vite: 6.4.3(@types/node@26.2.0)(yaml@2.9.0)
@ -1550,6 +1672,12 @@ snapshots:
alien-signals@0.2.0: {} alien-signals@0.2.0: {}
ansi-regex@5.0.1: {}
ansi-styles@4.0.0:
dependencies:
color-convert: 2.0.1
ast-kit@2.1.2: ast-kit@2.1.2:
dependencies: dependencies:
'@babel/parser': 7.29.8 '@babel/parser': 7.29.8
@ -1593,10 +1721,24 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
camelcase@5.3.1: {}
chokidar@5.0.0: chokidar@5.0.0:
dependencies: dependencies:
readdirp: 5.1.1 readdirp: 5.1.1
cliui@6.0.0:
dependencies:
string-width: 4.2.0
strip-ansi: 6.0.0
wrap-ansi: 6.2.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
@ -1628,14 +1770,20 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
dijkstrajs@1.0.3: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
emoji-regex@8.0.0: {}
entities@7.0.1: {} entities@7.0.1: {}
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
@ -1697,6 +1845,11 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.5
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
follow-redirects@1.16.0(debug@4.4.3): follow-redirects@1.16.0(debug@4.4.3):
optionalDependencies: optionalDependencies:
debug: 4.4.3 debug: 4.4.3
@ -1714,6 +1867,8 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -1757,6 +1912,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
is-fullwidth-code-point@3.0.0: {}
js-tokens@10.0.0: {} js-tokens@10.0.0: {}
jsesc@3.1.0: {} jsesc@3.1.0: {}
@ -1769,6 +1926,10 @@ snapshots:
pkg-types: 2.3.1 pkg-types: 2.3.1
quansync: 0.2.11 quansync: 0.2.11
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
lodash-es@4.18.1: {} lodash-es@4.18.1: {}
lodash@4.18.1: {} lodash@4.18.1: {}
@ -1836,8 +1997,20 @@ snapshots:
obug@2.1.4: {} obug@2.1.4: {}
p-limit@2.2.0:
dependencies:
p-try: 2.2.0
p-locate@4.1.0:
dependencies:
p-limit: 2.2.0
p-try@2.2.0: {}
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-exists@4.0.0: {}
pathe@2.0.3: {} pathe@2.0.3: {}
perfect-debounce@2.1.0: {} perfect-debounce@2.1.0: {}
@ -1858,6 +2031,8 @@ snapshots:
exsolve: 1.1.1 exsolve: 1.1.1
pathe: 2.0.3 pathe: 2.0.3
pngjs@5.0.0: {}
postcss@8.5.26: postcss@8.5.26:
dependencies: dependencies:
nanoid: 3.3.18 nanoid: 3.3.18
@ -1866,10 +2041,20 @@ snapshots:
proxy-from-env@2.1.0: {} proxy-from-env@2.1.0: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.3.1
quansync@0.2.11: {} quansync@0.2.11: {}
readdirp@5.1.1: {} readdirp@5.1.1: {}
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
rollup@4.62.4: rollup@4.62.4:
dependencies: dependencies:
'@types/estree': 1.0.9 '@types/estree': 1.0.9
@ -1908,8 +2093,20 @@ snapshots:
semver@7.8.5: {} semver@7.8.5: {}
set-blocking@2.0.0: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
string-width@4.2.0:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.0
strip-ansi@6.0.0:
dependencies:
ansi-regex: 5.0.1
strip-literal@4.0.0: strip-literal@4.0.0:
dependencies: dependencies:
js-tokens: 10.0.0 js-tokens: 10.0.0
@ -2100,4 +2297,33 @@ snapshots:
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
which-module@2.0.1: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.0.0
string-width: 4.2.0
strip-ansi: 6.0.0
y18n@4.0.3: {}
yaml@2.9.0: {} yaml@2.9.0: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.3.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
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.1
y18n: 4.0.3
yargs-parser: 18.1.3

View File

@ -1,14 +1,37 @@
import { get, post } from "@/utils/request" import { get, post } from "@/utils/request"
import Source from "@/types/global" import Source from "@/types/global"
/** 支持扫码登录的平台 */
export interface QrLoginSource {
name: string
source: string
}
/** 扫码登录会话 */
export interface QrLoginSession {
source: string
key: string
url: string
expires_at: number
}
/** 扫码登录状态 */
export interface QrLoginStatus {
source: string
key: string
status: string
message: string
extra?: Record<string, unknown>
}
const SystemApi = { const SystemApi = {
/** /**
* Cookies * Cookies
* @description JSON Cookies * @description JSON Cookies
* @returns * @returns
*/ */
cookies: () => { cookies: (): Promise<Record<string, string>> => {
return get<any>(`/api/v1/system/cookies`) return get<Record<string, string>>(`/api/v1/system/cookies`)
}, },
/** /**
* Cookies * Cookies
@ -23,17 +46,18 @@ const SystemApi = {
* @description API * @description API
* @returns * @returns
*/ */
qrLoginSource: () => { qrLoginSource: (): Promise<QrLoginSource[]> => {
return get<any>(`/api/v1/system/qr_login/sources`) return get<QrLoginSource[]>(`/api/v1/system/qr_login/sources`)
}, },
/** /**
* *
* @description 使 key cookies.json * @description 使 key cookies.json
* @param source * @param source
* @param key key
* @returns * @returns
*/ */
loginStatus: (source: Source) => { loginStatus: (source: Source, key: string): Promise<QrLoginStatus> => {
return get<any>(`/api/v1/system/qr_login/${source}`) return get<QrLoginStatus>(`/api/v1/system/qr_login/${source}?key=${encodeURIComponent(key)}`)
}, },
/** /**
* *
@ -41,8 +65,8 @@ const SystemApi = {
* @param source * @param source
* @returns * @returns
*/ */
qrLogin: (source: Source) => { qrLogin: (source: Source): Promise<QrLoginSession> => {
return post<any>(`/api/v1/system/qr_login/${source}`) return post<QrLoginSession>(`/api/v1/system/qr_login/${source}`)
}, },
} }

View File

@ -1,4 +1,5 @@
<template> <template>
<Teleport to="body">
<Transition name="fullscreen"> <Transition name="fullscreen">
<div v-if="show" class="fullscreen-player"> <div v-if="show" class="fullscreen-player">
<div class="fullscreen-bg" :style="current ? { background: coverBackground(current.cover) } : undefined" /> <div class="fullscreen-bg" :style="current ? { background: coverBackground(current.cover) } : undefined" />
@ -91,6 +92,7 @@
</div> </div>
</div> </div>
</Transition> </Transition>
</Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -250,14 +252,16 @@ watch(currentLyricIndex, (newIndex) => {
.cover-rotating { .cover-rotating {
position: relative; position: relative;
width: 360px; width: 300px;
height: 360px; height: 300px;
border-radius: 50%; border-radius: 50%;
overflow: hidden;
background: var(--app-card); background: var(--app-card);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation-duration: 20s; animation-duration: 20s;
animation-timing-function: linear; animation-timing-function: linear;
animation-iteration-count: infinite; animation-iteration-count: infinite;
flex-shrink: 0;
} }
.cover-rotating.spinning { .cover-rotating.spinning {
@ -331,8 +335,8 @@ watch(currentLyricIndex, (newIndex) => {
} }
.lyric-line { .lyric-line {
padding: 16px 0; padding: 12px 0;
font-size: 24px; font-size: 16px;
line-height: 1.6; line-height: 1.6;
color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 0.4);
text-align: center; text-align: center;
@ -340,7 +344,7 @@ watch(currentLyricIndex, (newIndex) => {
} }
.lyric-line.active { .lyric-line.active {
font-size: 32px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: var(--app-text); color: var(--app-text);
text-shadow: 0 2px 8px rgba(109, 93, 246, 0.4); text-shadow: 0 2px 8px rgba(109, 93, 246, 0.4);

352
src/components/QrLogin.vue Normal file
View File

@ -0,0 +1,352 @@
<template>
<section class="setting-card">
<div class="setting-head">
<h3>扫码登录</h3>
<p>选择平台生成二维码用对应 App 扫码后自动写入平台 Cookies可提升搜索与播放的可用性</p>
</div>
<div v-if="loadingSources" class="login-loading">
<n-spin size="small" />
</div>
<template v-else-if="sources.length > 0">
<n-radio-group :value="selectedSource" @update:value="onSelectSource" class="login-sources">
<n-radio-button
v-for="item in sources"
:key="item.source"
:value="item.source"
:label="item.name"
/>
</n-radio-group>
<div class="login-body">
<div class="qr-box">
<canvas ref="qrCanvasRef" class="qr-canvas" />
<div v-if="status.type === 'success'" class="qr-mask success">登录成功</div>
<div v-else-if="status.type === 'expired'" class="qr-mask expired">二维码已过期</div>
<div v-else-if="status.type === 'error'" class="qr-mask error">生成失败</div>
</div>
<div class="login-side">
<p class="status-text" :class="status.type">{{ status.text }}</p>
<n-button
type="primary"
round
:loading="creating"
:disabled="polling && status.type !== 'expired' && status.type !== 'error'"
@click="startLogin"
>
{{ session ? "重新生成" : "生成二维码" }}
</n-button>
<p class="hint">请使用{{ currentSourceName }}App 扫码</p>
</div>
</div>
</template>
<n-empty v-else description="暂无支持扫码登录的平台" class="empty" />
<div v-if="cookies.length > 0" class="cookie-row">
<span class="cookie-label">已配置 Cookies</span>
<n-tag v-for="name in cookies" :key="name" size="small" :bordered="false" type="success">
{{ name }}
</n-tag>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
import { NButton, NEmpty, NRadioButton, NRadioGroup, NSpin, NTag, useMessage } from "naive-ui"
import QRCode from "qrcode"
import SystemApi, {
type QrLoginSession,
type QrLoginSource,
type QrLoginStatus,
} from "@/api/system"
import Source from "@/types/global"
import { SOURCE_LABELS } from "@/stores/settings"
const message = useMessage()
type StatusType = "waiting" | "scanned" | "success" | "expired" | "error"
const sources = ref<QrLoginSource[]>([])
const loadingSources = ref(true)
const selectedSource = ref<Source | null>(null)
const qrCanvasRef = ref<HTMLCanvasElement | null>(null)
const session = ref<QrLoginSession | null>(null)
const creating = ref(false)
const polling = ref(false)
const status = ref<{ type: StatusType; text: string }>({
type: "waiting",
text: "等待扫码",
})
const cookies = ref<string[]>([])
let pollTimer: number | undefined
let pollSeq = 0
const currentSourceName = computed(
() => sources.value.find((item) => item.source === selectedSource.value)?.name ?? SOURCE_LABELS[selectedSource.value as Source] ?? "",
)
onMounted(async () => {
try {
const list = await SystemApi.qrLoginSource()
sources.value = list
if (list.length > 0) {
selectedSource.value = list[0].source as Source
}
} catch {
sources.value = []
} finally {
loadingSources.value = false
}
void refreshCookies()
})
onBeforeUnmount(() => {
stopPolling()
})
async function refreshCookies() {
try {
const data = await SystemApi.cookies()
cookies.value = Object.entries(data ?? {})
.filter(([, value]) => Boolean(value))
.map(([key]) => SOURCE_LABELS[key as Source] ?? key)
} catch {
//
}
}
function onSelectSource(value: string | number | boolean) {
selectedSource.value = value as Source
stopPolling()
session.value = null
status.value = { type: "waiting", text: "等待扫码" }
}
async function startLogin() {
if (!selectedSource.value) return
stopPolling()
session.value = null
status.value = { type: "waiting", text: "等待扫码" }
creating.value = true
try {
const result = await SystemApi.qrLogin(selectedSource.value)
session.value = result
await renderQr(result.url)
status.value = { type: "waiting", text: "等待扫码" }
startPolling(result)
} catch {
status.value = { type: "error", text: "生成二维码失败,请重试" }
} finally {
creating.value = false
}
}
async function renderQr(text: string) {
if (!qrCanvasRef.value) return
await QRCode.toCanvas(qrCanvasRef.value, text, {
width: 216,
margin: 2,
errorCorrectionLevel: "M",
color: { dark: "#000000", light: "#ffffff" },
})
}
function startPolling(result: QrLoginSession) {
const seq = ++pollSeq
polling.value = true
pollTimer = window.setInterval(async () => {
if (seq !== pollSeq) return
try {
const res = await SystemApi.loginStatus(result.source as Source, result.key)
if (seq !== pollSeq) return
applyStatus(res)
if (status.value.type === "success") {
stopPolling()
message.success(`${currentSourceName.value}」登录成功Cookies 已保存`)
void refreshCookies()
} else if (status.value.type === "expired" || status.value.type === "error") {
stopPolling()
}
} catch {
if (seq !== pollSeq) return
status.value = { type: "error", text: "查询登录状态失败,请重试" }
stopPolling()
}
}, 2000)
}
function applyStatus(res: QrLoginStatus) {
const code = String(res.extra?.code ?? "")
const raw = res.status ?? ""
if (raw.includes("success") || code === "803") {
status.value = { type: "success", text: res.message || "登录成功" }
} else if (raw.includes("expired") || code === "800") {
status.value = { type: "expired", text: res.message || "二维码已过期" }
} else if (raw.includes("scan") || code === "802") {
status.value = { type: "scanned", text: res.message || "已扫码,请在手机上确认" }
} else {
status.value = { type: "waiting", text: res.message || "等待扫码" }
}
}
function stopPolling() {
pollSeq += 1
polling.value = false
if (pollTimer !== undefined) {
window.clearInterval(pollTimer)
pollTimer = undefined
}
}
</script>
<style scoped>
.setting-card {
margin-bottom: 20px;
padding: clamp(16px, 2vw, 22px);
border: 1px solid var(--app-border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.02);
}
.setting-head {
margin-bottom: 16px;
}
.setting-head h3 {
margin: 0 0 4px;
font-size: 16px;
font-weight: 600;
}
.setting-head p {
margin: 0;
font-size: 12px;
color: var(--app-text-muted);
}
.login-loading {
display: flex;
justify-content: center;
padding: 24px 0;
}
.login-sources {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.login-body {
display: flex;
align-items: center;
gap: clamp(18px, 3vw, 28px);
}
.qr-box {
position: relative;
width: 220px;
height: 220px;
border-radius: 12px;
overflow: hidden;
flex-shrink: 0;
background: #fff;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
}
.qr-canvas {
display: block;
width: 100%;
height: 100%;
}
.qr-mask {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 15px;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(2px);
}
.qr-mask.success {
background: rgba(34, 197, 94, 0.72);
}
.qr-mask.expired {
background: rgba(239, 68, 68, 0.72);
}
.login-side {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
min-width: 0;
}
.status-text {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.status-text.waiting {
color: var(--app-text-secondary);
}
.status-text.scanned {
color: #f59e0b;
}
.status-text.success {
color: var(--app-green);
}
.status-text.expired,
.status-text.error {
color: #ef4444;
}
.hint {
margin: 0;
font-size: 12px;
color: var(--app-text-muted);
}
.cookie-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--app-border);
}
.cookie-label {
font-size: 12px;
color: var(--app-text-muted);
}
.empty {
padding: 24px 0;
}
@media (max-width: 640px) {
.login-body {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@ -20,6 +20,7 @@ declare module 'vue' {
NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
PlayerBar: typeof import('./../components/PlayerBar.vue')['default'] PlayerBar: typeof import('./../components/PlayerBar.vue')['default']
QrLogin: typeof import('./../components/QrLogin.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SongTable: typeof import('./../components/SongTable.vue')['default'] SongTable: typeof import('./../components/SongTable.vue')['default']

View File

@ -45,11 +45,14 @@
/> />
</n-radio-group> </n-radio-group>
</section> </section>
<QrLogin />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NCheckboxGroup, NRadioGroup } from "naive-ui" import { NCheckboxGroup, NRadioGroup } from "naive-ui"
import QrLogin from "@/components/QrLogin.vue"
import { ALL_SOURCES, SOURCE_LABELS, setDefaultSource, setEnabledSources, settingsState } from "@/stores/settings" import { ALL_SOURCES, SOURCE_LABELS, setDefaultSource, setEnabledSources, settingsState } from "@/stores/settings"
</script> </script>