fix(pda): display latest APK download QR codes for both apps
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"echarts": "^6.1.0",
|
||||
"element-plus": "^2.14.3",
|
||||
"pinia": "^4.0.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"sortablejs": "^1.15.7",
|
||||
"vue": "^3.5.40",
|
||||
"vue-echarts": "^8.3.2",
|
||||
@@ -27,6 +28,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/sortablejs": "^1.15.9",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"@vue/test-utils": "^2.4.11",
|
||||
|
||||
Generated
+170
@@ -29,6 +29,9 @@ importers:
|
||||
pinia:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.40(typescript@5.9.3))
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
sortablejs:
|
||||
specifier: ^1.15.7
|
||||
version: 1.15.7
|
||||
@@ -45,6 +48,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^24.10.0
|
||||
version: 24.13.3
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.6
|
||||
version: 1.5.6
|
||||
'@types/sortablejs':
|
||||
specifier: ^1.15.9
|
||||
version: 1.15.9
|
||||
@@ -376,6 +382,9 @@ packages:
|
||||
'@types/node@24.13.3':
|
||||
resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
||||
|
||||
'@types/sortablejs@1.15.9':
|
||||
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
|
||||
|
||||
@@ -567,6 +576,10 @@ packages:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -575,6 +588,9 @@ packages:
|
||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||
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'}
|
||||
@@ -629,6 +645,10 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
@@ -640,6 +660,9 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -719,6 +742,10 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
follow-redirects@1.16.0:
|
||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -744,6 +771,10 @@ packages:
|
||||
function-bind@1.1.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -908,6 +939,10 @@ packages:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lodash-es@4.18.1:
|
||||
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||
|
||||
@@ -990,6 +1025,18 @@ packages:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
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'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
@@ -999,6 +1046,10 @@ packages:
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1036,6 +1087,10 @@ packages:
|
||||
pkg-types@2.3.1:
|
||||
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.23:
|
||||
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1051,6 +1106,11 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
@@ -1058,10 +1118,17 @@ packages:
|
||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||
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-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
rolldown@1.1.5:
|
||||
resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1079,6 +1146,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1394,6 +1464,9 @@ packages:
|
||||
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1404,6 +1477,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1419,11 +1496,22 @@ packages:
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
yaml@2.9.0:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
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'}
|
||||
|
||||
zrender@6.1.0:
|
||||
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
|
||||
|
||||
@@ -1674,6 +1762,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
dependencies:
|
||||
'@types/node': 24.13.3
|
||||
|
||||
'@types/sortablejs@1.15.9': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
@@ -1912,12 +2004,20 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chokidar@5.0.0:
|
||||
dependencies:
|
||||
readdirp: 5.0.0
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -1967,12 +2067,16 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -2053,6 +2157,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3
|
||||
@@ -2075,6 +2184,8 @@ snapshots:
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -2240,6 +2351,10 @@ snapshots:
|
||||
pkg-types: 2.3.1
|
||||
quansync: 0.2.11
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
lodash-es@4.18.1: {}
|
||||
|
||||
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
|
||||
@@ -2303,6 +2418,16 @@ snapshots:
|
||||
|
||||
obug@2.1.4: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parse5@8.0.1:
|
||||
@@ -2311,6 +2436,8 @@ snapshots:
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
@@ -2346,6 +2473,8 @@ snapshots:
|
||||
exsolve: 1.1.0
|
||||
pathe: 2.0.3
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
postcss@8.5.23:
|
||||
dependencies:
|
||||
nanoid: 3.3.16
|
||||
@@ -2358,12 +2487,22 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
readdirp@5.0.0: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
rolldown@1.1.5:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.139.0
|
||||
@@ -2393,6 +2532,8 @@ snapshots:
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
@@ -2665,6 +2806,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
@@ -2674,6 +2817,12 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -2690,8 +2839,29 @@ snapshots:
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
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
|
||||
|
||||
zrender@6.1.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, type PropType } from 'vue'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus, { ElMessageBox, ElRadioGroup } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { pdaReleaseRoutes } from '../routes'
|
||||
import { adminNavigation } from '@/core/navigation/adminNavigation'
|
||||
import CopyButton from '@/shared/components/CopyButton.vue'
|
||||
import { copyText } from '@/shared/utils/copyText'
|
||||
import { listPdaReleases, publishPdaRelease, withdrawPdaRelease, type PdaRelease } from '../api/pdaReleases'
|
||||
import PdaReleasesView from './PdaReleasesView.vue'
|
||||
|
||||
const can = vi.hoisted(() => vi.fn(() => true))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
||||
vi.mock('../api/pdaReleases', () => ({
|
||||
listPdaReleases: vi.fn(), publishPdaRelease: vi.fn(), uploadPdaRelease: vi.fn(), withdrawPdaRelease: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/shared/utils/copyText', () => ({ copyText: vi.fn() }))
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
const page = readFileSync(resolve(process.cwd(), 'src/modules/pda-releases/pages/PdaReleasesView.vue'), 'utf8')
|
||||
|
||||
@@ -18,7 +36,8 @@ describe('PDA release workbench', () => {
|
||||
it('provides separate apps, an APK download and manage-only upload/publish/withdraw controls', () => {
|
||||
expect(page).toContain('value="domestic"')
|
||||
expect(page).toContain('value="overseas"')
|
||||
expect(page).toContain('row.packageType === \'apk\' && row.downloadUrl')
|
||||
expect(page).toContain('aria-label="最新 APK 下载"')
|
||||
expect(page).not.toContain('@click="download(asRelease(row))"')
|
||||
expect(page).toContain('v-if="canManage"')
|
||||
expect(page).toContain('row.status === \'DRAFT\'')
|
||||
expect(page).toContain('row.current')
|
||||
@@ -35,10 +54,207 @@ describe('PDA release workbench', () => {
|
||||
const toolbar = page.match(/<template #toolbar-start>([\s\S]*?)<\/template>/)?.[1]
|
||||
expect(toolbar).toContain('class="pda-release-context query-toolbar workbench-list-tabs"')
|
||||
expect(toolbar).toContain('<el-radio-group v-model="app" size="small" aria-label="选择PDA应用">')
|
||||
expect(toolbar).toContain('当前 APK')
|
||||
expect(toolbar).toContain('当前 WGT')
|
||||
expect(page).not.toContain('<template #filters>')
|
||||
expect(page).not.toContain('<h1>PDA版本</h1>')
|
||||
expect(pdaReleaseRoutes[0]?.meta?.showPageHeading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
const apk: PdaRelease = {
|
||||
id: 'apk-current', app: 'domestic', appid: 'domestic-pda', packageType: 'apk',
|
||||
versionName: '0.1.0', versionCode: 10, minAppVersionCode: 10, mandatory: false,
|
||||
releaseNotes: '更新扫码', status: 'PUBLISHED', current: true, sha256: 'checksum', sizeBytes: 1024,
|
||||
downloadUrl: '/gateway-api/pda/domestic/current.apk', createdAt: '2026-11-06T14:00:00', publishedAt: '2026-11-06T14:10:00',
|
||||
}
|
||||
const apkUrl = new URL(apk.downloadUrl!, window.location.href).href
|
||||
const overseasApk: PdaRelease = { ...apk, id: 'overseas-current', app: 'overseas', appid: 'overseas-pda',
|
||||
downloadUrl: 'https://downloads.example.com/pda/overseas/current.apk' }
|
||||
|
||||
const TableStub = defineComponent({
|
||||
props: { data: { type: Array as PropType<PdaRelease[]>, default: () => [] } },
|
||||
template: `<div><slot name="toolbar-start" /><slot name="toolbar-actions" /><slot name="notices" />
|
||||
<div v-for="row in data" :key="row.id"><slot name="actions" :row="row" /></div></div>`,
|
||||
})
|
||||
|
||||
function renderPage() {
|
||||
return mount(PdaReleasesView, {
|
||||
global: { plugins: [ElementPlus], stubs: { DataTable: TableStub, AppDialog: true } },
|
||||
})
|
||||
}
|
||||
|
||||
describe('latest APK download', () => {
|
||||
beforeEach(() => {
|
||||
can.mockReturnValue(true)
|
||||
vi.mocked(listPdaReleases).mockReset().mockImplementation(async (targetApp) => targetApp === 'domestic' ? [apk] : [overseasApk])
|
||||
vi.mocked(publishPdaRelease).mockReset().mockResolvedValue(apk)
|
||||
vi.mocked(withdrawPdaRelease).mockReset().mockResolvedValue({ ...apk, current: false, status: 'WITHDRAWN' })
|
||||
vi.spyOn(QRCode, 'toString')
|
||||
})
|
||||
|
||||
it('encodes the current published APK as an absolute URL and shares it with read-only users', async () => {
|
||||
can.mockReturnValue(false)
|
||||
vi.mocked(listPdaReleases).mockResolvedValue([
|
||||
{ ...apk, id: 'apk-history', current: false, downloadUrl: '/old.apk' },
|
||||
{ ...apk, id: 'apk-draft', current: false, status: 'DRAFT', versionCode: 20, downloadUrl: '/draft.apk' },
|
||||
{ ...apk, id: 'wgt-current', packageType: 'wgt', versionCode: 30, downloadUrl: '/current.wgt' },
|
||||
overseasApk, apk,
|
||||
])
|
||||
const downloads: HTMLAnchorElement[] = []
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) { downloads.push(this) })
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
|
||||
expect(QRCode.toString).toHaveBeenCalledTimes(2)
|
||||
expect(QRCode.toString).toHaveBeenCalledWith(apkUrl, expect.objectContaining({ type: 'svg', margin: 4, width: 160 }))
|
||||
expect(QRCode.toString).toHaveBeenCalledWith(overseasApk.downloadUrl, expect.objectContaining({ type: 'svg' }))
|
||||
const domestic = wrapper.get('[data-app="domestic"]')
|
||||
const overseas = wrapper.get('[data-app="overseas"]')
|
||||
expect(domestic.findAll('img')).toHaveLength(1)
|
||||
expect(overseas.findAll('img')).toHaveLength(1)
|
||||
expect(wrapper.get('.list-page-layout__toolbar .pda-apk-downloads').exists()).toBe(true)
|
||||
expect(wrapper.get('.workbench-table').find('.pda-apk-downloads').exists()).toBe(false)
|
||||
const image = domestic.get('.pda-apk-qr img')
|
||||
expect(image.attributes('alt')).toBe('国内 PDA最新APK下载二维码')
|
||||
expect(decodeURIComponent(image.attributes('src').split(',')[1]!)).toContain('<svg')
|
||||
expect(domestic.get('.pda-apk-download-url').attributes('href')).toBe(apkUrl)
|
||||
expect(domestic.findComponent(CopyButton).props('value')).toBe(apkUrl)
|
||||
await domestic.get('[aria-label="复制国内 PDA APK下载链接"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(copyText).toHaveBeenCalledWith(apkUrl)
|
||||
await domestic.findAll('button').find((button) => button.text() === '下载 APK')!.trigger('click')
|
||||
expect(downloads[0]?.href).toBe(apkUrl)
|
||||
expect(downloads[0]?.download).toBe('domestic-pda-0.1.0.apk')
|
||||
wrapper.findComponent(ElRadioGroup).vm.$emit('update:modelValue', 'overseas')
|
||||
await flushPromises()
|
||||
await overseas.findAll('button').find((button) => button.text() === '下载 APK')!.trigger('click')
|
||||
expect(downloads[1]?.href).toBe(overseasApk.downloadUrl)
|
||||
expect(downloads[1]?.download).toBe('overseas-pda-0.1.0.apk')
|
||||
expect(wrapper.text()).not.toContain('上传版本')
|
||||
})
|
||||
|
||||
it('offers no download for drafts, withdrawn APKs or WGT-only releases', async () => {
|
||||
vi.mocked(listPdaReleases).mockResolvedValue([
|
||||
{ ...apk, status: 'DRAFT', current: true },
|
||||
{ ...apk, id: 'withdrawn', status: 'WITHDRAWN', current: true },
|
||||
{ ...apk, id: 'wgt', packageType: 'wgt' },
|
||||
])
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('尚未发布 APK')
|
||||
expect(wrapper.find('.pda-apk-qr img').exists()).toBe(false)
|
||||
expect(wrapper.find('.pda-apk-qr').exists()).toBe(false)
|
||||
expect(wrapper.find('.pda-apk-download-url').exists()).toBe(false)
|
||||
expect(QRCode.toString).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows an unavailable state when a published APK has no download URL', async () => {
|
||||
vi.mocked(listPdaReleases).mockResolvedValue([{ ...apk, downloadUrl: null }])
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('当前 APK 下载地址暂不可用')
|
||||
expect(wrapper.find('.pda-apk-download-url').exists()).toBe(false)
|
||||
expect(QRCode.toString).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps both download cards independent of the selected list tab, including pending QR generation', async () => {
|
||||
let finishDomestic!: (svg: string) => void
|
||||
vi.mocked(QRCode.toString).mockImplementationOnce(() => new Promise<string>((resolve) => { finishDomestic = resolve }))
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
const domestic = wrapper.get('[data-app="domestic"]')
|
||||
const overseas = wrapper.get('[data-app="overseas"]')
|
||||
const overseasImage = overseas.get('.pda-apk-qr img').attributes('src')
|
||||
wrapper.findComponent(ElRadioGroup).vm.$emit('update:modelValue', 'overseas')
|
||||
await flushPromises()
|
||||
|
||||
finishDomestic('<svg>domestic QR</svg>')
|
||||
await flushPromises()
|
||||
expect(overseas.get('.pda-apk-qr img').attributes('src')).toBe(overseasImage)
|
||||
expect(domestic.get('.pda-apk-qr img').attributes('alt')).toBe('国内 PDA最新APK下载二维码')
|
||||
expect(overseas.get('.pda-apk-qr img').attributes('alt')).toBe('海外 PDA最新APK下载二维码')
|
||||
expect(domestic.get('.pda-apk-download-url').attributes('href')).toBe(apkUrl)
|
||||
expect(overseas.get('.pda-apk-download-url').attributes('href')).toBe(overseasApk.downloadUrl)
|
||||
expect(wrapper.findComponent(TableStub).props('data')).toEqual([overseasApk])
|
||||
expect(listPdaReleases).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('ignores a previous QR generation after refreshing the latest APK', async () => {
|
||||
let finishOld!: (svg: string) => void
|
||||
vi.mocked(QRCode.toString).mockImplementationOnce(() => new Promise<string>((resolve) => { finishOld = resolve }))
|
||||
const newerApk = { ...apk, id: 'newer-apk', downloadUrl: '/newer.apk' }
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
vi.mocked(listPdaReleases).mockImplementation(async (targetApp) => targetApp === 'domestic' ? [newerApk] : [overseasApk])
|
||||
await wrapper.findAll('button').find((button) => button.text() === '刷新')!.trigger('click')
|
||||
await flushPromises()
|
||||
const domestic = wrapper.get('[data-app="domestic"]')
|
||||
const newerImage = domestic.get('.pda-apk-qr img').attributes('src')
|
||||
finishOld('<svg>old QR</svg>')
|
||||
await flushPromises()
|
||||
|
||||
expect(domestic.get('.pda-apk-download-url').attributes('href')).toBe(new URL('/newer.apk', window.location.href).href)
|
||||
expect(domestic.get('.pda-apk-qr img').attributes('src')).toBe(newerImage)
|
||||
expect(listPdaReleases).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('shows the QR only after publishing and removes it on withdrawal without selecting history', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm')
|
||||
const domesticResponses: PdaRelease[][] = [
|
||||
[{ ...apk, status: 'DRAFT', current: false }], [apk],
|
||||
[{ ...apk, status: 'WITHDRAWN', current: false }, { ...apk, id: 'history', current: false }],
|
||||
]
|
||||
vi.mocked(listPdaReleases).mockImplementation(async (targetApp) => targetApp === 'domestic' ? domesticResponses.shift()! : [overseasApk])
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
const domestic = wrapper.get('[data-app="domestic"]')
|
||||
const overseasImage = wrapper.get('[data-app="overseas"] img').attributes('src')
|
||||
expect(domestic.find('.pda-apk-qr img').exists()).toBe(false)
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === '发布')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(publishPdaRelease).toHaveBeenCalledWith(apk.id)
|
||||
expect(domestic.find('.pda-apk-qr img').exists()).toBe(true)
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === '停发')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(withdrawPdaRelease).toHaveBeenCalledWith(apk.id)
|
||||
expect(domestic.find('.pda-apk-qr').exists()).toBe(false)
|
||||
expect(domestic.find('.pda-apk-download-url').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-app="overseas"] img').attributes('src')).toBe(overseasImage)
|
||||
expect(listPdaReleases).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('clears the previous download when refresh fails and recovers on retry', async () => {
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
const domestic = wrapper.get('[data-app="domestic"]')
|
||||
vi.mocked(listPdaReleases).mockRejectedValueOnce(new Error('network error'))
|
||||
await wrapper.findAll('button').find((button) => button.text() === '刷新')!.trigger('click')
|
||||
expect(domestic.find('.pda-apk-qr img').exists()).toBe(false)
|
||||
await flushPromises()
|
||||
expect(domestic.find('.pda-apk-download-url').exists()).toBe(false)
|
||||
expect(domestic.get('[role="alert"]').text()).toBeTruthy()
|
||||
expect(wrapper.get('[data-app="overseas"] .pda-apk-download-url').attributes('href')).toBe(overseasApk.downloadUrl)
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === '刷新')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(domestic.get('.pda-apk-download-url').attributes('href')).toBe(apkUrl)
|
||||
expect(domestic.find('.pda-apk-qr img').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('reports QR generation failure while preserving the APK link and allows refresh to retry', async () => {
|
||||
vi.mocked(QRCode.toString).mockImplementationOnce(() => Promise.reject(new Error('QR unavailable')))
|
||||
const wrapper = renderPage()
|
||||
await flushPromises()
|
||||
expect(wrapper.get('[role="alert"]').text()).toBe('下载二维码生成失败,请刷新后重试')
|
||||
expect(wrapper.get('.pda-apk-download-url').attributes('href')).toBe(apkUrl)
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === '刷新')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.pda-apk-qr img').exists()).toBe(true)
|
||||
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,8 +4,10 @@ import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { FileUp, Plus, RefreshCw } from '@lucide/vue'
|
||||
import QRCode from 'qrcode'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import CopyButton from '@/shared/components/CopyButton.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -18,9 +20,14 @@ import {
|
||||
const access = useAccessPreviewStore()
|
||||
const canManage = computed(() => access.can('platform:pda-release:manage'))
|
||||
const app = ref<PdaApp>('domestic')
|
||||
const rows = ref<PdaRelease[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const apps: PdaApp[] = ['domestic', 'overseas']
|
||||
const releaseLists = reactive<Record<PdaApp, { rows: PdaRelease[]; loading: boolean; error: string; request: number }>>({
|
||||
domestic: { rows: [], loading: false, error: '', request: 0 },
|
||||
overseas: { rows: [], loading: false, error: '', request: 0 },
|
||||
})
|
||||
const rows = computed(() => releaseLists[app.value].rows)
|
||||
const loading = computed(() => releaseLists[app.value].loading)
|
||||
const refreshing = computed(() => apps.some((targetApp) => releaseLists[targetApp].loading))
|
||||
const actionId = ref('')
|
||||
const uploadOpen = ref(false)
|
||||
const uploading = ref(false)
|
||||
@@ -42,27 +49,65 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'publishedAt', label: '发布时间', width: 170, slotName: 'publishedAt' },
|
||||
]
|
||||
|
||||
const currentApk = computed(() => rows.value.find((row) => row.current && row.packageType === 'apk'))
|
||||
const apkDownloads = apps.map((targetApp) => {
|
||||
const list = releaseLists[targetApp]
|
||||
const release = computed(() => list.rows.find((row) => row.app === targetApp
|
||||
&& row.current && row.status === 'PUBLISHED' && row.packageType === 'apk'))
|
||||
const url = computed(() => {
|
||||
if (list.loading || list.error || !release.value?.downloadUrl) return ''
|
||||
return new URL(release.value.downloadUrl, window.location.href).href
|
||||
})
|
||||
const qrImage = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const qrError = ref('')
|
||||
|
||||
watch(url, async (downloadUrl, _previous, onCleanup) => {
|
||||
let cancelled = false
|
||||
onCleanup(() => { cancelled = true })
|
||||
qrImage.value = ''
|
||||
qrError.value = ''
|
||||
qrLoading.value = Boolean(downloadUrl)
|
||||
if (!downloadUrl) return
|
||||
try {
|
||||
const svg = await QRCode.toString(downloadUrl, { type: 'svg', margin: 4, width: 160 })
|
||||
if (!cancelled) qrImage.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
} catch {
|
||||
if (!cancelled) qrError.value = '下载二维码生成失败,请刷新后重试'
|
||||
} finally {
|
||||
if (!cancelled) qrLoading.value = false
|
||||
}
|
||||
}, { immediate: true, flush: 'sync' })
|
||||
|
||||
return reactive({ app: targetApp, label: targetApp === 'domestic' ? '国内 PDA' : '海外 PDA',
|
||||
release, url, qrImage, qrLoading, qrError, loading: computed(() => list.loading), error: computed(() => list.error) })
|
||||
})
|
||||
|
||||
const currentApk = computed(() => apkDownloads.find((card) => card.app === app.value)?.release)
|
||||
const currentWgt = computed(() => rows.value.find((row) => row.current && row.packageType === 'wgt'))
|
||||
|
||||
function asRelease(row: unknown) { return row as PdaRelease }
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const requestedApp = app.value
|
||||
async function load(requestedApp: PdaApp) {
|
||||
const list = releaseLists[requestedApp]
|
||||
const request = ++list.request
|
||||
list.loading = true
|
||||
list.error = ''
|
||||
try {
|
||||
const result = await listPdaReleases(requestedApp)
|
||||
if (app.value === requestedApp) rows.value = result
|
||||
if (request === list.request) list.rows = result
|
||||
} catch (cause) {
|
||||
if (app.value === requestedApp) error.value = getApiErrorMessage(cause, '版本列表加载失败')
|
||||
if (request === list.request) list.error = getApiErrorMessage(cause, '版本列表加载失败')
|
||||
} finally {
|
||||
if (app.value === requestedApp) loading.value = false
|
||||
if (request === list.request) list.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(app, () => { rows.value = []; void load() })
|
||||
async function refreshAll() {
|
||||
await Promise.all(apps.map(load))
|
||||
}
|
||||
|
||||
watch(() => form.packageType, () => { form.file = null })
|
||||
onMounted(() => { void load() })
|
||||
onMounted(() => { void refreshAll() })
|
||||
|
||||
function openUpload() {
|
||||
Object.assign(form, { packageType: 'wgt', versionName: '', versionCode: currentApk.value?.versionCode ?? 1,
|
||||
@@ -83,11 +128,12 @@ async function upload() {
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
const uploadedApp = app.value
|
||||
try {
|
||||
await uploadPdaRelease(app.value, { ...form, file: form.file })
|
||||
await uploadPdaRelease(uploadedApp, { ...form, file: form.file })
|
||||
uploadOpen.value = false
|
||||
ElMessage.success('版本草稿已上传,请核对后发布')
|
||||
await load()
|
||||
await load(uploadedApp)
|
||||
} catch (cause) {
|
||||
ElMessage.error(getApiErrorMessage(cause, '更新包上传失败'))
|
||||
} finally { uploading.value = false }
|
||||
@@ -104,18 +150,19 @@ async function changeStatus(row: PdaRelease, action: 'publish' | 'withdraw') {
|
||||
if (action === 'publish') await publishPdaRelease(row.id)
|
||||
else await withdrawPdaRelease(row.id)
|
||||
ElMessage.success(`版本已${label}`)
|
||||
await load()
|
||||
await load(row.app)
|
||||
} catch (cause) {
|
||||
ElMessage.error(getApiErrorMessage(cause, `${label}失败,请刷新后重试`))
|
||||
} finally { actionId.value = '' }
|
||||
}
|
||||
|
||||
function download(row: PdaRelease) {
|
||||
if (!row.downloadUrl) return
|
||||
function downloadApk(targetApp: PdaApp) {
|
||||
const card = apkDownloads.find((item) => item.app === targetApp)
|
||||
if (!card?.url || !card.release) return
|
||||
const link = document.createElement('a')
|
||||
link.href = row.downloadUrl
|
||||
link.href = card.url
|
||||
link.rel = 'noopener noreferrer'
|
||||
link.download = `${row.app}-pda-${row.versionName}.${row.packageType}`
|
||||
link.download = `${targetApp}-pda-${card.release.versionName}.apk`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
@@ -125,6 +172,29 @@ function download(row: PdaRelease) {
|
||||
<template>
|
||||
<AdminPage class="admin-density pda-releases-page admin-workbench">
|
||||
<ListPageLayout aria-label="PDA版本">
|
||||
<template #toolbar>
|
||||
<div class="pda-apk-downloads" aria-label="最新 APK 下载">
|
||||
<section v-for="card in apkDownloads" :key="card.app" v-loading="card.loading || card.qrLoading" class="pda-apk-download" :data-app="card.app" :aria-label="`${card.label}下载`" :aria-busy="card.loading || card.qrLoading">
|
||||
<div v-if="card.release?.downloadUrl" class="pda-apk-qr">
|
||||
<img v-if="card.qrImage" :src="card.qrImage" :alt="`${card.label}最新APK下载二维码`" width="160" height="160" />
|
||||
</div>
|
||||
<div class="pda-apk-download-content">
|
||||
<h2>{{ card.label }} · 扫码下载 APK</h2>
|
||||
<template v-if="card.url">
|
||||
<p>{{ card.release?.versionName }} ({{ card.release?.versionCode }})</p>
|
||||
<a class="pda-apk-download-url" :href="card.url" rel="noopener noreferrer">{{ card.url }}</a>
|
||||
<div class="density-actions">
|
||||
<el-button @click="downloadApk(card.app)">下载 APK</el-button>
|
||||
<CopyButton :value="card.url" :label="`${card.label} APK下载链接`" button-text="复制链接" />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else-if="card.error" role="alert">{{ card.error }}</p>
|
||||
<p v-else-if="!card.loading">{{ card.release ? '当前 APK 下载地址暂不可用' : '尚未发布 APK' }}</p>
|
||||
<p v-if="card.qrError" role="alert">{{ card.qrError }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<div class="workbench-table is-page-surface"><DataTable appearance="integrated" column-settings-mode="show" preference-key="pda-releases-table" :data="rows" :columns="columns" :loading="loading" :aria-busy="loading" row-key="id" border empty-text="暂无版本记录">
|
||||
<template #toolbar-start>
|
||||
<div class="pda-release-context query-toolbar workbench-list-tabs">
|
||||
@@ -133,21 +203,16 @@ function download(row: PdaRelease) {
|
||||
<el-radio-button value="overseas">海外 PDA</el-radio-button>
|
||||
</el-radio-group>
|
||||
<dl class="pda-release-current">
|
||||
<div><dt>当前 APK</dt><dd>{{ currentApk ? `${currentApk.versionName} (${currentApk.versionCode})` : '尚未发布' }}</dd></div>
|
||||
<div><dt>当前 WGT</dt><dd>{{ currentWgt ? `${currentWgt.versionName} · 最低 APK ${currentWgt.minAppVersionCode}` : '尚未发布' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
<template #notices><el-alert v-if="error" class="pda-release-error" type="error" :title="error" show-icon :closable="false" /></template>
|
||||
|
||||
|
||||
<template #toolbar-actions>
|
||||
<el-button :loading="loading" @click="load"><RefreshCw :size="16" />刷新</el-button>
|
||||
<el-button :loading="refreshing" @click="refreshAll"><RefreshCw :size="16" />刷新</el-button>
|
||||
<el-button v-if="canManage" type="primary" @click="openUpload"><Plus :size="16" />上传版本</el-button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="pda-release-actions density-actions row-actions admin-text-actions">
|
||||
<el-button v-if="row.packageType === 'apk' && row.downloadUrl" class="density-action" data-tone="neutral" @click="download(asRelease(row))">下载 APK</el-button>
|
||||
<el-button v-if="canManage && row.status === 'DRAFT'" class="density-action" data-tone="primary" :loading="actionId === row.id" @click="changeStatus(asRelease(row), 'publish')">发布</el-button>
|
||||
<el-button v-if="canManage && row.current" class="density-action" data-tone="danger" :loading="actionId === row.id" @click="changeStatus(asRelease(row), 'withdraw')">停发</el-button>
|
||||
</div>
|
||||
@@ -190,12 +255,19 @@ function download(row: PdaRelease) {
|
||||
|
||||
<style scoped>
|
||||
.pda-release-panel { min-width: 0; overflow: hidden; }
|
||||
.pda-release-error { border: 0; border-bottom: 1px solid var(--el-color-danger-light-7); border-radius: 0; }
|
||||
.pda-release-context { display: flex; min-width: 0; align-items: center; gap: var(--admin-space-6); }
|
||||
.pda-release-current { display: flex; min-width: 0; flex-wrap: wrap; gap: var(--admin-space-3) var(--admin-space-6); margin: 0; }
|
||||
.pda-release-current > div { display: flex; min-width: 0; align-items: baseline; gap: var(--admin-space-2); }
|
||||
.pda-release-current dt { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
|
||||
.pda-release-current dd { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body); font-variant-numeric: tabular-nums; }
|
||||
.pda-apk-downloads { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: var(--admin-space-5); }
|
||||
.pda-apk-download { display: flex; min-width: 0; align-items: center; gap: var(--admin-space-6); padding: var(--admin-space-5); border: 1px solid var(--cc-border); border-radius: 4px; background: var(--cc-surface); }
|
||||
.pda-apk-qr { display: grid; width: 160px; height: 160px; flex-shrink: 0; place-items: center; background: #fff; }
|
||||
.pda-apk-qr img { display: block; }
|
||||
.pda-apk-download-content { display: grid; min-width: 0; gap: var(--admin-space-5); }
|
||||
.pda-apk-download-content h2 { margin: 0; color: var(--cc-text); font-size: var(--admin-font-section); }
|
||||
.pda-apk-download-content p { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-body); }
|
||||
.pda-apk-download-url { color: var(--el-color-primary); font-size: var(--admin-font-body); overflow-wrap: anywhere; }
|
||||
.pda-release-actions { display: flex; align-items: center; gap: var(--admin-space-2); }
|
||||
.pda-release-actions :deep(.el-button) { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.pda-release-options { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 0 var(--admin-space-6); }
|
||||
|
||||
Reference in New Issue
Block a user