This commit is contained in:
2026-05-09 13:56:35 +08:00
parent b694fb3461
commit 698ea070ae
10 changed files with 1289 additions and 156 deletions

View File

@ -44,6 +44,9 @@ importers:
pinia: pinia:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))
qrcode:
specifier: ^1.5.4
version: 1.5.4
vue: vue:
specifier: ^3.5.29 specifier: ^3.5.29
version: 3.5.29(typescript@5.9.3) version: 3.5.29(typescript@5.9.3)
@ -1262,6 +1265,14 @@ packages:
alien-signals@3.1.2: alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3: ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1323,6 +1334,10 @@ 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'}
caniuse-lite@1.0.30001774: caniuse-lite@1.0.30001774:
resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==}
@ -1338,10 +1353,20 @@ packages:
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==}
codepage@1.15.0: codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
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'}
@ -1398,6 +1423,10 @@ 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'}
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -1421,6 +1450,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dom7@3.0.0: dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==} resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
@ -1436,6 +1468,9 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.3.0 vue: ^3.3.0
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'}
@ -1607,6 +1642,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1647,6 +1686,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
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'}
@ -1715,6 +1758,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1803,6 +1850,10 @@ packages:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'} engines: {node: '>=14'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1966,14 +2017,26 @@ packages:
oxlint-tsgolint: oxlint-tsgolint:
optional: true optional: true
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
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==}
@ -2025,6 +2088,10 @@ packages:
pkg-types@2.3.0: pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2056,6 +2123,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
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==}
@ -2074,6 +2146,13 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
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==}
reusify@1.1.0: reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@ -2113,6 +2192,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2156,6 +2238,14 @@ packages:
ssr-window@3.0.0: ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==} resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-literal@3.1.0: strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
@ -2382,6 +2472,9 @@ 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==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -2407,6 +2500,10 @@ packages:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wsl-utils@0.1.0: wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2420,6 +2517,9 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'} engines: {node: '>=12'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -2428,6 +2528,14 @@ packages:
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.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3516,6 +3624,12 @@ snapshots:
alien-signals@3.1.2: {} alien-signals@3.1.2: {}
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {} ansi-styles@6.2.3: {}
ansis@4.2.0: {} ansis@4.2.0: {}
@ -3575,6 +3689,8 @@ 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: {}
caniuse-lite@1.0.30001774: {} caniuse-lite@1.0.30001774: {}
cfb@1.2.2: cfb@1.2.2:
@ -3590,8 +3706,20 @@ snapshots:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
codepage@1.15.0: {} codepage@1.15.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
@ -3633,6 +3761,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
default-browser-id@5.0.1: {} default-browser-id@5.0.1: {}
@ -3649,6 +3779,8 @@ snapshots:
detect-libc@2.1.2: detect-libc@2.1.2:
optional: true optional: true
dijkstrajs@1.0.3: {}
dom7@3.0.0: dom7@3.0.0:
dependencies: dependencies:
ssr-window: 3.0.0 ssr-window: 3.0.0
@ -3681,6 +3813,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
emoji-regex@8.0.0: {}
entities@7.0.1: {} entities@7.0.1: {}
error-stack-parser-es@1.0.5: {} error-stack-parser-es@1.0.5: {}
@ -3896,6 +4030,11 @@ snapshots:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
@ -3927,6 +4066,8 @@ snapshots:
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.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
@ -3987,6 +4128,8 @@ snapshots:
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@ -4050,6 +4193,10 @@ snapshots:
pkg-types: 2.3.0 pkg-types: 2.3.0
quansync: 0.2.11 quansync: 0.2.11
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@ -4208,14 +4355,24 @@ snapshots:
'@oxlint/binding-win32-ia32-msvc': 1.50.0 '@oxlint/binding-win32-ia32-msvc': 1.50.0
'@oxlint/binding-win32-x64-msvc': 1.50.0 '@oxlint/binding-win32-x64-msvc': 1.50.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
@ -4255,6 +4412,8 @@ snapshots:
exsolve: 1.0.8 exsolve: 1.0.8
pathe: 2.0.3 pathe: 2.0.3
pngjs@5.0.0: {}
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
dependencies: dependencies:
cssesc: 3.0.0 cssesc: 3.0.0
@ -4278,6 +4437,12 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
quansync@0.2.11: {} quansync@0.2.11: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@ -4291,6 +4456,10 @@ snapshots:
readdirp@5.0.0: {} readdirp@5.0.0: {}
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
reusify@1.1.0: {} reusify@1.1.0: {}
rfdc@1.4.1: {} rfdc@1.4.1: {}
@ -4350,6 +4519,8 @@ snapshots:
semver@7.7.4: {} semver@7.7.4: {}
set-blocking@2.0.0: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@ -4387,6 +4558,16 @@ snapshots:
ssr-window@3.0.0: {} ssr-window@3.0.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-literal@3.1.0: strip-literal@3.1.0:
dependencies: dependencies:
js-tokens: 9.0.1 js-tokens: 9.0.1
@ -4635,6 +4816,8 @@ snapshots:
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
@ -4651,6 +4834,12 @@ snapshots:
word@0.3.0: {} word@0.3.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wsl-utils@0.1.0: wsl-utils@0.1.0:
dependencies: dependencies:
is-wsl: 3.1.1 is-wsl: 3.1.1
@ -4667,8 +4856,29 @@ snapshots:
xml-name-validator@4.0.0: {} xml-name-validator@4.0.0: {}
y18n@4.0.3: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@2.8.2: {} yaml@2.8.2: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.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.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

