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 @@