diff --git a/.gitignore b/.gitignore
index c91affc..23a9575 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
.history
.github
+.claude
openspec
CLAUDE.md
docs
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a466545..9ff8fc8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,9 @@
{
- "i18n-ally.localesPaths": []
-}
\ No newline at end of file
+ "i18n-ally.localesPaths": [],
+ "files.watcherExclude": {
+ "**/dist/**": true
+ },
+ "search.exclude": {
+ "**/dist/**": true
+ }
+}
diff --git a/jsconfig.json b/jsconfig.json
index 77fcff3..5429ea1 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -12,5 +12,6 @@
},
"vueCompilerOptions": {
"plugins": ["@uni-helper/uni-types/volar-plugin"]
- }
+ },
+ "exclude": ["node_modules", "dist"]
}
diff --git a/package-lock.json b/package-lock.json
index 049156a..55935d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,8 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"@dcloudio/uni-ui": "^1.5.11",
"pinia": "2.0.36",
+ "pinia-plugin-persistedstate": "3.2.1",
+ "protobufjs": "^8.7.0",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
@@ -2422,6 +2424,70 @@
"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": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
@@ -2438,6 +2504,294 @@
"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": {
"version": "9.1.9",
"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": {
"version": "4.39.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz",
@@ -3437,6 +3817,227 @@
"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": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
@@ -7535,6 +8136,12 @@
"dev": true,
"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": {
"version": "5.1.1",
"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": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -8530,6 +9146,18 @@
"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": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
diff --git a/package.json b/package.json
index a50c6be..4fe0d15 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@dcloudio/uni-ui": "^1.5.11",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
+ "protobufjs": "^8.7.0",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
diff --git a/project.config.json b/project.config.json
index f6d496c..0ffafb0 100644
--- a/project.config.json
+++ b/project.config.json
@@ -3,7 +3,240 @@
"compileType": "miniprogram",
"libVersion": "3.7.7",
"packOptions": {
- "ignore": [],
+ "ignore": [
+ {
+ "type": "file",
+ "value": "static/common/dialog-light.png"
+ },
+ {
+ "type": "file",
+ "value": "static/common/dialog-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/common/dialog-icon.png"
+ },
+ {
+ "type": "file",
+ "value": "static/prompt-bg-square.png"
+ },
+ {
+ "type": "file",
+ "value": "static/coach-comment.png"
+ },
+ {
+ "type": "file",
+ "value": "static/screen-hint-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/red-team-win.png"
+ },
+ {
+ "type": "file",
+ "value": "static/blue-team-win.png"
+ },
+ {
+ "type": "file",
+ "value": "static/my-grow.png"
+ },
+ {
+ "type": "file",
+ "value": "static/gold-shining.png"
+ },
+ {
+ "type": "file",
+ "value": "static/bow-target.png"
+ },
+ {
+ "type": "file",
+ "value": "static/matching-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/versus.png"
+ },
+ {
+ "type": "file",
+ "value": "static/point-champion.png"
+ },
+ {
+ "type": "file",
+ "value": "static/my-practise.png"
+ },
+ {
+ "type": "file",
+ "value": "static/shining-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/donate.png"
+ },
+ {
+ "type": "file",
+ "value": "static/friend-battle.png"
+ },
+ {
+ "type": "file",
+ "value": "static/user-upgrade.png"
+ },
+ {
+ "type": "file",
+ "value": "static/finish-frame.png"
+ },
+ {
+ "type": "file",
+ "value": "static/vip/svip-jian.png"
+ },
+ {
+ "type": "file",
+ "value": "static/battle-header.png"
+ },
+ {
+ "type": "file",
+ "value": "static/battle-header-melee.png"
+ },
+ {
+ "type": "file",
+ "value": "static/player-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/mvp-blue.png"
+ },
+ {
+ "type": "file",
+ "value": "static/finish-tip.png"
+ },
+ {
+ "type": "file",
+ "value": "static/2unfinish-tip.png"
+ },
+ {
+ "type": "file",
+ "value": "static/have-no-device.png"
+ },
+ {
+ "type": "file",
+ "value": "static/device-icon.png"
+ },
+ {
+ "type": "file",
+ "value": "static/unfinish-tip.png"
+ },
+ {
+ "type": "file",
+ "value": "static/test-tip.png"
+ },
+ {
+ "type": "file",
+ "value": "static/mvp-red.png"
+ },
+ {
+ "type": "file",
+ "value": "static/vip/svip-lie.png"
+ },
+ {
+ "type": "file",
+ "value": "static/mvp-tip.png"
+ },
+ {
+ "type": "file",
+ "value": "static/rank/battle-choose.png"
+ },
+ {
+ "type": "file",
+ "value": "static/back-to-game-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/complete-light1.png"
+ },
+ {
+ "type": "file",
+ "value": "static/complete-light2.png"
+ },
+ {
+ "type": "file",
+ "value": "static/title-2v2.png"
+ },
+ {
+ "type": "file",
+ "value": "static/tab-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/scan.png"
+ },
+ {
+ "type": "file",
+ "value": "static/choose-battle-mode.png"
+ },
+ {
+ "type": "file",
+ "value": "static/shooter.png"
+ },
+ {
+ "type": "file",
+ "value": "static/my-growth.png"
+ },
+ {
+ "type": "file",
+ "value": "static/juezhanbang.png"
+ },
+ {
+ "type": "file",
+ "value": "static/title-3v3.png"
+ },
+ {
+ "type": "file",
+ "value": "static/point-book-tip-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/reward-us.png"
+ },
+ {
+ "type": "file",
+ "value": "static/rank/star.png"
+ },
+ {
+ "type": "file",
+ "value": "static/tab-point-book.png"
+ },
+ {
+ "type": "file",
+ "value": "static/pk-icon.png"
+ },
+ {
+ "type": "file",
+ "value": "static/first-try.png"
+ },
+ {
+ "type": "file",
+ "value": "static/row-yellow-bg.png"
+ },
+ {
+ "type": "file",
+ "value": "static/room-notfound-title.png"
+ },
+ {
+ "type": "file",
+ "value": "static/title-mvp.png"
+ },
+ {
+ "type": "file",
+ "value": "static/long-bubble-tall.png"
+ },
+ {
+ "type": "file",
+ "value": "static/battle-result.png"
+ },
+ {
+ "type": "file",
+ "value": "static/tab-mall.png"
+ }
+ ],
"include": []
},
"setting": {
diff --git a/src/App.vue b/src/App.vue
index 08b356c..608b1ec 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -7,9 +7,13 @@
onHide
} from "@dcloudio/uni-app";
import websocket from "@/websocket";
+ import matchWebsocket from "@/matchWebsocket";
import {
getDeviceBatteryAPI
} from "@/apis";
+ import {
+ MESSAGETYPES
+ } from "@/constants";
import useStore from "@/store";
import {
storeToRefs
@@ -36,6 +40,9 @@
websocket.createWebSocket(token, onShootWsMsg);
}
if (!newVal) {
+ matchWebsocket.closeMatchWebSocket({
+ reason: "logout"
+ });
websocket.closeWebSocket();
}
}, {
@@ -73,18 +80,50 @@
// audioManager.play("射箭声音")
}
+ function onNetworkStatusChange(status) {
+ matchWebsocket.handleMatchNetworkStatusChange(status);
+ }
+
+ 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) {
if(content.type === 'shoot-trigger'){
onDeviceShoot()
}
+ connectMatchServerFromMessage(content);
uni.$emit("socket-inbox", content);
}
onShow(() => {
+ void audioManager.warmButton();
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
uni.$on("session-kicked-out", onSessionKickedOut);
uni.$on("device-bind-invalid", onDeviceBindInvalid);
+ if (typeof uni.offNetworkStatusChange === "function") {
+ uni.offNetworkStatusChange(onNetworkStatusChange);
+ }
+ if (typeof uni.onNetworkStatusChange === "function") {
+ uni.onNetworkStatusChange(onNetworkStatusChange);
+ }
+ if (typeof uni.getNetworkType === "function") {
+ uni.getNetworkType({
+ success: onNetworkStatusChange
+ });
+ }
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -99,6 +138,12 @@
uni.$off("update-online", emitUpdateOnline);
uni.$off("session-kicked-out", onSessionKickedOut);
uni.$off("device-bind-invalid", onDeviceBindInvalid);
+ if (typeof uni.offNetworkStatusChange === "function") {
+ uni.offNetworkStatusChange(onNetworkStatusChange);
+ }
+ matchWebsocket.closeMatchWebSocket({
+ reason: "app-hide"
+ });
websocket.closeWebSocket();
});
@@ -258,6 +303,69 @@
text-overflow: ellipsis;
}
+ .member-nickname {
+ position: relative;
+ display: inline-flex;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ .member-nickname__text,
+ .member-nickname__shine {
+ display: block;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .member-nickname--vip .member-nickname__text {
+ color: #E7BA80;
+ }
+
+ .member-nickname--svip .member-nickname__text {
+ background: linear-gradient(90deg, #ffb86c, #ff4fd8, #7c5cff, #35d6ff);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+
+ .member-nickname__shine {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 110deg,
+ transparent 0%,
+ transparent 38%,
+ rgba(255, 255, 255, 0.15) 45%,
+ rgba(255, 255, 255, 1) 50%,
+ rgba(255, 255, 255, 0.15) 55%,
+ transparent 62%,
+ transparent 100%
+ );
+ background-size: 220% 100%;
+ background-position: 120% 0;
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ pointer-events: none;
+ animation: memberNicknameShine 3.5s infinite ease-in-out;
+ }
+
+ @keyframes memberNicknameShine {
+ 0%,
+ 50% {
+ background-position: 120% 0;
+ }
+
+ 100% {
+ background-position: -200% 0;
+ }
+ }
+
.modal {
height: 100%;
display: flex;
diff --git a/src/apis.js b/src/apis.js
index 93c4c7f..35fe672 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -1,3 +1,5 @@
+import { normalizeBattleApiResult } from "@/utils/matchAdapter";
+
let BASE_URL = "https://api.shelingxingqiu.com/api/shoot"; // 默认正式版
try {
@@ -6,7 +8,7 @@ try {
switch (envVersion) {
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";
break;
case "trial": // 体验版
@@ -23,7 +25,11 @@ try {
console.error("获取环境信息失败,使用默认正式环境", e);
}
-function request(method, url, data = {}) {
+const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
+const PRACTICE_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api");
+
+// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
+function request(method, url, data = {}, baseUrl = BASE_URL) {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -31,16 +37,21 @@ function request(method, url, data = {}) {
if (token) header.Authorization = `Bearer ${token || ""}`;
return new Promise((resolve, reject) => {
uni.request({
- url: `${BASE_URL}${url}`,
+ url: `${baseUrl}${url}`,
method,
header,
data,
timeout: 10000,
success: (res) => {
+ if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
+ resolve({});
+ return;
+ }
if (res.data) {
const {code, data, message} = res.data;
if (code === 0) resolve(data);
else if (message) {
+ const error = {code, data, message};
if (message.indexOf("登录身份已失效") !== -1) {
console.log('1111111111111111111,token失效')
uni.removeStorageSync(
@@ -50,6 +61,10 @@ function request(method, url, data = {}) {
reject({ type: "AUTH_INVALID", message });
return;
}
+ if (message.indexOf("已达上限") !== -1) {
+ reject(error);
+ return;
+ }
if (message === "ROOM_FULL") {
resolve({full: true});
return;
@@ -97,8 +112,10 @@ function request(method, url, data = {}) {
title: message,
icon: "none",
});
+ reject(error);
+ return;
}
- reject("");
+ reject({code, data, message});
}
},
fail: (err) => {
@@ -167,6 +184,10 @@ export const getAppConfig = () => {
return request("GET", "/index/appConfig");
};
+export const getDailyCountAPI = () => {
+ return request("GET", "/index/dailyCount", {}, ADDONS_BASE_URL);
+};
+
export const getHomeData = (seasonId) => {
return request("GET", `/user/myHome?seasonId=${seasonId}`);
};
@@ -243,12 +264,21 @@ export const createPractiseAPI = (arrows, time, target) => {
});
};
-export const startPractiseAPI = () => {
- return request("POST", "/user/practice/begin");
+export const createPractiseV2API = (arrows, time, target, deviceId) => {
+ return request("POST", "/practice/create-v2", {
+ shootNumber: arrows,
+ shootTime: time,
+ targetType: Number(target || 1) * 20,
+ deviceId,
+ }, PRACTICE_BASE_URL);
};
-export const endPractiseAPI = () => {
- return request("POST", "/user/practice/stop");
+export const startPractiseAPI = (id) => {
+ return request("POST", "/user/practice/begin", { id });
+};
+
+export const endPractiseAPI = (id) => {
+ return request("POST", "/user/practice/stop", { id });
};
export const getPractiseAPI = async (id) => {
@@ -342,9 +372,23 @@ export const createOrderAPI = (vipId) => {
quanity: 1,
tradeType: "mini",
payType: "wxpay",
+ returnUrl: "",
+ remark: "",
+ mockTest: false,
});
};
+export const virtualPayOrderAPI = (vipId = 0, code = "") => {
+ return request("POST", "/user/virtualPay/createOrder", {
+ vipId,
+ code,
+ });
+};
+
+export const getOrderDetailAPI = (orderId) => {
+ return request("GET", `/user/order/detail?orderId=${encodeURIComponent(orderId)}`);
+};
+
export const payOrderAPI = (id) => {
return request("POST", "/user/order/pay", {
id,
@@ -466,6 +510,26 @@ export const getDeviceBatteryAPI = async () => {
return request("GET", "/user/device/battery");
};
+// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
+export const connectDeviceWifiAPI = async (ssid, password) => {
+ return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
+};
+
+// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
+export const getHardwareBoxVersionAPI = async () => {
+ return request("GET", "/user/hardwareBox/version");
+};
+
+// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
+export const sendHardwareBoxUpdateAPI = async (data) => {
+ return request("POST", "/user/hardwareBox/sendUpdate", data);
+};
+
+// 根据任务 ID 获取硬件盒子 OTA 更新状态。
+export const getHardwareBoxTaskStatusAPI = async (taskId) => {
+ return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`);
+};
+
export const addNoteAPI = async (id, remark) => {
return request("POST", "/user/score/sheet/remark", {id, remark});
};
@@ -478,6 +542,10 @@ export const getPhoneNumberAPI = (data) => {
return request("POST", "/index/getPhone", data);
};
+export const getPhoneNumberAPIv2 = (data) => {
+ return request("POST", "/index/getPhone/v2", data);
+};
+
export const getPointBookRankListAPI = (page = 1) => {
return request(
"GET",
@@ -506,9 +574,10 @@ export const getReadyAPI = (roomId) => {
};
export const getBattleAPI = async (battleId) => {
- return request("POST", "/user/match/info", {
+ const result = await request("POST", "/user/match/info", {
id: battleId,
});
+ return normalizeBattleApiResult(result);
};
export const kickPlayerAPI = (number, userId) => {
diff --git a/src/audioManager.js b/src/audioManager.js
index a0f55bb..fc7f899 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -40,6 +40,8 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
比赛开始:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
+ 下半场开始:
+ "https://static.shelingxingqiu.com/shootmini/static/audio/%E4%B8%8B%E5%8D%8A%E5%9C%BA%E5%BC%80%E5%A7%8B.mp3",
请开始射击:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
射击无效:
@@ -93,6 +95,28 @@ export const audioFils = {
"https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3"
};
+const AUDIO_LOAD_TIMEOUT_MS = 5000;
+const AUDIO_WARM_CONCURRENCY = 3;
+const AUDIO_WARM_RETRIES = 1;
+const AUDIO_WARM_PRIORITY_KEYS = [
+ "点击按钮",
+ "比赛开始",
+ "练习开始",
+ "请开始射击",
+ "轮到你了",
+ "比赛结束",
+ "射击无效",
+ "中场休息",
+ "下半场开始",
+ "决金箭轮",
+ "请蓝方射箭",
+ "请红方射箭",
+ "距离合格",
+ "距离不足",
+ "未上靶",
+ "X环",
+];
+
// 版本控制日志函数
function debugLog(...args) {
// 获取当前环境信息
@@ -109,18 +133,15 @@ class AudioManager {
constructor() {
this.audioMap = new Map();
this.currentPlayingKey = null;
- this.maxRetries = 3;
- // 多轮统一重试:最多重试的轮次与每轮间隔
- this.maxRetryRounds = 10;
- this.retryRoundIntervalMs = 1500;
// 显式授权播放标记,防止 iOS 在设置 src 后误播
this.allowPlayMap = new Map();
- // 串行加载相关属性
- this.audioKeys = [];
- this.currentLoadingIndex = 0;
+ // 后台预热与单条加载状态
this.isLoading = false;
- this.loadingPromise = null;
+ this.loadingKeyPromises = new Map();
+ this.pendingPlayKeys = new Set();
+ this.cacheWritePromises = new Map();
+ this.warmAllPromise = null;
// 连续播放队列相关属性
this.sequenceQueue = [];
@@ -131,23 +152,22 @@ class AudioManager {
this.lastPlayKey = null;
this.lastPlayAt = 0;
this.isInterrupted = false;
+ this.interruptedAt = 0;
+ this.interruptionFallbackMs = 5000;
+ this.playWatchdogMs = 8000;
+ this.playWatchdogTimers = new Map();
// 静音开关
this.isMuted = false;
this.pendingPlayKey = null;
- // 新增:就绪状态映射
this.readyMap = new Map();
- // 新增:首轮失败的音频集合与重试阶段标识
- this.failedLoadKeys = new Set();
- // 加载代数,用于 reloadAll 时作废旧的加载循环
+ // 加载代数,用于 reloadAll 时作废旧的异步加载
this.loadGeneration = 0;
// 本地路径缓存 Map: { url: localPath }
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
// 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件)
this.cleanObsoleteCache();
this.bindAudioInterruptionEvents();
-
- this.initAudios();
}
bindAudioInterruptionEvents() {
@@ -157,6 +177,7 @@ class AudioManager {
const begin = () => {
if (this.isInterrupted) return;
this.isInterrupted = true;
+ this.interruptedAt = Date.now();
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
@@ -168,6 +189,7 @@ class AudioManager {
const end = () => {
if (!this.isInterrupted) return;
this.isInterrupted = false;
+ this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll();
};
@@ -209,128 +231,122 @@ class AudioManager {
}
}
- // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
- initAudios() {
- if (this.isLoading) {
- debugLog("音频正在加载中,跳过重复初始化");
- return this.loadingPromise;
- }
- debugLog("开始串行加载音频...");
- this.isLoading = true;
- this.audioKeys = Object.keys(audioFils);
- this.currentLoadingIndex = 0;
- this.failedLoadKeys.clear();
-
- // 增加代数,使得旧的加载循环失效
- this.loadGeneration = (this.loadGeneration || 0) + 1;
- const currentGen = this.loadGeneration;
-
- this.loadingPromise = new Promise((resolve) => {
- const finalize = () => {
- if (currentGen !== this.loadGeneration) return;
- const runRounds = (round) => {
- if (currentGen !== this.loadGeneration) return;
- // 达到最大轮次或没有失败项,收尾
- if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) {
- this.isLoading = false;
- resolve();
- return;
- }
-
- const retryKeys = Array.from(this.failedLoadKeys);
- this.failedLoadKeys.clear();
- debugLog(`开始第 ${round} 轮串行加载,共 ${retryKeys.length} 个`);
-
- this.loadKeysSequentially(
- retryKeys,
- () => {
- if (currentGen !== this.loadGeneration) return;
- // 如仍有失败项,继续下一轮;否则结束
- if (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) {
- setTimeout(
- () => runRounds(round + 1),
- this.retryRoundIntervalMs
- );
- } else {
- this.isLoading = false;
- resolve();
- }
- },
- currentGen
- );
- };
-
- // 启动第 1 轮重试(如有失败项)
- runRounds(1);
- };
- this.loadNextAudio(finalize, currentGen);
- });
- return this.loadingPromise;
+ getWarmupKeys() {
+ return Array.from(
+ new Set([...AUDIO_WARM_PRIORITY_KEYS, ...Object.keys(audioFils)])
+ );
}
- // 按自定义列表串行加载音频(避免并发过多)
- loadKeysSequentially(keys, onComplete, gen) {
- if (gen !== undefined && gen !== this.loadGeneration) return;
- let idx = 0;
- const list = Array.from(keys);
- const next = () => {
- if (gen !== undefined && gen !== this.loadGeneration) return;
- if (idx >= list.length) {
- if (onComplete) onComplete();
- return;
- }
- const k = list[idx++];
+ ensureAudio(key) {
+ if (!audioFils[key]) return Promise.resolve(false);
+ if (this.readyMap.get(key) && this.audioMap.has(key)) {
+ return Promise.resolve(true);
+ }
- // 已就绪的音频不再重载,避免把 ready 状态重置为 false
- if (this.readyMap.get(k)) {
- setTimeout(next, 50);
- return;
- }
+ const loadingPromise = this.loadingKeyPromises.get(key);
+ if (loadingPromise) return loadingPromise;
- // 未就绪:已存在则重载;不存在则创建
- if (this.audioMap.has(k)) {
- this.retryLoadAudio(k);
- setTimeout(next, 100);
- } else {
- this.createAudio(k, () => {
- setTimeout(next, 100);
+ let promise;
+ promise = new Promise((resolve) => {
+ try {
+ this.createAudio(key, () => {
+ resolve(this.readyMap.get(key) === true && this.audioMap.has(key));
});
- return; // createAudio 内部会触发 next
+ } catch (error) {
+ debugLog(`音频 ${key} 创建失败`, error);
+ resolve(false);
+ }
+ }).finally(() => {
+ if (this.loadingKeyPromises.get(key) === promise) {
+ this.loadingKeyPromises.delete(key);
+ }
+ });
+ this.loadingKeyPromises.set(key, promise);
+ return promise;
+ }
+
+ async warmKeys(
+ keys,
+ { concurrency = AUDIO_WARM_CONCURRENCY, retries = AUDIO_WARM_RETRIES } = {}
+ ) {
+ const queue = Array.from(new Set(keys)).filter((key) => !!audioFils[key]);
+ if (!queue.length) return;
+
+ let index = 0;
+ const worker = async () => {
+ while (index < queue.length) {
+ const key = queue[index++];
+ let ready = await this.ensureAudio(key);
+ for (let attempt = 0; !ready && attempt < retries; attempt += 1) {
+ ready = await this.retryLoadAudio(key);
+ }
}
};
- next();
+
+ const workerCount = Math.min(Math.max(1, concurrency), queue.length);
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
}
- // 串行加载下一个音频(首轮)
- loadNextAudio(onComplete, gen) {
- if (gen !== undefined && gen !== this.loadGeneration) return;
- if (this.currentLoadingIndex >= this.audioKeys.length) {
- debugLog("首轮加载遍历完成", this.currentLoadingIndex);
- if (onComplete) onComplete();
+ warmButton() {
+ return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 });
+ }
+
+ warmAll() {
+ if (this.warmAllPromise) return this.warmAllPromise;
+
+ debugLog("开始后台分级预热全部语音");
+ this.isLoading = true;
+ this.warmAllPromise = this.warmKeys(this.getWarmupKeys())
+ .catch((error) => {
+ debugLog("后台语音预热异常", error);
+ })
+ .finally(() => {
+ this.isLoading = false;
+ debugLog("后台语音预热结束", this.getLoadProgress());
+ });
+ return this.warmAllPromise;
+ }
+
+ // 保留旧入口兼容测试页或其他历史调用,实际行为改为非阻塞后台预热。
+ initAudios() {
+ return this.warmAll();
+ }
+
+ // 创建单个音频实例:缓存命中优先使用,未命中直接使用 CDN。
+ createAudio(key, callback) {
+ const src = audioFils[key];
+ const generation = this.loadGeneration;
+ let completed = false;
+
+ const complete = () => {
+ if (completed) return;
+ completed = true;
+ if (callback) callback();
+ };
+
+ if (!src) {
+ complete();
return;
}
- const key = this.audioKeys[this.currentLoadingIndex];
- debugLog(
- `开始加载音频 ${this.currentLoadingIndex + 1}/${
- this.audioKeys.length
- }: ${key}`
- );
- this.createAudio(key, () => {
- setTimeout(() => {
- this.loadNextAudio(onComplete, gen);
- }, 100);
- });
- }
- // 创建单个音频实例(支持本地缓存)
- createAudio(key, callback) {
- this.currentLoadingIndex++;
- const src = audioFils[key];
+ const existingAudio = this.audioMap.get(key);
+ if (existingAudio && !this.readyMap.get(key)) {
+ try {
+ existingAudio.destroy();
+ } catch (_) {}
+ this.audioMap.delete(key);
+ }
+
+ const isCurrentGeneration = () => generation === this.loadGeneration;
+
+ const setupAudio = (realSrc, resumeAfterReady = false) => {
+ if (!isCurrentGeneration()) {
+ complete();
+ return;
+ }
- const setupAudio = (realSrc) => {
const audio = uni.createInnerAudioContext();
audio.autoplay = false;
- audio.src = realSrc;
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
@@ -347,115 +363,185 @@ class AudioManager {
}
});
- const loadTimeout = setTimeout(() => {
- debugLog(`音频 ${key} 加载超时`);
- this.recordLoadFailure(key);
+ let loadTimeout = null;
+ let readyHandled = false;
+ const clearLoadTimeout = () => {
+ if (!loadTimeout) return;
+ clearTimeout(loadTimeout);
+ loadTimeout = null;
+ };
+
+ const handleLoadFailure = (message) => {
+ if (this.audioMap.get(key) !== audio) return;
+ clearLoadTimeout();
+ debugLog(`音频 ${key} ${message}`);
+
+ const isLocalCache =
+ realSrc !== src && this.localFileCache[src] === realSrc;
+ const shouldResume =
+ this.allowPlayMap.get(key) === true || this.currentPlayingKey === key;
+ const shouldResumeDirectly = shouldResume && completed;
+
+ this.clearPlayWatchdog(key);
+ this.allowPlayMap.set(key, false);
+ if (this.currentPlayingKey === key) {
+ this.currentPlayingKey = null;
+ }
+ if (this.lastPlayKey === key) {
+ this.lastPlayKey = null;
+ this.lastPlayAt = 0;
+ }
+ this.readyMap.set(key, false);
+ this.audioMap.delete(key);
try {
audio.destroy();
} catch (_) {}
- if (callback) callback();
- }, 10000);
+
+ if (isLocalCache && isCurrentGeneration()) {
+ debugLog(`本地缓存失效,立即切换 CDN: ${key}`);
+ delete this.localFileCache[src];
+ uni.setStorageSync("audio_local_files", this.localFileCache);
+ uni.removeSavedFile({ filePath: realSrc });
+ setupAudio(src, shouldResumeDirectly);
+ void this.cacheRemoteFile(src, key);
+ return;
+ }
+
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
+ complete();
+ };
+
+ loadTimeout = setTimeout(() => {
+ handleLoadFailure("加载超时");
+ }, AUDIO_LOAD_TIMEOUT_MS);
audio.onCanplay(() => {
+ if (this.audioMap.get(key) !== audio) return;
+ if (readyHandled) return;
+ readyHandled = true;
+ if (!isCurrentGeneration()) {
+ clearLoadTimeout();
+ this.audioMap.delete(key);
+ try {
+ audio.destroy();
+ } catch (_) {}
+ complete();
+ return;
+ }
if (!this.allowPlayMap.get(key)) {
try {
audio.pause();
} catch (_) {}
}
- clearTimeout(loadTimeout);
+ clearLoadTimeout();
this.readyMap.set(key, true);
- this.failedLoadKeys.delete(key);
- // debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress());
uni.$emit("audioLoaded", key);
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
loadedAudioKeys[key] = true;
uni.setStorageSync("loadedAudioKeys", loadedAudioKeys);
- if (callback) callback();
+ complete();
+
+ if (resumeAfterReady) {
+ setTimeout(() => {
+ const isSequenceCurrent =
+ this.isSequenceRunning &&
+ this.sequenceQueue[this.sequenceIndex] === key;
+ if (isSequenceCurrent) this._playSingle(key, false);
+ }, 0);
+ }
});
audio.onError((res) => {
- clearTimeout(loadTimeout);
- debugLog(`音频 ${key} 加载失败:`, res.errMsg);
- // 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
- if (realSrc !== src && this.localFileCache[src] === realSrc) {
- debugLog(`本地缓存失效,移除记录: ${key}`);
- delete this.localFileCache[src];
- uni.setStorageSync("audio_local_files", this.localFileCache);
- // 移除文件
- uni.removeSavedFile({ filePath: realSrc });
- }
- this.recordLoadFailure(key);
- this.audioMap.delete(key);
- audio.destroy();
- if (this.readyMap.get(key)) {
- // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
- // this.readyMap.set(key, false);
- } else {
- if (callback) callback();
- }
+ handleLoadFailure(`加载失败:${res.errMsg || "unknown"}`);
});
audio.onEnded(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
- this.onAudioEnded(key);
- uni.$emit('audioEnded', key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
});
audio.onStop(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key);
});
this.audioMap.set(key, audio);
+ audio.src = realSrc;
};
// 检查是否有可用的本地缓存
this.checkLocalFile(src).then((localPath) => {
+ if (!isCurrentGeneration()) {
+ complete();
+ return;
+ }
if (localPath) {
debugLog(`命中本地缓存: ${key}`);
setupAudio(localPath);
} else {
- // 下载并尝试保存
- uni.downloadFile({
- url: src,
- timeout: 20000,
- success: (res) => {
- if (res.tempFilePath) {
- // 尝试保存文件到本地存储(持久化)
- uni.getFileSystemManager().saveFile({
- tempFilePath: res.tempFilePath,
- success: (saveRes) => {
- const savedPath = saveRes.savedFilePath;
- this.localFileCache[src] = savedPath;
- uni.setStorageSync("audio_local_files", this.localFileCache);
- debugLog(`音频已缓存到本地: ${key}`);
- setupAudio(savedPath);
- },
- fail: (err) => {
- debugLog(
- `保存音频失败(可能空间不足),使用临时文件: ${key}`,
- err
- );
- setupAudio(res.tempFilePath);
- },
- });
- } else {
- this.recordLoadFailure(key);
- if (callback) callback();
- }
- },
- fail: () => {
- this.recordLoadFailure(key);
- if (callback) callback();
- },
- });
+ setupAudio(src);
+ void this.cacheRemoteFile(src, key);
+ }
+ }).catch(() => {
+ setupAudio(src);
+ void this.cacheRemoteFile(src, key);
+ });
+ }
+
+ cacheRemoteFile(url, key) {
+ if (this.localFileCache[url]) return Promise.resolve(true);
+ const currentPromise = this.cacheWritePromises.get(url);
+ if (currentPromise) return currentPromise;
+
+ let promise;
+ promise = new Promise((resolve) => {
+ if (typeof uni.downloadFile !== "function") {
+ resolve(false);
+ return;
+ }
+
+ uni.downloadFile({
+ url,
+ timeout: 20000,
+ success: (res) => {
+ if (!res.tempFilePath) {
+ resolve(false);
+ return;
+ }
+
+ let fileSystemManager;
+ try {
+ fileSystemManager = uni.getFileSystemManager();
+ } catch (_) {
+ resolve(false);
+ return;
+ }
+
+ fileSystemManager.saveFile({
+ tempFilePath: res.tempFilePath,
+ success: (saveRes) => {
+ this.localFileCache[url] = saveRes.savedFilePath;
+ uni.setStorageSync("audio_local_files", this.localFileCache);
+ debugLog(`音频已后台缓存到本地: ${key}`);
+ resolve(true);
+ },
+ fail: () => resolve(false),
+ });
+ },
+ fail: () => resolve(false),
+ });
+ }).finally(() => {
+ if (this.cacheWritePromises.get(url) === promise) {
+ this.cacheWritePromises.delete(url);
}
});
+
+ this.cacheWritePromises.set(url, promise);
+ return promise;
}
// 检查本地文件是否有效
@@ -482,20 +568,134 @@ class AudioManager {
});
}
- // 新增:记录失败(首轮与次轮都会用到)
- recordLoadFailure(key) {
- this.failedLoadKeys.add(key);
+ clearPlayWatchdog(key) {
+ const timer = this.playWatchdogTimers.get(key);
+ if (timer) {
+ clearTimeout(timer);
+ this.playWatchdogTimers.delete(key);
+ }
+ }
+
+ clearAllPlayWatchdogs() {
+ for (const timer of this.playWatchdogTimers.values()) {
+ clearTimeout(timer);
+ }
+ this.playWatchdogTimers.clear();
+ }
+
+ startPlayWatchdog(key) {
+ this.clearPlayWatchdog(key);
+ const timer = setTimeout(() => {
+ if (this.currentPlayingKey !== key) return;
+ debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }, this.playWatchdogMs);
+ this.playWatchdogTimers.set(key, timer);
+ }
+
+ finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) {
+ const wasCurrent = this.currentPlayingKey === key;
+ const isSequenceCurrent =
+ this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
+
+ this.clearPlayWatchdog(key);
+ this.allowPlayMap.set(key, false);
+
+ if (!force && !wasCurrent && !isSequenceCurrent) return false;
+
+ if (wasCurrent) {
+ this.currentPlayingKey = null;
+ }
+
+ if (advanceSequence && isSequenceCurrent) {
+ this.onAudioEnded(key);
+ }
+
+ if (emitEnded) {
+ uni.$emit("audioEnded", key);
+ }
+
+ return true;
+ }
+
+ recoverFromInterruptionIfStale(force = false) {
+ if (!this.isInterrupted) return false;
+ const interruptedFor = Date.now() - (this.interruptedAt || Date.now());
+ if (!force && interruptedFor < this.interruptionFallbackMs) return false;
+
+ debugLog("音频中断状态超时,执行兜底恢复");
+ this.isInterrupted = false;
+ this.interruptedAt = 0;
+ uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
+ void this.reloadAll();
+ return true;
+ }
+
+ recoverIfStale(expectedKey) {
+ if (this.recoverFromInterruptionIfStale(true)) return;
+
+ const key =
+ expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex];
+ if (!key) {
+ if (this.isSequenceRunning) {
+ this.sequenceQueue = [];
+ this.sequenceIndex = 0;
+ this.isSequenceRunning = false;
+ }
+ return;
+ }
+
+ const isStaleCurrent =
+ this.currentPlayingKey === key ||
+ (this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key);
+ if (!isStaleCurrent) return;
+
+ debugLog(`音频 ${key} 等待超时,执行轻量恢复`);
+ const audio = this.audioMap.get(key);
+ if (audio) {
+ try {
+ audio.stop();
+ } catch (_) {}
+ }
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }
+
+ reloadAudioKey(key) {
+ return this.retryLoadAudio(key);
}
// 重新加载音频
retryLoadAudio(key) {
+ const loadingPromise = this.loadingKeyPromises.get(key);
+ if (loadingPromise) return loadingPromise;
+
+ this.clearPlayWatchdog(key);
const oldAudio = this.audioMap.get(key);
- if (oldAudio) oldAudio.destroy();
- this.createAudio(key);
+ if (oldAudio) {
+ try {
+ oldAudio.destroy();
+ } catch (_) {}
+ }
+ this.audioMap.delete(key);
+ this.readyMap.set(key, false);
+ return this.ensureAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog("音频处理中断状态,忽略播放请求");
return;
@@ -551,8 +751,37 @@ class AudioManager {
}
}
+ waitForAudioAndPlay(key) {
+ if (this.pendingPlayKeys.has(key)) return;
+ this.pendingPlayKeys.add(key);
+
+ this.ensureAudio(key)
+ .then((ready) => {
+ const isSequenceCurrent =
+ this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
+ if (!isSequenceCurrent) return;
+
+ if (ready) {
+ this._playSingle(key, false);
+ return;
+ }
+
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ })
+ .finally(() => {
+ this.pendingPlayKeys.delete(key);
+ });
+ }
+
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog(`音频处理中断状态,跳过播放: ${key}`);
return;
@@ -561,6 +790,11 @@ class AudioManager {
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
return;
}
@@ -574,7 +808,7 @@ class AudioManager {
}
const audio = this.audioMap.get(key);
- if (audio) {
+ if (audio && this.readyMap.get(key)) {
// 播放前确保遵循当前静音状态
try {
if (typeof audio.volume === "number") {
@@ -603,31 +837,21 @@ class AudioManager {
try {
audio.play();
} catch (err) {
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
return;
}
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
+ this.startPlayWatchdog(key);
} else {
- debugLog(`音频 ${key} 不存在,尝试重新加载...`);
- this.retryLoadAudio(key);
- const handler = (loadedKey) => {
- if (loadedKey === key) {
- try {
- uni.$off("audioLoaded", handler);
- } catch (_) {}
- // 再次校验是否存在且就绪
- const a = this.audioMap.get(key);
- if (a && this.readyMap.get(key)) {
- this._playSingle(key, false);
- }
- }
- };
- try {
- uni.$on("audioLoaded", handler);
- } catch (_) {}
+ debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`);
+ this.waitForAudioAndPlay(key);
}
}
@@ -653,6 +877,7 @@ class AudioManager {
// 停止指定音频
stop(key) {
const audio = this.audioMap.get(key);
+ this.clearPlayWatchdog(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
@@ -664,6 +889,7 @@ class AudioManager {
// 停止所有音频
stopAll() {
+ this.clearAllPlayWatchdogs();
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
@@ -718,9 +944,11 @@ class AudioManager {
debugLog("本地音频缓存清理完成");
}
- // 手动重置并重新加载所有音频(用于卡住时恢复)
+ // 手动重置音频实例,并按之前的预热范围在后台恢复。
reloadAll() {
- debugLog("执行 reloadAll: 重置所有状态并重新加载");
+ debugLog("执行 reloadAll: 重置音频实例并后台恢复");
+ const shouldWarmAll = this.warmAllPromise !== null;
+ this.loadGeneration += 1;
// 1. 停止所有播放
this.stopAll();
@@ -735,8 +963,10 @@ class AudioManager {
// 3. 重置状态
this.readyMap.clear();
- this.failedLoadKeys.clear();
this.allowPlayMap.clear();
+ this.loadingKeyPromises.clear();
+ this.pendingPlayKeys.clear();
+ this.clearAllPlayWatchdogs();
this.currentPlayingKey = null;
this.sequenceQueue = [];
this.sequenceIndex = 0;
@@ -746,13 +976,12 @@ class AudioManager {
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
// this.clearCache();
- // 4. 强制重置加载锁
+ // 4. 重置后台预热状态
this.isLoading = false;
- this.loadingPromise = null;
- this.currentLoadingIndex = 0;
+ this.warmAllPromise = null;
- // 5. 重新初始化 (initAudios 会自增 loadGeneration,从而终止之前的任何异步循环)
- return this.initAudios();
+ // 5. 之前已启动过全量预热则继续全量恢复,否则只恢复按钮音效。
+ return shouldWarmAll ? this.warmAll() : this.warmButton();
}
}
diff --git a/src/canvas.js b/src/canvas.js
index e5e41f4..7c5b66b 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -456,23 +456,29 @@ export const generateShareImage = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
// 顶部导入与工具方法
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
- const { node: canvas } = res[0] || {};
+ const canvasInfo = res && res[0];
+ const { node: canvas } = canvasInfo || {};
+ if (!canvas || typeof canvas.getContext !== "function") {
+ reject(new Error(`canvas ${canvasId} not found`));
+ return;
+ }
const ctx = canvas.getContext("2d");
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
- const w = targetWidth || res[0].width;
- const h = targetHeight || res[0].height;
+ const w = targetWidth || canvasInfo.width;
+ const h = targetHeight || canvasInfo.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
@@ -561,6 +567,7 @@ export const sharePointData = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
@@ -778,6 +785,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
// 2D 模式下无需 ctx.draw()
} catch (err) {
console.log(err);
+ throw err;
}
}
diff --git a/src/components/AppBackground.vue b/src/components/AppBackground.vue
index 68c0cec..6974cbf 100644
--- a/src/components/AppBackground.vue
+++ b/src/components/AppBackground.vue
@@ -77,10 +77,16 @@ const props = defineProps({
/>
+
diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue
index 05f2ebe..f65b8fc 100644
--- a/src/components/AppFooter.vue
+++ b/src/components/AppFooter.vue
@@ -1,14 +1,14 @@
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
players.length > 5 && e.stopPropagation()"
:style="{ paddingTop: showHeader ? '15px' : '0' }"
>
@@ -105,7 +132,16 @@ defineProps({
:size="40"
:rank="showRank ? index + 1 : 0"
/>
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
@@ -116,6 +152,7 @@ defineProps({
.container {
width: 100%;
position: relative;
+ z-index: 999;
margin-bottom: 10px;
}
.container > image:first-child {
@@ -123,6 +160,7 @@ defineProps({
width: 100%;
top: -5px;
z-index: 1;
+ pointer-events: none;
}
.players {
display: flex;
@@ -153,15 +191,12 @@ defineProps({
.players-melee {
height: 80px;
width: 100%;
-}
-.players-melee::-webkit-scrollbar {
- width: 0;
- height: 0;
- color: transparent;
+ white-space: nowrap;
}
/* 小程序 scroll-view 不支持直接 flex,通过内层 wrapper 承载横向排列 */
.players-melee-inner {
- display: flex;
+ display: inline-flex;
+ min-width: 100%;
height: 100%;
flex-wrap: nowrap;
}
@@ -172,7 +207,7 @@ defineProps({
justify-content: center;
color: #fff9;
font-size: 12px;
- padding-top: 7px;
+ /* padding-top: 7px; */
flex: 0 0 auto;
}
.player-name {
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis;
text-align: center;
}
+view.player-name {
+ justify-content: center;
+}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 12px;
+}
.left-winner-badge {
position: absolute;
width: 50px;
diff --git a/src/components/BowData.vue b/src/components/BowData.vue
index 7854645..409f98f 100644
--- a/src/components/BowData.vue
+++ b/src/components/BowData.vue
@@ -1,4 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index 1a26f5a..7da471d 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -1,6 +1,15 @@
-
+
-
+
@@ -270,8 +443,7 @@ onBeforeUnmount(() => {
v-if="latestOne"
class="round-tip fade-in-out"
:style="getRoundTipStyle(latestOne)"
- >{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
- }}环
+ >{{ getRoundText(latestOne) }}
{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
- }}环{{ getRoundText(bluelatestOne) }}
+
{
>
+
{
{{ index + 1 }}
-
+
+
diff --git a/src/components/Guide.vue b/src/components/Guide.vue
index 29f2433..9cf5c9d 100644
--- a/src/components/Guide.vue
+++ b/src/components/Guide.vue
@@ -12,13 +12,13 @@ defineProps({
const bubbleTypes = [
"../static/long-bubble.png",
"../static/long-bubble-middle.png",
- "../static/long-bubble-tall.png",
+ "https://static.shelingxingqiu.com/shootmini/static/long-bubble-tall.png",
];
-
+
({}),
+ },
});
const onClick = () => {
@@ -55,6 +59,9 @@ const signin = () => {
}
};
+const isSVip = computed(() => user.value.sVip === true);
+const isVip = computed(() => user.value.vip === true && user.value.sVip !== true);
+
const loading = ref(false);
const pointBook = ref(null);
const heat = ref(0);
@@ -115,7 +122,9 @@ onBeforeUnmount(() => {
mode="widthFix"
/>
-
+
-->
-
+
{{ arrows.length }}
diff --git a/src/pages/my-device.vue b/src/pages/my-device.vue
index c6313b9..c0aab72 100644
--- a/src/pages/my-device.vue
+++ b/src/pages/my-device.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+ 请先开启 WiFi 后刷新重试
+
+
+
+
+
+
+
+
+
+
+ {{ currentState === 'CONNECTED' ? '连接成功' : '连接无线网络' }}
+
+ 网络名称仅支持英文字符及数字
+
+
+ 请先在系统 WiFi 设置中连接目标网络,返回后选择当前 WiFi 并填写密码
+
+
+
+
+ 连接网络
+
+
+
+
+ {{ connectedWifi?.SSID }}
+
+
+
+
+
+
+
+
+
+ 网络
+
+
+
+
+
+
+
+ 其他...
+
+
+
+
+ {{ wifi.SSID }}
+
+
+
+
+
+
+ 其他...
+
+
+
+
+
+
+
+ 开始更新
+
+
+
+
+
+
+
+ 更新中,请稍等片刻...
+
+
+
+
+ {{ Math.floor(progress) }}%
+
+
+
+
+
+
+ 更新完成
+ 请关机并重启智能弓
+
+ 完成
+
+
+
+
+
+
+ 更新失败
+ 请确保:
+ 1、智能弓已开启
+ 2、网路连接稳定
+
+ 重试
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 密码
+
+
+
+
+
+
+ {{ connectStatusText }}
+ {{ connectError }}
+
+
+
+
+
+ {{ connectStatusText }}
+ 该网络为开放网络,点击 ✓ 加入
+ {{ connectError }}
+
+
+
+
+
+
+ 名称
+
+
+
+ 密码
+
+
+
+
+
+
+ {{ ssidWarning }}
+ {{ connectStatusText }}
+ {{ connectError }}
+
+
+
+
+
+
+
+
+
+ 拒绝授权获取wifi列表失败,请重新授权或手动输入
+
+
+ 取消授权
+
+
+ 重新授权
+
+
+
+
+
+
+
+
diff --git a/src/pages/point-book-detail.vue b/src/pages/point-book-detail.vue
index 74fc1ab..98b2666 100644
--- a/src/pages/point-book-detail.vue
+++ b/src/pages/point-book-detail.vue
@@ -81,9 +81,17 @@ const loading = ref(false);
const shareImage = async () => {
if (loading.value) return;
loading.value = true;
- await generateShareImage("shareImageCanvas", record.value);
- await wxShare("shareImageCanvas");
- loading.value = false;
+ try {
+ await generateShareImage("shareImageCanvas", record.value);
+ await wxShare("shareImageCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ loading.value = false;
+ }
};
onLoad(async (options) => {
diff --git a/src/pages/point-book-rank.vue b/src/pages/point-book-rank.vue
index eeddf96..68de914 100644
--- a/src/pages/point-book-rank.vue
+++ b/src/pages/point-book-rank.vue
@@ -15,11 +15,22 @@ const list = ref([]);
const mine = ref({
averageRing: 0,
});
+const sharing = ref(false);
const shareImage = async () => {
- if (!mine.value.id) return;
- await sharePointData("shareCanvas", mine.value);
- await wxShare("shareCanvas");
+ if (!mine.value.id || sharing.value) return;
+ sharing.value = true;
+ try {
+ await sharePointData("shareCanvas", mine.value);
+ await wxShare("shareCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ sharing.value = false;
+ }
};
onMounted(async () => {
@@ -33,7 +44,7 @@ onMounted(async () => {
-
+
{
/>
{
diff --git a/src/pages/practise-one.vue b/src/pages/practise-one.vue
index e8c210b..28390ad 100644
--- a/src/pages/practise-one.vue
+++ b/src/pages/practise-one.vue
@@ -1,5 +1,5 @@
@@ -151,6 +190,7 @@ onBeforeUnmount(() => {
:bgType="1"
title="个人单组练习"
:showBottom="!start && !scores.length"
+ :onBack="exitPractise"
>
@@ -178,6 +218,7 @@ onBeforeUnmount(() => {
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"
+ :isSvip="isSvip"
/>
{
:total="total"
:onClose="onComplete"
:result="practiseResult"
- :tipSrc="`../static/${
- practiseResult.details.filter(
- (arrow) => arrow.x !== -30 && arrow.y !== -30
- ).length < total
- ? 'un'
- : ''
- }finish-tip.png`"
+ :tipSrc="getResultTipSrc(practiseResult)"
/>
diff --git a/src/pages/practise-two.vue b/src/pages/practise-two.vue
index 0feea11..ef48621 100644
--- a/src/pages/practise-two.vue
+++ b/src/pages/practise-two.vue
@@ -1,5 +1,5 @@
@@ -157,6 +204,7 @@ onBeforeUnmount(() => {
:bgType="1"
title="日常耐力挑战"
:showBottom="!start && !scores.length"
+ :onBack="exitPractise"
>
@@ -179,6 +227,7 @@ onBeforeUnmount(() => {
:currentRound="scores.length"
:totalRound="start ? total : 0"
:scores="scores"
+ :isSvip="isSvip"
/>
{
:rowCount="9"
:onClose="onComplete"
:result="practiseResult"
- :tipSrc="`../static/${
- practiseResult.details.filter(
- (arrow) => arrow.x !== -30 && arrow.y !== -30
- ).length < total
- ? '2un'
- : ''
- }finish-tip.png`"
+ :tipSrc="getResultTipSrc(practiseResult)"
/>
diff --git a/src/pages/practise.vue b/src/pages/practise.vue
index 88fcb12..89030cd 100644
--- a/src/pages/practise.vue
+++ b/src/pages/practise.vue
@@ -1,5 +1,5 @@
-
+
-
-
+
+
{
:rankLvl="seasonStats.rankLvl"
:size="30"
/>
- {{ seasonStats.nickName || user.nickName }}
+
+
+ {{ seasonStats.nickName || user.nickName }}
+
+
+ {{ seasonStats.nickName || user.nickName }}
+
+
{
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.totalScore || 0 }}分
@@ -499,7 +570,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.mvpCount || 0 }}次
@@ -522,7 +599,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.tenRings ?? item.TenRings ?? 0 }}次
@@ -538,6 +621,14 @@ onShow(async () => {
+
diff --git a/src/pages/team-battle.vue b/src/pages/team-battle.vue
index 1a0d802..4778323 100644
--- a/src/pages/team-battle.vue
+++ b/src/pages/team-battle.vue
@@ -49,8 +49,6 @@ const battleWay = ref(0);
const lastToSomeoneShootKey = ref("");
/** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false);
-/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
-const xRingStreaks = ref({});
/**
* 监听设备在线状态,比赛进行中设备离线时弹窗提示用户
@@ -86,7 +84,7 @@ const recoverData = (battleInfo, {force = false, arrowOnly = false} = {}) => {
start.value = 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) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain - 0.2);
@@ -233,31 +231,6 @@ function onNewRound(msg, prevRound) {
}
}
-/**
- * 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
- * @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value)
- * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
- */
-function isTenPlusRing(shot) {
- return !!(shot?.ringX || Number(shot?.ring) >= 10);
-}
-
-function checkAndPlayTententen(shooterId, isTenPlusRingShot) {
- if (!shooterId) return;
- if (isTenPlusRingShot) {
- xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
- // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
- if (xRingStreaks.value[shooterId] >= 3) {
- xRingStreaks.value[shooterId] = 0;
- // nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
- nextTick(() => audioManager.play("tententen", false));
- }
- } else {
- // 低于 10 环或未上靶则重置该玩家的连续计数
- xRingStreaks.value[shooterId] = 0;
- }
-}
-
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
@@ -272,9 +245,9 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
showRoundTip.value = false;
recoverData(msg, {arrowOnly: true});
- // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
- // currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
- checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
+ if (msg.shootData?.threeConsecutive10Rings === true) {
+ nextTick(() => audioManager.play("tententen", false));
+ }
} else if (msg.type === MESSAGETYPESV2.NewRound) {
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
const prevRound = currentRound.value;
diff --git a/src/pages/team-battle/components/BackToGame.vue b/src/pages/team-battle/components/BackToGame.vue
index 748db2e..12a432e 100644
--- a/src/pages/team-battle/components/BackToGame.vue
+++ b/src/pages/team-battle/components/BackToGame.vue
@@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app";
import { getBattleAPI, getUserGameState } from "@/apis";
import { debounce } from "@/util";
+import { returnToBattle } from "@/utils/matchReturn";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -18,12 +19,14 @@ const props = defineProps({
},
});
const loading = ref(false);
+const navigating = ref(false);
/** 统一获取当前环境 token,用于守卫:无有效 token 时不发起接口请求 */
const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => {
+ navigating.value = false;
if (user.value.id && getToken()) {
setTimeout(async () => {
const state = await getUserGameState();
@@ -45,28 +48,30 @@ watch(
}
);
+const navigateOnce = (url) =>
+ new Promise((resolve, reject) => {
+ navigating.value = true;
+ uni.navigateTo({
+ url,
+ success: resolve,
+ fail: (error) => {
+ navigating.value = false;
+ reject(error);
+ },
+ });
+ });
+
const onClick = debounce(async () => {
- if (loading.value) return;
+ if (loading.value || navigating.value) return;
try {
loading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
- await uni.$checkAudio();
- if (result.mode <= 3) {
- uni.navigateTo({
- url: `/pages/team-battle/index?battleId=${result.matchId}`,
- });
- } else {
- uni.navigateTo({
- url: `/pages/melee-battle?battleId=${result.matchId}`,
- });
- }
+ await returnToBattle(result, navigateOnce);
return;
}
if (game.value.roomID) {
- uni.navigateTo({
- url: "/pages/battle-room?roomNumber=" + game.value.roomID,
- });
+ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
} else {
updateGame(false, "");
}
@@ -91,9 +96,9 @@ onBeforeUnmount(() => {
class="back-to-game"
@click="onClick"
>
-
+
-
+
返回进行中的对局
diff --git a/src/pages/team-battle/components/BattleFooter.vue b/src/pages/team-battle/components/BattleFooter.vue
index c7c2442..4e56a27 100644
--- a/src/pages/team-battle/components/BattleFooter.vue
+++ b/src/pages/team-battle/components/BattleFooter.vue
@@ -31,7 +31,7 @@ const normalRounds = computed(() => {
-
+
+
蓝队({{ bluePoints }})
红队({{ redPoints }})
@@ -51,7 +51,7 @@ const normalRounds = computed(() => {
@@ -86,7 +86,7 @@ const normalRounds = computed(() => {
diff --git a/src/pages/team-battle/components/BattleHeader.vue b/src/pages/team-battle/components/BattleHeader.vue
index 633d086..05f8d7d 100644
--- a/src/pages/team-battle/components/BattleHeader.vue
+++ b/src/pages/team-battle/components/BattleHeader.vue
@@ -27,13 +27,21 @@ defineProps({
default: true,
},
});
+
+const getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
+
+const isMember = (player = {}) => player.vip === true || player.sVip === true;
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
@@ -116,6 +151,7 @@ defineProps({
.container {
width: 100%;
position: relative;
+ z-index: 999;
margin-bottom: 10px;
}
.container > image:first-child {
@@ -183,6 +219,13 @@ defineProps({
text-overflow: ellipsis;
text-align: center;
}
+view.player-name {
+ justify-content: center;
+}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 12px;
+}
.left-winner-badge {
position: absolute;
width: 50px;
diff --git a/src/pages/team-battle/components/BowTarget.vue b/src/pages/team-battle/components/BowTarget.vue
index 4555309..d22375d 100644
--- a/src/pages/team-battle/components/BowTarget.vue
+++ b/src/pages/team-battle/components/BowTarget.vue
@@ -1,6 +1,15 @@
-
+
-
+
@@ -287,8 +471,20 @@ onBeforeUnmount(() => {
}}环
+
{
>
+
{
{{ index + 1 }}
-
+
+