APP热更新
This commit is contained in:
@@ -8,6 +8,13 @@ const config: CapacitorConfig = {
|
||||
android: {
|
||||
allowMixedContent: true,
|
||||
},
|
||||
plugins: {
|
||||
CapacitorUpdater: {
|
||||
autoUpdate: 'off',
|
||||
version: '1.0.0',
|
||||
appReadyTimeout: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
Generated
+535
-2
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^8.4.2",
|
||||
"@capacitor/barcode-scanner": "^3.1.0",
|
||||
"@capacitor/core": "^8.4.2",
|
||||
"@capacitor/geolocation": "^8.2.0",
|
||||
"@capacitor/ios": "^8.4.2",
|
||||
"@capgo/capacitor-updater": "^8.51.2",
|
||||
"@ckeditor/ckeditor5-vue": "^8.2.0",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.18.1",
|
||||
@@ -33,6 +34,7 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"archiver": "^8.0.0",
|
||||
"typescript": "~6.0.2",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
@@ -185,6 +187,15 @@
|
||||
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@capgo/capacitor-updater": {
|
||||
"version": "8.51.2",
|
||||
"resolved": "https://registry.npmjs.org/@capgo/capacitor-updater/-/capacitor-updater-8.51.2.tgz",
|
||||
"integrity": "sha512-pV1pTUl30dHLjgSHoHTffCo2cIEbn1MfdMz6Olpqh9K1dkr/WnRfezdvngBBPXo5X1OFSxyu7DFAon8hdnGz8w==",
|
||||
"license": "MPL-2.0",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ckeditor/ckeditor5-adapter-ckfinder": {
|
||||
"version": "48.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-48.3.1.tgz",
|
||||
@@ -2111,6 +2122,19 @@
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
@@ -2167,6 +2191,54 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz",
|
||||
"integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^3.2.4",
|
||||
"buffer-crc32": "^1.0.0",
|
||||
"is-stream": "^4.0.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^4.0.0",
|
||||
"readdir-glob": "^3.0.0",
|
||||
"tar-stream": "^3.0.0",
|
||||
"zip-stream": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver/node_modules/buffer-crc32": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
@@ -2177,6 +2249,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -2211,6 +2290,21 @@
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
||||
"integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
@@ -2231,6 +2325,91 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
|
||||
"integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
|
||||
"integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
|
||||
"integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.13.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
|
||||
"integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.8.1",
|
||||
"streamx": "^2.25.0",
|
||||
"teex": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*",
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz",
|
||||
"integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
@@ -2313,6 +2492,31 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -2578,6 +2782,40 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz",
|
||||
"integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"crc32-stream": "^7.0.1",
|
||||
"is-stream": "^4.0.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
@@ -2597,6 +2835,57 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/crc32-stream": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-7.0.1.tgz",
|
||||
"integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/crc32-stream/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2908,6 +3197,36 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
|
||||
@@ -2921,6 +3240,13 @@
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
@@ -3456,6 +3782,27 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -3516,6 +3863,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
@@ -3529,6 +3889,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -3583,6 +3950,52 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
@@ -4899,6 +5312,16 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
@@ -5181,6 +5604,23 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -5283,6 +5723,22 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-3.0.0.tgz",
|
||||
"integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/yqnn"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
@@ -5665,6 +6121,18 @@
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.28.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
|
||||
"integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -5755,6 +6223,39 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
|
||||
"integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"bare-fs": "^4.5.5",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/teex": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"streamx": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
@@ -6525,6 +7026,38 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz",
|
||||
"integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compress-commons": "^7.0.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
|
||||
|
||||
+4
-1
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build:capacitor": "vue-tsc -b && vite build --mode capacitor",
|
||||
"ota:package": "npm run build:capacitor && node scripts/package-app-update.mjs",
|
||||
"preview": "vite preview",
|
||||
"cap:configure": "node scripts/configure-capacitor.mjs",
|
||||
"cap:sync": "npx cap sync && npm run cap:configure",
|
||||
@@ -21,6 +22,7 @@
|
||||
"@capacitor/core": "^8.4.2",
|
||||
"@capacitor/geolocation": "^8.2.0",
|
||||
"@capacitor/ios": "^8.4.2",
|
||||
"@capgo/capacitor-updater": "^8.51.2",
|
||||
"@ckeditor/ckeditor5-vue": "^8.2.0",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.18.1",
|
||||
@@ -41,6 +43,7 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"archiver": "^8.0.0",
|
||||
"typescript": "~6.0.2",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream, createWriteStream } from 'node:fs'
|
||||
import { mkdir, readFile, stat } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { ZipArchive } from 'archiver'
|
||||
|
||||
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
|
||||
const webDirectory = path.resolve(scriptDirectory, '..')
|
||||
const distDirectory = path.join(webDirectory, 'dist')
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(path.join(webDirectory, 'package.json'), 'utf8'),
|
||||
)
|
||||
const versionIndex = process.argv.indexOf('--version')
|
||||
const version =
|
||||
versionIndex >= 0 ? process.argv[versionIndex + 1] : packageJson.version
|
||||
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(version ?? '')) {
|
||||
throw new Error('请使用 --version 1.0.1 指定语义版本。')
|
||||
}
|
||||
|
||||
await stat(path.join(distDirectory, 'index.html')).catch(() => {
|
||||
throw new Error('dist/index.html 不存在,请先完成 Capacitor 前端构建。')
|
||||
})
|
||||
|
||||
const outputDirectory = path.resolve(
|
||||
webDirectory,
|
||||
'..',
|
||||
'.artifacts',
|
||||
'app-updates',
|
||||
)
|
||||
await mkdir(outputDirectory, { recursive: true })
|
||||
const outputPath = path.join(
|
||||
outputDirectory,
|
||||
`jiaowu-web-${version}.zip`,
|
||||
)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const output = createWriteStream(outputPath)
|
||||
const archive = new ZipArchive({ zlib: { level: 9 } })
|
||||
output.on('close', resolve)
|
||||
output.on('error', reject)
|
||||
archive.on('warning', reject)
|
||||
archive.on('error', reject)
|
||||
archive.pipe(output)
|
||||
archive.directory(distDirectory, false)
|
||||
archive.finalize()
|
||||
})
|
||||
|
||||
const hash = createHash('sha256')
|
||||
await new Promise((resolve, reject) => {
|
||||
const source = createReadStream(outputPath)
|
||||
source.on('data', (chunk) => hash.update(chunk))
|
||||
source.on('end', resolve)
|
||||
source.on('error', reject)
|
||||
})
|
||||
const file = await stat(outputPath)
|
||||
|
||||
console.log(`更新包:${outputPath}`)
|
||||
console.log(`版本:${version}`)
|
||||
console.log(`大小:${file.size} bytes`)
|
||||
console.log(`SHA-256:${hash.digest('hex')}`)
|
||||
Vendored
+2
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppUpdateManagementPanel: typeof import('./components/AppUpdateManagementPanel.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
@@ -50,6 +51,7 @@ declare module 'vue' {
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
RichMessageContent: typeof import('./components/RichMessageContent.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import type { UploadFile, UploadFiles } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh, UploadFilled } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
|
||||
type UpdatePlatform = 'Android' | 'Ios'
|
||||
type UpdateChannel = 'Production' | 'Staging'
|
||||
type UpdateStatus = 'Draft' | 'Published' | 'Archived'
|
||||
|
||||
interface AppUpdateRelease {
|
||||
id: string
|
||||
platform: UpdatePlatform
|
||||
channel: UpdateChannel
|
||||
version: string
|
||||
nativeVersion: string
|
||||
status: UpdateStatus
|
||||
releaseNotes?: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
sha256: string
|
||||
createdByUserName: string
|
||||
createdAt: string
|
||||
publishedByUserName?: string
|
||||
publishedAt?: string
|
||||
}
|
||||
|
||||
interface PageResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const uploadBusy = ref(false)
|
||||
const rows = ref<AppUpdateRelease[]>([])
|
||||
const total = ref(0)
|
||||
const uploadVisible = ref(false)
|
||||
const selectedBundle = ref<File>()
|
||||
const uploadFiles = ref<UploadFiles>([])
|
||||
const filter = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
platform: '',
|
||||
channel: '',
|
||||
})
|
||||
const form = reactive({
|
||||
platform: 'Android' as UpdatePlatform,
|
||||
channel: 'Production' as UpdateChannel,
|
||||
version: '',
|
||||
nativeVersion: '1.0',
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return '—'
|
||||
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
function formatBytes(value: number) {
|
||||
if (value < 1024) return `${value} B`
|
||||
if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KB`
|
||||
return `${(value / 1024 ** 2).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function platformLabel(value: UpdatePlatform) {
|
||||
return value === 'Android' ? 'Android' : 'iOS'
|
||||
}
|
||||
|
||||
function channelLabel(value: UpdateChannel) {
|
||||
return value === 'Production' ? '正式' : '测试'
|
||||
}
|
||||
|
||||
function statusLabel(value: UpdateStatus) {
|
||||
return value === 'Published'
|
||||
? '已发布'
|
||||
: value === 'Archived'
|
||||
? '已归档'
|
||||
: '草稿'
|
||||
}
|
||||
|
||||
function statusType(value: UpdateStatus) {
|
||||
return value === 'Published'
|
||||
? 'success'
|
||||
: value === 'Archived'
|
||||
? 'info'
|
||||
: 'warning'
|
||||
}
|
||||
|
||||
async function loadReleases() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, string | number> = {
|
||||
page: filter.page,
|
||||
pageSize: filter.pageSize,
|
||||
}
|
||||
if (filter.platform) params.platform = filter.platform
|
||||
if (filter.channel) params.channel = filter.channel
|
||||
const { data } = await http.get<PageResult<AppUpdateRelease>>(
|
||||
'/app-updates/releases',
|
||||
{ params },
|
||||
)
|
||||
rows.value = data.items
|
||||
total.value = data.total
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
filter.page = 1
|
||||
loadReleases()
|
||||
}
|
||||
|
||||
function openUpload() {
|
||||
Object.assign(form, {
|
||||
platform: 'Android',
|
||||
channel: 'Production',
|
||||
version: '',
|
||||
nativeVersion: '1.0',
|
||||
releaseNotes: '',
|
||||
})
|
||||
selectedBundle.value = undefined
|
||||
uploadFiles.value = []
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
function handleBundleChange(file: UploadFile, files: UploadFiles) {
|
||||
uploadFiles.value = files.slice(-1)
|
||||
selectedBundle.value = file.raw
|
||||
}
|
||||
|
||||
function handleBundleRemove() {
|
||||
selectedBundle.value = undefined
|
||||
uploadFiles.value = []
|
||||
}
|
||||
|
||||
async function uploadRelease() {
|
||||
if (!selectedBundle.value) {
|
||||
ElMessage.warning('请选择更新 ZIP。')
|
||||
return
|
||||
}
|
||||
if (!form.version.trim() || !form.nativeVersion.trim()) {
|
||||
ElMessage.warning('请填写热更新版本和兼容的原生版本。')
|
||||
return
|
||||
}
|
||||
|
||||
const body = new FormData()
|
||||
body.append('bundle', selectedBundle.value)
|
||||
body.append('platform', form.platform)
|
||||
body.append('channel', form.channel)
|
||||
body.append('version', form.version.trim())
|
||||
body.append('nativeVersion', form.nativeVersion.trim())
|
||||
body.append('releaseNotes', form.releaseNotes.trim())
|
||||
|
||||
uploadBusy.value = true
|
||||
try {
|
||||
await http.post('/app-updates/releases', body, {
|
||||
timeout: 10 * 60 * 1000,
|
||||
})
|
||||
ElMessage.success('更新包已上传为草稿,确认无误后再发布。')
|
||||
uploadVisible.value = false
|
||||
await loadReleases()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
uploadBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function publishRelease(tableRow: Record<string, unknown>) {
|
||||
const row = tableRow as unknown as AppUpdateRelease
|
||||
const isRollback = row.status === 'Archived'
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
isRollback
|
||||
? `将把 ${platformLabel(row.platform)} ${row.nativeVersion} 的正式通道回滚到 ${row.version}。新启动的 App 将下载此版本。`
|
||||
: `确认发布 ${row.version}?同平台、通道和原生版本下当前生效的更新将自动归档。`,
|
||||
isRollback ? '确认版本回滚' : '确认发布更新',
|
||||
{
|
||||
type: isRollback ? 'warning' : 'info',
|
||||
confirmButtonText: isRollback ? '确认回滚' : '发布',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
)
|
||||
await http.post(`/app-updates/releases/${row.id}/publish`)
|
||||
ElMessage.success(isRollback ? '已回滚到指定版本。' : '更新已发布。')
|
||||
await loadReleases()
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRelease(tableRow: Record<string, unknown>) {
|
||||
const row = tableRow as unknown as AppUpdateRelease
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将永久删除更新包 ${row.fileName}。此操作不会影响已安装到设备的版本。`,
|
||||
'删除更新包',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
)
|
||||
await http.delete(`/app-updates/releases/${row.id}`)
|
||||
ElMessage.success('更新包已删除。')
|
||||
await loadReleases()
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadReleases)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="update-panel">
|
||||
<header class="update-heading">
|
||||
<div>
|
||||
<span>SELF-HOSTED OTA</span>
|
||||
<h3>App 前端热更新</h3>
|
||||
<p>上传构建后的 ZIP,按原生版本和通道发布;保留历史包用于快速回滚。</p>
|
||||
</div>
|
||||
<div class="heading-actions">
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadReleases">
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="UploadFilled" @click="openUpload">
|
||||
上传更新包
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="update-rules">
|
||||
<strong>发布边界</strong>
|
||||
<span>仅限 HTML、JavaScript、CSS 和静态资源</span>
|
||||
<span>新增原生插件、权限或 Android/iOS 代码必须重新发版</span>
|
||||
<span>更新失败时 App 自动回退到上一个可用版本</span>
|
||||
</div>
|
||||
|
||||
<div class="update-filter">
|
||||
<el-select v-model="filter.platform" clearable placeholder="全部平台">
|
||||
<el-option label="Android" value="Android" />
|
||||
<el-option label="iOS" value="Ios" />
|
||||
</el-select>
|
||||
<el-select v-model="filter.channel" clearable placeholder="全部通道">
|
||||
<el-option label="正式通道" value="Production" />
|
||||
<el-option label="测试通道" value="Staging" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" stripe>
|
||||
<el-table-column label="版本" min-width="142">
|
||||
<template #default="{ row }">
|
||||
<strong class="version">{{ row.version }}</strong>
|
||||
<small>原生 {{ row.nativeVersion }}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标" width="132">
|
||||
<template #default="{ row }">
|
||||
{{ platformLabel(row.platform) }} · {{ channelLabel(row.channel) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="94">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" effect="plain">
|
||||
{{ statusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新说明" min-width="210" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.releaseNotes || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新包" min-width="205" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="file-name">{{ row.fileName }}</span>
|
||||
<small>{{ formatBytes(row.fileSize) }} · {{ row.sha256.slice(0, 12) }}…</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="168">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.publishedAt || row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="152">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status !== 'Published'"
|
||||
link
|
||||
type="primary"
|
||||
@click="publishRelease(row)"
|
||||
>
|
||||
{{ row.status === 'Archived' ? '回滚至此版本' : '发布' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status !== 'Published'"
|
||||
link
|
||||
type="danger"
|
||||
@click="deleteRelease(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<span v-else class="active-label">当前生效</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span>尚未上传 App 热更新包。</span>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="filter.page"
|
||||
v-model:page-size="filter.pageSize"
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:total="total"
|
||||
@current-change="loadReleases"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="uploadVisible"
|
||||
title="上传 App 热更新包"
|
||||
width="min(620px, 92vw)"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="平台">
|
||||
<el-select v-model="form.platform">
|
||||
<el-option label="Android" value="Android" />
|
||||
<el-option label="iOS" value="Ios" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="通道">
|
||||
<el-select v-model="form.channel">
|
||||
<el-option label="正式通道" value="Production" />
|
||||
<el-option label="测试通道" value="Staging" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="热更新版本">
|
||||
<el-input v-model="form.version" placeholder="例如 1.0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="兼容的原生版本">
|
||||
<el-input v-model="form.nativeVersion" placeholder="当前 Android 为 1.0" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="更新说明">
|
||||
<el-input
|
||||
v-model="form.releaseNotes"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="说明本次修复内容,不要包含敏感信息。"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="更新 ZIP">
|
||||
<el-upload
|
||||
v-model:file-list="uploadFiles"
|
||||
accept=".zip,application/zip"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="handleBundleChange"
|
||||
:on-remove="handleBundleRemove"
|
||||
>
|
||||
<el-button :icon="UploadFilled">选择 ZIP</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
最大 30 MB,ZIP 根目录必须直接包含 index.html。
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="uploadVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="uploadBusy" @click="uploadRelease">
|
||||
上传为草稿
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.update-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d8dee1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.update-heading {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #d8dee1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.update-heading span {
|
||||
color: #74818a;
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: .09em;
|
||||
}
|
||||
|
||||
.update-heading h3 { font-size: 17px; margin: 3px 0 4px; }
|
||||
.update-heading p { color: #66747e; font-size: 12px; margin: 0; }
|
||||
.heading-actions { display: flex; gap: 8px; }
|
||||
|
||||
.update-rules {
|
||||
align-items: center;
|
||||
background: #f5f8f7;
|
||||
border-bottom: 1px solid #e0e6e5;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.update-rules strong { color: #1f7468; font-size: 11px; }
|
||||
.update-rules span { color: #66747e; font-size: 11px; }
|
||||
.update-rules span::before { content: "·"; margin-right: 8px; }
|
||||
|
||||
.update-filter {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
padding: 14px 18px 0;
|
||||
}
|
||||
|
||||
.update-filter .el-select { width: 160px; }
|
||||
.version,
|
||||
.file-name { display: block; font-family: "Cascadia Mono", Consolas, monospace; }
|
||||
.version { color: #1f7468; font-size: 13px; }
|
||||
.file-name { font-size: 11px; }
|
||||
small { color: #74818a; display: block; font-size: 10px; margin-top: 4px; }
|
||||
.active-label { color: #1f7468; font-size: 11px; font-weight: 700; }
|
||||
.el-table { margin-top: 12px; }
|
||||
.el-pagination { justify-content: flex-end; padding: 16px 18px; }
|
||||
.form-grid { display: grid; gap: 0 14px; grid-template-columns: 1fr 1fr; }
|
||||
.form-grid .el-select { width: 100%; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.update-heading { align-items: flex-start; display: block; }
|
||||
.heading-actions { margin-top: 12px; }
|
||||
.update-filter { flex-wrap: wrap; }
|
||||
.update-filter .el-select { flex: 1 1 140px; }
|
||||
.form-grid { display: block; }
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { initializeAppUpdates } from './services/appUpdates'
|
||||
import { setRouter } from './utils/navigate'
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -10,3 +11,4 @@ app.use(createPinia())
|
||||
app.use(router)
|
||||
setRouter(router)
|
||||
app.mount('#app')
|
||||
void initializeAppUpdates()
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { CapacitorUpdater } from '@capgo/capacitor-updater'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import http from '../api/http'
|
||||
|
||||
interface AppUpdateCheckResponse {
|
||||
available: boolean
|
||||
version?: string
|
||||
nativeVersion: string
|
||||
platform: 'Android' | 'Ios'
|
||||
channel: 'Production' | 'Staging'
|
||||
downloadUrl?: string
|
||||
releaseNotes?: string
|
||||
fileSize?: number
|
||||
sha256?: string
|
||||
publishedAt?: string
|
||||
}
|
||||
|
||||
const updateChannel =
|
||||
import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production'
|
||||
|
||||
function absoluteApiUrl(relativePath: string) {
|
||||
const configuredBase = String(http.defaults.baseURL ?? '/api')
|
||||
const publicBase =
|
||||
import.meta.env.VITE_PUBLIC_BASE_URL?.trim() || window.location.origin
|
||||
const apiBase = new URL(
|
||||
configuredBase.endsWith('/') ? configuredBase : `${configuredBase}/`,
|
||||
publicBase,
|
||||
)
|
||||
return new URL(relativePath, apiBase).toString()
|
||||
}
|
||||
|
||||
export async function initializeAppUpdates() {
|
||||
if (!Capacitor.isNativePlatform()) return
|
||||
|
||||
try {
|
||||
// Only confirm the bundle after Vue has mounted successfully. If a newly
|
||||
// installed bundle cannot reach this point, the native plugin rolls back.
|
||||
await CapacitorUpdater.notifyAppReady()
|
||||
|
||||
const platform = Capacitor.getPlatform()
|
||||
if (platform !== 'android' && platform !== 'ios') return
|
||||
|
||||
const [current, builtin, next] = await Promise.all([
|
||||
CapacitorUpdater.current(),
|
||||
CapacitorUpdater.getBuiltinVersion(),
|
||||
CapacitorUpdater.getNextBundle(),
|
||||
])
|
||||
const currentVersion =
|
||||
current.bundle.id === 'builtin'
|
||||
? builtin.version
|
||||
: current.bundle.version
|
||||
|
||||
const { data } = await http.get<AppUpdateCheckResponse>(
|
||||
'/app-updates/latest',
|
||||
{
|
||||
params: {
|
||||
platform,
|
||||
channel: updateChannel,
|
||||
nativeVersion: current.native,
|
||||
currentVersion,
|
||||
},
|
||||
timeout: 15000,
|
||||
},
|
||||
)
|
||||
if (
|
||||
!data.available ||
|
||||
!data.version ||
|
||||
!data.downloadUrl ||
|
||||
!data.sha256 ||
|
||||
next?.version === data.version
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const downloaded = await CapacitorUpdater.download({
|
||||
url: absoluteApiUrl(data.downloadUrl),
|
||||
version: data.version,
|
||||
checksum: data.sha256,
|
||||
})
|
||||
await CapacitorUpdater.next({ id: downloaded.id })
|
||||
|
||||
ElNotification({
|
||||
title: `新版本 ${data.version} 已就绪`,
|
||||
message: data.releaseNotes || '更新将在下次启动应用时自动生效。',
|
||||
type: 'success',
|
||||
duration: 6000,
|
||||
})
|
||||
} catch (error) {
|
||||
// OTA failure must never stop users from entering the bundled application.
|
||||
console.warn('[app-update] update check failed', error)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import AppUpdateManagementPanel from '../components/AppUpdateManagementPanel.vue'
|
||||
|
||||
type HealthStatus = 'healthy' | 'warning' | 'unhealthy'
|
||||
|
||||
@@ -505,6 +506,8 @@ onMounted(refreshAll)
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<AppUpdateManagementPanel />
|
||||
|
||||
<section class="ledger-panel">
|
||||
<div class="ledger-tabs" role="tablist" aria-label="审计查询类型">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user