Compare commits
8
Commits
ota-img
...
725a3ddcb9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
725a3ddcb9 | ||
|
|
33187e0cc3 | ||
|
|
bf6b87b59c | ||
|
|
12729a1cef | ||
|
|
f8efe4c4a4 | ||
|
|
af07d09ab6 | ||
|
|
e0d976fcce | ||
|
|
01ac4f9c03 |
Generated
+628
@@ -26,6 +26,8 @@
|
|||||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
|
||||||
"@dcloudio/uni-ui": "^1.5.11",
|
"@dcloudio/uni-ui": "^1.5.11",
|
||||||
"pinia": "2.0.36",
|
"pinia": "2.0.36",
|
||||||
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
|
"protobufjs": "^8.7.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
},
|
},
|
||||||
@@ -2422,6 +2424,70 @@
|
|||||||
"vite": "^5.2.8"
|
"vite": "^5.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||||
@@ -2438,6 +2504,294 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "9.1.9",
|
"version": "9.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
|
||||||
@@ -3424,6 +3778,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.39.0",
|
"version": "4.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz",
|
||||||
@@ -3437,6 +3817,227 @@
|
|||||||
"darwin"
|
"darwin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz",
|
||||||
|
"integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@sinonjs/commons": {
|
"node_modules/@sinonjs/commons": {
|
||||||
"version": "1.8.6",
|
"version": "1.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
|
||||||
@@ -7535,6 +8136,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -8222,6 +8829,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia-plugin-persistedstate": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pinia": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
@@ -8530,6 +9146,18 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "8.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.0.tgz",
|
||||||
|
"integrity": "sha512-uu52JNxLh3vsL7tXU/h0gDaywufvuUCTbGSi0NKQKBZ2ZopkmrWQJSQO/EFqzu/5YhiwgVM8rq/a/iVpx4eZ0g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"long": "^5.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@dcloudio/uni-ui": "^1.5.11",
|
"@dcloudio/uni-ui": "^1.5.11",
|
||||||
"pinia": "2.0.36",
|
"pinia": "2.0.36",
|
||||||
"pinia-plugin-persistedstate": "3.2.1",
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
|
"protobufjs": "^8.7.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
},
|
},
|
||||||
|
|||||||
+26
@@ -7,9 +7,13 @@
|
|||||||
onHide
|
onHide
|
||||||
} from "@dcloudio/uni-app";
|
} from "@dcloudio/uni-app";
|
||||||
import websocket from "@/websocket";
|
import websocket from "@/websocket";
|
||||||
|
import matchWebsocket from "@/matchWebsocket";
|
||||||
import {
|
import {
|
||||||
getDeviceBatteryAPI
|
getDeviceBatteryAPI
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
import {
|
||||||
|
MESSAGETYPES
|
||||||
|
} from "@/constants";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import {
|
import {
|
||||||
storeToRefs
|
storeToRefs
|
||||||
@@ -36,6 +40,9 @@
|
|||||||
websocket.createWebSocket(token, onShootWsMsg);
|
websocket.createWebSocket(token, onShootWsMsg);
|
||||||
}
|
}
|
||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
|
matchWebsocket.closeMatchWebSocket({
|
||||||
|
reason: "logout"
|
||||||
|
});
|
||||||
websocket.closeWebSocket();
|
websocket.closeWebSocket();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@@ -73,10 +80,26 @@
|
|||||||
// audioManager.play("射箭声音")
|
// audioManager.play("射箭声音")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectMatchServerFromMessage(content) {
|
||||||
|
const messages = Array.isArray(content) ? content : [content];
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (
|
||||||
|
message?.constructor ===
|
||||||
|
MESSAGETYPES.ShootSyncBattleCreateMatchLinkID
|
||||||
|
) {
|
||||||
|
matchWebsocket.connectMatchWebSocketFromNotice(
|
||||||
|
message,
|
||||||
|
user.value.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onShootWsMsg(content) {
|
function onShootWsMsg(content) {
|
||||||
if(content.type === 'shoot-trigger'){
|
if(content.type === 'shoot-trigger'){
|
||||||
onDeviceShoot()
|
onDeviceShoot()
|
||||||
}
|
}
|
||||||
|
connectMatchServerFromMessage(content);
|
||||||
uni.$emit("socket-inbox", content);
|
uni.$emit("socket-inbox", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +122,9 @@
|
|||||||
uni.$off("update-online", emitUpdateOnline);
|
uni.$off("update-online", emitUpdateOnline);
|
||||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||||
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
|
matchWebsocket.closeMatchWebSocket({
|
||||||
|
reason: "app-hide"
|
||||||
|
});
|
||||||
websocket.closeWebSocket();
|
websocket.closeWebSocket();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+5
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeBattleApiResult } from "@/utils/matchAdapter";
|
||||||
|
|
||||||
let BASE_URL = "https://api.shelingxingqiu.com/api/shoot"; // 默认正式版
|
let BASE_URL = "https://api.shelingxingqiu.com/api/shoot"; // 默认正式版
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -6,7 +8,7 @@ try {
|
|||||||
|
|
||||||
switch (envVersion) {
|
switch (envVersion) {
|
||||||
case "develop": // 开发版
|
case "develop": // 开发版
|
||||||
// BASE_URL = "http://192.168.1.30:8000/api/shoot";
|
// BASE_URL = "http://192.168.1.2:8000/api/shoot";
|
||||||
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
|
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
|
||||||
break;
|
break;
|
||||||
case "trial": // 体验版
|
case "trial": // 体验版
|
||||||
@@ -553,9 +555,10 @@ export const getReadyAPI = (roomId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getBattleAPI = async (battleId) => {
|
export const getBattleAPI = async (battleId) => {
|
||||||
return request("POST", "/user/match/info", {
|
const result = await request("POST", "/user/match/info", {
|
||||||
id: battleId,
|
id: battleId,
|
||||||
});
|
});
|
||||||
|
return normalizeBattleApiResult(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const kickPlayerAPI = (number, userId) => {
|
export const kickPlayerAPI = (number, userId) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app";
|
|||||||
|
|
||||||
import { getBattleAPI, getUserGameState } from "@/apis";
|
import { getBattleAPI, getUserGameState } from "@/apis";
|
||||||
import { debounce } from "@/util";
|
import { debounce } from "@/util";
|
||||||
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -67,11 +68,7 @@ const onClick = debounce(async () => {
|
|||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await uni.$checkAudio();
|
await uni.$checkAudio();
|
||||||
if (result.mode <= 3) {
|
await returnToBattle(result, navigateOnce);
|
||||||
await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
|
|
||||||
} else {
|
|
||||||
await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (game.value.roomID) {
|
if (game.value.roomID) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ScreenHint from "@/components/ScreenHint.vue";
|
|||||||
import BackToGame from "@/components/BackToGame.vue";
|
import BackToGame from "@/components/BackToGame.vue";
|
||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
import AudioManager from "@/audioManager";
|
import AudioManager from "@/audioManager";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -110,6 +111,15 @@ onShow(() => {
|
|||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigateTo = (url) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const backToGame = debounce(async () => {
|
const backToGame = debounce(async () => {
|
||||||
if (isLoading.value) return; // 防止重复点击
|
if (isLoading.value) return; // 防止重复点击
|
||||||
|
|
||||||
@@ -118,15 +128,7 @@ const backToGame = debounce(async () => {
|
|||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await checkAudioProgress();
|
await checkAudioProgress();
|
||||||
if (result.mode <= 3) {
|
await returnToBattle(result, navigateTo);
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/team-battle/index?battleId=${result.matchId}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/melee-battle?battleId=${result.matchId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取当前游戏失败:", error);
|
console.error("获取当前游戏失败:", error);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const createRoom = debounce(async () => {
|
|||||||
}
|
}
|
||||||
if (loading.value === true) return;
|
if (loading.value === true) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
let keepLoading = false;
|
||||||
let size = 2;
|
let size = 2;
|
||||||
if (battleMode.value === 2) size = 10;
|
if (battleMode.value === 2) size = 10;
|
||||||
if (battleMode.value === 3) size = 4;
|
if (battleMode.value === 3) size = 4;
|
||||||
@@ -49,14 +50,18 @@ const createRoom = debounce(async () => {
|
|||||||
if (result.number) {
|
if (result.number) {
|
||||||
props.onConfirm();
|
props.onConfirm();
|
||||||
await joinRoomAPI(result.number);
|
await joinRoomAPI(result.number);
|
||||||
|
keepLoading = true;
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value,
|
url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value,
|
||||||
|
fail: () => {
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!keepLoading) loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -112,7 +117,7 @@ const createRoom = debounce(async () => {
|
|||||||
<text>40厘米全环靶</text>
|
<text>40厘米全环靶</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<SButton :onClick="() => $clickSound(createRoom)">创建房间</SButton>
|
<SButton :disabled="loading" :onClick="() => { $clickSound(); return createRoom(); }">创建房间</SButton>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const MESSAGETYPES = {
|
export const MESSAGETYPES = {
|
||||||
ShootSyncMeArrowID: parseInt("0x789b6b0d"), // 2023451405
|
ShootSyncMeArrowID: parseInt("0x789b6b0d"), // 2023451405
|
||||||
ShootSyncMePracticeID: parseInt("0xD88AE05E"), // 3632980062
|
ShootSyncMePracticeID: parseInt("0xD88AE05E"), // 3632980062
|
||||||
|
ShootSyncBattleCreateMatchLinkID: parseInt("0xF86FF466"), // 2538869862
|
||||||
WaitForAllReady: parseInt("0x615C13BE"), // 1633424318
|
WaitForAllReady: parseInt("0x615C13BE"), // 1633424318
|
||||||
AllReady: parseInt("0x1CCB49FD"), // 483084797
|
AllReady: parseInt("0x1CCB49FD"), // 483084797
|
||||||
MeleeAllReady: parseInt("0x37132BD5"), // 924003285
|
MeleeAllReady: parseInt("0x37132BD5"), // 924003285
|
||||||
|
|||||||
@@ -0,0 +1,607 @@
|
|||||||
|
import {
|
||||||
|
ClientMessageType,
|
||||||
|
ServerMessageType,
|
||||||
|
createAckMessage,
|
||||||
|
createHeartbeatAckMessage,
|
||||||
|
createLeaveMessage,
|
||||||
|
decodeServerMessage,
|
||||||
|
getServerMessageTypeName,
|
||||||
|
} from "@/utils/matchProtocol";
|
||||||
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
|
import { getDirectionText } from "@/util";
|
||||||
|
import {
|
||||||
|
normalizeId,
|
||||||
|
normalizeMatchInfo,
|
||||||
|
normalizePlainObject,
|
||||||
|
pickField,
|
||||||
|
} from "@/utils/matchAdapter";
|
||||||
|
|
||||||
|
// 比赛服 websocket 独立管理器:
|
||||||
|
// 负责连接、解码、ACK/LEAVE,并把比赛服消息适配成项目现有 socket-inbox 业务事件。
|
||||||
|
let socket = null;
|
||||||
|
let currentContext = null;
|
||||||
|
let isConnecting = false;
|
||||||
|
let manualClose = false;
|
||||||
|
let audioAckListenerReady = false;
|
||||||
|
let lastReadyRouteKey = "";
|
||||||
|
|
||||||
|
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
|
||||||
|
const pendingAcks = [];
|
||||||
|
const ACK_AUDIO_TIMEOUT_MS = 9000;
|
||||||
|
const READY_ROUTE_FALLBACK_DELAY_MS = 300;
|
||||||
|
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
||||||
|
|
||||||
|
// 比赛服消息类型先映射成项目里已有的 V2 业务消息,页面仍然复用原来的 socket-inbox 流程。
|
||||||
|
const BUSINESS_TYPE_BY_SERVER_TYPE = {
|
||||||
|
[ServerMessageType.SERVER_MSG_MATCH_READY]: MESSAGETYPESV2.AboutToStart,
|
||||||
|
[ServerMessageType.SERVER_MSG_MATCH_START]: MESSAGETYPESV2.BattleStart,
|
||||||
|
[ServerMessageType.SERVER_MSG_NOW_YOU]: MESSAGETYPESV2.ToSomeoneShoot,
|
||||||
|
[ServerMessageType.SERVER_MSG_SHOT]: MESSAGETYPESV2.ShootResult,
|
||||||
|
[ServerMessageType.SERVER_MSG_NEW_ROUND]: MESSAGETYPESV2.NewRound,
|
||||||
|
[ServerMessageType.SERVER_MSG_MATCH_END]: MESSAGETYPESV2.BattleEnd,
|
||||||
|
[ServerMessageType.SERVER_MSG_TIMEOUT]: MESSAGETYPESV2.BattleEnd,
|
||||||
|
[ServerMessageType.SERVER_MSG_CHECK]: MESSAGETYPESV2.TestDistance,
|
||||||
|
[ServerMessageType.SERVER_MSG_NOT_ENOUGH_DISTANCE]: MESSAGETYPESV2.InvalidShot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||||
|
export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack";
|
||||||
|
|
||||||
|
function normalizeShootData(shootData) {
|
||||||
|
if (!shootData || typeof shootData !== "object") return shootData;
|
||||||
|
const normalized = { ...shootData };
|
||||||
|
if (normalized.distance === undefined && normalized.dst !== undefined) {
|
||||||
|
normalized.distance = normalized.dst;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容普通接口通知的 camelCase 和 protobuf 解码后的 snake_case。
|
||||||
|
function buildBusinessMessage(message) {
|
||||||
|
const businessType = BUSINESS_TYPE_BY_SERVER_TYPE[message.type];
|
||||||
|
if (!businessType) return null;
|
||||||
|
|
||||||
|
const matchInfo = normalizeMatchInfo(message.match_info);
|
||||||
|
const matchId = normalizeId(
|
||||||
|
pickField(message, "matchId", "match_id") ||
|
||||||
|
matchInfo.matchId ||
|
||||||
|
currentContext?.matchId
|
||||||
|
);
|
||||||
|
const shootData = normalizeShootData(
|
||||||
|
matchInfo.shootData ||
|
||||||
|
(message.shoot_data ? normalizePlainObject(message.shoot_data) : undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...matchInfo,
|
||||||
|
type: businessType,
|
||||||
|
id: matchId,
|
||||||
|
matchId,
|
||||||
|
shootData,
|
||||||
|
sequence: message.sequence,
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
matchWsType: message.type,
|
||||||
|
matchWsTypeName: getServerMessageTypeName(message.type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReadySnapshotKey(matchId) {
|
||||||
|
return `${MATCH_READY_SNAPSHOT_PREFIX}${normalizeId(matchId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistReadyTeams(message) {
|
||||||
|
const teams = message?.teams || {};
|
||||||
|
if (Number(message?.mode) > 3) return;
|
||||||
|
|
||||||
|
const bluePlayers = teams[1]?.players || teams["1"]?.players;
|
||||||
|
const redPlayers = teams[2]?.players || teams["2"]?.players;
|
||||||
|
if (Array.isArray(bluePlayers)) uni.setStorageSync("blue-team", bluePlayers);
|
||||||
|
if (Array.isArray(redPlayers)) uni.setStorageSync("red-team", redPlayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistReadySnapshot(message) {
|
||||||
|
const matchId = normalizeId(message?.matchId || message?.id);
|
||||||
|
if (!matchId) return;
|
||||||
|
uni.setStorageSync(getReadySnapshotKey(matchId), {
|
||||||
|
...message,
|
||||||
|
savedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBattlePageUrl(message) {
|
||||||
|
const matchId = normalizeId(message?.matchId || message?.id);
|
||||||
|
const mode = Number(message?.mode);
|
||||||
|
if (!matchId) return "";
|
||||||
|
if (!Number.isFinite(mode)) return "";
|
||||||
|
return mode <= 3
|
||||||
|
? `/pages/team-battle/index?battleId=${matchId}`
|
||||||
|
: `/pages/melee-battle?battleId=${matchId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPageInfo() {
|
||||||
|
if (typeof getCurrentPages !== "function") return null;
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
return pages[pages.length - 1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRoute(route) {
|
||||||
|
return String(route || "").replace(/^\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentBattlePage(matchId) {
|
||||||
|
const page = getCurrentPageInfo();
|
||||||
|
const route = normalizeRoute(page?.route);
|
||||||
|
if (route !== "pages/team-battle/index" && route !== "pages/melee-battle") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = page?.options || page?.$page?.options || {};
|
||||||
|
return !matchId || normalizeId(options.battleId) === normalizeId(matchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReadyRouteFallback(message) {
|
||||||
|
const url = getBattlePageUrl(message);
|
||||||
|
const matchId = normalizeId(message?.matchId || message?.id);
|
||||||
|
const routeKey = `${matchId}:${url}`;
|
||||||
|
if (!url || lastReadyRouteKey === routeKey) return;
|
||||||
|
|
||||||
|
lastReadyRouteKey = routeKey;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isCurrentBattlePage(matchId)) return;
|
||||||
|
|
||||||
|
const currentRoute = normalizeRoute(getCurrentPageInfo()?.route);
|
||||||
|
// battle-room 和 match-page 本身会消费 AboutToStart;这里不绕过页面内状态,避免好友房误触退房逻辑。
|
||||||
|
if (currentRoute === "pages/battle-room" || currentRoute === "pages/match-page") return;
|
||||||
|
|
||||||
|
uni.redirectTo({
|
||||||
|
url,
|
||||||
|
fail: (err) => {
|
||||||
|
console.log("[match-ws] ready route fallback failed", err, { url });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, READY_ROUTE_FALLBACK_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitBusinessMessage(message) {
|
||||||
|
if (!message) return;
|
||||||
|
if (message.type === MESSAGETYPESV2.AboutToStart) {
|
||||||
|
persistReadyTeams(message);
|
||||||
|
persistReadySnapshot(message);
|
||||||
|
scheduleReadyRouteFallback(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.$emit("socket-inbox", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlayerTeam(message, playerId) {
|
||||||
|
const teams = message?.teams || {};
|
||||||
|
const redPlayers = teams[2]?.players || teams["2"]?.players || [];
|
||||||
|
return redPlayers.some((item) => String(item.id) === String(playerId))
|
||||||
|
? "red"
|
||||||
|
: "blue";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNowYouAudioKeys(message) {
|
||||||
|
const current = message?.current || {};
|
||||||
|
const keys = [];
|
||||||
|
|
||||||
|
if (current.goldRound) {
|
||||||
|
keys.push("决金箭轮");
|
||||||
|
} else if (current.round) {
|
||||||
|
const roundIndex = Math.max(0, Number(current.round) - 1);
|
||||||
|
const roundName = ROUND_AUDIO_NAMES[roundIndex] || current.round;
|
||||||
|
keys.push(`第${roundName}轮`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(current.playerId) === String(currentContext?.userId)) {
|
||||||
|
keys.push("轮到你了");
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = resolvePlayerTeam(message, current.playerId);
|
||||||
|
keys.push(team === "red" ? "请红方射箭" : "请蓝方射箭");
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShootResultAudioKeys(shootData) {
|
||||||
|
if (!shootData) return [];
|
||||||
|
const keys = [
|
||||||
|
shootData.ring ? `${shootData.ringX ? "X" : shootData.ring}环` : "未上靶",
|
||||||
|
];
|
||||||
|
if (shootData.angle !== null && shootData.angle !== undefined) {
|
||||||
|
keys.push(`向${getDirectionText(shootData.angle)}调整`);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestDistanceAudioKeys(shootData) {
|
||||||
|
const distance = Number(shootData?.distance ?? shootData?.dst);
|
||||||
|
if (Number.isNaN(distance)) return [];
|
||||||
|
return [distance / 100 >= 5 ? "\u8ddd\u79bb\u5408\u683c" : "\u8ddd\u79bb\u4e0d\u8db3"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAckAudioKeys(message, businessMessage) {
|
||||||
|
switch (message.type) {
|
||||||
|
case ServerMessageType.SERVER_MSG_MATCH_START:
|
||||||
|
return ["比赛开始"];
|
||||||
|
case ServerMessageType.SERVER_MSG_NOW_YOU:
|
||||||
|
return getNowYouAudioKeys(businessMessage);
|
||||||
|
case ServerMessageType.SERVER_MSG_SHOT:
|
||||||
|
return getShootResultAudioKeys(businessMessage?.shootData);
|
||||||
|
case ServerMessageType.SERVER_MSG_MATCH_END:
|
||||||
|
case ServerMessageType.SERVER_MSG_TIMEOUT:
|
||||||
|
return ["比赛结束"];
|
||||||
|
case ServerMessageType.SERVER_MSG_CHECK:
|
||||||
|
return getTestDistanceAudioKeys(businessMessage?.shootData);
|
||||||
|
case ServerMessageType.SERVER_MSG_NOT_ENOUGH_DISTANCE:
|
||||||
|
return ["射击无效"];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCloseAfterAck(message) {
|
||||||
|
return (
|
||||||
|
message.type === ServerMessageType.SERVER_MSG_MATCH_END ||
|
||||||
|
message.type === ServerMessageType.SERVER_MSG_TIMEOUT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加 token 查询参数;如果后端地址已经带 token,则保持原样。
|
||||||
|
function appendQuery(url, key, value) {
|
||||||
|
if (!value || new RegExp(`[?&]${key}=`).test(url)) return url;
|
||||||
|
return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(
|
||||||
|
value
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端可能下发完整 ws 地址,也可能只给 host:port,这里统一转成可连接地址。
|
||||||
|
function normalizeServerUrl(serverAddr, token) {
|
||||||
|
if (!serverAddr || typeof serverAddr !== "string") return "";
|
||||||
|
|
||||||
|
let url = serverAddr.trim();
|
||||||
|
if (!url) return "";
|
||||||
|
|
||||||
|
if (!/^wss?:\/\//i.test(url)) {
|
||||||
|
const [address, query = ""] = url.split("?");
|
||||||
|
const hasPath = address.includes("/");
|
||||||
|
const host = hasPath
|
||||||
|
? address
|
||||||
|
: `${address}${address.includes(":") ? "" : ":8011"}/ws`;
|
||||||
|
url = `ws://${host}${query ? `?${query}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appendQuery(url, "token", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一发送二进制 protobuf 消息,并在日志里保留发送类型。
|
||||||
|
function sendBuffer(buffer, label, clientMessage) {
|
||||||
|
if (!socket || !buffer) return;
|
||||||
|
|
||||||
|
const currentSocket = socket;
|
||||||
|
currentSocket.send({
|
||||||
|
data: buffer,
|
||||||
|
success: () => {
|
||||||
|
console.log("发送比赛服 WebSocket 消息", label, clientMessage);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (socket !== currentSocket) return;
|
||||||
|
console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendHeartbeatAck() {
|
||||||
|
// 心跳 ACK 不进入 pendingAcks,收到后立即回复。
|
||||||
|
const clientMessage = {
|
||||||
|
type: ClientMessageType.CLIENT_MSG_HEARTBEAT_ACK,
|
||||||
|
};
|
||||||
|
sendBuffer(
|
||||||
|
createHeartbeatAckMessage(),
|
||||||
|
"CLIENT_MSG_HEARTBEAT_ACK",
|
||||||
|
clientMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAck({ matchId, sequence }) {
|
||||||
|
// sequence 由后端处理,前端只原样带回,不做重排和断线补发。
|
||||||
|
if (sequence === undefined || sequence === null || sequence === "") return;
|
||||||
|
const clientMessage = {
|
||||||
|
type: ClientMessageType.CLIENT_MSG_ACK,
|
||||||
|
match_id: matchId,
|
||||||
|
sequence,
|
||||||
|
};
|
||||||
|
sendBuffer(
|
||||||
|
createAckMessage({ matchId, sequence }),
|
||||||
|
"CLIENT_MSG_ACK",
|
||||||
|
clientMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeAckTask(task) {
|
||||||
|
if (task?.timer) clearTimeout(task.timer);
|
||||||
|
sendAck(task);
|
||||||
|
if (task.leaveAfterAck) {
|
||||||
|
setTimeout(() => {
|
||||||
|
closeMatchWebSocket({ reason: "match-end" });
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameAckTask(task, payload = {}) {
|
||||||
|
if (String(task?.sequence) !== String(payload.sequence)) return false;
|
||||||
|
if (!payload.matchId) return true;
|
||||||
|
return String(task?.matchId) === String(payload.matchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function completePendingAckByMessage(payload = {}) {
|
||||||
|
if (payload.sequence === undefined || payload.sequence === null || payload.sequence === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = pendingAcks.findIndex((task) => isSameAckTask(task, payload));
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const [task] = pendingAcks.splice(index, 1);
|
||||||
|
console.log("[match-ws] ack after page audio", task.sequence, payload);
|
||||||
|
completeAckTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushPendingAcks() {
|
||||||
|
// 每次语音播报完成,只确认队首那条等待相同语音 key 的服务端消息。
|
||||||
|
if (!pendingAcks.length) return;
|
||||||
|
const endedKey = arguments[0];
|
||||||
|
const task = pendingAcks[0];
|
||||||
|
if (task.expectedAudioKey && endedKey && task.expectedAudioKey !== endedKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completeAckTask(pendingAcks.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAudioAckListener() {
|
||||||
|
// 复用现有全局 audioEnded 事件,确保 ACK 时机落在播报结束之后。
|
||||||
|
if (audioAckListenerReady) return;
|
||||||
|
uni.$on("audioEnded", flushPendingAcks);
|
||||||
|
uni.$on(MATCH_WS_AUDIO_ACK_EVENT, completePendingAckByMessage);
|
||||||
|
audioAckListenerReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAudioAckListener() {
|
||||||
|
if (!audioAckListenerReady) return;
|
||||||
|
uni.$off("audioEnded", flushPendingAcks);
|
||||||
|
uni.$off(MATCH_WS_AUDIO_ACK_EVENT, completePendingAckByMessage);
|
||||||
|
audioAckListenerReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueAckAfterAudio(message, businessMessage) {
|
||||||
|
// 除心跳外,只要服务端带了 sequence,都需要走 ACK;没有语音的消息立即 ACK。
|
||||||
|
if (
|
||||||
|
message.sequence === undefined ||
|
||||||
|
message.sequence === null ||
|
||||||
|
message.sequence === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = {
|
||||||
|
matchId: normalizeId(
|
||||||
|
pickField(message, "matchId", "match_id") || currentContext?.matchId
|
||||||
|
),
|
||||||
|
sequence: message.sequence,
|
||||||
|
leaveAfterAck: shouldCloseAfterAck(message),
|
||||||
|
};
|
||||||
|
|
||||||
|
const audioKeys = getAckAudioKeys(message, businessMessage).filter(Boolean);
|
||||||
|
if (!audioKeys.length) {
|
||||||
|
console.log(
|
||||||
|
"[match-ws] ack immediately without audio",
|
||||||
|
getServerMessageTypeName(message.type),
|
||||||
|
message.sequence
|
||||||
|
);
|
||||||
|
completeAckTask(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.expectedAudioKey = audioKeys[audioKeys.length - 1];
|
||||||
|
task.timer = setTimeout(() => {
|
||||||
|
const index = pendingAcks.indexOf(task);
|
||||||
|
if (index === -1) return;
|
||||||
|
pendingAcks.splice(index, 1);
|
||||||
|
console.log(
|
||||||
|
"[match-ws] ack audio wait timeout",
|
||||||
|
getServerMessageTypeName(message.type),
|
||||||
|
message.sequence,
|
||||||
|
task.expectedAudioKey
|
||||||
|
);
|
||||||
|
completeAckTask(task);
|
||||||
|
}, ACK_AUDIO_TIMEOUT_MS);
|
||||||
|
pendingAcks.push(task);
|
||||||
|
console.log(
|
||||||
|
"[match-ws] ack queued until audioEnded",
|
||||||
|
getServerMessageTypeName(message.type),
|
||||||
|
message.sequence,
|
||||||
|
task.expectedAudioKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(data) {
|
||||||
|
// 比赛服下发 raw protobuf frame,先解码,再根据消息类型决定是否 ACK。
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = decodeServerMessage(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[match-ws] decode failed", err, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id"));
|
||||||
|
if (decodedMatchId && currentContext) {
|
||||||
|
currentContext.matchId = decodedMatchId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === ServerMessageType.SERVER_MSG_HEARTBEAT) {
|
||||||
|
sendHeartbeatAck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeName = getServerMessageTypeName(message.type);
|
||||||
|
console.log("收到比赛服 WebSocket 消息", typeName, message);
|
||||||
|
|
||||||
|
const businessMessage = buildBusinessMessage(message);
|
||||||
|
if (businessMessage?.matchId && currentContext) {
|
||||||
|
currentContext.matchId = businessMessage.matchId;
|
||||||
|
}
|
||||||
|
queueAckAfterAudio(message, businessMessage);
|
||||||
|
emitBusinessMessage(businessMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLeave() {
|
||||||
|
// 主动关闭、比赛结束、页面离开时发 CLIENT_MSG_LEAVE。
|
||||||
|
if (!socket || !currentContext?.matchId) return;
|
||||||
|
const clientMessage = {
|
||||||
|
type: ClientMessageType.CLIENT_MSG_LEAVE,
|
||||||
|
match_id: currentContext.matchId,
|
||||||
|
user_id: currentContext.userId,
|
||||||
|
};
|
||||||
|
sendBuffer(
|
||||||
|
createLeaveMessage({
|
||||||
|
matchId: clientMessage.match_id,
|
||||||
|
userId: clientMessage.user_id,
|
||||||
|
}),
|
||||||
|
"CLIENT_MSG_LEAVE",
|
||||||
|
clientMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectMatchWebSocket(options = {}) {
|
||||||
|
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
|
||||||
|
const { serverAddr, matchId, userId, token } = options;
|
||||||
|
const url = normalizeServerUrl(serverAddr, token);
|
||||||
|
const normalizedMatchId = normalizeId(matchId);
|
||||||
|
const normalizedUserId = normalizeId(userId);
|
||||||
|
|
||||||
|
if (!url || !normalizedMatchId) {
|
||||||
|
console.log("[match-ws] missing serverAddr or matchId", options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
socket &&
|
||||||
|
currentContext?.url === url &&
|
||||||
|
currentContext?.matchId === normalizedMatchId
|
||||||
|
) {
|
||||||
|
// 同一场比赛同一地址重复通知时不重复建连。
|
||||||
|
console.log("[match-ws] already connected or connecting", {
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换比赛服地址时先关闭旧连接,不发送 LEAVE,避免误通知旧比赛离场。
|
||||||
|
closeMatchWebSocket({ sendLeave: false, reason: "switch" });
|
||||||
|
|
||||||
|
manualClose = false;
|
||||||
|
isConnecting = true;
|
||||||
|
currentContext = {
|
||||||
|
serverAddr,
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
userId: normalizedUserId,
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
ensureAudioAckListener();
|
||||||
|
|
||||||
|
const socketTask = uni.connectSocket({
|
||||||
|
url,
|
||||||
|
success: () => {
|
||||||
|
console.log("[match-ws] connect requested", {
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
socket = null;
|
||||||
|
isConnecting = false;
|
||||||
|
console.log("[match-ws] connect failed", err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
socket = socketTask;
|
||||||
|
|
||||||
|
socketTask.onOpen(() => {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
isConnecting = false;
|
||||||
|
console.log("[match-ws] connected", {
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socketTask.onMessage((res) => {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
handleMessage(res.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketTask.onError((err) => {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
console.log("[match-ws] socket error", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketTask.onClose((result) => {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
socket = null;
|
||||||
|
isConnecting = false;
|
||||||
|
console.log("[match-ws] closed", {
|
||||||
|
manualClose,
|
||||||
|
result,
|
||||||
|
context: currentContext,
|
||||||
|
});
|
||||||
|
if (!manualClose) removeAudioAckListener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectMatchWebSocketFromNotice(notice, fallbackUserId) {
|
||||||
|
// 原 websocket 下发的比赛服通知统一从这里转成连接参数。
|
||||||
|
if (!notice) return;
|
||||||
|
connectMatchWebSocket({
|
||||||
|
serverAddr: pickField(notice, "serverAddr", "server_addr"),
|
||||||
|
matchId: pickField(notice, "matchId", "match_id"),
|
||||||
|
userId: pickField(notice, "userId", "user_id") || fallbackUserId,
|
||||||
|
token: notice.token || notice.wsToken || notice.ws_token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeMatchWebSocket(options = {}) {
|
||||||
|
// 默认关闭时会发送 LEAVE;内部切换连接可通过 sendLeave:false 跳过。
|
||||||
|
const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
|
||||||
|
|
||||||
|
manualClose = true;
|
||||||
|
pendingAcks.forEach((task) => {
|
||||||
|
if (task?.timer) clearTimeout(task.timer);
|
||||||
|
});
|
||||||
|
pendingAcks.length = 0;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
const currentSocket = socket;
|
||||||
|
if (shouldSendLeave) sendLeave();
|
||||||
|
socket = null;
|
||||||
|
try {
|
||||||
|
currentSocket.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[match-ws] close failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnecting = false;
|
||||||
|
removeAudioAckListener();
|
||||||
|
console.log("[match-ws] close requested", { reason, currentContext });
|
||||||
|
currentContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
ClientMessageType,
|
||||||
|
ServerMessageType,
|
||||||
|
connectMatchWebSocket,
|
||||||
|
connectMatchWebSocketFromNotice,
|
||||||
|
closeMatchWebSocket,
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
getReadyAPI,
|
getReadyAPI,
|
||||||
kickPlayerAPI,
|
kickPlayerAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import { isLimitError } from "@/util";
|
import { debounce, isLimitError } from "@/util";
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -59,6 +59,7 @@ const allReady = ref(false);
|
|||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
const goBattle = ref(false);
|
const goBattle = ref(false);
|
||||||
const showLimitModal = ref(false);
|
const showLimitModal = ref(false);
|
||||||
|
const readySubmitting = ref(false);
|
||||||
/** 从结算页返回时为 true,跳过进场靶纸语音 */
|
/** 从结算页返回时为 true,跳过进场靶纸语音 */
|
||||||
const skipTargetAudio = ref(false);
|
const skipTargetAudio = ref(false);
|
||||||
|
|
||||||
@@ -139,17 +140,22 @@ async function refreshRoomData() {
|
|||||||
// timer.value = setTimeout(refreshRoomData, 2000);
|
// timer.value = setTimeout(refreshRoomData, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getReady = async () => {
|
const getReady = debounce(async () => {
|
||||||
|
if (readySubmitting.value) return;
|
||||||
|
readySubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
await getReadyAPI(roomNumber.value);
|
await getReadyAPI(roomNumber.value);
|
||||||
|
ready.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isLimitError(error)) {
|
if (isLimitError(error)) {
|
||||||
showLimitModal.value = true;
|
showLimitModal.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("room ready error", error);
|
console.log("room ready error", error);
|
||||||
|
} finally {
|
||||||
|
readySubmitting.value = false;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const closeLimitModal = () => {
|
const closeLimitModal = () => {
|
||||||
showLimitModal.value = false;
|
showLimitModal.value = false;
|
||||||
@@ -465,7 +471,10 @@ onBeforeUnmount(() => {
|
|||||||
<PlayerSeats v-if="room.battleType === 2" :total="room.count || 10" :players="players"
|
<PlayerSeats v-if="room.battleType === 2" :total="room.count || 10" :players="players"
|
||||||
:removePlayer="removePlayer" :isOwner="owner.id === user.id" />
|
:removePlayer="removePlayer" :isOwner="owner.id === user.id" />
|
||||||
<view>
|
<view>
|
||||||
<SButton :disabled="!canClick" :onClick="() => $clickSound(getReady)">
|
<SButton
|
||||||
|
:disabled="!canClick || readySubmitting"
|
||||||
|
:onClick="() => { $clickSound(); return getReady(); }"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
allReady.value
|
allReady.value
|
||||||
? "即将进入对局..."
|
? "即将进入对局..."
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const data = ref({});
|
|||||||
const roomID = ref("");
|
const roomID = ref("");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showLimitModal = ref(false);
|
const showLimitModal = ref(false);
|
||||||
|
const createRoomLoading = ref(false);
|
||||||
const isSVip = computed(() => user.value.sVip === true);
|
const isSVip = computed(() => user.value.sVip === true);
|
||||||
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
||||||
const challengeLimitText = computed(() =>
|
const challengeLimitText = computed(() =>
|
||||||
@@ -45,6 +46,8 @@ const enterRoom = debounce(async (number) => {
|
|||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loading.value = true;
|
||||||
|
let keepLoading = false;
|
||||||
try {
|
try {
|
||||||
const room = await getRoomAPI(number);
|
const room = await getRoomAPI(number);
|
||||||
if (!room.number) {
|
if (!room.number) {
|
||||||
@@ -63,16 +66,22 @@ const enterRoom = debounce(async (number) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loading.value = true;
|
keepLoading = true;
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/battle-room?roomNumber=" + number,
|
url: "/pages/battle-room?roomNumber=" + number,
|
||||||
|
fail: () => {
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!keepLoading) loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const onCreateRoom = async () => {
|
const onCreateRoom = debounce(async () => {
|
||||||
|
if (createRoomLoading.value) return;
|
||||||
if (!canEenter(user.value, device.value, online.value)) return;
|
if (!canEenter(user.value, device.value, online.value)) return;
|
||||||
|
createRoomLoading.value = true;
|
||||||
|
try {
|
||||||
const countData = await loadDailyCount();
|
const countData = await loadDailyCount();
|
||||||
if (isLimitReached(countData.challenge)) {
|
if (isLimitReached(countData.challenge)) {
|
||||||
showLimitModal.value = true;
|
showLimitModal.value = true;
|
||||||
@@ -80,7 +89,10 @@ const onCreateRoom = async () => {
|
|||||||
}
|
}
|
||||||
warnning.value = "";
|
warnning.value = "";
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
} finally {
|
||||||
|
createRoomLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const closeLimitModal = () => {
|
const closeLimitModal = () => {
|
||||||
showLimitModal.value = false;
|
showLimitModal.value = false;
|
||||||
@@ -116,6 +128,7 @@ const goMyRecord = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
|
loading.value = false;
|
||||||
if (user.value.id) {
|
if (user.value.id) {
|
||||||
const [result] = await Promise.all([
|
const [result] = await Promise.all([
|
||||||
getBattleDataAPI(),
|
getBattleDataAPI(),
|
||||||
@@ -208,7 +221,12 @@ onLoad(async (options) => {
|
|||||||
<view v-if="challengeLimitText" class="pp-text">
|
<view v-if="challengeLimitText" class="pp-text">
|
||||||
{{ challengeLimitText }}
|
{{ challengeLimitText }}
|
||||||
</view>
|
</view>
|
||||||
<SButton width="80%" :rounded="30" :onClick="() => $clickSound(onCreateRoom)">
|
<SButton
|
||||||
|
width="80%"
|
||||||
|
:rounded="30"
|
||||||
|
:disabled="createRoomLoading"
|
||||||
|
:onClick="() => { $clickSound(); return onCreateRoom(); }"
|
||||||
|
>
|
||||||
创建约战房
|
创建约战房
|
||||||
</SButton>
|
</SButton>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+144
-6
@@ -13,7 +13,13 @@ import TestDistance from "@/components/TestDistance.vue";
|
|||||||
import SModal from "@/components/SModal.vue";
|
import SModal from "@/components/SModal.vue";
|
||||||
import audioManager from "@/audioManager";
|
import audioManager from "@/audioManager";
|
||||||
import { getBattleAPI, laserCloseAPI } from "@/apis";
|
import { getBattleAPI, laserCloseAPI } from "@/apis";
|
||||||
|
import {
|
||||||
|
closeMatchWebSocket,
|
||||||
|
connectMatchWebSocket,
|
||||||
|
MATCH_WS_AUDIO_ACK_EVENT,
|
||||||
|
} from "@/matchWebsocket";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
|
import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -31,12 +37,22 @@ const playersScores = ref([]);
|
|||||||
const halfTimeTip = ref(false);
|
const halfTimeTip = ref(false);
|
||||||
const halfRest = ref(false);
|
const halfRest = ref(false);
|
||||||
const HALF_REST_SECONDS = 20;
|
const HALF_REST_SECONDS = 20;
|
||||||
|
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||||
|
const MATCH_STATUS_BY_TEXT = {
|
||||||
|
MATCH_STATUS_READY: 0,
|
||||||
|
MATCH_STATUS_STARTED: 1,
|
||||||
|
MATCH_STATUS_END: 2,
|
||||||
|
MATCH_STATUS_TIMEOUT: 3,
|
||||||
|
MATCH_STATUS_UNEXPECTEDLY: 4,
|
||||||
|
};
|
||||||
const halfRestRemain = ref(HALF_REST_SECONDS);
|
const halfRestRemain = ref(HALF_REST_SECONDS);
|
||||||
let halfRestTimer = null;
|
let halfRestTimer = null;
|
||||||
/** 控制设备离线提示弹窗的显示状态 */
|
/** 控制设备离线提示弹窗的显示状态 */
|
||||||
const showOfflineModal = ref(false);
|
const showOfflineModal = ref(false);
|
||||||
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
|
let battleEnded = false;
|
||||||
|
let skipNextRestoreOnShow = false;
|
||||||
|
|
||||||
function clearHalfRestCountdown() {
|
function clearHalfRestCountdown() {
|
||||||
if (halfRestTimer) {
|
if (halfRestTimer) {
|
||||||
@@ -93,6 +109,90 @@ const currentPlayer = computed(() =>
|
|||||||
);
|
);
|
||||||
const isCurrentUserSvip = computed(() => currentPlayer.value?.sVip === true);
|
const isCurrentUserSvip = computed(() => currentPlayer.value?.sVip === true);
|
||||||
|
|
||||||
|
function getReadySnapshotKey(matchId) {
|
||||||
|
return `${MATCH_READY_SNAPSHOT_PREFIX}${String(matchId || "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBattleInfo(battleInfo) {
|
||||||
|
if (!battleInfo || typeof battleInfo !== "object") return battleInfo;
|
||||||
|
|
||||||
|
const statusText = battleInfo.statusText || battleInfo.status_text;
|
||||||
|
if (
|
||||||
|
(battleInfo.status === undefined ||
|
||||||
|
battleInfo.status === null ||
|
||||||
|
battleInfo.status === "") &&
|
||||||
|
MATCH_STATUS_BY_TEXT[statusText] !== undefined
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...battleInfo,
|
||||||
|
status: MATCH_STATUS_BY_TEXT[statusText],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return battleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasKnownStatus(battleInfo) {
|
||||||
|
return !(
|
||||||
|
battleInfo?.status === undefined ||
|
||||||
|
battleInfo?.status === null ||
|
||||||
|
battleInfo?.status === ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStartedSnapshot(battleInfo) {
|
||||||
|
if (hasKnownStatus(battleInfo)) return Number(battleInfo.status) !== 0;
|
||||||
|
|
||||||
|
const current = battleInfo?.current || {};
|
||||||
|
return !!(
|
||||||
|
current.playerId ||
|
||||||
|
current.startTime ||
|
||||||
|
current.round ||
|
||||||
|
(Array.isArray(battleInfo?.rounds) && battleInfo.rounds.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeReadySnapshot(matchId) {
|
||||||
|
const key = getReadySnapshotKey(matchId);
|
||||||
|
const snapshot = uni.getStorageSync(key);
|
||||||
|
if (snapshot) uni.removeStorageSync(key);
|
||||||
|
return normalizeBattleInfo(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnectMatchServer(battleInfo) {
|
||||||
|
const status = Number(battleInfo?.status);
|
||||||
|
if ([2, 3, 4].includes(status)) return;
|
||||||
|
if (!battleInfo?.serverAddr) return;
|
||||||
|
|
||||||
|
connectMatchWebSocket({
|
||||||
|
serverAddr: battleInfo.serverAddr,
|
||||||
|
matchId: battleInfo.matchId || battleId.value,
|
||||||
|
userId: user.value.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBattleServer(reason) {
|
||||||
|
if (battleEnded) return;
|
||||||
|
closeMatchWebSocket({ reason });
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyMatchAudioAck(message) {
|
||||||
|
if (
|
||||||
|
message?.sequence === undefined ||
|
||||||
|
message?.sequence === null ||
|
||||||
|
message?.sequence === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.$emit(MATCH_WS_AUDIO_ACK_EVENT, {
|
||||||
|
matchId: message.matchId || battleId.value,
|
||||||
|
sequence: message.sequence,
|
||||||
|
matchWsType: message.matchWsType,
|
||||||
|
matchWsTypeName: message.matchWsTypeName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
|
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
|
||||||
*/
|
*/
|
||||||
@@ -103,7 +203,11 @@ watch(online, (newVal, oldVal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function recoverData(battleInfo, { force = false } = {}) {
|
function recoverData(battleInfo, { force = false } = {}) {
|
||||||
|
battleInfo = normalizeBattleInfo(battleInfo);
|
||||||
if (!battleInfo) return;
|
if (!battleInfo) return;
|
||||||
|
if (battleInfo.status !== undefined) {
|
||||||
|
battleEnded = [2, 4].includes(Number(battleInfo.status));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗";
|
if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗";
|
||||||
if (battleInfo.way === 2) title.value = "排位赛 - 大乱斗";
|
if (battleInfo.way === 2) title.value = "排位赛 - 大乱斗";
|
||||||
@@ -121,19 +225,27 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
players.value = [];
|
players.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasKnownStatus(battleInfo)) {
|
||||||
start.value = battleInfo.status !== 0;
|
start.value = battleInfo.status !== 0;
|
||||||
|
} else if (!hasStartedSnapshot(battleInfo)) {
|
||||||
|
if (start.value !== true) start.value = false;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
start.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (battleInfo.status === 0) {
|
if (battleInfo.status === 0) {
|
||||||
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
|
const readyRemain = (Date.now() - (battleInfo.serverTime || Date.now())) / 1000;
|
||||||
if (readyRemain > 0 && readyRemain < 15) {
|
if (readyRemain > 0 && readyRemain < 15) {
|
||||||
setTimeout(() => uni.$emit("update-timer", 15 - readyRemain - 0.2), 200);
|
setTimeout(() => uni.$emit("update-timer", 15 - readyRemain - 0.2), 200);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
||||||
tips.value =
|
tips.value =
|
||||||
(battleInfo.rounds.length !== 2 ? "上" : "下") + "半场:请先射6箭";
|
(rounds.length !== 2 ? "上" : "下") + "半场:请先射6箭";
|
||||||
playersScores.value = battleInfo.rounds.map((r) => ({ ...r.shoots }));
|
playersScores.value = rounds.map((r) => ({ ...r.shoots }));
|
||||||
const totals = {};
|
const totals = {};
|
||||||
players.value.forEach((p) => {
|
players.value.forEach((p) => {
|
||||||
const total = playersScores.value.reduce((acc, round) => {
|
const total = playersScores.value.reduce((acc, round) => {
|
||||||
@@ -170,7 +282,18 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
if (options.battleId) battleId.value = options.battleId;
|
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
||||||
|
skipNextRestoreOnShow = false;
|
||||||
|
if (returnSnapshot?.matchId) battleId.value = returnSnapshot.matchId;
|
||||||
|
else if (options.battleId) battleId.value = options.battleId;
|
||||||
|
if (returnSnapshot) {
|
||||||
|
skipNextRestoreOnShow = true;
|
||||||
|
reconnectMatchServer(returnSnapshot);
|
||||||
|
recoverData(returnSnapshot, { force: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const readySnapshot = takeReadySnapshot(battleId.value);
|
||||||
|
if (readySnapshot?.status === 0) recoverData(readySnapshot);
|
||||||
// uni.enableAlertBeforeUnload({
|
// uni.enableAlertBeforeUnload({
|
||||||
// message: "离开比赛可能导致比赛失败,是否继续?",
|
// message: "离开比赛可能导致比赛失败,是否继续?",
|
||||||
// success: (res) => {
|
// success: (res) => {
|
||||||
@@ -207,7 +330,9 @@ function checkAndPlayTententen(playerId, isTenPlusRingShot) {
|
|||||||
|
|
||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
if (Array.isArray(msg)) return;
|
if (Array.isArray(msg)) return;
|
||||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
if (msg.type === MESSAGETYPESV2.AboutToStart) {
|
||||||
|
recoverData(msg);
|
||||||
|
} else if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
clearHalfRestCountdown();
|
clearHalfRestCountdown();
|
||||||
halfTimeTip.value = false;
|
halfTimeTip.value = false;
|
||||||
halfRest.value = false;
|
halfRest.value = false;
|
||||||
@@ -241,6 +366,8 @@ async function onReceiveMessage(msg) {
|
|||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
startHalfRestCountdown();
|
startHalfRestCountdown();
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
|
battleEnded = true;
|
||||||
|
notifyMatchAudioAck(msg);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 全部跳转到新结算页
|
// 全部跳转到新结算页
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
@@ -262,14 +389,24 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
clearHalfRestCountdown();
|
clearHalfRestCountdown();
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
|
closeBattleServer("melee-battle-unmount");
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
closeBattleServer("melee-battle-hide");
|
||||||
|
});
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
|
if (skipNextRestoreOnShow) {
|
||||||
|
skipNextRestoreOnShow = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (battleId.value) {
|
if (battleId.value) {
|
||||||
const result = await getBattleAPI(battleId.value);
|
const result = normalizeBattleInfo(await getBattleAPI(battleId.value));
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
if (result.status === 2) {
|
if (result.status === 2) {
|
||||||
|
battleEnded = true;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "比赛已结束",
|
title: "比赛已结束",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -278,6 +415,7 @@ onShow(async () => {
|
|||||||
delta: 2,
|
delta: 2,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
reconnectMatchServer(result);
|
||||||
recoverData(result, { force: true });
|
recoverData(result, { force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const recoverData = (battleInfo, {force = false, arrowOnly = false} = {}) => {
|
|||||||
start.value = battleInfo.status !== 0;
|
start.value = battleInfo.status !== 0;
|
||||||
|
|
||||||
if (battleInfo.status === 0) {
|
if (battleInfo.status === 0) {
|
||||||
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
|
const readyRemain = (Date.now() - (battleInfo.serverTime || Date.now())) / 1000;
|
||||||
if (readyRemain > 0 && readyRemain < 15) {
|
if (readyRemain > 0 && readyRemain < 15) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.$emit("update-timer", 15 - readyRemain - 0.2);
|
uni.$emit("update-timer", 15 - readyRemain - 0.2);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app";
|
|||||||
|
|
||||||
import { getBattleAPI, getUserGameState } from "@/apis";
|
import { getBattleAPI, getUserGameState } from "@/apis";
|
||||||
import { debounce } from "@/util";
|
import { debounce } from "@/util";
|
||||||
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -67,11 +68,7 @@ const onClick = debounce(async () => {
|
|||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await uni.$checkAudio();
|
await uni.$checkAudio();
|
||||||
if (result.mode <= 3) {
|
await returnToBattle(result, navigateOnce);
|
||||||
await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
|
|
||||||
} else {
|
|
||||||
await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (game.value.roomID) {
|
if (game.value.roomID) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ScreenHint from "./ScreenHint.vue";
|
|||||||
import BackToGame from "./BackToGame.vue";
|
import BackToGame from "./BackToGame.vue";
|
||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
import AudioManager from "@/audioManager";
|
import AudioManager from "@/audioManager";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -120,6 +121,15 @@ onShow(() => {
|
|||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigateTo = (url) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const backToGame = debounce(async () => {
|
const backToGame = debounce(async () => {
|
||||||
if (isLoading.value) return; // 防止重复点击
|
if (isLoading.value) return; // 防止重复点击
|
||||||
|
|
||||||
@@ -128,15 +138,7 @@ const backToGame = debounce(async () => {
|
|||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await checkAudioProgress();
|
await checkAudioProgress();
|
||||||
if (result.mode <= 3) {
|
await returnToBattle(result, navigateTo);
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/team-battle/index?battleId=${result.matchId}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/melee-battle?battleId=${result.matchId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取当前游戏失败:", error);
|
console.error("获取当前游戏失败:", error);
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ import TeamAvatars from "./components/TeamAvatars.vue";
|
|||||||
import ShootProgress2 from "./components/ShootProgress2.vue";
|
import ShootProgress2 from "./components/ShootProgress2.vue";
|
||||||
import SModal from "./components/SModal.vue";
|
import SModal from "./components/SModal.vue";
|
||||||
import { laserCloseAPI, getBattleAPI } from "@/apis";
|
import { laserCloseAPI, getBattleAPI } from "@/apis";
|
||||||
|
import {
|
||||||
|
closeMatchWebSocket,
|
||||||
|
connectMatchWebSocket,
|
||||||
|
MATCH_WS_AUDIO_ACK_EVENT,
|
||||||
|
} from "@/matchWebsocket";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
import { getDirectionText } from "@/util";
|
import { getDirectionText } from "@/util";
|
||||||
|
import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
|
||||||
import audioManager, {
|
import audioManager, {
|
||||||
AUDIO_INTERRUPTION_BEGIN_EVENT,
|
AUDIO_INTERRUPTION_BEGIN_EVENT,
|
||||||
AUDIO_INTERRUPTION_END_EVENT,
|
AUDIO_INTERRUPTION_END_EVENT,
|
||||||
@@ -42,6 +48,14 @@ const AUDIO_TIMEOUT_MAX = 12000;
|
|||||||
const BATTLE_CANCEL_RETURN_DELAY = 2000;
|
const BATTLE_CANCEL_RETURN_DELAY = 2000;
|
||||||
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
||||||
const X_RING_STREAKS_KEY = "team-battle-x-ring-streaks";
|
const X_RING_STREAKS_KEY = "team-battle-x-ring-streaks";
|
||||||
|
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||||
|
const MATCH_STATUS_BY_TEXT = {
|
||||||
|
MATCH_STATUS_READY: 0,
|
||||||
|
MATCH_STATUS_STARTED: 1,
|
||||||
|
MATCH_STATUS_END: 2,
|
||||||
|
MATCH_STATUS_TIMEOUT: 3,
|
||||||
|
MATCH_STATUS_UNEXPECTEDLY: 4,
|
||||||
|
};
|
||||||
const PROGRESS_ZERO_EVENT = "team-battle-progress-zero";
|
const PROGRESS_ZERO_EVENT = "team-battle-progress-zero";
|
||||||
const COUNTDOWN_READY_EVENT = "team-battle-countdown-ready";
|
const COUNTDOWN_READY_EVENT = "team-battle-countdown-ready";
|
||||||
|
|
||||||
@@ -89,6 +103,7 @@ let restoreLoadingTimer = null;
|
|||||||
let pendingRoundAudio = false;
|
let pendingRoundAudio = false;
|
||||||
// 一旦收到 BattleEnd,后续普通消息就不再进入队列。
|
// 一旦收到 BattleEnd,后续普通消息就不再进入队列。
|
||||||
let battleEnded = false;
|
let battleEnded = false;
|
||||||
|
let skipNextRestoreOnShow = false;
|
||||||
const handledMessageKeys = new Set();
|
const handledMessageKeys = new Set();
|
||||||
const handledMessageKeyOrder = [];
|
const handledMessageKeyOrder = [];
|
||||||
const queuedMessageKeys = new Set();
|
const queuedMessageKeys = new Set();
|
||||||
@@ -129,6 +144,68 @@ function normalizeTimestamp(value) {
|
|||||||
return numberValue < 1000000000000 ? numberValue * 1000 : numberValue;
|
return numberValue < 1000000000000 ? numberValue * 1000 : numberValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReadySnapshotKey(matchId) {
|
||||||
|
return `${MATCH_READY_SNAPSHOT_PREFIX}${String(matchId || "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBattleInfo(battleInfo) {
|
||||||
|
if (!battleInfo || typeof battleInfo !== "object") return battleInfo;
|
||||||
|
|
||||||
|
const statusText = battleInfo.statusText || battleInfo.status_text;
|
||||||
|
if (
|
||||||
|
(battleInfo.status === undefined ||
|
||||||
|
battleInfo.status === null ||
|
||||||
|
battleInfo.status === "") &&
|
||||||
|
MATCH_STATUS_BY_TEXT[statusText] !== undefined
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...battleInfo,
|
||||||
|
status: MATCH_STATUS_BY_TEXT[statusText],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return battleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasKnownStatus(battleInfo) {
|
||||||
|
return !(
|
||||||
|
battleInfo?.status === undefined ||
|
||||||
|
battleInfo?.status === null ||
|
||||||
|
battleInfo?.status === ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStartedSnapshot(battleInfo) {
|
||||||
|
if (hasKnownStatus(battleInfo)) return Number(battleInfo.status) !== 0;
|
||||||
|
|
||||||
|
const current = battleInfo?.current || {};
|
||||||
|
return !!(
|
||||||
|
current.playerId ||
|
||||||
|
current.startTime ||
|
||||||
|
current.round ||
|
||||||
|
(Array.isArray(battleInfo?.rounds) && battleInfo.rounds.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeReadySnapshot(matchId) {
|
||||||
|
const key = getReadySnapshotKey(matchId);
|
||||||
|
const snapshot = uni.getStorageSync(key);
|
||||||
|
if (snapshot) uni.removeStorageSync(key);
|
||||||
|
return normalizeBattleInfo(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnectMatchServer(battleInfo) {
|
||||||
|
const status = Number(battleInfo?.status);
|
||||||
|
if ([2, 3, 4].includes(status)) return;
|
||||||
|
if (!battleInfo?.serverAddr) return;
|
||||||
|
|
||||||
|
connectMatchWebSocket({
|
||||||
|
serverAddr: battleInfo.serverAddr,
|
||||||
|
matchId: battleInfo.matchId || battleId.value,
|
||||||
|
userId: user.value.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 从不同消息结构中提取服务端时间,作为恢复和去重的时间基准。
|
// 从不同消息结构中提取服务端时间,作为恢复和去重的时间基准。
|
||||||
function getServerTime(message) {
|
function getServerTime(message) {
|
||||||
return normalizeTimestamp(
|
return normalizeTimestamp(
|
||||||
@@ -289,6 +366,10 @@ function hideRestoreLoading() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showRestoreLoading() {
|
function showRestoreLoading() {
|
||||||
|
if (start.value === false) {
|
||||||
|
hideRestoreLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
clearRestoreLoadingTimer();
|
clearRestoreLoadingTimer();
|
||||||
restoreLoading.value = true;
|
restoreLoading.value = true;
|
||||||
restoreLoadingTimer = setTimeout(() => {
|
restoreLoadingTimer = setTimeout(() => {
|
||||||
@@ -391,7 +472,9 @@ async function executeBattleTask(task, runId) {
|
|||||||
if (!task || !isQueueAlive(runId)) return;
|
if (!task || !isQueueAlive(runId)) return;
|
||||||
const type = task.type;
|
const type = task.type;
|
||||||
|
|
||||||
if (type === MESSAGETYPESV2.BattleStart) {
|
if (type === MESSAGETYPESV2.AboutToStart) {
|
||||||
|
applyBattleSnapshot(task.message, { restore: true });
|
||||||
|
} else if (type === MESSAGETYPESV2.BattleStart) {
|
||||||
await runBattleStartTask(task, runId);
|
await runBattleStartTask(task, runId);
|
||||||
} else if (type === MESSAGETYPESV2.ToSomeoneShoot) {
|
} else if (type === MESSAGETYPESV2.ToSomeoneShoot) {
|
||||||
await runToSomeoneShootTask(task, runId);
|
await runToSomeoneShootTask(task, runId);
|
||||||
@@ -402,7 +485,7 @@ async function executeBattleTask(task, runId) {
|
|||||||
} else if (type === MESSAGETYPESV2.BattleEnd) {
|
} else if (type === MESSAGETYPESV2.BattleEnd) {
|
||||||
await runBattleEndTask(task, runId);
|
await runBattleEndTask(task, runId);
|
||||||
} else if (type === MESSAGETYPESV2.InvalidShot) {
|
} else if (type === MESSAGETYPESV2.InvalidShot) {
|
||||||
await runInvalidShotTask(runId);
|
await runInvalidShotTask(task, runId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +531,24 @@ function onAudioEnded(key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notifyMatchAudioAck(task) {
|
||||||
|
const message = task?.message;
|
||||||
|
if (
|
||||||
|
message?.sequence === undefined ||
|
||||||
|
message?.sequence === null ||
|
||||||
|
message?.sequence === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.$emit(MATCH_WS_AUDIO_ACK_EVENT, {
|
||||||
|
matchId: message.matchId || battleId.value,
|
||||||
|
sequence: message.sequence,
|
||||||
|
matchWsType: message.matchWsType,
|
||||||
|
matchWsTypeName: message.matchWsTypeName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleBattleCovered() {
|
function handleBattleCovered() {
|
||||||
if (pendingRestoreTimer) {
|
if (pendingRestoreTimer) {
|
||||||
clearTimeout(pendingRestoreTimer);
|
clearTimeout(pendingRestoreTimer);
|
||||||
@@ -462,6 +563,11 @@ function handleBattleRecovered() {
|
|||||||
scheduleRestoreLatestBattle();
|
scheduleRestoreLatestBattle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeBattleServer(reason) {
|
||||||
|
if (battleEnded) return;
|
||||||
|
closeMatchWebSocket({ reason });
|
||||||
|
}
|
||||||
|
|
||||||
// 队伍信息优先用接口返回值;接口缺失时使用本地缓存,避免重进页面时头像为空。
|
// 队伍信息优先用接口返回值;接口缺失时使用本地缓存,避免重进页面时头像为空。
|
||||||
function loadTeamPlayers(teamInfo, storageKey) {
|
function loadTeamPlayers(teamInfo, storageKey) {
|
||||||
if (Array.isArray(teamInfo?.players)) return [...teamInfo.players];
|
if (Array.isArray(teamInfo?.players)) return [...teamInfo.players];
|
||||||
@@ -526,6 +632,7 @@ function applyRestoreNewRoundSnapshot(battleInfo) {
|
|||||||
|
|
||||||
// 回填比赛基础信息:队伍、比分、轮次、金箭状态等公共字段都在这里统一处理。
|
// 回填比赛基础信息:队伍、比分、轮次、金箭状态等公共字段都在这里统一处理。
|
||||||
function applyBattleBase(battleInfo) {
|
function applyBattleBase(battleInfo) {
|
||||||
|
battleInfo = normalizeBattleInfo(battleInfo);
|
||||||
if (!battleInfo) return;
|
if (!battleInfo) return;
|
||||||
if (battleInfo.matchId) battleId.value = battleInfo.matchId;
|
if (battleInfo.matchId) battleId.value = battleInfo.matchId;
|
||||||
if (battleInfo.status !== undefined) {
|
if (battleInfo.status !== undefined) {
|
||||||
@@ -734,8 +841,8 @@ function applyReadyState(battleInfo) {
|
|||||||
clearProgressZeroWaiters();
|
clearProgressZeroWaiters();
|
||||||
cancelRoundTipDisplay();
|
cancelRoundTipDisplay();
|
||||||
|
|
||||||
const createTime = normalizeTimestamp(battleInfo?.createTime || Date.now());
|
const serverTime = normalizeTimestamp(battleInfo?.serverTime || Date.now());
|
||||||
const readyElapsed = (Date.now() - createTime) / 1000;
|
const readyElapsed = (Date.now() - serverTime) / 1000;
|
||||||
if (readyElapsed > 0 && readyElapsed < READY_SECONDS) {
|
if (readyElapsed > 0 && readyElapsed < READY_SECONDS) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.$emit("update-timer", READY_SECONDS - readyElapsed - 0.2);
|
uni.$emit("update-timer", READY_SECONDS - readyElapsed - 0.2);
|
||||||
@@ -745,6 +852,8 @@ function applyReadyState(battleInfo) {
|
|||||||
|
|
||||||
// 快照恢复入口:只把页面拉到服务端最新状态,不重放已经发生过的语音。
|
// 快照恢复入口:只把页面拉到服务端最新状态,不重放已经发生过的语音。
|
||||||
function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0 } = {}) {
|
function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0 } = {}) {
|
||||||
|
battleInfo = normalizeBattleInfo(battleInfo);
|
||||||
|
if (!battleInfo) return;
|
||||||
// 快照恢复只负责“把页面拉回最新状态”,不重放历史语音。
|
// 快照恢复只负责“把页面拉回最新状态”,不重放历史语音。
|
||||||
applyBattleBase(battleInfo);
|
applyBattleBase(battleInfo);
|
||||||
if (battleInfo.status === 0) {
|
if (battleInfo.status === 0) {
|
||||||
@@ -752,6 +861,12 @@ function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasStartedSnapshot(battleInfo)) {
|
||||||
|
hideRestoreLoading();
|
||||||
|
if (start.value === false) applyReadyState(battleInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
start.value = true;
|
start.value = true;
|
||||||
showRoundTip.value = false;
|
showRoundTip.value = false;
|
||||||
|
|
||||||
@@ -793,6 +908,7 @@ async function runBattleStartTask(task, runId) {
|
|||||||
pendingRoundAudio = true;
|
pendingRoundAudio = true;
|
||||||
updateShotInfo(task.message);
|
updateShotInfo(task.message);
|
||||||
await playAudioKeys("比赛开始", { interrupt: false });
|
await playAudioKeys("比赛开始", { interrupt: false });
|
||||||
|
notifyMatchAudioAck(task);
|
||||||
if (!isQueueAlive(runId)) return;
|
if (!isQueueAlive(runId)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,6 +962,7 @@ async function runToSomeoneShootTask(task, runId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await audioPromise;
|
await audioPromise;
|
||||||
|
notifyMatchAudioAck(task);
|
||||||
if (!isQueueAlive(runId)) return;
|
if (!isQueueAlive(runId)) return;
|
||||||
|
|
||||||
const remainingSeconds = getRemainingSeconds(battleInfo, task, {
|
const remainingSeconds = getRemainingSeconds(battleInfo, task, {
|
||||||
@@ -918,6 +1035,7 @@ async function runShootResultTask(task) {
|
|||||||
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
||||||
if (isTententen) audioKeys.push("tententen");
|
if (isTententen) audioKeys.push("tententen");
|
||||||
await playAudioKeys(audioKeys, { interrupt: false });
|
await playAudioKeys(audioKeys, { interrupt: false });
|
||||||
|
notifyMatchAudioAck(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新回合任务:展示上一回合结算弹窗,弹窗关闭后再允许下一轮继续。
|
// 新回合任务:展示上一回合结算弹窗,弹窗关闭后再允许下一轮继续。
|
||||||
@@ -977,6 +1095,7 @@ async function runBattleEndTask(task, runId) {
|
|||||||
|
|
||||||
// 终局语音必须完整播完,再决定跳转或返回。
|
// 终局语音必须完整播完,再决定跳转或返回。
|
||||||
await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
|
await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
|
||||||
|
notifyMatchAudioAck(task);
|
||||||
if (!isQueueAlive(runId)) return;
|
if (!isQueueAlive(runId)) return;
|
||||||
|
|
||||||
if (matchStatus.value === 2) {
|
if (matchStatus.value === 2) {
|
||||||
@@ -984,20 +1103,28 @@ async function runBattleEndTask(task, runId) {
|
|||||||
url: `/pages/friend-battle-result?battleId=${battleId.value}`,
|
url: `/pages/friend-battle-result?battleId=${battleId.value}`,
|
||||||
});
|
});
|
||||||
} else if (matchStatus.value === 4) {
|
} else if (matchStatus.value === 4) {
|
||||||
|
const roomNumber = battleInfo.roomId || store.game.roomNumber || store.game.roomID;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (roomNumber) {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: `/pages/battle-room?roomNumber=${encodeURIComponent(roomNumber)}&fromResult=1`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
|
}
|
||||||
}, BATTLE_CANCEL_RETURN_DELAY);
|
}, BATTLE_CANCEL_RETURN_DELAY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无效射击任务:弹 toast 并播报无效射击,不修改比赛主状态。
|
// 无效射击任务:弹 toast 并播报无效射击,不修改比赛主状态。
|
||||||
async function runInvalidShotTask(runId) {
|
async function runInvalidShotTask(task, runId) {
|
||||||
if (!isQueueAlive(runId)) return;
|
if (!isQueueAlive(runId)) return;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "距离不足,无效",
|
title: "距离不足,无效",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
await playAudioKeys("射击无效", { interrupt: false });
|
await playAudioKeys("射击无效", { interrupt: false });
|
||||||
|
notifyMatchAudioAck(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回前台恢复入口:拉取服务端快照,处理结束态,然后继续消费增量队列。
|
// 回前台恢复入口:拉取服务端快照,处理结束态,然后继续消费增量队列。
|
||||||
@@ -1016,6 +1143,7 @@ async function restoreLatestBattle() {
|
|||||||
console.log("restore latest battle failed:", err);
|
console.log("restore latest battle failed:", err);
|
||||||
}
|
}
|
||||||
if (currentRestoreId !== restoreGeneration) return;
|
if (currentRestoreId !== restoreGeneration) return;
|
||||||
|
result = normalizeBattleInfo(result);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
hideRestoreLoading();
|
hideRestoreLoading();
|
||||||
runBattleQueue();
|
runBattleQueue();
|
||||||
@@ -1041,6 +1169,7 @@ async function restoreLatestBattle() {
|
|||||||
if (result.status === 4) {
|
if (result.status === 4) {
|
||||||
clearXRingStreaks();
|
clearXRingStreaks();
|
||||||
}
|
}
|
||||||
|
reconnectMatchServer(result);
|
||||||
|
|
||||||
if (restoreEventType === MESSAGETYPESV2.NewRound) {
|
if (restoreEventType === MESSAGETYPESV2.NewRound) {
|
||||||
const prevRound = getRestorePrevRound(result);
|
const prevRound = getRestorePrevRound(result);
|
||||||
@@ -1094,10 +1223,12 @@ function onReceiveMessage(message) {
|
|||||||
// 新对局入口:彻底清空上一局残留的状态、队列和缓存。
|
// 新对局入口:彻底清空上一局残留的状态、队列和缓存。
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
console.log('重新进入了')
|
console.log('重新进入了')
|
||||||
|
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
||||||
|
skipNextRestoreOnShow = false;
|
||||||
// 新对局入口:把所有会串场的状态、队列、时间戳和缓存一次性清空。
|
// 新对局入口:把所有会串场的状态、队列、时间戳和缓存一次性清空。
|
||||||
start.value = null;
|
start.value = null;
|
||||||
tips.value = "";
|
tips.value = "";
|
||||||
battleId.value = options.battleId || "";
|
battleId.value = returnSnapshot?.matchId || options.battleId || "";
|
||||||
currentRound.value = 0;
|
currentRound.value = 0;
|
||||||
roundTipRound.value = 0;
|
roundTipRound.value = 0;
|
||||||
goldenRound.value = 0;
|
goldenRound.value = 0;
|
||||||
@@ -1135,6 +1266,16 @@ onLoad((options) => {
|
|||||||
store.updateShotInfo(0, 0);
|
store.updateShotInfo(0, 0);
|
||||||
store.updateTips("");
|
store.updateTips("");
|
||||||
latestShotFlash.value = null;
|
latestShotFlash.value = null;
|
||||||
|
if (returnSnapshot) {
|
||||||
|
skipNextRestoreOnShow = true;
|
||||||
|
applyBattleSnapshot(returnSnapshot, { restore: true });
|
||||||
|
reconnectMatchServer(returnSnapshot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const readySnapshot = takeReadySnapshot(battleId.value);
|
||||||
|
if (readySnapshot?.status === 0) {
|
||||||
|
applyBattleSnapshot(readySnapshot, { restore: true });
|
||||||
|
}
|
||||||
scheduleRestoreLatestBattle();
|
scheduleRestoreLatestBattle();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1167,6 +1308,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
hideRestoreLoading();
|
hideRestoreLoading();
|
||||||
invalidateBattleQueue({ stopAudio: true, stopProgress: true });
|
invalidateBattleQueue({ stopAudio: true, stopProgress: true });
|
||||||
|
closeBattleServer("team-battle-unmount");
|
||||||
console.log('onBeforeUnmount', '页面卸载前')
|
console.log('onBeforeUnmount', '页面卸载前')
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
uni.$off(AUDIO_INTERRUPTION_BEGIN_EVENT, handleBattleCovered);
|
uni.$off(AUDIO_INTERRUPTION_BEGIN_EVENT, handleBattleCovered);
|
||||||
@@ -1176,11 +1318,16 @@ onBeforeUnmount(() => {
|
|||||||
onHide(()=>{
|
onHide(()=>{
|
||||||
console.log('onHide', '页面大退')
|
console.log('onHide', '页面大退')
|
||||||
handleBattleCovered();
|
handleBattleCovered();
|
||||||
|
closeBattleServer("team-battle-hide");
|
||||||
})
|
})
|
||||||
|
|
||||||
// 每次回到前台都重新拉最新比赛快照,确保画面与后端一致。
|
// 每次回到前台都重新拉最新比赛快照,确保画面与后端一致。
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
console.log('onshow')
|
console.log('onshow')
|
||||||
|
if (skipNextRestoreOnShow) {
|
||||||
|
skipNextRestoreOnShow = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
scheduleRestoreLatestBattle();
|
scheduleRestoreLatestBattle();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import * as $protobuf from "protobufjs";
|
||||||
|
const $root=$protobuf.Root.create({nested:{rpc:{ServerMessageType:{SERVER_MSG_UNKNOWN:0,SERVER_MSG_MATCH_READY:1,SERVER_MSG_MATCH_START:2,SERVER_MSG_NOW_YOU:3,SERVER_MSG_SHOT:4,SERVER_MSG_NEW_ROUND:5,SERVER_MSG_MATCH_END:6,SERVER_MSG_TIMEOUT:7,SERVER_MSG_CHECK:8,SERVER_MSG_RAND:9,SERVER_MSG_NOT_ENOUGH_DISTANCE:10,SERVER_MSG_PLAYER_LEFT:11,SERVER_MSG_HEARTBEAT:12,SERVER_MSG_PRACTICE_END:13},ClientMessageType:{CLIENT_MSG_UNKNOWN:0,CLIENT_MSG_HEARTBEAT_ACK:1,CLIENT_MSG_SHOOT_DATA:2,CLIENT_MSG_ACK:3,CLIENT_MSG_LEAVE:4},MatchStatus:{MATCH_STATUS_READY:0,MATCH_STATUS_STARTED:1,MATCH_STATUS_END:2,MATCH_STATUS_TIMEOUT:3,MATCH_STATUS_UNEXPECTEDLY:4},MatchPlayerStatus:{MATCH_PLAYER_STATUS_READY:0,MATCH_PLAYER_STATUS_STARTED:1,MATCH_PLAYER_STATUS_END:2},MatchShoot:{fields:{player_id:{type:"int64",id:1},status:{type:"int32",id:2},x:{type:"float",id:3},y:{type:"float",id:4},ring:{type:"int32",id:5},ring_x:{type:"bool",id:6},angle:{type:"float",id:7},distance:{type:"float",id:8}}},MatchShootList:{fields:{items:{rule:"repeated",type:"MatchShoot",id:1}}},RoundScore:{fields:{total_ring:{type:"int32",id:1},score:{type:"int32",id:2},if_win:{type:"bool",id:3}}},MatchRound:{fields:{shoots:{rule:"map",type:"MatchShootList",id:1,keytype:"int64"},scores:{rule:"map",type:"RoundScore",id:2,keytype:"int32"},round:{type:"int32",id:3},if_gold:{type:"bool",id:4},status:{type:"int32",id:5},gold_round:{type:"int32",id:6}}},PlayerMatchResult:{fields:{total_ring:{type:"int32",id:1},user_id:{type:"int64",id:2},ten_ring_count:{type:"int32",id:3},average_ring:{type:"float",id:4}}},PlayerFull:{fields:{id:{type:"int64",id:1},name:{type:"string",id:2},avatar:{type:"string",id:3},exp:{type:"int32",id:4},score:{type:"int32",id:5},level:{type:"int32",id:6},before_level:{type:"int32",id:7},before_exp:{type:"int32",id:8},current_exp:{type:"int32",id:9},upgrade_exp:{type:"int32",id:10},active:{type:"int32",id:11},device_id:{type:"string",id:12},s_vip:{type:"bool",id:13},vip:{type:"bool",id:14},player_match_result:{type:"PlayerMatchResult",id:15}}},TeamInfo:{fields:{players:{rule:"repeated",type:"PlayerFull",id:1},id:{type:"int32",id:2},name:{type:"string",id:3},score:{type:"int32",id:4}}},CurrentShoot:{fields:{round:{type:"int32",id:1},round_id:{type:"int64",id:2},index:{type:"int32",id:3},start_time:{type:"int64",id:4},player_id:{type:"int64",id:5},gold_round:{type:"bool",id:6},start_time_text:{type:"string",id:7},my_index:{type:"int32",id:8},index_map:{rule:"map",type:"int32",id:9,keytype:"int64"}}},ShootData:{fields:{x:{type:"float",id:1},y:{type:"float",id:2},r:{type:"float",id:3},dst:{type:"float",id:4},m:{type:"string",id:5},adc:{type:"float",id:6},device_id:{type:"string",id:7},shoot_id:{type:"string",id:8}}},PracticeInfo:{fields:{id:{type:"int64",id:1},user_id:{type:"int64",id:2},status:{type:"int32",id:3},status_text:{type:"string",id:4},start_time:{type:"int64",id:5},target_type:{type:"int32",id:6},vip:{type:"bool",id:7},s_vip:{type:"bool",id:8},device_id:{type:"string",id:9},shoot_data:{type:"MatchShoot",id:10},details:{rule:"repeated",type:"MatchShoot",id:11}}},MatchInfo:{fields:{match_id:{type:"int64",id:1},create_time:{type:"int64",id:2},start_time:{type:"int64",id:3},server_time:{type:"int64",id:4},shoot_time:{type:"int32",id:5},shoot_number:{type:"int32",id:6},ready_time:{type:"int32",id:7},way:{type:"int32",id:8},mode:{type:"int32",id:9},status:{type:"MatchStatus",id:10},status_text:{type:"string",id:11},rounds:{rule:"repeated",type:"MatchRound",id:12},teams:{rule:"map",type:"TeamInfo",id:13,keytype:"int32"},current:{type:"CurrentShoot",id:14},next:{type:"CurrentShoot",id:15},shoot_data:{type:"MatchShoot",id:16},win_team:{type:"int32",id:17},mvp:{type:"PlayerFull",id:18},room_id:{type:"string",id:19},result_list:{rule:"repeated",type:"PlayerMatchResult",id:20},timeout_time:{type:"int64",id:21},target_type:{type:"int32",id:22},event_type:{type:"int32",id:23},timeout:{type:"int32",id:24},server_addr:{type:"string",id:25}}},ServerMessage:{fields:{type:{type:"ServerMessageType",id:1},match_id:{type:"int64",id:2},timestamp:{type:"int64",id:3},match_info:{type:"MatchInfo",id:4,oneof:"payload"},shoot_data:{type:"ShootData",id:5,oneof:"payload"},practice_info:{type:"PracticeInfo",id:6,oneof:"payload"},sequence:{type:"int64",id:7}}},ClientMessage:{fields:{type:{type:"ClientMessageType",id:1},match_id:{type:"int64",id:2},user_id:{type:"int64",id:3},sequence:{type:"int64",id:4},data:{type:"bytes",id:5}}}}}});
|
||||||
|
export default $root;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// 比赛数据适配器:统一接口返回和比赛服 websocket 的字段结构。
|
||||||
|
// 页面继续使用原来的 camelCase 数据,后端可以返回 snake_case 或新的 matchInfo 包装结构。
|
||||||
|
export const MATCH_STATUS_BY_TEXT = {
|
||||||
|
MATCH_STATUS_READY: 0,
|
||||||
|
MATCH_STATUS_STARTED: 1,
|
||||||
|
MATCH_STATUS_END: 2,
|
||||||
|
MATCH_STATUS_TIMEOUT: 3,
|
||||||
|
MATCH_STATUS_UNEXPECTEDLY: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pickField(source, camelKey, snakeKey) {
|
||||||
|
return source?.[camelKey] ?? source?.[snakeKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeId(value) {
|
||||||
|
if (value === undefined || value === null || value === "") return "";
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCamelKey(key) {
|
||||||
|
return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePlainObject(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(normalizePlainObject);
|
||||||
|
if (!value || typeof value !== "object") return value;
|
||||||
|
|
||||||
|
return Object.keys(value).reduce((result, key) => {
|
||||||
|
result[toCamelKey(key)] = normalizePlainObject(value[key]);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMapValues(map = {}) {
|
||||||
|
return Object.keys(map || {}).reduce((result, key) => {
|
||||||
|
result[key] = normalizePlainObject(map[key]);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeShootListMap(map = {}) {
|
||||||
|
return Object.keys(map || {}).reduce((result, key) => {
|
||||||
|
const value = map[key];
|
||||||
|
const items = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(value?.items)
|
||||||
|
? value.items
|
||||||
|
: [];
|
||||||
|
result[key] = items.map(normalizePlainObject);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRound(round = {}) {
|
||||||
|
const normalized = normalizePlainObject(round);
|
||||||
|
normalized.shoots = normalizeShootListMap(round.shoots || normalized.shoots);
|
||||||
|
normalized.scores = normalizeMapValues(round.scores || normalized.scores);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMatchInfo(matchInfo = {}) {
|
||||||
|
if (!matchInfo || typeof matchInfo !== "object") return matchInfo;
|
||||||
|
|
||||||
|
const normalized = normalizePlainObject(matchInfo);
|
||||||
|
const statusText = normalized.statusText || matchInfo.status_text;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(normalized.status === undefined ||
|
||||||
|
normalized.status === null ||
|
||||||
|
normalized.status === "") &&
|
||||||
|
MATCH_STATUS_BY_TEXT[statusText] !== undefined
|
||||||
|
) {
|
||||||
|
normalized.status = MATCH_STATUS_BY_TEXT[statusText];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchInfo.teams || normalized.teams) {
|
||||||
|
normalized.teams = normalizeMapValues(matchInfo.teams || normalized.teams);
|
||||||
|
}
|
||||||
|
if (Array.isArray(matchInfo.rounds || normalized.rounds)) {
|
||||||
|
normalized.rounds = (matchInfo.rounds || normalized.rounds).map(normalizeRound);
|
||||||
|
}
|
||||||
|
if (matchInfo.current || normalized.current) {
|
||||||
|
normalized.current = normalizePlainObject(matchInfo.current || normalized.current);
|
||||||
|
}
|
||||||
|
if (matchInfo.next || normalized.next) {
|
||||||
|
normalized.next = normalizePlainObject(matchInfo.next || normalized.next);
|
||||||
|
}
|
||||||
|
if (matchInfo.shoot_data || normalized.shootData) {
|
||||||
|
normalized.shootData = normalizePlainObject(matchInfo.shoot_data || normalized.shootData);
|
||||||
|
}
|
||||||
|
if (Array.isArray(matchInfo.result_list || normalized.resultList)) {
|
||||||
|
normalized.resultList = (matchInfo.result_list || normalized.resultList).map(
|
||||||
|
normalizePlainObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBattleApiResult(result) {
|
||||||
|
if (!result || typeof result !== "object") return result;
|
||||||
|
|
||||||
|
// request 通常已经解出顶层 data;这里额外兼容未解包和旧版扁平结构。
|
||||||
|
const resultData = result.data && typeof result.data === "object" ? result.data : null;
|
||||||
|
const payload =
|
||||||
|
resultData && (resultData.matchInfo || resultData.match_info)
|
||||||
|
? resultData
|
||||||
|
: result;
|
||||||
|
const matchInfo = payload.matchInfo || payload.match_info || payload;
|
||||||
|
const normalized = normalizeMatchInfo(matchInfo);
|
||||||
|
const serverAddr =
|
||||||
|
payload.serverAddr ||
|
||||||
|
payload.server_addr ||
|
||||||
|
matchInfo.serverAddr ||
|
||||||
|
matchInfo.server_addr ||
|
||||||
|
normalized.serverAddr;
|
||||||
|
|
||||||
|
if (serverAddr) normalized.serverAddr = serverAddr;
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
import protobuf from "protobufjs/minimal.js";
|
||||||
|
|
||||||
|
const { Reader, Writer } = protobuf;
|
||||||
|
|
||||||
|
// 比赛服 protobuf 协议适配层:
|
||||||
|
// 小程序环境不支持 protobufjs 反射模式里的动态 Function codegen,
|
||||||
|
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||||
|
export const ServerMessageType = {
|
||||||
|
SERVER_MSG_UNKNOWN: 0,
|
||||||
|
SERVER_MSG_MATCH_READY: 1,
|
||||||
|
SERVER_MSG_MATCH_START: 2,
|
||||||
|
SERVER_MSG_NOW_YOU: 3,
|
||||||
|
SERVER_MSG_SHOT: 4,
|
||||||
|
SERVER_MSG_NEW_ROUND: 5,
|
||||||
|
SERVER_MSG_MATCH_END: 6,
|
||||||
|
SERVER_MSG_TIMEOUT: 7,
|
||||||
|
SERVER_MSG_CHECK: 8,
|
||||||
|
SERVER_MSG_RAND: 9,
|
||||||
|
SERVER_MSG_NOT_ENOUGH_DISTANCE: 10,
|
||||||
|
SERVER_MSG_PLAYER_LEFT: 11,
|
||||||
|
SERVER_MSG_HEARTBEAT: 12,
|
||||||
|
SERVER_MSG_PRACTICE_END: 13,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientMessageType = {
|
||||||
|
CLIENT_MSG_UNKNOWN: 0,
|
||||||
|
CLIENT_MSG_HEARTBEAT_ACK: 1,
|
||||||
|
CLIENT_MSG_SHOOT_DATA: 2,
|
||||||
|
CLIENT_MSG_ACK: 3,
|
||||||
|
CLIENT_MSG_LEAVE: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。
|
||||||
|
const ServerMessageTypeNameByValue = Object.keys(ServerMessageType).reduce(
|
||||||
|
(result, name) => {
|
||||||
|
result[ServerMessageType[name]] = name;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 当前只需要解码比赛服下发的字段并打印,字段表按后端 proto 定义维护。
|
||||||
|
const SCHEMAS = {
|
||||||
|
MatchShoot: {
|
||||||
|
1: { name: "player_id", kind: "int64" },
|
||||||
|
2: { name: "status", kind: "int32" },
|
||||||
|
3: { name: "x", kind: "float" },
|
||||||
|
4: { name: "y", kind: "float" },
|
||||||
|
5: { name: "ring", kind: "int32" },
|
||||||
|
6: { name: "ring_x", kind: "bool" },
|
||||||
|
7: { name: "angle", kind: "float" },
|
||||||
|
8: { name: "distance", kind: "float" },
|
||||||
|
},
|
||||||
|
MatchShootList: {
|
||||||
|
1: { name: "items", kind: "message", type: "MatchShoot", repeated: true },
|
||||||
|
},
|
||||||
|
RoundScore: {
|
||||||
|
1: { name: "total_ring", kind: "int32" },
|
||||||
|
2: { name: "score", kind: "int32" },
|
||||||
|
3: { name: "if_win", kind: "bool" },
|
||||||
|
},
|
||||||
|
MatchRound: {
|
||||||
|
1: {
|
||||||
|
name: "shoots",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int64",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "MatchShootList",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
name: "scores",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int32",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "RoundScore",
|
||||||
|
},
|
||||||
|
3: { name: "round", kind: "int32" },
|
||||||
|
4: { name: "if_gold", kind: "bool" },
|
||||||
|
5: { name: "status", kind: "int32" },
|
||||||
|
6: { name: "gold_round", kind: "int32" },
|
||||||
|
},
|
||||||
|
PlayerMatchResult: {
|
||||||
|
1: { name: "total_ring", kind: "int32" },
|
||||||
|
2: { name: "user_id", kind: "int64" },
|
||||||
|
3: { name: "ten_ring_count", kind: "int32" },
|
||||||
|
4: { name: "average_ring", kind: "float" },
|
||||||
|
},
|
||||||
|
PlayerFull: {
|
||||||
|
1: { name: "id", kind: "int64" },
|
||||||
|
2: { name: "name", kind: "string" },
|
||||||
|
3: { name: "avatar", kind: "string" },
|
||||||
|
4: { name: "exp", kind: "int32" },
|
||||||
|
5: { name: "score", kind: "int32" },
|
||||||
|
6: { name: "level", kind: "int32" },
|
||||||
|
7: { name: "before_level", kind: "int32" },
|
||||||
|
8: { name: "before_exp", kind: "int32" },
|
||||||
|
9: { name: "current_exp", kind: "int32" },
|
||||||
|
10: { name: "upgrade_exp", kind: "int32" },
|
||||||
|
11: { name: "active", kind: "int32" },
|
||||||
|
12: { name: "device_id", kind: "string" },
|
||||||
|
13: { name: "s_vip", kind: "bool" },
|
||||||
|
14: { name: "vip", kind: "bool" },
|
||||||
|
15: { name: "player_match_result", kind: "message", type: "PlayerMatchResult" },
|
||||||
|
},
|
||||||
|
TeamInfo: {
|
||||||
|
1: { name: "players", kind: "message", type: "PlayerFull", repeated: true },
|
||||||
|
2: { name: "id", kind: "int32" },
|
||||||
|
3: { name: "name", kind: "string" },
|
||||||
|
4: { name: "score", kind: "int32" },
|
||||||
|
},
|
||||||
|
CurrentShoot: {
|
||||||
|
1: { name: "round", kind: "int32" },
|
||||||
|
2: { name: "round_id", kind: "int64" },
|
||||||
|
3: { name: "index", kind: "int32" },
|
||||||
|
4: { name: "start_time", kind: "int64" },
|
||||||
|
5: { name: "player_id", kind: "int64" },
|
||||||
|
6: { name: "gold_round", kind: "bool" },
|
||||||
|
7: { name: "start_time_text", kind: "string" },
|
||||||
|
8: { name: "my_index", kind: "int32" },
|
||||||
|
9: {
|
||||||
|
name: "index_map",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int64",
|
||||||
|
valueKind: "int32",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ShootData: {
|
||||||
|
1: { name: "x", kind: "float" },
|
||||||
|
2: { name: "y", kind: "float" },
|
||||||
|
3: { name: "r", kind: "float" },
|
||||||
|
4: { name: "dst", kind: "float" },
|
||||||
|
5: { name: "m", kind: "string" },
|
||||||
|
6: { name: "adc", kind: "float" },
|
||||||
|
7: { name: "device_id", kind: "string" },
|
||||||
|
8: { name: "shoot_id", kind: "string" },
|
||||||
|
},
|
||||||
|
PracticeInfo: {
|
||||||
|
1: { name: "id", kind: "int64" },
|
||||||
|
2: { name: "user_id", kind: "int64" },
|
||||||
|
3: { name: "status", kind: "int32" },
|
||||||
|
4: { name: "status_text", kind: "string" },
|
||||||
|
5: { name: "start_time", kind: "int64" },
|
||||||
|
6: { name: "target_type", kind: "int32" },
|
||||||
|
7: { name: "vip", kind: "bool" },
|
||||||
|
8: { name: "s_vip", kind: "bool" },
|
||||||
|
9: { name: "device_id", kind: "string" },
|
||||||
|
10: { name: "shoot_data", kind: "message", type: "MatchShoot" },
|
||||||
|
11: { name: "details", kind: "message", type: "MatchShoot", repeated: true },
|
||||||
|
},
|
||||||
|
MatchInfo: {
|
||||||
|
1: { name: "match_id", kind: "int64" },
|
||||||
|
2: { name: "create_time", kind: "int64" },
|
||||||
|
3: { name: "start_time", kind: "int64" },
|
||||||
|
4: { name: "server_time", kind: "int64" },
|
||||||
|
5: { name: "shoot_time", kind: "int32" },
|
||||||
|
6: { name: "shoot_number", kind: "int32" },
|
||||||
|
7: { name: "ready_time", kind: "int32" },
|
||||||
|
8: { name: "way", kind: "int32" },
|
||||||
|
9: { name: "mode", kind: "int32" },
|
||||||
|
10: { name: "status", kind: "int32" },
|
||||||
|
11: { name: "status_text", kind: "string" },
|
||||||
|
12: { name: "rounds", kind: "message", type: "MatchRound", repeated: true },
|
||||||
|
13: {
|
||||||
|
name: "teams",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int32",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "TeamInfo",
|
||||||
|
},
|
||||||
|
14: { name: "current", kind: "message", type: "CurrentShoot" },
|
||||||
|
15: { name: "next", kind: "message", type: "CurrentShoot" },
|
||||||
|
16: { name: "shoot_data", kind: "message", type: "MatchShoot" },
|
||||||
|
17: { name: "win_team", kind: "int32" },
|
||||||
|
18: { name: "mvp", kind: "message", type: "PlayerFull" },
|
||||||
|
19: { name: "room_id", kind: "string" },
|
||||||
|
20: {
|
||||||
|
name: "result_list",
|
||||||
|
kind: "message",
|
||||||
|
type: "PlayerMatchResult",
|
||||||
|
repeated: true,
|
||||||
|
},
|
||||||
|
21: { name: "timeout_time", kind: "int64" },
|
||||||
|
22: { name: "target_type", kind: "int32" },
|
||||||
|
23: { name: "event_type", kind: "int32" },
|
||||||
|
24: { name: "timeout", kind: "int32" },
|
||||||
|
25: { name: "server_addr", kind: "string" },
|
||||||
|
},
|
||||||
|
ServerMessage: {
|
||||||
|
1: { name: "type", kind: "int32" },
|
||||||
|
2: { name: "match_id", kind: "int64" },
|
||||||
|
3: { name: "timestamp", kind: "int64" },
|
||||||
|
4: { name: "match_info", kind: "message", type: "MatchInfo", oneof: "payload" },
|
||||||
|
5: { name: "shoot_data", kind: "message", type: "ShootData", oneof: "payload" },
|
||||||
|
6: { name: "practice_info", kind: "message", type: "PracticeInfo", oneof: "payload" },
|
||||||
|
7: { name: "sequence", kind: "int64" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 小程序 websocket 收到的 data 可能是 ArrayBuffer、TypedArray 或字符串,这里统一成 Uint8Array。
|
||||||
|
function toUint8Array(data) {
|
||||||
|
if (data instanceof Uint8Array) return data;
|
||||||
|
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
||||||
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
}
|
||||||
|
if (typeof data === "string") {
|
||||||
|
const bytes = new Uint8Array(data.length);
|
||||||
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
|
bytes[i] = data.charCodeAt(i) & 0xff;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
throw new TypeError("Unsupported match websocket message payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
// uni.sendSocketMessage 发送二进制时使用 ArrayBuffer。
|
||||||
|
function toArrayBuffer(bytes) {
|
||||||
|
const output = new Uint8Array(bytes.length);
|
||||||
|
output.set(bytes);
|
||||||
|
return output.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readScalar(reader, kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case "int32":
|
||||||
|
return reader.int32();
|
||||||
|
case "int64":
|
||||||
|
// 统一把 int64 转成字符串,避免小程序环境下大整数精度丢失。
|
||||||
|
return reader.int64().toString();
|
||||||
|
case "float":
|
||||||
|
return reader.float();
|
||||||
|
case "bool":
|
||||||
|
return reader.bool();
|
||||||
|
case "string":
|
||||||
|
return reader.string();
|
||||||
|
case "bytes":
|
||||||
|
return Array.from(reader.bytes());
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported protobuf scalar kind: ${kind}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMapEntry(reader, field) {
|
||||||
|
const end = reader.uint32() + reader.pos;
|
||||||
|
let key = "";
|
||||||
|
let value;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
key = readScalar(reader, field.keyKind);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
value =
|
||||||
|
field.valueKind === "message"
|
||||||
|
? decodeMessage(field.valueType, reader, reader.uint32())
|
||||||
|
: readScalar(reader, field.valueKind);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: String(key), value };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readValue(reader, field) {
|
||||||
|
if (field.kind === "message") {
|
||||||
|
return decodeMessage(field.type, reader, reader.uint32());
|
||||||
|
}
|
||||||
|
return readScalar(reader, field.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeMessage(schemaName, readerOrData, length) {
|
||||||
|
const schema = SCHEMAS[schemaName];
|
||||||
|
const reader =
|
||||||
|
readerOrData instanceof Reader ? readerOrData : Reader.create(readerOrData);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = {};
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
const field = schema[tag >>> 3];
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.kind === "map") {
|
||||||
|
const entry = readMapEntry(reader, field);
|
||||||
|
const map = message[field.name] || {};
|
||||||
|
map[entry.key] = entry.value;
|
||||||
|
message[field.name] = map;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = readValue(reader, field);
|
||||||
|
|
||||||
|
if (field.repeated) {
|
||||||
|
if (!message[field.name]) message[field.name] = [];
|
||||||
|
message[field.name].push(value);
|
||||||
|
} else {
|
||||||
|
message[field.name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.oneof) {
|
||||||
|
message[field.oneof] = field.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeServerMessage(data) {
|
||||||
|
// 后端已确认比赛服 websocket 下发的是原始 protobuf frame,直接解 ServerMessage。
|
||||||
|
return decodeMessage("ServerMessage", toUint8Array(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeClientMessage(payload) {
|
||||||
|
// ClientMessage 字段名按 proto 定义写入,例如 match_id、user_id。
|
||||||
|
const writer = Writer.create();
|
||||||
|
const matchId = payload.match_id ?? payload.matchId;
|
||||||
|
const userId = payload.user_id ?? payload.userId;
|
||||||
|
|
||||||
|
if (payload.type !== undefined) writer.uint32(8).int32(payload.type);
|
||||||
|
if (matchId !== undefined && matchId !== null) writer.uint32(16).int64(matchId);
|
||||||
|
if (userId !== undefined && userId !== null) writer.uint32(24).int64(userId);
|
||||||
|
if (payload.sequence !== undefined && payload.sequence !== null) {
|
||||||
|
writer.uint32(32).int64(payload.sequence);
|
||||||
|
}
|
||||||
|
if (payload.data !== undefined && payload.data !== null) {
|
||||||
|
writer.uint32(42).bytes(toUint8Array(payload.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return toArrayBuffer(writer.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerMessageTypeName(type) {
|
||||||
|
return ServerMessageTypeNameByValue[type] || String(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHeartbeatAckMessage() {
|
||||||
|
// 心跳 ACK 按文档立即回,不等待语音播报。
|
||||||
|
return encodeClientMessage({
|
||||||
|
type: ClientMessageType.CLIENT_MSG_HEARTBEAT_ACK,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAckMessage({ matchId, sequence }) {
|
||||||
|
// 普通消息 ACK 仍携带 sequence,但前端不做 sequence 补发或排序。
|
||||||
|
return encodeClientMessage({
|
||||||
|
type: ClientMessageType.CLIENT_MSG_ACK,
|
||||||
|
match_id: matchId,
|
||||||
|
sequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLeaveMessage({ matchId, userId }) {
|
||||||
|
// 比赛结束或页面离开时通知比赛服释放当前玩家连接。
|
||||||
|
return encodeClientMessage({
|
||||||
|
type: ClientMessageType.CLIENT_MSG_LEAVE,
|
||||||
|
match_id: matchId,
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { connectMatchWebSocket } from "@/matchWebsocket";
|
||||||
|
import useStore from "@/store";
|
||||||
|
|
||||||
|
export const MATCH_RETURN_SNAPSHOT_KEY = "match-return-snapshot";
|
||||||
|
|
||||||
|
function getMatchId(battleInfo) {
|
||||||
|
return battleInfo?.matchId || battleInfo?.id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBattlePageUrl(battleInfo) {
|
||||||
|
return Number(battleInfo?.mode) <= 3
|
||||||
|
? "/pages/team-battle/index?fromReturn=1"
|
||||||
|
: "/pages/melee-battle?fromReturn=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function takeMatchReturnSnapshot() {
|
||||||
|
const snapshot = uni.getStorageSync(MATCH_RETURN_SNAPSHOT_KEY);
|
||||||
|
if (snapshot) uni.removeStorageSync(MATCH_RETURN_SNAPSHOT_KEY);
|
||||||
|
return snapshot && typeof snapshot === "object" ? snapshot : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function returnToBattle(battleInfo, navigate) {
|
||||||
|
const matchId = getMatchId(battleInfo);
|
||||||
|
if (!battleInfo || !matchId || !battleInfo.serverAddr) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "比赛连接信息异常",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
uni.setStorageSync(MATCH_RETURN_SNAPSHOT_KEY, battleInfo);
|
||||||
|
connectMatchWebSocket({
|
||||||
|
serverAddr: battleInfo.serverAddr,
|
||||||
|
matchId,
|
||||||
|
userId: store.user?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await navigate(getBattlePageUrl(battleInfo));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+1
-1
@@ -24,7 +24,7 @@ function createWebSocket(token, onMessage) {
|
|||||||
|
|
||||||
switch (envVersion) {
|
switch (envVersion) {
|
||||||
case "develop": // 开发版
|
case "develop": // 开发版
|
||||||
// url = "ws://192.168.1.30:8000/socket";
|
// url = "ws://192.168.1.2:8000/socket";
|
||||||
url = "wss://apitest.shelingxingqiu.com/socket";
|
url = "wss://apitest.shelingxingqiu.com/socket";
|
||||||
break;
|
break;
|
||||||
case "trial": // 体验版
|
case "trial": // 体验版
|
||||||
|
|||||||
Reference in New Issue
Block a user