commit 83a3135b01828ce1612a2a02a1f4fb923d165d2c Author: feie9456 Date: Fri Jun 27 08:42:27 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..8bb7962 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "front-end", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.8.3", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@vue/tsconfig": "^0.7.0", + "sass-embedded": "^1.86.0", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vue-tsc": "^2.2.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..eab4827 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1344 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + axios: + specifier: ^1.8.3 + version: 1.8.3 + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.7.3) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.3(vite@6.2.2(sass-embedded@1.86.0))(vue@3.5.13(typescript@5.7.3)) + '@vue/tsconfig': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + sass-embedded: + specifier: ^1.86.0 + version: 1.86.0 + typescript: + specifier: ~5.7.2 + version: 5.7.3 + vite: + specifier: ^6.2.0 + version: 6.2.2(sass-embedded@1.86.0) + vue-tsc: + specifier: ^2.2.4 + version: 2.2.8(typescript@5.7.3) + +packages: + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.10': + resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.10': + resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.2.4': + resolution: {integrity: sha512-P9xQgtMh71TA7tHTnbDe68zcI+TPnkyyfBIhGaUr4iUEIXN7yI01DyjmmdEwXTk5OlISBJYkoxCVj2dwmHqIkA==} + + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@rollup/rollup-android-arm-eabi@4.36.0': + resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.36.0': + resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.36.0': + resolution: {integrity: sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.36.0': + resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.36.0': + resolution: {integrity: sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.36.0': + resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.36.0': + resolution: {integrity: sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.36.0': + resolution: {integrity: sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + resolution: {integrity: sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + resolution: {integrity: sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.36.0': + resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.36.0': + resolution: {integrity: sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.36.0': + resolution: {integrity: sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.36.0': + resolution: {integrity: sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.36.0': + resolution: {integrity: sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.36.0': + resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@vitejs/plugin-vue@5.2.3': + resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.12': + resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} + + '@volar/source-map@2.4.12': + resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==} + + '@volar/typescript@2.4.12': + resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==} + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.8': + resolution: {integrity: sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vue/tsconfig@0.7.0': + resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + alien-signals@1.0.4: + resolution: {integrity: sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.8.3: + resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + buffer-builder@0.2.0: + resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.10: + resolution: {integrity: sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + rollup@4.36.0: + resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sass-embedded-android-arm64@1.86.0: + resolution: {integrity: sha512-r7MZtlAI2VFUnKE8B5UOrpoE6OGpdf1dIB6ndoxb3oiURgMyfTVU7yvJcL12GGvtVwQ2boCj6dq//Lqq9CXPlQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.86.0: + resolution: {integrity: sha512-NS8v6BCbzskXUMBtzfuB+j2yQMgiwg5edKHTYfQU7gAWai2hkRhS06YNEMff3aRxV0IFInxPRHOobd8xWPHqeA==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-ia32@1.86.0: + resolution: {integrity: sha512-UjfElrGaOTNOnxLZLxf6MFndFIe7zyK+81f83BioZ7/jcoAd6iCHZT8yQMvu8wINyVodPcaXZl8KxlKcl62VAA==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [android] + + sass-embedded-android-riscv64@1.86.0: + resolution: {integrity: sha512-TsqCLxHWLFS2mbpUkL/nge3jSkaPK2VmLkkoi5iO/EQT4SFvm1lNUgPwlLXu9DplZ+aqGVzRS9Y6Psjv+qW7kw==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.86.0: + resolution: {integrity: sha512-8Q263GgwGjz7Jkf7Eghp7NrwqskDL95WO9sKrNm9iOd2re/M48W7RN/lpdcZwrUnEOhueks0RRyYyZYBNRz8Tg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.86.0: + resolution: {integrity: sha512-d8oMEaIweq1tjrb/BT43igDviOMS1TeDpc51QF7vAHkt9drSjPmqEmbqStdFYPAGZj1j0RA4WCRoVl6jVixi/w==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.86.0: + resolution: {integrity: sha512-5NLRtn0ZUDBkfpKOsgLGl9B34po4Qui8Nff/lXTO+YkxBQFX4GoMkYNk9EJqHwoLLzICsxIhNDMMDiPGz7Fdrw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.86.0: + resolution: {integrity: sha512-50A+0rhahRDRkKkv+qS7GDAAkW1VPm2RCX4zY4JWydhV4NwMXr6HbkLnsJ2MGixCyibPh59iflMpNBhe7SEMNg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-arm@1.86.0: + resolution: {integrity: sha512-b6wm0+Il+blJDleRXAqA6JISGMjRb0/thTEg4NWgmiJwUoZjDycj5FTbfYPnLXjCEIMGaYmW3patrJ3JMJcT3Q==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-ia32@1.86.0: + resolution: {integrity: sha512-h0mr9w71TV3BRPk9JHr0flnRCznhkraY14gaj5T+t78vUFByOUMxp4hTr+JpZAR5mv0mIeoMwrQYwWJoqKI0mw==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [linux] + + sass-embedded-linux-musl-arm64@1.86.0: + resolution: {integrity: sha512-5OZjiJIUyhvKJIGNDEjyRUWDe+W91hq4Bji27sy8gdEuDzPWLx4NzwpKwsBUALUfyW/J5dxgi0ZAQnI3HieyQg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-musl-arm@1.86.0: + resolution: {integrity: sha512-KZU70jBMVykC9HzS+o2FhrJaprFLDk3LWXVPtBFxgLlkcQ/apCkUCh2WVNViLhI2U4NrMSnTvd4kDnC/0m8qIw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-ia32@1.86.0: + resolution: {integrity: sha512-vq9wJ7kaELrsNU6Ld6kvrIHxoIUWaD+5T6TQVj4SJP/iw1NjonyCDMQGGs6UgsIEzvaIwtlSlDbRewAq+4PchA==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [linux] + + sass-embedded-linux-musl-riscv64@1.86.0: + resolution: {integrity: sha512-UZJPu4zKe3phEzoSVRh5jcSicBBPe+jEbVNALHSSz881iOAYnDQXHITGeQ4mM1/7e/LTyryHk6EPBoaLOv6JrA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-musl-x64@1.86.0: + resolution: {integrity: sha512-8taAgbWMk4QHneJcouWmWZJlmKa2O03g4I/CFo4bfMPL87bibY90pAsSDd+C+t81g0+2aK0/lY/BoB0r3qXLiA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-linux-riscv64@1.86.0: + resolution: {integrity: sha512-yREY6o2sLwiiA03MWHVpnUliLscz0flEmFW/wzxYZJDqg9eZteB3hUWgZD63eLm2PTZsYxDQpjAHpa48nnIEmA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-x64@1.86.0: + resolution: {integrity: sha512-sH0F8np9PTgTbFcJWxfr1NzPkL5ID2NcpMtZyKPTdnn9NkE/L2UwXSo6xOvY0Duc4Hg+58wSrDnj6KbvdeHCPg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-win32-arm64@1.86.0: + resolution: {integrity: sha512-4O1XVUxLTIjMOvrziYwEZgvFqC5sF6t0hTAPJ+h2uiAUZg9Joo0PvuEedXurjISgDBsb5W5DTL9hH9q1BbP4cQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-ia32@1.86.0: + resolution: {integrity: sha512-zuSP2axkGm4VaJWt38P464H+4424Swr9bzFNfbbznxe3Ue4RuqSBqwiLiYdg9Q1cecTQ2WGH7G7WO56KK7WLwg==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [win32] + + sass-embedded-win32-x64@1.86.0: + resolution: {integrity: sha512-GVX0CHtukr3kjqfqretSlPiJzV7V4JxUjpRZV+yC9gUMTiDErilJh2Chw1r0+MYiYvumCDUSDlticmvJs7v0tA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.86.0: + resolution: {integrity: sha512-Ibq5DzxjSf9f/IJmKeHVeXlVqiZWdRJF+RXy6v6UupvMYVMU5Ei+teSFBvvpPD5bB2QhhnU/OJlSM0EBCtfr9g==} + engines: {node: '>=16.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.1.3: + resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} + engines: {node: '>=16.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vite@6.2.2: + resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-tsc@2.2.8: + resolution: {integrity: sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.10': + dependencies: + '@babel/types': 7.26.10 + + '@babel/types@7.26.10': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bufbuild/protobuf@2.2.4': {} + + '@esbuild/aix-ppc64@0.25.1': + optional: true + + '@esbuild/android-arm64@0.25.1': + optional: true + + '@esbuild/android-arm@0.25.1': + optional: true + + '@esbuild/android-x64@0.25.1': + optional: true + + '@esbuild/darwin-arm64@0.25.1': + optional: true + + '@esbuild/darwin-x64@0.25.1': + optional: true + + '@esbuild/freebsd-arm64@0.25.1': + optional: true + + '@esbuild/freebsd-x64@0.25.1': + optional: true + + '@esbuild/linux-arm64@0.25.1': + optional: true + + '@esbuild/linux-arm@0.25.1': + optional: true + + '@esbuild/linux-ia32@0.25.1': + optional: true + + '@esbuild/linux-loong64@0.25.1': + optional: true + + '@esbuild/linux-mips64el@0.25.1': + optional: true + + '@esbuild/linux-ppc64@0.25.1': + optional: true + + '@esbuild/linux-riscv64@0.25.1': + optional: true + + '@esbuild/linux-s390x@0.25.1': + optional: true + + '@esbuild/linux-x64@0.25.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.1': + optional: true + + '@esbuild/netbsd-x64@0.25.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.1': + optional: true + + '@esbuild/openbsd-x64@0.25.1': + optional: true + + '@esbuild/sunos-x64@0.25.1': + optional: true + + '@esbuild/win32-arm64@0.25.1': + optional: true + + '@esbuild/win32-ia32@0.25.1': + optional: true + + '@esbuild/win32-x64@0.25.1': + optional: true + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@rollup/rollup-android-arm-eabi@4.36.0': + optional: true + + '@rollup/rollup-android-arm64@4.36.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.36.0': + optional: true + + '@rollup/rollup-darwin-x64@4.36.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.36.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.36.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.36.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.36.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.36.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.36.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.36.0': + optional: true + + '@types/estree@1.0.6': {} + + '@vitejs/plugin-vue@5.2.3(vite@6.2.2(sass-embedded@1.86.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + vite: 6.2.2(sass-embedded@1.86.0) + vue: 3.5.13(typescript@5.7.3) + + '@volar/language-core@2.4.12': + dependencies: + '@volar/source-map': 2.4.12 + + '@volar/source-map@2.4.12': {} + + '@volar/typescript@2.4.12': + dependencies: + '@volar/language-core': 2.4.12 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.10 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.26.10 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.3 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.8(typescript@5.7.3)': + dependencies: + '@volar/language-core': 2.4.12 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 1.0.4 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.7.3 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.7.3) + + '@vue/shared@3.5.13': {} + + '@vue/tsconfig@0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))': + optionalDependencies: + typescript: 5.7.3 + vue: 3.5.13(typescript@5.7.3) + + alien-signals@1.0.4: {} + + asynckit@0.4.0: {} + + axios@1.8.3: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + buffer-builder@0.2.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + colorjs.io@0.5.2: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + csstype@3.1.3: {} + + de-indent@1.0.2: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 + + estree-walker@2.0.2: {} + + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + immutable@5.0.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + muggle-string@0.4.1: {} + + nanoid@3.3.10: {} + + path-browserify@1.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.10 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + rollup@4.36.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.36.0 + '@rollup/rollup-android-arm64': 4.36.0 + '@rollup/rollup-darwin-arm64': 4.36.0 + '@rollup/rollup-darwin-x64': 4.36.0 + '@rollup/rollup-freebsd-arm64': 4.36.0 + '@rollup/rollup-freebsd-x64': 4.36.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.36.0 + '@rollup/rollup-linux-arm-musleabihf': 4.36.0 + '@rollup/rollup-linux-arm64-gnu': 4.36.0 + '@rollup/rollup-linux-arm64-musl': 4.36.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.36.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.36.0 + '@rollup/rollup-linux-riscv64-gnu': 4.36.0 + '@rollup/rollup-linux-s390x-gnu': 4.36.0 + '@rollup/rollup-linux-x64-gnu': 4.36.0 + '@rollup/rollup-linux-x64-musl': 4.36.0 + '@rollup/rollup-win32-arm64-msvc': 4.36.0 + '@rollup/rollup-win32-ia32-msvc': 4.36.0 + '@rollup/rollup-win32-x64-msvc': 4.36.0 + fsevents: 2.3.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sass-embedded-android-arm64@1.86.0: + optional: true + + sass-embedded-android-arm@1.86.0: + optional: true + + sass-embedded-android-ia32@1.86.0: + optional: true + + sass-embedded-android-riscv64@1.86.0: + optional: true + + sass-embedded-android-x64@1.86.0: + optional: true + + sass-embedded-darwin-arm64@1.86.0: + optional: true + + sass-embedded-darwin-x64@1.86.0: + optional: true + + sass-embedded-linux-arm64@1.86.0: + optional: true + + sass-embedded-linux-arm@1.86.0: + optional: true + + sass-embedded-linux-ia32@1.86.0: + optional: true + + sass-embedded-linux-musl-arm64@1.86.0: + optional: true + + sass-embedded-linux-musl-arm@1.86.0: + optional: true + + sass-embedded-linux-musl-ia32@1.86.0: + optional: true + + sass-embedded-linux-musl-riscv64@1.86.0: + optional: true + + sass-embedded-linux-musl-x64@1.86.0: + optional: true + + sass-embedded-linux-riscv64@1.86.0: + optional: true + + sass-embedded-linux-x64@1.86.0: + optional: true + + sass-embedded-win32-arm64@1.86.0: + optional: true + + sass-embedded-win32-ia32@1.86.0: + optional: true + + sass-embedded-win32-x64@1.86.0: + optional: true + + sass-embedded@1.86.0: + dependencies: + '@bufbuild/protobuf': 2.2.4 + buffer-builder: 0.2.0 + colorjs.io: 0.5.2 + immutable: 5.0.3 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-android-arm: 1.86.0 + sass-embedded-android-arm64: 1.86.0 + sass-embedded-android-ia32: 1.86.0 + sass-embedded-android-riscv64: 1.86.0 + sass-embedded-android-x64: 1.86.0 + sass-embedded-darwin-arm64: 1.86.0 + sass-embedded-darwin-x64: 1.86.0 + sass-embedded-linux-arm: 1.86.0 + sass-embedded-linux-arm64: 1.86.0 + sass-embedded-linux-ia32: 1.86.0 + sass-embedded-linux-musl-arm: 1.86.0 + sass-embedded-linux-musl-arm64: 1.86.0 + sass-embedded-linux-musl-ia32: 1.86.0 + sass-embedded-linux-musl-riscv64: 1.86.0 + sass-embedded-linux-musl-x64: 1.86.0 + sass-embedded-linux-riscv64: 1.86.0 + sass-embedded-linux-x64: 1.86.0 + sass-embedded-win32-arm64: 1.86.0 + sass-embedded-win32-ia32: 1.86.0 + sass-embedded-win32-x64: 1.86.0 + + source-map-js@1.2.1: {} + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.1.3 + + sync-message-port@1.1.3: {} + + tslib@2.8.1: {} + + typescript@5.7.3: {} + + varint@6.0.0: {} + + vite@6.2.2(sass-embedded@1.86.0): + dependencies: + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.36.0 + optionalDependencies: + fsevents: 2.3.3 + sass-embedded: 1.86.0 + + vscode-uri@3.1.0: {} + + vue-tsc@2.2.8(typescript@5.7.3): + dependencies: + '@volar/typescript': 2.4.12 + '@vue/language-core': 2.2.8(typescript@5.7.3) + typescript: 5.7.3 + + vue@3.5.13(typescript@5.7.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.7.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.7.3 diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..0ad5e84 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/src/VuplexAPI.ts b/src/VuplexAPI.ts new file mode 100644 index 0000000..961e6ae --- /dev/null +++ b/src/VuplexAPI.ts @@ -0,0 +1,113 @@ +declare const window: any; + +class VuplexEvents { + private vuplex: any; + + constructor() { + console.log(window.vuplex); + + if (!window.vuplex) { + console.error('window.vuplex is not defined'); + Object.assign(window, { + vuplex: { + postMessage: (msg: any) => { +/* console.log(`%c[vuplex]%c ${JSON.stringify(msg)}`, "color: lightgreen; font-style: italic; background-color: orange;padding: 2px", ''); + */ } + } + }); + document.body.style.backgroundColor = 'lightgray'; + } + + // 创建一个代理对象,拦截所有对 postMessage 的调用 + this.vuplex = new Proxy(window.vuplex, { + get: (target, property) => { + if (property === 'postMessage') { + return (msg: any) => { + // 记录到控制台 + console.log(`%c[Vuplex]%c ${JSON.stringify(msg)}`, "background-color: orange; color: white; font-weight: bold; padding: 2px 4px; border-radius: 2px", ''); + // 调用原始方法 + return target.postMessage(msg); + }; + } + return target[property]; + } + }); + } + + selectSkin(index: number) { + this.vuplex.postMessage({ + type: "selectSkin", + index: index + }); + } + + moveCamera(index: number) { + this.vuplex.postMessage({ + type: "moveCamera", + index: index + }); + } + + quitGame() { + this.vuplex.postMessage({ + type: "quit" + }); + } + + createRoom(gameName: string, sceneIndex: number) { + this.vuplex.postMessage({ + type: "createRoom", + gameName, + sceneIndex + }); + } + + joinRoom(gameName: string, sceneIndex: number) { + this.vuplex.postMessage({ + type: "joinRoom", + gameName, + sceneIndex + }); + } + + updateSettings(settings: Record) { + this.vuplex.postMessage({ + type: "updateSettings", + settings + }); + } + + gameStart(data: { + players: { id: number, name: string, skin: number }[], + scene: number + }) { + this.vuplex.postMessage({ + type: "gameStart", + data + }); + } + + facePlayer(playerId: number) { + this.vuplex.postMessage({ + type: "facePlayer", + playerId + }); + } + + playerAction(playerId: number, action: 'speak' | 'vote' | 'dead' |'reasoning') { + this.vuplex.postMessage({ + type: "playerAction", + playerId, + action, + }); + } + + setTime(time: 'day'| 'night'){ + this.vuplex.postMessage({ + type: "setTime", + time + }); + } +} + +export default VuplexEvents; \ No newline at end of file diff --git a/src/assets/AdventureTime.woff2 b/src/assets/AdventureTime.woff2 new file mode 100644 index 0000000..32f1dc3 Binary files /dev/null and b/src/assets/AdventureTime.woff2 differ diff --git a/src/assets/AdventureTimeLogo.woff2 b/src/assets/AdventureTimeLogo.woff2 new file mode 100644 index 0000000..aedfb58 Binary files /dev/null and b/src/assets/AdventureTimeLogo.woff2 differ diff --git a/src/assets/MaShanZheng-Regular.woff2 b/src/assets/MaShanZheng-Regular.woff2 new file mode 100644 index 0000000..dd002e6 Binary files /dev/null and b/src/assets/MaShanZheng-Regular.woff2 differ diff --git a/src/assets/ZCOOLXiaoWei-Regular.woff2 b/src/assets/ZCOOLXiaoWei-Regular.woff2 new file mode 100644 index 0000000..cc82f62 Binary files /dev/null and b/src/assets/ZCOOLXiaoWei-Regular.woff2 differ diff --git a/src/assets/audios/bgm_day_1.ogg b/src/assets/audios/bgm_day_1.ogg new file mode 100644 index 0000000..6754873 Binary files /dev/null and b/src/assets/audios/bgm_day_1.ogg differ diff --git a/src/assets/audios/bgm_day_2.ogg b/src/assets/audios/bgm_day_2.ogg new file mode 100644 index 0000000..4660100 Binary files /dev/null and b/src/assets/audios/bgm_day_2.ogg differ diff --git a/src/assets/audios/bgm_day_3.ogg b/src/assets/audios/bgm_day_3.ogg new file mode 100644 index 0000000..a4b7f15 Binary files /dev/null and b/src/assets/audios/bgm_day_3.ogg differ diff --git a/src/assets/audios/bgm_home.ogg b/src/assets/audios/bgm_home.ogg new file mode 100644 index 0000000..8f89a52 Binary files /dev/null and b/src/assets/audios/bgm_home.ogg differ diff --git a/src/assets/audios/bgm_night_1.ogg b/src/assets/audios/bgm_night_1.ogg new file mode 100644 index 0000000..33a198c Binary files /dev/null and b/src/assets/audios/bgm_night_1.ogg differ diff --git a/src/assets/audios/bgm_night_2.ogg b/src/assets/audios/bgm_night_2.ogg new file mode 100644 index 0000000..563e24e Binary files /dev/null and b/src/assets/audios/bgm_night_2.ogg differ diff --git a/src/assets/audios/bgm_night_3.ogg b/src/assets/audios/bgm_night_3.ogg new file mode 100644 index 0000000..21b90f3 Binary files /dev/null and b/src/assets/audios/bgm_night_3.ogg differ diff --git a/src/assets/audios/bgm_wait.ogg b/src/assets/audios/bgm_wait.ogg new file mode 100644 index 0000000..f10b1f2 Binary files /dev/null and b/src/assets/audios/bgm_wait.ogg differ diff --git a/src/assets/audios/button_down.ogg b/src/assets/audios/button_down.ogg new file mode 100644 index 0000000..05f0bff Binary files /dev/null and b/src/assets/audios/button_down.ogg differ diff --git a/src/assets/audios/button_up.ogg b/src/assets/audios/button_up.ogg new file mode 100644 index 0000000..4f9ae07 Binary files /dev/null and b/src/assets/audios/button_up.ogg differ diff --git a/src/assets/audios/defeat.mp3 b/src/assets/audios/defeat.mp3 new file mode 100644 index 0000000..d5bbd59 Binary files /dev/null and b/src/assets/audios/defeat.mp3 differ diff --git a/src/assets/audios/game_over.ogg b/src/assets/audios/game_over.ogg new file mode 100644 index 0000000..a4a8dde Binary files /dev/null and b/src/assets/audios/game_over.ogg differ diff --git a/src/assets/audios/index.ts b/src/assets/audios/index.ts new file mode 100644 index 0000000..1ba5191 --- /dev/null +++ b/src/assets/audios/index.ts @@ -0,0 +1,40 @@ +import button_down from './button_down.ogg' +import button_up from './button_up.ogg' +import bgm_wait from './bgm_wait.ogg' +import bgm_day_1 from './bgm_day_1.ogg' +import bgm_day_2 from './bgm_day_2.ogg' +import bgm_day_3 from './bgm_day_3.ogg' +import bgm_home from './bgm_home.ogg' +import bgm_night_1 from './bgm_night_1.ogg' +import bgm_night_2 from './bgm_night_2.ogg' +import bgm_night_3 from './bgm_night_3.ogg' +import notice from './phase_change.ogg' +import witch from './witch.ogg' +import wolf from './wolf.ogg' +import vote from './vote.ogg' +import write from './write.ogg' +import note_drop from './note_drop.ogg' +import note_take from './note_take.ogg' +import game_over from './game_over.ogg' +import defeat from './defeat.mp3' +export default { + button_down, + button_up, + bgm_wait, + bgm_day_1, + bgm_day_2, + bgm_day_3, + bgm_home, + bgm_night_1, + bgm_night_2, + bgm_night_3, + notice, + witch, + wolf, + vote, + write, + note_drop, + note_take, + game_over, + defeat, +} \ No newline at end of file diff --git a/src/assets/audios/note_drop.ogg b/src/assets/audios/note_drop.ogg new file mode 100644 index 0000000..47ead28 Binary files /dev/null and b/src/assets/audios/note_drop.ogg differ diff --git a/src/assets/audios/note_take.ogg b/src/assets/audios/note_take.ogg new file mode 100644 index 0000000..bcad9dc Binary files /dev/null and b/src/assets/audios/note_take.ogg differ diff --git a/src/assets/audios/ogg.py b/src/assets/audios/ogg.py new file mode 100644 index 0000000..e8b4772 --- /dev/null +++ b/src/assets/audios/ogg.py @@ -0,0 +1,35 @@ +import os +import subprocess + +def convert_mp3_to_ogg(): + # 获取当前目录下所有文件 + files = os.listdir('.') + + # 筛选出所有MP3文件 + mp3_files = [file for file in files if file.lower().endswith('.mp3')] + + if not mp3_files: + print("当前目录下没有找到MP3文件。") + return + + print(f"找到 {len(mp3_files)} 个MP3文件,开始转换...") + + # 遍历并转换每个MP3文件 + for mp3_file in mp3_files: + # 构建输出文件名(替换扩展名为.ogg) + ogg_file = os.path.splitext(mp3_file)[0] + '.ogg' + + # 构建ffmpeg命令 + cmd = ['ffmpeg', '-i', mp3_file, '-c:a', 'libvorbis', '-q:a', '4', ogg_file] + + try: + print(f"正在转换: {mp3_file} -> {ogg_file}") + subprocess.run(cmd, check=True) + print(f"成功转换: {ogg_file}") + except subprocess.CalledProcessError as e: + print(f"转换失败: {mp3_file}, 错误: {e}") + + print("转换完成!") + +if __name__ == "__main__": + convert_mp3_to_ogg() \ No newline at end of file diff --git a/src/assets/audios/phase_change.ogg b/src/assets/audios/phase_change.ogg new file mode 100644 index 0000000..f783e78 Binary files /dev/null and b/src/assets/audios/phase_change.ogg differ diff --git a/src/assets/audios/vote.ogg b/src/assets/audios/vote.ogg new file mode 100644 index 0000000..db53eef Binary files /dev/null and b/src/assets/audios/vote.ogg differ diff --git a/src/assets/audios/witch.ogg b/src/assets/audios/witch.ogg new file mode 100644 index 0000000..d90cba8 Binary files /dev/null and b/src/assets/audios/witch.ogg differ diff --git a/src/assets/audios/wolf.ogg b/src/assets/audios/wolf.ogg new file mode 100644 index 0000000..b24c328 Binary files /dev/null and b/src/assets/audios/wolf.ogg differ diff --git a/src/assets/audios/write.ogg b/src/assets/audios/write.ogg new file mode 100644 index 0000000..e8cdf1a Binary files /dev/null and b/src/assets/audios/write.ogg differ diff --git a/src/assets/fonts.css b/src/assets/fonts.css new file mode 100644 index 0000000..5da2e9f --- /dev/null +++ b/src/assets/fonts.css @@ -0,0 +1,27 @@ + +@font-face { + font-family: 'ZCOOL XiaoWei'; + src: url('ZCOOLXiaoWei-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Adventure Time'; + src: url('AdventureTime.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + + +@font-face { + font-family: 'Ma Shan Zheng'; + src: url('MaShanZheng-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + + diff --git a/src/assets/imgs/BlueHover.webp b/src/assets/imgs/BlueHover.webp new file mode 100644 index 0000000..8c82c5c Binary files /dev/null and b/src/assets/imgs/BlueHover.webp differ diff --git a/src/assets/imgs/BluePlain.webp b/src/assets/imgs/BluePlain.webp new file mode 100644 index 0000000..f78ccfc Binary files /dev/null and b/src/assets/imgs/BluePlain.webp differ diff --git a/src/assets/imgs/BluePressed.webp b/src/assets/imgs/BluePressed.webp new file mode 100644 index 0000000..ba7db97 Binary files /dev/null and b/src/assets/imgs/BluePressed.webp differ diff --git a/src/assets/imgs/Board.webp b/src/assets/imgs/Board.webp new file mode 100644 index 0000000..6b1de7e Binary files /dev/null and b/src/assets/imgs/Board.webp differ diff --git a/src/assets/imgs/GreenHover.webp b/src/assets/imgs/GreenHover.webp new file mode 100644 index 0000000..ed9e407 Binary files /dev/null and b/src/assets/imgs/GreenHover.webp differ diff --git a/src/assets/imgs/GreenPlain.webp b/src/assets/imgs/GreenPlain.webp new file mode 100644 index 0000000..60e5c2e Binary files /dev/null and b/src/assets/imgs/GreenPlain.webp differ diff --git a/src/assets/imgs/GreenPressed.webp b/src/assets/imgs/GreenPressed.webp new file mode 100644 index 0000000..f3420e5 Binary files /dev/null and b/src/assets/imgs/GreenPressed.webp differ diff --git a/src/assets/imgs/Note.webp b/src/assets/imgs/Note.webp new file mode 100644 index 0000000..51c73fc Binary files /dev/null and b/src/assets/imgs/Note.webp differ diff --git a/src/assets/imgs/Notice.webp b/src/assets/imgs/Notice.webp new file mode 100644 index 0000000..493e60c Binary files /dev/null and b/src/assets/imgs/Notice.webp differ diff --git a/src/assets/imgs/Plain.webp b/src/assets/imgs/Plain.webp new file mode 100644 index 0000000..6b1de7e Binary files /dev/null and b/src/assets/imgs/Plain.webp differ diff --git a/src/assets/imgs/RedHover.webp b/src/assets/imgs/RedHover.webp new file mode 100644 index 0000000..6145940 Binary files /dev/null and b/src/assets/imgs/RedHover.webp differ diff --git a/src/assets/imgs/RedPlain.webp b/src/assets/imgs/RedPlain.webp new file mode 100644 index 0000000..673a382 Binary files /dev/null and b/src/assets/imgs/RedPlain.webp differ diff --git a/src/assets/imgs/RedPressed.webp b/src/assets/imgs/RedPressed.webp new file mode 100644 index 0000000..ba17e33 Binary files /dev/null and b/src/assets/imgs/RedPressed.webp differ diff --git a/src/assets/imgs/SmHover.webp b/src/assets/imgs/SmHover.webp new file mode 100644 index 0000000..21451c4 Binary files /dev/null and b/src/assets/imgs/SmHover.webp differ diff --git a/src/assets/imgs/SmPlain.webp b/src/assets/imgs/SmPlain.webp new file mode 100644 index 0000000..5d29643 Binary files /dev/null and b/src/assets/imgs/SmPlain.webp differ diff --git a/src/assets/imgs/SmPressed.webp b/src/assets/imgs/SmPressed.webp new file mode 100644 index 0000000..368de05 Binary files /dev/null and b/src/assets/imgs/SmPressed.webp differ diff --git a/src/assets/imgs/assistant/normal.png b/src/assets/imgs/assistant/normal.png new file mode 100644 index 0000000..8e100d7 Binary files /dev/null and b/src/assets/imgs/assistant/normal.png differ diff --git a/src/assets/imgs/icons/index.ts b/src/assets/imgs/icons/index.ts new file mode 100644 index 0000000..a18ca4b --- /dev/null +++ b/src/assets/imgs/icons/index.ts @@ -0,0 +1,5 @@ +import login from './passkey_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg' +import earth from './public_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg' +import mic from './mic.svg' + +export default { login, earth, mic } \ No newline at end of file diff --git a/src/assets/imgs/icons/mic.svg b/src/assets/imgs/icons/mic.svg new file mode 100644 index 0000000..b7873ed --- /dev/null +++ b/src/assets/imgs/icons/mic.svg @@ -0,0 +1 @@ + diff --git a/src/assets/imgs/icons/passkey_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/assets/imgs/icons/passkey_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..729f5f2 --- /dev/null +++ b/src/assets/imgs/icons/passkey_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/imgs/icons/public_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/assets/imgs/icons/public_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..700b322 --- /dev/null +++ b/src/assets/imgs/icons/public_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/imgs/webp.py b/src/assets/imgs/webp.py new file mode 100644 index 0000000..970e5ee --- /dev/null +++ b/src/assets/imgs/webp.py @@ -0,0 +1,35 @@ +import os +import subprocess + +def convert_png_to_webp(): + # 获取当前目录下所有文件 + files = os.listdir('.') + + # 筛选出所有PNG文件 + png_files = [file for file in files if file.lower().endswith('.png')] + + if not png_files: + print("当前目录下没有找到PNG文件。") + return + + print(f"找到 {len(png_files)} 个PNG文件,开始转换...") + + # 遍历并转换每个PNG文件 + for png_file in png_files: + # 构建输出文件名(替换扩展名为.webp) + webp_file = os.path.splitext(png_file)[0] + '.webp' + + # 构建ffmpeg命令,设置压缩质量为80%(可以根据需要调整) + cmd = ['ffmpeg', '-i', png_file, '-c:v', 'libwebp', '-quality', '80', webp_file] + + try: + print(f"正在转换: {png_file} -> {webp_file}") + subprocess.run(cmd, check=True) + print(f"成功转换: {webp_file}") + except subprocess.CalledProcessError as e: + print(f"转换失败: {png_file}, 错误: {e}") + + print("转换完成!") + +if __name__ == "__main__": + convert_png_to_webp() \ No newline at end of file diff --git a/src/components/Board.vue b/src/components/Board.vue new file mode 100644 index 0000000..91242cf --- /dev/null +++ b/src/components/Board.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/src/components/Button.vue b/src/components/Button.vue new file mode 100644 index 0000000..223baa2 --- /dev/null +++ b/src/components/Button.vue @@ -0,0 +1,200 @@ + + + + + \ No newline at end of file diff --git a/src/components/GameAssistant.vue b/src/components/GameAssistant.vue new file mode 100644 index 0000000..d608e47 --- /dev/null +++ b/src/components/GameAssistant.vue @@ -0,0 +1,547 @@ + + + + + diff --git a/src/components/InGame.vue b/src/components/InGame.vue new file mode 100644 index 0000000..9e3eac2 --- /dev/null +++ b/src/components/InGame.vue @@ -0,0 +1,1391 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Input.vue b/src/components/Input.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Note.vue b/src/components/Note.vue new file mode 100644 index 0000000..938c4fb --- /dev/null +++ b/src/components/Note.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/components/OptionGroup.vue b/src/components/OptionGroup.vue new file mode 100644 index 0000000..8c9bdb6 --- /dev/null +++ b/src/components/OptionGroup.vue @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/src/components/OptionSelector.vue b/src/components/OptionSelector.vue new file mode 100644 index 0000000..4c89132 --- /dev/null +++ b/src/components/OptionSelector.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue new file mode 100644 index 0000000..349889d --- /dev/null +++ b/src/components/ProgressBar.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/src/components/dialog-sound-effects.ts b/src/components/dialog-sound-effects.ts new file mode 100644 index 0000000..08d84b3 --- /dev/null +++ b/src/components/dialog-sound-effects.ts @@ -0,0 +1,277 @@ +/** + * 对话音效角色类型 + */ +export type DialogCharacterType = + | 'undertale' // 类似Undertale的方波音效 + | 'zelda' // 三角波为基础的音效 + | 'pokemon' // 高音调短促音效 + | 'robot' // 机械音效 + | 'pixie' // 精灵/仙女音效 + | 'monster' // 怪物/低沉音效 + | 'custom'; // 自定义音效 + +/** + * 音效配置接口 + */ +export interface DialogSoundConfig { + oscillatorType: OscillatorType; // 振荡器类型 + baseFrequency: number; // 基础频率 + frequencyVariation: number; // 频率变化范围 + gain: number; // 音量 + attackTime: number; // 起音时间 + releaseTime: number; // 释音时间 + duration: number; // 持续时间 + highPassFrequency?: number; // 高通滤波器频率 + lowPassFrequency?: number; // 低通滤波器频率 + distortion?: number; // 失真度 + detune?: number; // 音高微调 +} + +/** + * 为每种角色类型预设的音效配置 + */ +export const CHARACTER_SOUND_CONFIGS: Record, DialogSoundConfig> = { + // Undertale风格 - 清脆的方波 + undertale: { + oscillatorType: 'square', + baseFrequency: 380, + frequencyVariation: 10, + gain: 0.05, + attackTime: 0.01, + releaseTime: 0.02, + duration: 0.05, + highPassFrequency: 700 + }, + + // 塞尔达风格 - 三角波 + zelda: { + oscillatorType: 'triangle', + baseFrequency: 300, + frequencyVariation: 8, + gain: 0.08, + attackTime: 0.01, + releaseTime: 0.08, + duration: 0.12, + highPassFrequency: 400 + }, + + // 宝可梦风格 - 高音调短促 + pokemon: { + oscillatorType: 'sine', + baseFrequency: 850, + frequencyVariation: 5, + gain: 0.06, + attackTime: 0.005, + releaseTime: 0.015, + duration: 0.03 + }, + + // 机器人风格 - 方波带失真 + robot: { + oscillatorType: 'square', + baseFrequency: 200, + frequencyVariation: 12, + gain: 0.04, + attackTime: 0.005, + releaseTime: 0.03, + duration: 0.07, + distortion: 15, + detune: 5 + }, + + // 精灵/仙女风格 - 高音调正弦波 + pixie: { + oscillatorType: 'sine', + baseFrequency: 1200, + frequencyVariation: 15, + gain: 0.03, + attackTime: 0.005, + releaseTime: 0.1, + duration: 0.15, + highPassFrequency: 900 + }, + + // 怪物/低沉风格 - 低音锯齿波 + monster: { + oscillatorType: 'sawtooth', + baseFrequency: 150, + frequencyVariation: 6, + gain: 0.07, + attackTime: 0.02, + releaseTime: 0.05, + duration: 0.1, + lowPassFrequency: 300, + distortion: 8 + } +}; + +/** + * 用于存储AudioContext单例 + */ +let audioContextInstance: AudioContext | null = null; + +/** + * 获取或创建AudioContext + */ +export function getAudioContext(): AudioContext { + if (!audioContextInstance) { + audioContextInstance = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + return audioContextInstance; +} + +/** + * 销毁AudioContext + */ +export function destroyAudioContext(): void { + if (audioContextInstance && audioContextInstance.state !== 'closed') { + audioContextInstance.close(); + audioContextInstance = null; + } +} + +/** + * 创建失真效果 + */ +function createDistortionEffect( + audioContext: AudioContext, + distortionAmount: number +): WaveShaperNode { + const waveShaperNode = audioContext.createWaveShaper(); + + // 创建失真曲线 + function makeDistortionCurve(amount: number): Float32Array { + const k = amount; + const samples = 44100; + const curve = new Float32Array(samples); + + for (let i = 0; i < samples; ++i) { + const x = (i * 2) / samples - 1; + curve[i] = ((3 + k) * x * 0.5 * (1 - Math.abs(x))) / (3 + k * Math.abs(x)); + } + + return curve; + } + + waveShaperNode.curve = makeDistortionCurve(distortionAmount); + return waveShaperNode; +} + +/** + * 播放单个字符的对话音效 + * @param char 要为其播放音效的字符 + * @param characterType 角色类型 + * @param customConfig 可选的自定义配置 + * @returns 音效的持续时间(秒) + */ +export function playDialogSound( + char: string, + characterType: DialogCharacterType | number, + customConfig?: Partial +): number { + // 空格不播放音效 + if (char === ' ') { + return 0; + } + + // 获取音频上下文 + const audioContext = getAudioContext(); + const now = audioContext.currentTime; + + // 获取基础配置 + let config: DialogSoundConfig; + + if (characterType === 'custom' && customConfig) { + // 使用自定义配置 + config = { + oscillatorType: customConfig.oscillatorType || 'square', + baseFrequency: customConfig.baseFrequency || 400, + frequencyVariation: customConfig.frequencyVariation || 10, + gain: customConfig.gain || 0.05, + attackTime: customConfig.attackTime || 0.01, + releaseTime: customConfig.releaseTime || 0.05, + duration: customConfig.duration || 0.08, + ...customConfig + }; + } else { + // 使用预设配置 + if (typeof characterType === 'number') { + config = Object.values(CHARACTER_SOUND_CONFIGS)[characterType] || CHARACTER_SOUND_CONFIGS.undertale; + } else { + config = { ...CHARACTER_SOUND_CONFIGS[characterType as Exclude] }; + } + // 应用自定义配置覆盖(如果有) + if (customConfig) { + config = { ...config, ...customConfig }; + } + } + + // 创建音频节点 + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + // 设置振荡器 + oscillator.type = config.oscillatorType; + + // 基于字符计算频率变化 + const charCode = char.charCodeAt(0); + const frequencyOffset = (charCode % config.frequencyVariation) / config.frequencyVariation; + + // 设置频率和可选的失谐 + oscillator.frequency.value = config.baseFrequency + (config.baseFrequency * 0.2 * frequencyOffset); + + if (config.detune) { + // 添加轻微的失谐,使声音更有特色 + oscillator.detune.value = (charCode % 10) * config.detune; + } + + // 连接音频节点链 + let currentNode: AudioNode = oscillator; + + // 连接到增益节点 + currentNode.connect(gainNode); + currentNode = gainNode; + + // 添加失真效果(如果指定) + if (config.distortion && config.distortion > 0) { + const distortionNode = createDistortionEffect(audioContext, config.distortion); + currentNode.connect(distortionNode); + currentNode = distortionNode; + } + + // 添加低通滤波器(如果指定) + if (config.lowPassFrequency) { + const lowPassFilter = audioContext.createBiquadFilter(); + lowPassFilter.type = 'lowpass'; + lowPassFilter.frequency.value = config.lowPassFrequency; + lowPassFilter.Q.value = 1; + + currentNode.connect(lowPassFilter); + currentNode = lowPassFilter; + } + + // 添加高通滤波器(如果指定) + if (config.highPassFrequency) { + const highPassFilter = audioContext.createBiquadFilter(); + highPassFilter.type = 'highpass'; + highPassFilter.frequency.value = config.highPassFrequency; + highPassFilter.Q.value = 1; + + currentNode.connect(highPassFilter); + currentNode = highPassFilter; + } + + // 连接到输出 + currentNode.connect(audioContext.destination); + + // 设置音量包络 + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(config.gain, now + config.attackTime); + gainNode.gain.linearRampToValueAtTime(0, now + config.duration); + + // 播放音效 + oscillator.start(now); + oscillator.stop(now + config.duration); + + return config.duration; +} \ No newline at end of file diff --git a/src/components/useWerewolfGame.ts b/src/components/useWerewolfGame.ts new file mode 100644 index 0000000..decd9f9 --- /dev/null +++ b/src/components/useWerewolfGame.ts @@ -0,0 +1,643 @@ +import { ref, reactive, onMounted, onUnmounted } from 'vue'; +import axios, { type AxiosInstance } from 'axios'; +import type { Ref } from 'vue'; +import VuplexAPI from '../VuplexAPI'; +import { EasyAudio } from '../utils'; +import audios from '../assets/audios'; + +// 玩家接口 +interface Player { + id: number; + name: string; + observer: boolean; + role?: string; + alive?: boolean; +} + +// 消息接口 +interface Message { + type: 'speak' | 'reasoning' + player: number; + message: string; + timestamp: number; + voice?: string; // 新增音频字段 +} + +// 投票接口 +interface Vote { + player: number; + target: number; +} + +// 全局通知接口 +interface GlobalNotice { + message: string; + timestamp: number; +} + +type NotePaperAction = { + type: 'appear', + content: string, +} | { + type: 'modify' + originalContent: string + newContent: string + startTime: number +} | { + type: 'drop' +} + +// 游戏状态接口 +interface GameState { + hasPrepared: boolean; // 是否准备好 + phase: 'waiting' | 'day' | 'night' | 'over'; + day: number; + players: Player[]; + role: string; + messages: Message[]; + votes: Vote[]; + killed: number[]; + actionRequired: boolean; + notePaperActions: NotePaperAction[]; // 小纸条操作 + actionType: string | null; + result?: 'wolf' | 'village' | 'victory/defeat', + globalNotices: GlobalNotice[]; // 新增全局通知 + gameoverMessage: string | null; // 游戏结束消息 + cameraFacing: number; // 镜头朝向 +} + +// 用户输入状态接口 +interface UserInput { + message: string; + voteTarget: number | null; + seerTarget: number | null; + witchAntidote: boolean; + witchAntidoteTarget: number | null; + witchPoison: boolean; + witchPoisonTarget: number | null; + wolfKillTarget: number | null; +} + +// 游戏事件接口 +interface GameEvent { + type: string; + data?: any; +} + +/** + * 狼人杀游戏客户端API封装 + */ +class WerewolfGameClient { + private api: AxiosInstance; + private gameId: string; + private playerId: number; + + constructor(baseURL: string, gameId: string, playerId: number) { + this.api = axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json' + } + }); + this.gameId = gameId; + this.playerId = playerId; + + } + + // 发送发言请求 + async speak(message: string): Promise { + await this.api.post(`/${this.gameId}/speak`, { + player: this.playerId, + message + }); + } + + // 发送投票请求 + async vote(targetId: number): Promise { + await this.api.post(`/${this.gameId}/vote`, { + player: this.playerId, + target: targetId + }); + } + + // 狼人聊天 + async wolfChat(message: string): Promise { + await this.api.post(`/${this.gameId}/wolf/chat`, { + player: this.playerId, + message + }); + } + + // 狼人杀人 + async wolfKill(targetId: number): Promise { + await this.api.post(`/${this.gameId}/wolf/action`, { + player: this.playerId, + action: "kill", + target: targetId + }); + } + + // 预言家查验 + async seerCheck(targetId: number): Promise { + await this.api.post(`/${this.gameId}/seer/action`, { + player: this.playerId, + target: targetId + }); + } + + // 女巫行动 + async witchAction( + usedAntidote: boolean, + antidoteTarget?: number, + usedPoison: boolean = false, + poisonTarget?: number + ): Promise { + const payload: any = { + player: this.playerId, + usedAntidote, + usedPoison + }; + + if (usedAntidote && antidoteTarget !== undefined) { + payload.antidoteTarget = antidoteTarget; + } + + if (usedPoison && poisonTarget !== undefined) { + payload.poisonTarget = poisonTarget; + } + + await this.api.post(`/${this.gameId}/witch/action`, payload); + } + + // 请求提前开始游戏 + async requestGameStart(): Promise { + await this.api.post(`/${this.gameId}/game/start`, { + player: this.playerId, + agree: true + }); + } +} + +/** + * 狼人杀游戏 Vue Composition API + */ +export function useWerewolfGame(baseURL: string, gameId: string, playerId: number) { + // 创建客户端实例 + const client = new WerewolfGameClient(baseURL, gameId, playerId); + const Vuplex = new VuplexAPI() + // 游戏状态 + const gameState = reactive({ + hasPrepared: false, + phase: 'waiting', + day: 0, + players: [], + role: '', + messages: [], + votes: [], + killed: [], + actionRequired: false, + actionType: null, + globalNotices: [], // 全局通知列表 + gameoverMessage: null, // 游戏结束消息 + cameraFacing: -1,// 镜头朝向 + notePaperActions: [] + }); + const ea = new EasyAudio([{ + name: 'notice', + audioUrl: audios.notice, + }, { + name: 'wolf', + audioUrl: audios.wolf, + }, { + name: 'witch', + audioUrl: audios.witch, + }, { + name: 'vote', + audioUrl: audios.vote, + }, { + name: 'note_take', + audioUrl: audios.note_take, + }, { + name: 'note_drop', + audioUrl: audios.note_drop, + }, { + name: 'write', + audioUrl: audios.write, + }, { + name: 'game_over', + audioUrl: audios.game_over, + }, { + name: 'defeat', + audioUrl: audios.defeat, + }]); + // 用户交互状态 + const userInput = reactive({ + message: '', + voteTarget: null, + seerTarget: null, + witchAntidote: false, + witchAntidoteTarget: null, + witchPoison: false, + witchPoisonTarget: null, + wolfKillTarget: null + }); + + // 保存被杀的玩家列表 (用于女巫救人) + const playersToBeAntidoted = ref([]); + + // 事件源引用 + let eventSource: EventSource | null = null; + + // 连接到服务器事件流 + const connectToEventSource = () => { + // 关闭已有连接 + if (eventSource) { + eventSource.close(); + } + + // 创建新连接 + eventSource = new EventSource(`${baseURL}/${gameId}/events`); + + // 处理接收到的消息 + eventSource.onmessage = (message) => { + try { + const event = JSON.parse(message.data) as GameEvent; + console.log('收到事件:', event.type, event.data); + + // 处理事件 + handleGameEvent(event); + } catch (error) { + console.error('解析事件数据失败:', error); + } + }; + + // 处理连接错误 + eventSource.onerror = (error) => { + console.error('SSE连接错误:', error); + // 重新连接 + setTimeout(connectToEventSource, 5000); + }; + }; + + // 处理游戏事件 + const handleGameEvent = (event: GameEvent) => { + switch (event.type) { + case 'gameStartRequest': + gameState.players = event.data.players.map((p: Player) => ({ ...p, alive: true })); + break; + + case 'gameStart': + gameState.phase = 'night'; + gameState.players = event.data.players.map((p: Player) => ({ ...p, alive: true })); + gameState.role = event.data.role; + Vuplex.moveCamera(event.data.scene) + Vuplex.gameStart(event.data) + /* Vuplex.facePlayer(-1) */ + break; + + case 'phaseChange': + gameState.phase = event.data.phase; + gameState.day = event.data.day; + /* Vuplex.facePlayer(-1) */ + if (!(event.data.phase == 'night' && gameState.day == 1)) { + Vuplex.facePlayer(-1) + } + gameState.cameraFacing = -1 + Vuplex.setTime(event.data.phase) + ea.play('notice') + break; + + case 'globalNotice': + // 处理全局通知 + gameState.globalNotices.push({ + message: event.data.message, + timestamp: Date.now() + }); + Vuplex.facePlayer(-1) + gameState.cameraFacing = -1 + + break; + + case 'turnToSpeak': + gameState.actionRequired = true; + gameState.actionType = 'speak'; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + break; + + case 'speak': + gameState.actionType = 'speak'; + gameState.messages.push({ + type: 'speak', + player: event.data.player, + message: event.data.message, + timestamp: Date.now(), + voice: event.data.voice // 新增音频字段 + }); + Vuplex.facePlayer(event.data.player) + gameState.cameraFacing = event.data.player + Vuplex.playerAction(event.data.player, 'speak') + break; + + case 'wolfChat': + gameState.actionType = 'speak'; + gameState.messages.push({ + type: 'speak', + player: event.data.player, + message: event.data.message, + timestamp: Date.now(), + voice: event.data.voice // 新增音频字段 + }); + Vuplex.facePlayer(event.data.player) + gameState.cameraFacing = event.data.player + Vuplex.playerAction(event.data.player, 'speak') + break; + case 'reasoning': + gameState.messages.push({ + type: 'reasoning', + player: event.data.player, + message: event.data.message, + timestamp: Date.now() + }); + Vuplex.facePlayer(event.data.player) + gameState.cameraFacing = event.data.player + Vuplex.playerAction(event.data.player, 'reasoning') + break; + case 'turnToVote': + gameState.actionRequired = true; + gameState.actionType = 'vote'; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + + // 清空之前的投票记录(每轮投票开始时) + gameState.votes = []; + break; + + case 'vote': + gameState.actionType = 'vote'; + gameState.votes.push({ + player: event.data.player, + target: event.data.target + }); + gameState.globalNotices.push({ + message: `${event.data.player}号玩家投票给${event.data.target}号玩家`, + timestamp: Date.now() + }); + Vuplex.facePlayer(-1) + gameState.cameraFacing = -1 + Vuplex.playerAction(event.data.player, 'speak') + break; + case 'voteEnd': + ea.play('vote') + break; + case 'playerKilled': + // 更新被杀玩家状态 + event.data.targets.forEach((id: number) => { + gameState.killed.push(id); + + const player = gameState.players.find(p => p.id === id); + if (player) { + player.alive = false; + } + Vuplex.facePlayer(id) + Vuplex.playerAction(id, 'dead') + gameState.cameraFacing = id + }); + break; + + case 'wolfTurn': + gameState.role = 'wolf' + gameState.actionRequired = true; + gameState.actionType = 'wolf'; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + + break; + + case 'seerTurn': + gameState.role = 'seer' + gameState.actionRequired = true; + gameState.actionType = 'seer'; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + + break; + + case 'witchTurn': + gameState.role = 'witch' + gameState.actionRequired = true; + gameState.actionType = 'witch'; + playersToBeAntidoted.value = event.data.playersToBeAntidoted || []; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + + break; + case 'turnToNote': + gameState.actionRequired = true; + gameState.actionType = 'note'; + Vuplex.facePlayer(event.data.target) + gameState.cameraFacing = event.data.target + + break; + + case 'gameOver': + gameState.phase = 'over'; + gameState.result = event.data.winningSide; + gameState.gameoverMessage = event.data.reason; + if (event.data.result == 'victory') { + + } else if (event.data.result == 'defeat') { + ea.play('defeat') + } else { + ea.play('game_over') + } + break; + + // 增加其他特殊角色事件处理 + case 'wolfAction': + gameState.globalNotices.push({ + message: `狼人杀了${event.data.target}号玩家`, + timestamp: Date.now() + }); + ea.play('wolf') + + break; + + case 'witchAction': + const actionStr = [] + if (event.data.usedAntidote) { + actionStr.push(`对${event.data.antidoteTarget}号玩家使用了解药`) + } + if (event.data.usedPoison) { + actionStr.push(`对${event.data.poisonTarget}号玩家使用了毒药`) + } + gameState.globalNotices.push({ + message: `女巫${actionStr.join(',')}。`, + timestamp: Date.now() + }); + ea.play('witch') + break; + case 'seerResult': + // 预言家查验结果 + const resultText = `预言家查验了${event.data.target}号玩家,TA是${translateRole(event.data.role)}`; + gameState.globalNotices.push({ + message: resultText, + timestamp: Date.now() + }); + + break; + + case 'noteAppear': + // 处理全局通知 + gameState.globalNotices.push({ + message: event.data.message, + timestamp: Date.now() + }); + Vuplex.facePlayer(-1) + gameState.cameraFacing = -1 + + break; + + case 'notePick': + gameState.actionType = 'note'; + Vuplex.facePlayer(event.data.player) + gameState.cameraFacing = event.data.player + Vuplex.playerAction(event.data.player, 'reasoning') + setTimeout(() => { + ea.play('note_take') + setTimeout(() => { + gameState.notePaperActions.push({ + type: 'appear', + content: event.data.content + }); + }, 500); + }, 800); + break; + + case 'noteAction': + gameState.actionType = 'note'; + switch (event.data.action) { + case 'modify': + gameState.notePaperActions.push({ + type: 'modify', + originalContent: event.data.originalContent, + newContent: event.data.newContent, + startTime: Date.now() + }); + Vuplex.facePlayer(event.data.player) + ea.play('write') + break; + case 'drop': + gameState.notePaperActions.push({ + type: 'drop', + }); + Vuplex.facePlayer(-1) + ea.play('note_drop') + break; + } + break + + } + }; + + // 提交当前操作 + const submitAction = async () => { + try { + switch (gameState.actionType) { + case 'speak': + if (userInput.message) { + await client.speak(userInput.message); + userInput.message = ''; + } + break; + + case 'vote': + if (userInput.voteTarget !== null) { + await client.vote(userInput.voteTarget); + userInput.voteTarget = null; + } + break; + + case 'wolf': + if (userInput.wolfKillTarget !== null) { + await client.wolfKill(userInput.wolfKillTarget); + userInput.wolfKillTarget = null; + } + break; + + case 'seer': + if (userInput.seerTarget !== null) { + await client.seerCheck(userInput.seerTarget); + userInput.seerTarget = null; + } + break; + + case 'witch': + await client.witchAction( + userInput.witchAntidote, + userInput.witchAntidoteTarget || undefined, + userInput.witchPoison, + userInput.witchPoisonTarget || undefined + ); + + userInput.witchAntidote = false; + userInput.witchAntidoteTarget = null; + userInput.witchPoison = false; + userInput.witchPoisonTarget = null; + break; + } + + // 重置操作状态 + gameState.actionRequired = false; + gameState.actionType = null; + + } catch (error) { + console.error('提交操作失败:', error); + } + }; + + // 请求开始游戏 + const requestGameStart = () => { + if (gameState.hasPrepared) { + console.warn('游戏已经准备好,无法重复请求开始游戏。'); + return; + } + gameState.hasPrepared = true; + client.requestGameStart(); + }; + + // 角色名称翻译 + const translateRole = (role: string): string => { + const roleMap: Record = { + 'wolf': '狼人', + 'village': '村民', + 'seer': '预言家', + 'witch': '女巫', + 'observer': '旁观者' + }; + return roleMap[role] || role; + }; + + // 组件挂载时连接到事件源 + onMounted(() => { + connectToEventSource(); + }); + + // 组件卸载时关闭连接 + onUnmounted(() => { + if (eventSource) { + eventSource.close(); + eventSource = null; + } + }); + + // 返回响应式状态和方法 + return { + gameState, + userInput, + playersToBeAntidoted, + submitAction, + requestGameStart, + translateRole + }; +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..5292afc --- /dev/null +++ b/src/config.ts @@ -0,0 +1,85 @@ +import { debounce } from "./utils"; +import VuplexAPI from './VuplexAPI'; +const Vuplex = new VuplexAPI() + +const graphicsSettings = { + // Max Frame Rate options + frameRate: { + name: "Max Frame Rate", + options: [ + { value: 30, label: "30 FPS" }, + { value: 60, label: "60 FPS" }, + { value: 120, label: "120 FPS" }, + { value: -1, label: "Unlimited" } + ], + default: 60 + }, + + // Overall quality preset + qualityPreset: { + name: "Quality Preset", + options: [ + { value: 0, label: "Very Low" }, + { value: 1, label: "Low" }, + { value: 2, label: "Medium" }, + { value: 3, label: "High" }, + { value: 4, label: "Very High" }, + { value: 5, label: "Ultra" } + ], + default: 3 + }, + + // Anti-aliasing + antiAliasing: { + name: "Anti-aliasing", + options: [ + { value: 0, label: "Off" }, + { value: 2, label: "2x MSAA" }, + { value: 4, label: "4x MSAA" }, + { value: 8, label: "8x MSAA" } + ], + default: 2 + }, +}; + +const applyGraphicsSettings = (settings: Record) => { + Object.entries(settings).forEach(([key, value]) => { + if (key in graphicsSettings) { + graphicsSettings[key as keyof typeof graphicsSettings].default = value; + } + }); +}; + +const SETTINGS_KEY = 'graphicsSettings'; + +const updateGraphicsSettings = debounce((newSettings: Record) => { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(newSettings)); + applyGraphicsSettings(newSettings); + Vuplex.updateSettings(newSettings); +}, 40); + +try { + const storedSettings = localStorage.getItem(SETTINGS_KEY); + if (storedSettings) { + applyGraphicsSettings(JSON.parse(storedSettings)); + } +} catch (error) { + console.error('加载存储设置失败:', error); +} + +const skinNames = [ + { id: 0, name: 'Female Villager' }, + { id: 1, name: 'Female Knight' }, + { id: 2, name: 'Male Knight' }, + { id: 3, name: 'Male Warrior' }, + { id: 4, name: 'Female Rogue' }, + { id: 5, name: 'Male Ranger' }, + { id: 6, name: 'Female Villager' }, + { id: 7, name: 'Male Villager' }, + { id: 8, name: 'Female Witch' }, + { id: 9, name: 'Male Wizard' }, + { id: 10, name: 'Female Cleric' }, + { id: 11, name: 'Male Cleric' }, +] + +export { graphicsSettings, updateGraphicsSettings, skinNames } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b52df26 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import './style.css' +import './assets/fonts.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..191af1a --- /dev/null +++ b/src/style.css @@ -0,0 +1,24 @@ +body{ + margin: 0; + user-select: none; + font-family: 'Adventure Time', 'ZCOOL XiaoWei'; +} + +input { + background-color: transparent; + border: none; + border-bottom: .3vh solid white; + color: white; + outline: none; + font-size: 3.4vh; + font-family: 'Adventure Time', 'ZCOOL XiaoWei'; + text-align: center; + text-shadow: 0 0 1vh black; +} +input::placeholder{ + color: rgba(245, 245, 220, 0.737); +} + +*{ + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..beb2920 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,663 @@ +/** + * 防抖函数 + * @param fn 需要防抖的函数 + * @param delay 延迟时间,单位毫秒 + * @param immediate 是否立即执行 + * @returns 防抖处理后的函数 + */ +export function debounce any>( + fn: T, + delay: number, + immediate: boolean = false +): (...args: Parameters) => void { + let timer: ReturnType | null = null; + + return function (this: any, ...args: Parameters): void { + const context = this; + + // 如果已经设定过定时器,则清空上次的定时器 + if (timer) clearTimeout(timer); + + if (immediate) { + // 如果是立即执行 + const callNow = !timer; + + // 设置定时器,在delay毫秒后将timer设为null + timer = setTimeout(() => { + timer = null; + }, delay); + + // 如果是第一次触发,则立即执行函数 + if (callNow) fn.apply(context, args); + } else { + // 如果不是立即执行,则设置定时器,在delay毫秒后执行函数 + timer = setTimeout(() => { + fn.apply(context, args); + timer = null; + }, delay); + } + }; +} + +/** + * @file utils.ts + * @description 这个文件包含了一些实用的函数和一个简化 Canvas 操作的类 EasyCanvas。 + * @version 1.0.0 + * @date 2024-08-31 + * @author feie9454 + */ + + + +/** + * 在每一帧调用指定的回调函数,直到达到指定的帧数。 + * @param {(index: number, total: number) => void} callback - 每一帧调用的回调函数。 + * @param {number} frameCount - 总帧数。 + */ +export function forEachFrame(callback: (index: number, total: number) => void, frameCount: number) { + if (frameCount <= 0) { + return; + } + let index = 0; + const total = frameCount; + const f = () => { + callback(index, total); + index++; + if (index < total) { + requestAnimationFrame(f); + } + }; + requestAnimationFrame(f); +} + +/** + * 在指定的毫秒数内,每一帧调用指定的回调函数。 + * @param {(elapsedTime: number, totalTime: number) => void} callback - 每一帧调用的回调函数。 + * @param {number} milliseconds - 总时间(毫秒)。 + */ +export function forEachFrameInMilliseconds(callback: (elapsedTime: number, totalTime: number) => void, milliseconds: number) { + let startTime = Date.now(); + let totalTime = milliseconds + const f = () => { + let elapsedTime = Date.now() - startTime; + if (elapsedTime < totalTime) { + callback(elapsedTime, totalTime); + requestAnimationFrame(f); + } else { + callback(totalTime, totalTime); + } + }; + requestAnimationFrame(f); +} + +/** + * 创建一个线性到三次贝塞尔曲线的转换函数。 + * @param {[number, number, number, number]} [f=[0.42, 0, 0.58, 1]] - 贝塞尔曲线参数。 + * @returns {(t: number) => number} 转换函数。 + */ +export function createLinear2CubicBezier(f: [number, number, number, number] = [0.42, 0, 0.58, 1]): (t: number) => number { + return (t: number) => { + return linear2CubicBezier(t, f) + } +} + +/** + * 将线性值转换为三次贝塞尔曲线值。 + * @param {number} source - 源值(0 到 1 之间)。 + * @param {[number, number, number, number]} [f=[0.42, 0, 0.58, 1]] - 贝塞尔曲线参数。 + * @returns {number} 转换后的值。 + * @throws {Error} 如果源值不在 0 到 1 之间,抛出错误。 + */ +export function linear2CubicBezier(source: number, f: [number, number, number, number] = [0.42, 0, 0.58, 1]): number { + if (source < 0 || source > 1) { + throw new Error("Source must be between 0 and 1"); + } + + const [x1, y1, x2, y2] = f; + + const cubicBezier = (t: number, p0: number, p1: number, p2: number, p3: number) => { + const u = 1 - t; + return (u * u * u * p0) + (3 * u * u * t * p1) + (3 * u * t * t * p2) + (t * t * t * p3); + } + + const solveCubicBezierX = (xTarget: number, epsilon = 0.00001) => { + let t = xTarget; + let x = cubicBezier(t, 0, x1, x2, 1); + let iteration = 0; + + while (Math.abs(x - xTarget) > epsilon && iteration < 100) { + const d = 3 * (1 - t) * (1 - t) * (x1 - 0) + 6 * (1 - t) * t * (x2 - x1) + 3 * t * t * (1 - x2); + if (d === 0) break; + t -= (x - xTarget) / d; + x = cubicBezier(t, 0, x1, x2, 1); + iteration++; + } + + return t; + } + + const t = solveCubicBezierX(source); + const y = cubicBezier(t, 0, y1, y2, 1); + + return y; +} + +/** + * 生成指定长度的随机十六进制字符串。 + * @param {number} length - 字符串长度。 + * @returns {string} 随机十六进制字符串。 + */ +export function generateRandomHexString(length: number): string { + const characters = '0123456789abcdef'; + let result = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + return result; +} + +/** + * EasyCanvas 类,用于简化 Canvas 操作。 + */ +export class EasyCanvas { + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + statusMap = new Map(); + + /** + * 构造函数 + * @param {HTMLCanvasElement} canvas - Canvas 元素。 + */ + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.ctx = canvas.getContext("2d")!; + } + + /** + * 将文本按最大宽度进行换行,并返回每行文本的数组。 + * @param {CanvasRenderingContext2D} ctx - Canvas 渲染上下文。 + * @param {string} text - 要换行的文本。 + * @param {number} maxWidth - 每行的最大宽度。 + * @returns {string[]} 换行后的文本数组。 + */ + static getWrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, considerMarkInLineEnd = true): string[] { + const txtList = []; + const markList = [ + ",", ".", "!", "?", ";", ":", ",", "。", "!", "?", ";", ":", + "-", "–", "—", "(", ")", "[", "]", "{", "}", "<", ">", "‘", "’", + "“", "”", "\"", "'", "`", "~", "@", "#", "$", "%", "^", "&", "*", + "_", "+", "=", "|", "\\", "/", "、", "·", "…", "《", "》", "〈", + "〉", "「", "」", "『", "』", "【", "】", "〖", "〗", "(", ")", + "〔", "〕", "!", "?", "﹏", "﹋", "﹌", "﹍", "﹎", "﹏", "﹐", + "﹑", "﹒", "﹔", "﹖", "﹗", "﹟", "﹠", "﹡", "﹢", "﹣", "﹤", + "﹥", "﹦", "﹨", "﹩", "﹪", "﹫" + ]; + let str = ""; + for (let i = 0, len = text.length; i < len; i++) { + str += text.charAt(i); + if (ctx.measureText(str).width > maxWidth) { + if (considerMarkInLineEnd && markList.includes(text.charAt(i))) { + continue + } + txtList.push(str.substring(0, str.length - 1)) + str = "" + i-- + } + } + txtList.push(str) + return txtList; + } + + /** + * 在 Canvas 上绘制文本。 + * @param {string} font - 字体样式。 + * @param {string} fillStyle - 填充样式。 + * @param {string} text - 文本内容。 + * @param {number} x - 文本的 x 坐标。 + * @param {number} y - 文本的 y 坐标。 + * @param {Object} [options] - 其他选项。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + fillText(font: string, fillStyle: string, text: string, x: number, y: number, + options?: + | { align?: "left" | "right" } + | { align?: "left" | "right", maxWidth: number, lineHeight: number } + | { align: "centerW", width: number } + | { align: "centerW", width: number, maxWidth: number, lineHeight: number } + | { align: "centerP" } + | { align: "centerP", maxWidth: number, lineHeight: number } + ): this { + this.ctx.font = font; + this.ctx.fillStyle = fillStyle; + + if (options) { + if ("align" in options && options.align == "right") { + if ("maxWidth" in options && "lineHeight" in options) { + EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => { + this.ctx.fillText(str, x - this.ctx.measureText(str).width, y + options.lineHeight * index) + }) + } else { + this.ctx.fillText(text, x - this.ctx.measureText(text).width, y) + } + } else if ("align" in options && options.align == "centerW") { + if ("maxWidth" in options && "lineHeight" in options) { + EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => { + let textWidth = this.ctx.measureText(str).width; + this.ctx.fillText(str, x + (options.width - textWidth) / 2, y + options.lineHeight * index) + }) + } else { + let textWidth = this.ctx.measureText(text).width; + this.ctx.fillText(text, x + (options.width - textWidth) / 2, y) + } + } else if ("align" in options && options.align == "centerP") { + if ("maxWidth" in options && "lineHeight" in options) { + EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => { + let textWidth = this.ctx.measureText(str).width; + this.ctx.fillText(str, x - textWidth / 2, y + options.lineHeight * index) + }) + } else { + let textWidth = this.ctx.measureText(text).width; + this.ctx.fillText(text, x - textWidth / 2, y) + } + } else if ("align" in options && options.align == "left" || !("align" in options)) { + if ("maxWidth" in options && "lineHeight" in options) { + EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => { + this.ctx.fillText(str, x, y + options.lineHeight * index) + }) + } else { + this.ctx.fillText(text, x, y) + } + } + } else { + this.ctx.fillText(text, x, y); + } + + return this + } + + /** + * 在 Canvas 上绘制图像。 + * @param {CanvasImageSource} image - 图像源。 + * @param {number} dx - 图像在目标 canvas 上的 x 坐标。 + * @param {number} dy - 图像在目标 canvas 上的 y 坐标。 + * @param {number} dWidth - 在目标 canvas 上绘制图像的宽度。 + * @param {number} dHeight - 在目标 canvas 上绘制图像的高度。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + drawImage(image: CanvasImageSource, dx: number, dy: number, dWidth: number, dHeight: number): this { + this.ctx.drawImage(image, dx, dy, dWidth, dHeight); + return this + } + + /** + * 保存当前 Canvas 状态。 + * @param {string} statusName - 状态名称。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + saveStatus(statusName: string): this { + let canvasData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + this.statusMap.set(statusName, canvasData); + return this + } + + /** + * 恢复之前保存的 Canvas 状态。 + * @param {string} statusName - 状态名称。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + restoreStatus(statusName: string): this { + if (this.statusMap.has(statusName)) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.putImageData(this.statusMap.get(statusName)!, 0, 0); + } + return this + } + + /** + * 删除保存的 Canvas 状态。 + * @param {string} statusName - 状态名称。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + deleteStatus(statusName: string): this { + this.statusMap.delete(statusName); + return this + } + + /** + * 在 Canvas 上绘制线段。 + * @param {...[number, number][]} points - 线段的点坐标。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + lineTo(...points: [number, number][]): this { + points.forEach(([x, y]) => this.ctx.lineTo(x, y)); + return this + } + + /** + * 创建路径。 + * @param {...[number, number][]} points - 路径的点坐标。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + createPath(...points: [number, number][]): this { + this.ctx.beginPath(); + points.forEach(([x, y], index) => { + if (index === 0) this.ctx.moveTo(x, y); + this.ctx.lineTo(x, y) + }); + this.ctx.closePath(); + return this + } + + /** + * 绘制路径的轮廓。 + * @param {string} strokeStyle - 轮廓样式。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + stroke(strokeStyle: string): this { + this.ctx.strokeStyle = strokeStyle; + this.ctx.stroke(); + return this + } + + /** + * 填充路径。 + * @param {string} fillStyle - 填充样式。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + fill(fillStyle: string): this { + this.ctx.fillStyle = fillStyle; + this.ctx.fill(); + return this + } + + /** + * 创建动画。 + * @param {(progress: number, p: this) => void} onFrame - 每一帧调用的回调函数。 + * @param {Object} options - 动画选项。 + * @param {number} options.duration - 动画持续时间。 + * @param {(t: number) => number} options.easing - 缓动函数。 + * @param {boolean} [options.autoRecover] - 是否自动恢复状态。 + * @param {number} [options.delay] - 动画延迟时间。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + createAnimation( + onFrame: (progress: number, p: this) => void, + options: { duration: number, easing: (t: number) => number, autoRecover?: boolean, delay?: number } + ): this { + const { duration, easing } = options; + let animationId = generateRandomHexString(16); + if (options.autoRecover) this.saveStatus(animationId); + setTimeout(() => { + forEachFrameInMilliseconds((index, total) => { + const progress = index / total; + if (options.autoRecover) this.restoreStatus(animationId); + onFrame(easing(progress), this); + }, duration); + }, options.delay); + return this + } + + /** + * 创建图形。 + * @param {Object} options - 图形选项。 + * @param {[number, number]} options.centerPoint - 中心点坐标。 + * @param {number} options.r - 半径。 + * @param {string} options.fillStyle - 填充样式。 + * @param {string} options.strokeStyle - 轮廓样式。 + * @param {number} [options.startAngle] - 起始角度。 + * @param {(index: number, domainCount: number) => number} [options.angleCal] - 角度计算函数。 + * @param {...number[]} value - 值数组。 + * @returns {this} 当前 EasyCanvas 实例。 + */ + createGraph(options: { centerPoint: [number, number], r: number, fillStyle: string, strokeStyle: string, startAngle?: number, angleCal?: (index: number, domainCount: number) => number }, ...value: number[]): this { + const { centerPoint, r, strokeStyle, fillStyle } = options; + let points: [number, number][] = []; + const domainCount = value.length; + for (let i = 0; i < domainCount; i++) { + if (!options.angleCal) { + options.angleCal = (index: number, domainCount: number) => index / domainCount * Math.PI * 2; + } + if (!options.startAngle) { + options.startAngle = 0; + } + const angle = options.angleCal(i, domainCount) + options.startAngle; + const x = centerPoint[0] + r * Math.cos(angle) * value[i]; + const y = centerPoint[1] + r * Math.sin(angle) * value[i]; + points.push([x, y]); + } + + return this.createPath(...points) + .fill(fillStyle) + .stroke(strokeStyle) + } +} + + +export class EasyAudio { + private static audioCtx: AudioContext | null = null; + private buffers: Map = new Map(); + private sources: Map = new Map(); // 保持不变 + private gains: Map = new Map(); // 新增:存储GainNode以便控制音量或断开连接 + private audioOptions: Map< + string, + { audioUrl: string; loop?: boolean; volume?: number } + > = new Map(); + + private get audioCtx(): AudioContext { + if (!EasyAudio.audioCtx) { + // 最好在用户交互后创建 AudioContext + try { + EasyAudio.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + } catch (e) { + console.error("Web Audio API is not supported in this browser", e); + // 可以抛出错误或返回一个模拟/null对象,具体取决于你的错误处理策略 + throw new Error("Web Audio API is not supported"); + } + } + return EasyAudio.audioCtx; + } + + constructor( + audios?: + | { name: string; audioUrl: string | URL; loop?: boolean; volume?: number } + | { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }[] + ) { + if (audios) { + this.add(audios); + } + } + + private async createAudioBuffer(audioUrl: string): Promise { + const response = await fetch(audioUrl); + if (!response.ok) { + throw new Error(`Failed to fetch audio: ${response.statusText}`); + } + const arrayBuffer = await response.arrayBuffer(); + // 使用 Promise 包装 decodeAudioData 以支持旧浏览器可能需要的回调方式 + return new Promise((resolve, reject) => { + this.audioCtx.decodeAudioData( + arrayBuffer, + buffer => resolve(buffer), + error => reject(new Error(`Failed to decode audio data: ${error?.message || error}`)) + ); + }); + } + + private async loadAudio(name: string): Promise { + // 检查是否已加载 + if (this.buffers.has(name)) { + return this.buffers.get(name)!; + } + + const options = this.audioOptions.get(name); + if (!options) { + throw new Error(`音频 ${name} 的配置未找到`); + } + console.log(`开始加载音频: ${name}`); + try { + const buffer = await this.createAudioBuffer(options.audioUrl); + this.buffers.set(name, buffer); + console.log(`音频加载完成: ${name}`); + return buffer; + } catch (error) { + console.error(`加载音频 ${name} 失败:`, error); + throw error; // 重新抛出错误,让调用者知道失败了 + } + } + + async load(): Promise { + const promises = Array.from(this.audioOptions.keys()).map(name => + this.loadAudio(name).catch(e => { + // 容错处理,避免一个失败导致 Promise.all 直接 reject + console.error(`预加载音频 ${name} 失败:`, e); + }) + ); + await Promise.all(promises); + console.log("所有请求的音频已尝试加载。"); + } + + add( + audios: + | { name: string; audioUrl: string | URL; loop?: boolean; volume?: number } + | { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }[] + ): void { + const processAudio = (audio: { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }) => { + if (this.audioOptions.has(audio.name)) { + console.warn(`音频名称 "${audio.name}" 已存在,将被覆盖。`); + // 如果正在播放,可能需要先停止?取决于你的需求 + // this.stop(audio.name); + } + this.audioOptions.set(audio.name, { + audioUrl: typeof (audio.audioUrl) === "string" ? audio.audioUrl : audio.audioUrl.href, + loop: audio.loop, + // 确保音量在 0 到 1 (或更高,但通常是0-1) 之间,且是数字 + volume: typeof audio.volume === 'number' ? Math.max(0, audio.volume) : undefined, + }); + }; + + if (Array.isArray(audios)) { + audios.forEach(processAudio); + } else if (audios) { + processAudio(audios); + } + } + + async play(name: string, onEnded?: () => void): Promise { // 返回 Promise 以便知道何时开始播放(或失败) + const options = this.audioOptions.get(name); + if (!options) { + throw new Error(`音频 ${name} 的配置未找到`); + } + + // --- 关键改动点 --- + // 1. 如果已存在同名source,先停止它 + this.stop(name); // 调用 stop 会处理 source 和 gain 的清理 + + // 2. 立即创建 Source 和 Gain Node + const source = this.audioCtx.createBufferSource(); + let gainNode: GainNode | null = null; + + // 3. 立即存储 Source 和 Gain Node + this.sources.set(name, source); + if (options.volume !== undefined) { + gainNode = this.audioCtx.createGain(); + gainNode.gain.value = options.volume; + this.gains.set(name, gainNode); // 存储 GainNode + } + // --- 结束关键改动点 --- + + try { + // 4. 加载或获取 Buffer (如果需要等待,stop 也能找到 source 了) + const buffer = await this.loadAudio(name); + + // 5. 配置 Source + source.buffer = buffer; + source.loop = options.loop || false; + + // 6. 连接节点 + const destinationNode = gainNode || this.audioCtx.destination; + if (gainNode) { + source.connect(gainNode); + gainNode.connect(this.audioCtx.destination); + } else { + source.connect(destinationNode); + } + + source.onended = () => { + if (this.sources.get(name) === source) { + this.stop(name); // 调用 stop 来统一处理清理逻辑 + } + onEnded?.(); // 调用外部传入的回调 + }; + + // 8. 开始播放 + source.start(); + console.log(`音频 ${name} 已调用 start()`); + + } catch (error) { + console.error(`播放音频 ${name} 时出错:`, error); + // 出错时也要清理 + this.stop(name); // 清理可能已创建的节点 + throw error; // 将错误继续抛出 + } + } + + stop(name: string): void { + const source = this.sources.get(name); + const gainNode = this.gains.get(name); + + if (source) { + try { + // source.stop() 可能会在某些状态下(如已结束)抛出错误,但通常可以安全调用 + source.stop(); + console.log(`音频 ${name} 已调用 stop()`); + } catch (e) { + // 忽略调用 stop() 时可能发生的错误 (例如 InvalidStateError) + // console.warn(`调用 source.stop() for ${name} 时出现异常 (可能已停止):`, e); + } finally { + // 清理 source + source.onended = null; // 移除 onended 监听器,防止 stop 后再次触发清理 + source.disconnect(); // 断开连接 + this.sources.delete(name); + } + } + + if (gainNode) { + gainNode.disconnect(); // 断开 GainNode 的连接 + this.gains.delete(name); // 从 Map 中移除 GainNode + } + + // 如果 source 或 gainNode 不存在,说明没有在播放或已被清理,无需操作 + } + + stopAll(): void { + console.log("停止所有音频..."); + // 复制一份 keys 来遍历,因为 stop(name) 会修改 this.sources + const names = Array.from(this.sources.keys()); + names.forEach(name => this.stop(name)); + // 理论上 stop(name) 已经清理了 map,但为保险起见可以清空 + if (this.sources.size > 0) { + console.warn("stopAll 后 sources Map 仍有残留,强制清空。这可能表示 stop() 逻辑有误。"); + this.sources.clear(); + this.gains.clear(); + } + } + + // (可选)添加清理方法,例如在组件卸载时调用 + dispose(): void { + this.stopAll(); + this.buffers.clear(); + this.audioOptions.clear(); + if (EasyAudio.audioCtx && EasyAudio.audioCtx.state !== 'closed') { + EasyAudio.audioCtx.close().then(() => { + console.log("AudioContext closed."); + EasyAudio.audioCtx = null; + }).catch(e => { + console.error("Error closing AudioContext:", e); + EasyAudio.audioCtx = null; // 即使关闭失败也设为 null + }); + } else { + EasyAudio.audioCtx = null; + } + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..36cb420 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,49 @@ +/// + +// Web Speech API 类型定义 +declare class SpeechRecognition extends EventTarget { + continuous: boolean; + interimResults: boolean; + lang: string; + maxAlternatives: number; + start(): void; + stop(): void; + abort(): void; + onresult: (event: SpeechRecognitionEvent) => void; + onerror: (event: SpeechRecognitionErrorEvent) => void; + onend: () => void; + onstart: () => void; +} + +interface Window { + SpeechRecognition: typeof SpeechRecognition; + webkitSpeechRecognition: typeof SpeechRecognition; +} + +interface SpeechRecognitionErrorEvent extends Event { + error: string; + message?: string; +} + +interface SpeechRecognitionResultList { + length: number; + item(index: number): SpeechRecognitionResult; + [index: number]: SpeechRecognitionResult; +} + +interface SpeechRecognitionResult { + length: number; + item(index: number): SpeechRecognitionAlternative; + [index: number]: SpeechRecognitionAlternative; + isFinal?: boolean; +} + +interface SpeechRecognitionAlternative { + transcript: string; + confidence: number; +} + +interface SpeechRecognitionEvent extends Event { + resultIndex: number; + results: SpeechRecognitionResultList; +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..6f348f5 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..85b71dc --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..117e1b0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + build: { + assetsInlineLimit: 256 * 1024 + } +})