5
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
vue-demi: set this to true or false

View File

@ -30,7 +30,7 @@ export async function bindEmailApi(data: BindEmailParams): Promise<void> {
// 修改用户名 // 修改用户名
export async function editUsernameApi(data: EditUsernameParams): Promise<void> { export async function editUsernameApi(data: EditUsernameParams): Promise<void> {
return request.post('/user/editUser', data); return request.post('/user/editUserName', data);
} }
// 验证码修改密码(统一接口) // 验证码修改密码(统一接口)

View File

@ -0,0 +1,65 @@
<template>
<div class="breadcrumb-container">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="index"
:to="item.path"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<script setup lang="ts">
import { useTabsStore } from '@/stores/modules/tabs'
interface BreadcrumbItem {
title: string
path: string
}
const tabsStore = useTabsStore()
// tabs store
const breadcrumbList = computed<BreadcrumbItem[]>(() => tabsStore.currentBreadcrumbs)
</script>
<style scoped lang="scss">
.breadcrumb-container {
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid var(--el-border-color-light);
:deep(.el-breadcrumb) {
font-size: 14px;
.el-breadcrumb__item {
.el-breadcrumb__inner {
color: #606266;
font-weight: normal;
&:hover {
color: var(--el-color-primary);
}
&.is-link {
color: #606266;
}
}
&:last-child {
.el-breadcrumb__inner {
color: #909399;
cursor: default;
&:hover {
color: #909399;
}
}
}
}
}
}
</style>

View File

@ -1,18 +1,45 @@
<template> <template>
<header> <header class="layout-header">
<div class="logo"></div> <!-- 左侧Logo 和系统名称 -->
<div class="user-info"> <div class="header-left">
<div class="logo">
<img src="@/assets/images/logo.webp" alt="logo" />
<span class="system-name">Admin System</span>
</div>
</div>
<!-- 右侧功能按钮和用户信息 -->
<div class="header-right">
<!-- 全屏 -->
<el-tooltip :content="isFullscreen ? '退出全屏' : '全屏'">
<el-button :icon="isFullscreen ? Crop : FullScreen" circle @click="toggleFullscreen" />
</el-tooltip>
<!-- 语言选择 -->
<LanguageSelect v-model="languageStore.lang" @change="languageStore.changeLang" /> <LanguageSelect v-model="languageStore.lang" @change="languageStore.changeLang" />
<el-dropdown @command="handleCommand">
<el-button class="user-button"> <!-- 用户信息下拉菜单 -->
{{ activeUserName }}<el-icon class="el-icon--right"> <el-dropdown @command="handleCommand" class="user-dropdown">
<ArrowDownBold /> <div class="user-info">
</el-icon> <el-avatar :size="32" :src="userAvatar" class="user-avatar">
</el-button> <el-icon><User /></el-icon>
</el-avatar>
<span class="username">{{ activeUserName }}</span>
<el-icon class="dropdown-icon"><ArrowDown /></el-icon>
</div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item v-for="item in dropdownItems" :key="item.command" :command="item.command"> <el-dropdown-item command="profile">
{{ item.label }} <el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="password">
<el-icon><Lock /></el-icon>
修改密码
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@ -22,95 +49,194 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowDownBold } from '@element-plus/icons-vue' import {
//Pinia Bell,
import LanguageSelect from '@/components/LanguageSelect/index.vue' Sunny,
import { useLanguageStore } from '@/stores/modules/language' Moon,
FullScreen,
//storestorage Crop,
User,
Lock,
SwitchButton,
ArrowDown
} from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { useLanguageStore } from '@/stores/modules/language'
import LanguageSelect from '@/components/LanguageSelect/index.vue'
import { getStorage } from '@/utils/storage' import { getStorage } from '@/utils/storage'
///Store
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const languageStore = useLanguageStore() const languageStore = useLanguageStore()
//
//
const userInfo = getStorage<any>('userInfo') const userInfo = getStorage<any>('userInfo')
const activeUserName = ref(userInfo?.username || 'Admin')
const userAvatar = ref(userInfo?.avatar || '')
const unreadCount = ref(3) //
// //
const activeUserName = ref(userInfo?.username || '') const isFullscreen = ref(false)
const activeItem = ref('currentUser') const toggleFullscreen = () => {
const dropdownItems = [ if (!document.fullscreenElement) {
// { label: '', command: 'account' }, document.documentElement.requestFullscreen()
{ label: '修改密码', command: 'password' }, isFullscreen.value = true
{ label: '退出登录', command: 'logout' } } else {
] if (document.exitFullscreen) {
document.exitFullscreen()
isFullscreen.value = false
const handleAccount = async () => { }
userStore.switchAccount() }
await router.push('/user/login')
} }
const handleChangePassword = () => { //
router.push('/user/change-password') onMounted(() => {
document.addEventListener('fullscreenchange', () => {
isFullscreen.value = !!document.fullscreenElement
})
})
//
const handleNotification = () => {
ElMessage.info('暂无新消息')
unreadCount.value = 0
} }
const handleLogout = () => { //
// 退
const deviceNo = getStorage('device_no') as string
userStore.logout({ device_no: String(deviceNo) })
}
//
const handleCommand = (command: string) => { const handleCommand = (command: string) => {
activeItem.value = command
//
switch (command) { switch (command) {
case 'currentUser': case 'profile':
// router.push('/profile/personal-center')
break
case 'account':
// /
handleAccount()
break break
case 'password': case 'password':
// router.push('/user/change-password')
handleChangePassword()
break break
case 'logout': case 'logout':
// 退
handleLogout() handleLogout()
break break
} }
} }
// 退
const handleLogout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const deviceNo = getStorage('device_no') as string
userStore.logout({ device_no: String(deviceNo) })
}).catch(() => {
// 退
})
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
header { .layout-header {
height: 60px; height: 60px;
border-bottom: solid 1px var(--el-menu-border-color); background: #fff;
padding: 0 10px; border-bottom: 1px solid var(--el-border-color-light);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
.header-left {
display: flex;
align-items: center;
gap: 20px;
.logo {
display: flex;
align-items: center;
gap: 10px;
img {
height: 32px;
width: auto;
}
.system-name {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
.notification-badge {
:deep(.el-badge__content) {
transform: translateY(-50%) translateX(50%);
}
}
.el-button {
border: none;
background: transparent;
padding: 8px;
color: var(--el-text-color-regular);
&:hover {
background: var(--el-fill-color-light);
color: var(--el-color-primary);
}
}
.user-dropdown {
margin-left: 8px;
cursor: pointer;
.user-info { .user-info {
.el-select { display: flex;
margin-right: 10px; align-items: center;
} gap: 8px;
.el-dropdown { padding: 4px 8px;
.user-button { border-radius: 4px;
border: none; transition: background 0.3s;
&:focus-visible {
outline: none;
}
&:hover { &:hover {
background-color: transparent; background: var(--el-fill-color-light);
}
.user-avatar {
background: var(--el-color-primary);
}
.username {
font-size: 14px;
color: var(--el-text-color-primary);
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-icon {
font-size: 12px;
color: var(--el-text-color-regular);
} }
} }
} }
} }
} }
//
:deep(.dark) {
.layout-header {
background: var(--el-bg-color);
border-bottom-color: var(--el-border-color-darker);
}
}
</style> </style>

View File

@ -1,23 +1,89 @@
<!-- src/components/layout/Layout.vue --> <template>
<template class="layout">
<section class="layout"> <section class="layout">
<!-- 左侧菜单Sidebar --> <!-- 顶部导航栏固定在顶部 -->
<Sidebar />
<!-- 标红区域MainContent主内容区 -->
<section class="main-container">
<!-- Header -->
<Header /> <Header />
<!-- 内容路由视图 --> <!-- 主体内容区域 -->
<main> <section class="main-container">
<router-view/> <!-- 左侧侧边菜单固定在左侧 -->
<Sidebar />
<!-- 右侧内容区面包屑 + 页签 + 主体内容 -->
<section class="content-container">
<!-- 面包屑导航 -->
<Breadcrumb />
<!-- 页签标签栏 -->
<Tabs />
<!-- 主体内容区 -->
<main class="main-content">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<KeepAlive>
<component :is="Component" :key="route.path" />
</KeepAlive>
</transition>
</router-view>
</main> </main>
</section> </section>
</section> </section>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Sidebar from './Sidebar.vue'
import Header from './Header.vue' import Header from './Header.vue'
import Sidebar from './Sidebar.vue'
import Breadcrumb from './Breadcrumb.vue'
import Tabs from './Tabs.vue'
const route = useRoute()
</script> </script>
<style scoped lang="scss">
.layout {
width: 100%;
height: 100vh;
overflow: hidden;
background: var(--el-bg-color-page);
.main-container {
display: flex;
height: calc(100vh - 60px);
margin-top: 60px;
.content-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin-left: 200px;
.main-content {
flex: 1;
padding: 16px;
overflow-y: auto;
overflow-x: hidden;
background: var(--el-bg-color-page);
}
}
}
}
//
.fade-transform-enter-active,
.fade-transform-leave-active {
transition: all 0.3s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>

View File

@ -1,34 +1,34 @@
<template> <template>
<aside> <aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<img src="@/assets/images/logo.webp" alt="logo" :router="true" />
<span>Admin System</span>
</div>
</div>
<!-- 菜单区域 --> <!-- 菜单区域 -->
<el-menu class="menu" :unique-opened="true" :default-active="activeMenu" router> <el-menu
class="sidebar-menu"
:unique-opened="true"
:default-active="activeMenu"
router
>
<template v-for="menu in menuList" :key="menu.id"> <template v-for="menu in menuList" :key="menu.id">
<!-- 无子菜单 --> <!-- 无子菜单 -->
<el-menu-item v-if="!menu.children || menu.children.length === 0" :index="menu.path"> <el-menu-item v-if="!menu.children || menu.children.length === 0" :index="menu.path">
<el-icon> <el-icon v-if="menu.icon">
<component :is="iconMap[menu.icon]" /> <component :is="iconMap[menu.icon]" />
</el-icon> </el-icon>
<template #title>{{ menu.label }}</template> <template #title>{{ menu.label }}</template>
</el-menu-item> </el-menu-item>
<!-- 有子菜单 -->
<el-sub-menu v-else :index="menu.path"> <el-sub-menu v-else :index="menu.path">
<template #title> <template #title>
<el-icon> <el-icon v-if="menu.icon">
<component :is="iconMap[menu.icon]" /> <component :is="iconMap[menu.icon]" />
</el-icon> </el-icon>
<span>{{ menu.label }}</span> <span>{{ menu.label }}</span>
</template> </template>
<el-menu-item v-for="subMenu in menu.children" :key="subMenu.id" :index="subMenu.path"> <el-menu-item
<!-- <el-icon v-if="subMenu.icon">--> v-for="subMenu in menu.children"
<!-- <component :is="iconMap[subMenu.icon]" />--> :key="subMenu.id"
<!-- </el-icon>--> :index="subMenu.path"
>
<template #title>{{ subMenu.label }}</template> <template #title>{{ subMenu.label }}</template>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
@ -48,22 +48,19 @@ import {
WarningFilled, WarningFilled,
Tools, Tools,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
//
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const menuList = ref<any[]>([])
// index fullPath // index
const activeMenu = computed(() => { const activeMenu = computed(() => {
return route.fullPath // 使 path return route.fullPath
}) })
//
onMounted(async () => { //
menuList.value = userStore.sideMenu || [] const menuList = computed(() => {
console.log('menuList.value', menuList.value) return userStore.sideMenu || []
}) })
// //
@ -77,36 +74,64 @@ const iconMap: Record<string, any> = {
WarningFilled, WarningFilled,
Tools, Tools,
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
aside { .sidebar {
.sidebar-header { position: fixed;
height: 60px; top: 60px;
border-bottom: solid 1px var(--el-menu-border-color); left: 0;
padding: 0 10px; bottom: 0;
display: flex; width: 200px;
justify-content: space-between; background: var(--el-bg-color-page);
align-items: center; border-right: 1px solid var(--el-border-color-light);
overflow-y: auto;
overflow-x: hidden;
z-index: 999;
.logo { .sidebar-menu {
display: flex;
align-items: center;
img {
width: 100px;
}
}
}
.menu {
height: 100%; height: 100%;
border: none; border: none;
background: transparent;
:deep(.el-menu-item.is-active) { :deep(.el-menu-item) {
background-color: var(--el-menu-hover-bg-color); height: 50px;
line-height: 50px;
&.is-active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-right: 3px solid var(--el-color-primary);
}
}
:deep(.el-sub-menu) {
.el-sub-menu__title {
height: 50px;
line-height: 50px;
}
&.is-active {
> .el-sub-menu__title {
color: var(--el-color-primary);
}
}
}
}
}
//
:deep(.dark) {
.sidebar {
background: var(--el-bg-color);
border-right-color: var(--el-border-color-darker);
.sidebar-menu {
:deep(.el-menu-item) {
&.is-active {
background: var(--el-color-primary-dark-2);
}
}
} }
} }
} }

View File

@ -0,0 +1,411 @@
<template>
<div class="tabs-container">
<el-tabs
v-model="tabsStore.activeTab"
type="card"
closable
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
>
<el-tab-pane
v-for="(tab, index) in tabsStore.tabs"
:key="tab.path"
:label="tab.title"
:name="tab.path"
>
<template #label>
<div
class="tab-label"
:class="{ 'is-context-active': contextMenuPath === tab.path }"
draggable="true"
@dragstart="handleDragStart($event, index)"
@dragover.prevent="handleDragOver($event, index)"
@drop="handleDrop($event, index)"
@dragend="handleDragEnd"
@contextmenu.prevent="handleContextMenu($event, tab.path)"
>
<span class="tab-text">{{ tab.title }}</span>
</div>
</template>
</el-tab-pane>
</el-tabs>
<!-- 右键菜单 -->
<el-dropdown
popper-class="el-droMenu"
:popper-style="style"
ref="contextMenuRef"
trigger="contextmenu"
@command="handleContextMenuCommand"
@visible-change="handleMenuVisibleChange"
>
<span class="context-menu-trigger"></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="refresh">刷新当前页</el-dropdown-item>
<el-dropdown-item command="close">关闭当前</el-dropdown-item>
<el-dropdown-item command="closeOther">关闭其他</el-dropdown-item>
<el-dropdown-item command="closeLeft">关闭左侧</el-dropdown-item>
<el-dropdown-item command="closeRight">关闭右侧</el-dropdown-item>
<el-dropdown-item command="closeAll" divided>关闭全部</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { useTabsStore } from '@/stores/modules/tabs'
import type { TabItem } from '@/stores/modules/tabs'
const router = useRouter()
const route = useRoute()
const tabsStore = useTabsStore()
const contextMenuRef = ref()
//
let contextMenuPath = ''
//
let dragIndex: number | null = null
let dropIndex: number | null = null
//
onMounted(() => {
//
if (tabsStore.activeTab && tabsStore.activeTab !== route.path) {
const targetTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
if (targetTab) {
router.replace({ path: targetTab.path, query: targetTab.query })
}
}
})
//
watch(
() => route.fullPath,
(newPath) => {
if (route.meta?.title && !route.meta?.isAuthPage) {
tabsStore.addTab({
title: route.meta.title as string,
path: route.path,
query: route.query
})
}
},
{ immediate: true }
)
//
const handleTabClick = (tab: any) => {
const targetTab = tabsStore.tabs.find(item => item.path === tab.paneName)
if (targetTab) {
router.push({ path: targetTab.path, query: targetTab.query })
}
}
//
const handleTabRemove = (path: string) => {
tabsStore.removeTab(path)
//
if (path === route.path && tabsStore.activeTab) {
const targetTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
if (targetTab) {
router.push({ path: targetTab.path, query: targetTab.query })
}
}
}
//
const handleContextMenuCommand = (command: string) => {
const currentPath = contextMenuPath || tabsStore.activeTab
switch (command) {
case 'refresh':
router.go(0)
break
case 'close':
if (currentPath) {
handleTabRemove(currentPath)
}
break
case 'closeOther':
if (currentPath) {
tabsStore.closeOtherTabs(currentPath)
}
break
case 'closeLeft':
if (currentPath) {
tabsStore.closeLeftTabs(currentPath)
}
break
case 'closeRight':
if (currentPath) {
tabsStore.closeRightTabs(currentPath)
}
break
case 'closeAll':
tabsStore.closeAllTabs()
router.push('/dashboard')
break
}
}
// /
const handleMenuVisibleChange = (visible: boolean) => {
if (!visible) {
contextMenuPath = ''
}
}
const style = ref('')
//
const handleContextMenu = (e: MouseEvent, path: string) => {
contextMenuPath = path
//
tabsStore.setActiveTab(path)
const targetTab = tabsStore.tabs.find(item => item.path === path)
if (targetTab && path !== route.path) {
router.push({ path: targetTab.path, query: targetTab.query })
}
// dropdown
const dropdown = contextMenuRef.value
if (dropdown) {
// dropdown
const dropdownInstance = dropdown
//
const x = e.clientX
const y = e.clientY
// 使 Element Plus
dropdownInstance?.handleOpen()
//
nextTick().then(() => {
const menu = document.querySelector('.el-droMenu')
if (menu) {
style.value = `position: fixed; z-index: 9999; left: ${x}px; top: ${y}px;`
}
})
}
}
//
const handleDragStart = (e: DragEvent, index: number) => {
dragIndex = index
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', index.toString())
}
//
const target = e.target as HTMLElement
target.classList.add('dragging')
}
//
const handleDragOver = (e: DragEvent, index: number) => {
e.preventDefault()
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'move'
}
dropIndex = index
}
//
const handleDragEnd = (e: DragEvent) => {
const target = e.target as HTMLElement
target.classList.remove('dragging')
dragIndex = null
dropIndex = null
}
//
const handleDrop = (e: DragEvent, index: number) => {
e.preventDefault()
if (dragIndex === null || dragIndex === index) {
return
}
// tabs
const newTabs = [...tabsStore.tabs]
const [draggedTab] = newTabs.splice(dragIndex, 1)
if (draggedTab) {
newTabs.splice(index, 0, draggedTab)
// store
tabsStore.setTabs(newTabs)
}
//
const target = e.target as HTMLElement
target.classList.remove('dragging')
}
</script>
<style scoped lang="scss">
.tabs-container {
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
border-bottom: 1px solid var(--el-border-color-light);
padding: 8px 20px 0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
position: relative;
z-index: 10;
:deep(.el-tabs) {
.el-tabs__header {
margin: 0;
border: none;
}
.el-tabs__nav {
border: none !important;
gap: 8px;
}
.el-tabs__item {
border: 1px solid var(--el-border-color-lighter) !important;
border-bottom: none !important;
margin-right: 0;
border-radius: 8px 8px 0 0;
background: linear-gradient(180deg, #f8f9fb 0%, #f0f2f5 100%);
color: #5a5e66;
height: 38px;
line-height: 38px;
padding: 0 18px;
font-size: 13px;
cursor: pointer;
user-select: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
//
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--el-color-primary), var(--el-color-primary-light-3));
transform: scaleX(0);
transition: transform 0.3s ease;
border-radius: 8px 8px 0 0;
}
//
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #fff;
transform: scaleX(0);
transition: transform 0.3s ease;
}
&:hover {
color: var(--el-color-primary);
background: linear-gradient(180deg, #fff 0%, #f5f7fa 100%);
border-color: var(--el-color-primary-light-6) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
&::before {
transform: scaleX(0.7);
}
&::after {
transform: scaleX(1);
}
}
&.is-active,
&.is-context-active {
background: #fff;
color: var(--el-color-primary);
// border-bottom: 1px solid #fff !important;
border-color: var(--el-color-primary-light-4) !important;
box-shadow: 0 4px 16px rgba(var(--el-color-primary-rgb), 0.15);
font-weight: 500;
z-index: 2;
&::before {
transform: scaleX(1);
}
&::after {
transform: scaleX(1);
}
}
//
&.is-context-active:not(.is-active) {
background: linear-gradient(180deg, #fff 0%, #f0f7ff 100%);
border-color: var(--el-color-primary-light-5) !important;
}
//
.el-icon-close {
width: 16px;
height: 16px;
border-radius: 50%;
transition: all 0.2s ease;
font-size: 12px;
margin-left: 6px;
vertical-align: middle;
&:hover {
background: var(--el-color-danger);
color: #fff;
transform: scale(1.15) rotate(90deg);
}
}
//
.tab-label {
display: inline-flex;
align-items: center;
width: 100%;
height: 100%;
gap: 6px;
.tab-text {
display: inline-block;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.dragging {
opacity: 0.5;
cursor: move;
}
}
}
.el-tabs__nav-wrap::after {
display: none;
}
//
.el-tabs__nav-prev,
.el-tabs__nav-next {
width: 32px;
line-height: 38px;
color: var(--el-text-color-secondary);
transition: all 0.2s ease;
border-radius: 4px;
&:hover {
color: var(--el-color-primary);
background: var(--el-fill-color-light);
}
}
}
.context-menu-trigger {
display: none;
}
}
</style>

229
src/stores/modules/tabs.ts Normal file
View File

@ -0,0 +1,229 @@
import { defineStore } from 'pinia'
import { getStorage, setStorage, removeStorage } from '@/utils/storage'
import { useUserStore } from './user'
export interface BreadcrumbItem {
title: string
path: string
}
export interface TabItem {
title: string
path: string
query?: any
breadcrumbs?: BreadcrumbItem[]
}
export const useTabsStore = defineStore('tabs', () => {
const userStore = useUserStore()
// 从本地存储恢复页签数据
const savedTabs = getStorage('tabs')
const savedActiveTab = getStorage('activeTab')
// 验证恢复的数据是否有效
const tabs = ref<TabItem[]>(Array.isArray(savedTabs) && savedTabs.length > 0 ? savedTabs : [])
const activeTab = ref<string>(
typeof savedActiveTab === 'string' &&
tabs.value.some(t => t.path === savedActiveTab)
? savedActiveTab
: '/dashboard'
)
// 如果没有有效的标签页,初始化添加首页
if (tabs.value.length === 0) {
tabs.value.push({
title: '首页',
path: '/dashboard'
})
activeTab.value = '/dashboard'
}
/**
*
*/
const findBreadcrumbPath = (path: string): BreadcrumbItem[] => {
const list: BreadcrumbItem[] = []
// 添加首页
list.push({
title: '首页',
path: '/dashboard'
})
// 如果是首页,直接返回
if (path === '/dashboard') {
return list
}
// 从菜单中查找当前路由的层级信息
const findMenuPath = (menus: any[], targetPath: string, currentPath: any[] = []): any[] | null => {
for (const menu of menus) {
const menuPath = menu.path || menu.api || `/menu-${menu.id}`
const newPath = [...currentPath, menu]
if (menuPath === targetPath) {
return newPath
}
// 兼容 children 和 son 两种子菜单字段名
const subMenus = menu.children || menu.son
if (subMenus && subMenus.length > 0) {
const result = findMenuPath(subMenus, targetPath, newPath)
if (result) {
return result
}
}
}
return null
}
const menuPath = findMenuPath(userStore.sideMenu, path)
console.log('menuPath:', menuPath);
if (menuPath) {
console.log('list:', menuPath)
menuPath.forEach((menu: any, index: number) => {
// 跳过第一个(首页已添加)
if (index === 0 && menu.label === '首页') {
return
}
list.push({
title: menu.label,
path: menu.path || menu.api || `/menu-${menu.id}`
})
})
}
;
return list
}
// 添加页签
const addTab = (tab: TabItem) => {
// 检查是否已存在
const existIndex = tabs.value.findIndex(item => item.path === tab.path)
// 计算面包屑路径
const breadcrumbs = findBreadcrumbPath(tab.path)
console.log('aaaaaaaaaaaaa:', breadcrumbs);
if (existIndex === -1) {
// 新标签页,添加到列表
tabs.value.push({ ...tab, breadcrumbs })
} else {
// 已存在的标签页,更新面包屑(确保数据是最新的)
const existingTab = tabs.value[existIndex]
if (existingTab) {
existingTab.breadcrumbs = breadcrumbs
}
}
activeTab.value = tab.path
saveTabs()
}
// 移除页签
const removeTab = (path: string) => {
const index = tabs.value.findIndex(item => item.path === path)
if (index !== -1) {
tabs.value.splice(index, 1)
// 如果移除的是当前激活的页签,则激活前一个或后一个
if (activeTab.value === path && tabs.value.length > 0) {
const newIndex = Math.max(0, index - 1)
const newTab = tabs.value[newIndex]
if (newTab) {
activeTab.value = newTab.path
}
}
saveTabs()
}
}
// 关闭其他页签
const closeOtherTabs = (path: string) => {
const currentTab = tabs.value.find(item => item.path === path)
if (currentTab) {
tabs.value = [currentTab]
activeTab.value = path
saveTabs()
}
}
// 关闭所有页签
const closeAllTabs = () => {
// 保留首页
tabs.value = [{
title: '首页',
path: '/dashboard'
}]
activeTab.value = '/dashboard'
saveTabs()
}
// 关闭左侧页签
const closeLeftTabs = (path: string) => {
const index = tabs.value.findIndex(item => item.path === path)
if (index > 0) {
tabs.value = tabs.value.slice(index)
saveTabs()
}
}
// 关闭右侧页签
const closeRightTabs = (path: string) => {
const index = tabs.value.findIndex(item => item.path === path)
if (index !== -1 && index < tabs.value.length - 1) {
tabs.value = tabs.value.slice(0, index + 1)
saveTabs()
}
}
// 设置当前激活的页签
const setActiveTab = (path: string) => {
activeTab.value = path
saveTabs()
}
// 保存到本地存储
const saveTabs = () => {
setStorage('tabs', tabs.value)
setStorage('activeTab', activeTab.value)
}
// 清空页签
const clearTabs = () => {
tabs.value = []
activeTab.value = ''
removeStorage('tabs')
removeStorage('activeTab')
}
// 设置标签页列表(用于拖拽排序)
const setTabs = (newTabs: TabItem[]) => {
tabs.value = newTabs
saveTabs()
}
// 获取当前激活标签页的面包屑
const currentBreadcrumbs = computed<BreadcrumbItem[]>(() => {
const currentTab = tabs.value.find(item => item.path === activeTab.value)
if (currentTab && currentTab.breadcrumbs) {
return currentTab.breadcrumbs
}
// 默认返回首页
return [{ title: '首页', path: '/dashboard' }]
})
return {
tabs,
activeTab,
currentBreadcrumbs,
addTab,
removeTab,
closeOtherTabs,
closeAllTabs,
closeLeftTabs,
closeRightTabs,
setActiveTab,
clearTabs,
setTabs
}
})

View File

@ -317,9 +317,7 @@ const handleLanguageChange = async (val: number) => {
await editLangTypeApi({ lang_type: val }); await editLangTypeApi({ lang_type: val });
userInfo.lang_type = val; userInfo.lang_type = val;
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US'); languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
ElMessage.success('语言切换成功');
} catch (error) { } catch (error) {
ElMessage.error('语言切换失败');
console.error('语言切换失败:', error); console.error('语言切换失败:', error);
} }
}; };
@ -331,7 +329,7 @@ const handleEditConfirm = async () => {
await editFormRef.value.validate(); await editFormRef.value.validate();
if (editField.value === 'username') { if (editField.value === 'username') {
const params: EditUsernameParams = { username: editForm.value, user_id: userInfo.id }; const params: EditUsernameParams = { username: editForm.value };
await editUsernameApi(params); await editUsernameApi(params);
userInfo.username = editForm.value; userInfo.username = editForm.value;
} }
@ -345,7 +343,6 @@ const handleEditConfirm = async () => {
await bindEmailApi(params); await bindEmailApi(params);
userInfo.email = editForm.value; userInfo.email = editForm.value;
} }
ElMessage.success('操作成功');
dialogVisible.value = false; dialogVisible.value = false;
editFormRef.value.resetFields(); editFormRef.value.resetFields();
} catch (err) { } catch (err) {
@ -424,7 +421,6 @@ const handlePasswordConfirm = async () => {
}; };
await updatePwdByCaptchaApi(params); await updatePwdByCaptchaApi(params);
ElMessage.success('密码修改成功');
passwordDialogVisible.value = false; passwordDialogVisible.value = false;
passwordFormRef.value.resetFields(); passwordFormRef.value.resetFields();
} catch (err) { } catch (err) {