feat: 整体重构

This commit is contained in:
Next
2026-03-15 19:21:04 +08:00
parent 7c78fc9e57
commit cc86f690b4
56 changed files with 2631 additions and 1973 deletions

View File

@@ -0,0 +1 @@
template/devtools/

View File

@@ -0,0 +1,281 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "cccdev-template-3x",
"devDependencies": {
"@preact/preset-vite": "^2.9.1",
"@preact/signals": "^1.3.0",
"preact": "^10.24.3",
"typescript": "^5.6.3",
"vite": "^5.4.11",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://mirrors.tencent.com/npm/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "https://mirrors.tencent.com/npm/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "https://mirrors.tencent.com/npm/@babel/core/-/core-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "https://mirrors.tencent.com/npm/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "https://mirrors.tencent.com/npm/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "https://mirrors.tencent.com/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "https://mirrors.tencent.com/npm/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "https://mirrors.tencent.com/npm/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "https://mirrors.tencent.com/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://mirrors.tencent.com/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://mirrors.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://mirrors.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "https://mirrors.tencent.com/npm/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "https://mirrors.tencent.com/npm/@babel/helpers/-/helpers-7.28.6.tgz", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.29.0", "https://mirrors.tencent.com/npm/@babel/parser/-/parser-7.29.0.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "https://mirrors.tencent.com/npm/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
"@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.28.6", "https://mirrors.tencent.com/npm/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-jsx": "^7.28.6", "@babel/types": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow=="],
"@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "https://mirrors.tencent.com/npm/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="],
"@babel/template": ["@babel/template@7.28.6", "https://mirrors.tencent.com/npm/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "https://mirrors.tencent.com/npm/@babel/traverse/-/traverse-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "https://mirrors.tencent.com/npm/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "https://mirrors.tencent.com/npm/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://mirrors.tencent.com/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://mirrors.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://mirrors.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@preact/preset-vite": ["@preact/preset-vite@2.10.3", "https://mirrors.tencent.com/npm/@preact/preset-vite/-/preset-vite-2.10.3.tgz", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@prefresh/vite": "^2.4.11", "@rollup/pluginutils": "^5.0.0", "babel-plugin-transform-hook-names": "^1.0.2", "debug": "^4.4.3", "picocolors": "^1.1.1", "vite-prerender-plugin": "^0.5.8" }, "peerDependencies": { "@babel/core": "7.x", "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x" } }, "sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg=="],
"@preact/signals": ["@preact/signals@1.3.4", "https://mirrors.tencent.com/npm/@preact/signals/-/signals-1.3.4.tgz", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="],
"@preact/signals-core": ["@preact/signals-core@1.14.0", "https://mirrors.tencent.com/npm/@preact/signals-core/-/signals-core-1.14.0.tgz", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
"@prefresh/babel-plugin": ["@prefresh/babel-plugin@0.5.3", "https://mirrors.tencent.com/npm/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz", {}, "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ=="],
"@prefresh/core": ["@prefresh/core@1.5.9", "https://mirrors.tencent.com/npm/@prefresh/core/-/core-1.5.9.tgz", { "peerDependencies": { "preact": "^10.0.0 || ^11.0.0-0" } }, "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ=="],
"@prefresh/utils": ["@prefresh/utils@1.2.1", "https://mirrors.tencent.com/npm/@prefresh/utils/-/utils-1.2.1.tgz", {}, "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw=="],
"@prefresh/vite": ["@prefresh/vite@2.4.12", "https://mirrors.tencent.com/npm/@prefresh/vite/-/vite-2.4.12.tgz", { "dependencies": { "@babel/core": "^7.22.1", "@prefresh/babel-plugin": "^0.5.2", "@prefresh/core": "^1.5.0", "@prefresh/utils": "^1.2.0", "@rollup/pluginutils": "^4.2.1" }, "peerDependencies": { "preact": "^10.4.0 || ^11.0.0-0", "vite": ">=2.0.0" } }, "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "https://mirrors.tencent.com/npm/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "https://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@types/estree": ["@types/estree@1.0.8", "https://mirrors.tencent.com/npm/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"babel-plugin-transform-hook-names": ["babel-plugin-transform-hook-names@1.0.2", "https://mirrors.tencent.com/npm/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", { "peerDependencies": { "@babel/core": "^7.12.10" } }, "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
"boolbase": ["boolbase@1.0.0", "https://mirrors.tencent.com/npm/boolbase/-/boolbase-1.0.0.tgz", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"browserslist": ["browserslist@4.28.1", "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.28.1.tgz", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001779", "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="],
"convert-source-map": ["convert-source-map@2.0.0", "https://mirrors.tencent.com/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"css-select": ["css-select@5.2.2", "https://mirrors.tencent.com/npm/css-select/-/css-select-5.2.2.tgz", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-what": ["css-what@6.2.2", "https://mirrors.tencent.com/npm/css-what/-/css-what-6.2.2.tgz", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"debug": ["debug@4.4.3", "https://mirrors.tencent.com/npm/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dom-serializer": ["dom-serializer@2.0.0", "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-2.0.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "https://mirrors.tencent.com/npm/domelementtype/-/domelementtype-2.3.0.tgz", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "https://mirrors.tencent.com/npm/domhandler/-/domhandler-5.0.3.tgz", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "https://mirrors.tencent.com/npm/domutils/-/domutils-3.2.2.tgz", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.313", "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],
"entities": ["entities@4.5.0", "https://mirrors.tencent.com/npm/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"esbuild": ["esbuild@0.21.5", "https://mirrors.tencent.com/npm/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"escalade": ["escalade@3.2.0", "https://mirrors.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@2.0.2", "https://mirrors.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"fsevents": ["fsevents@2.3.3", "https://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "https://mirrors.tencent.com/npm/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"he": ["he@1.2.0", "https://mirrors.tencent.com/npm/he/-/he-1.2.0.tgz", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"js-tokens": ["js-tokens@4.0.0", "https://mirrors.tencent.com/npm/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "https://mirrors.tencent.com/npm/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "https://mirrors.tencent.com/npm/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kolorist": ["kolorist@1.8.0", "https://mirrors.tencent.com/npm/kolorist/-/kolorist-1.8.0.tgz", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"lru-cache": ["lru-cache@5.1.1", "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "https://mirrors.tencent.com/npm/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"ms": ["ms@2.1.3", "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "https://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-html-parser": ["node-html-parser@6.1.13", "https://mirrors.tencent.com/npm/node-html-parser/-/node-html-parser-6.1.13.tgz", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg=="],
"node-releases": ["node-releases@2.0.36", "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.36.tgz", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"nth-check": ["nth-check@2.1.1", "https://mirrors.tencent.com/npm/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"picocolors": ["picocolors@1.1.1", "https://mirrors.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "https://mirrors.tencent.com/npm/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.8", "https://mirrors.tencent.com/npm/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"preact": ["preact@10.29.0", "https://mirrors.tencent.com/npm/preact/-/preact-10.29.0.tgz", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="],
"rollup": ["rollup@4.59.0", "https://mirrors.tencent.com/npm/rollup/-/rollup-4.59.0.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"semver": ["semver@6.3.1", "https://mirrors.tencent.com/npm/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"simple-code-frame": ["simple-code-frame@1.3.0", "https://mirrors.tencent.com/npm/simple-code-frame/-/simple-code-frame-1.3.0.tgz", { "dependencies": { "kolorist": "^1.6.0" } }, "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w=="],
"source-map": ["source-map@0.7.6", "https://mirrors.tencent.com/npm/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "https://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stack-trace": ["stack-trace@1.0.0-pre2", "https://mirrors.tencent.com/npm/stack-trace/-/stack-trace-1.0.0-pre2.tgz", {}, "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A=="],
"typescript": ["typescript@5.9.3", "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"vite": ["vite@5.4.21", "https://mirrors.tencent.com/npm/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vite-prerender-plugin": ["vite-prerender-plugin@0.5.13", "https://mirrors.tencent.com/npm/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz", { "dependencies": { "kolorist": "^1.8.0", "magic-string": "0.x >= 0.26.0", "node-html-parser": "^6.1.12", "simple-code-frame": "^1.3.0", "source-map": "^0.7.4", "stack-trace": "^1.0.0-pre2" }, "peerDependencies": { "vite": "5.x || 6.x || 7.x || 8.x" } }, "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g=="],
"yallist": ["yallist@3.1.1", "https://mirrors.tencent.com/npm/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@prefresh/vite/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "https://mirrors.tencent.com/npm/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="],
"@prefresh/vite/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "https://mirrors.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "cccdev-template-3x",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build"
},
"devDependencies": {
"@preact/preset-vite": "^2.9.1",
"@preact/signals": "^1.3.0",
"preact": "^10.24.3",
"typescript": "^5.6.3",
"vite": "^5.4.11"
}
}

View File

@@ -0,0 +1,208 @@
import { useEffect, useRef } from 'preact/hooks';
import { useComputed, useSignal } from '@preact/signals';
import { devtoolsOpen, profilerOpen } from '../store';
import { TreePanel } from './TreePanel';
import { PropPanel } from './PropPanel';
import { ProfilerPanel } from './ProfilerPanel';
const MIN_WIDTH = 240;
const MAX_WIDTH = 600;
const STORAGE_KEY = 'cc_devtools_width';
function getSavedWidth(): number {
const v = parseInt(localStorage.getItem(STORAGE_KEY) ?? '', 10);
return isNaN(v) ? 320 : Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, v));
}
function getToolbarHeight(): number {
const toolbar = document.querySelector('.toolbar') as HTMLElement | null;
return toolbar ? toolbar.getBoundingClientRect().height : 42;
}
/** 将游戏内容区向左推开,避免面板遮挡 */
function pushGameCanvas(width: number) {
const content = document.getElementById('content');
if (content) content.style.paddingRight = `${width + 16}px`; // 面板宽 + 两侧间距
}
function restoreGameCanvas() {
const content = document.getElementById('content');
if (content) content.style.paddingRight = '';
}
function ToggleButton({ toolbarH }: { toolbarH: number }) {
const active = devtoolsOpen.value;
return (
<button
class={`ccdev-toggle${active ? ' active' : ''}`}
style={{ top: `${Math.round(toolbarH / 2)}px` }}
onClick={() => {
devtoolsOpen.value = !devtoolsOpen.value;
}}
title={active ? '关闭 DevTools' : '打开 DevTools'}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 2l1.88 1.88" />
<path d="M14.12 3.88L16 2" />
<path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" />
<path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" />
<path d="M12 20v-9" />
<path d="M6.53 9C4.6 8.8 3 7.1 3 5" />
<path d="M6 13H2" />
<path d="M3 21c0-2.1 1.7-3.9 3.8-4" />
<path d="M20.97 5c0 2.1-1.6 3.8-3.5 4" />
<path d="M22 13h-4" />
<path d="M17.2 17c2.1.1 3.8 1.9 3.8 4" />
</svg>
</button>
);
}
export function App() {
const open = useComputed(() => devtoolsOpen.value);
const panelRef = useRef<HTMLDivElement>(null);
const handleRef = useRef<HTMLDivElement>(null);
const toolbarH = useSignal(getToolbarHeight());
// 向外暴露 toggle供 toolbar 按钮调用
(window as any).__ccDevToolsToggle = () => {
devtoolsOpen.value = !devtoolsOpen.value;
};
(window as any).__ccProfilerToggle = () => {
profilerOpen.value = !profilerOpen.value;
};
// 持续监听 toolbar 实际高度,同步到 signal
useEffect(() => {
let ro: ResizeObserver | null = null;
function attach() {
const toolbar = document.querySelector('.toolbar') as HTMLElement | null;
if (!toolbar) return false;
// 用 getBoundingClientRect 拿含 border 的完整高度
toolbarH.value = Math.round(toolbar.getBoundingClientRect().bottom);
ro = new ResizeObserver(() => {
toolbarH.value = Math.round(toolbar.getBoundingClientRect().bottom);
});
ro.observe(toolbar);
return true;
}
if (!attach()) {
// toolbar 尚未注入(由 cocosToolBar include 动态插入),轮询等待
const id = setInterval(() => {
if (attach()) clearInterval(id);
}, 100);
return () => clearInterval(id);
}
return () => ro?.disconnect();
}, []);
// 面板展开/收起时同步推开画布
useEffect(() => {
if (open.value) {
const w = getSavedWidth();
document.documentElement.style.setProperty('--devtools-width', `${w}px`);
pushGameCanvas(w);
} else {
restoreGameCanvas();
}
return () => {
restoreGameCanvas();
};
}, [open.value]);
// 左侧拖拽调整宽度
useEffect(() => {
const handle = handleRef.current;
const panel = panelRef.current;
if (!handle || !panel) return;
let startX = 0;
let startW = 0;
function onMouseMove(e: MouseEvent) {
const delta = startX - e.clientX; // 向左拖 = 变宽
const newW = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startW + delta));
document.documentElement.style.setProperty('--devtools-width', `${newW}px`);
pushGameCanvas(newW);
}
function onMouseUp() {
if (!handle) return;
handle.classList.remove('dragging');
const content = document.getElementById('content');
if (content) content.style.pointerEvents = '';
const w = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--devtools-width'),
10,
);
localStorage.setItem(STORAGE_KEY, String(w));
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
function onMouseDown(e: MouseEvent) {
if (!handle || !panel) return;
e.preventDefault();
startX = e.clientX;
startW = panel.getBoundingClientRect().width;
handle.classList.add('dragging');
const content = document.getElementById('content');
if (content) content.style.pointerEvents = 'none';
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
handle.addEventListener('mousedown', onMouseDown);
return () => handle.removeEventListener('mousedown', onMouseDown);
}, [open.value]);
const GAP = 8; // 上下各留 8px 空隙
const top = `${toolbarH.value + GAP}px`;
const height = `calc(100vh - ${toolbarH.value + GAP * 2}px)`;
if (!open.value)
return (
<>
<ToggleButton toolbarH={toolbarH.value} />
<ProfilerPanel />
</>
);
return (
<>
<ToggleButton toolbarH={toolbarH.value} />
<div id="cc-devtools" ref={panelRef} style={{ top, height }}>
<div id="cc-devtools-resize" ref={handleRef} title="拖拽调整面板宽度" />
<a
class="ccdev-github"
href="https://github.com/potato47/ccc-devtools"
target="_blank"
title="GitHub"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
</a>
<TreePanel />
<PropPanel />
</div>
<ProfilerPanel />
</>
);
}

View File

@@ -0,0 +1,56 @@
import { useSignal } from '@preact/signals';
import { PropItem } from './PropItem';
import { getComponentViewModel } from '../models/ComponentModels';
import { outputToConsole } from '../engine';
interface ComponentPanelProps {
name: string;
component: any;
updateKey: number;
}
export function ComponentPanel({ name, component, updateKey: _updateKey }: ComponentPanelProps) {
const collapsed = useSignal(false);
const model = getComponentViewModel(name, () => component);
return (
<div class="comp-panel">
<div
class="comp-header"
onClick={() => {
collapsed.value = !collapsed.value;
}}
>
<input
type="checkbox"
class="comp-enabled"
checked={component?.enabled}
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
if (component) component.enabled = (e.target as HTMLInputElement).checked;
}}
/>
<span class="comp-name">{name}</span>
<span class="comp-arrow">{collapsed.value ? '' : '⌄'}</span>
<button
class="icon-btn"
title="输出到控制台"
onClick={(e) => {
e.stopPropagation();
outputToConsole(component);
}}
>
&gt;
</button>
</div>
{!collapsed.value && model && (
<div class="comp-props">
{model.props.map((prop) => (
<PropItem key={prop.key} model={model} propName={prop.name} propKey={prop.key} />
))}
</div>
)}
{!collapsed.value && !model && <div class="comp-empty"></div>}
</div>
);
}

View File

@@ -0,0 +1,120 @@
import { useEffect, useRef } from 'preact/hooks';
import { useSignal } from '@preact/signals';
import { profilerOpen } from '../store';
import { cc } from '../engine';
interface StatItem {
key: string;
desc: string;
value: string;
}
const STAT_KEYS = [
'fps',
'draws',
'frame',
'instances',
'tricount',
'logic',
'physics',
'render',
'textureMemory',
'bufferMemory',
];
export function ProfilerPanel() {
const items = useSignal<StatItem[]>(STAT_KEYS.map((key) => ({ key, desc: key, value: '—' })));
const panelRef = useRef<HTMLDivElement>(null);
const posX = useSignal(window.innerWidth - 260);
const posY = useSignal(60);
// 轮询 cc.profiler.stats
useEffect(() => {
if (!profilerOpen.value) return;
function refresh() {
const c = cc();
if (!c?.profiler?.stats) return;
const stats = c.profiler.stats;
items.value = STAT_KEYS.map((key) => {
const data = stats[key];
if (!data) return { key, desc: key, value: '—' };
const val = data.isInteger
? String(data.counter._value | 0)
: data.counter._value.toFixed(2);
return { key, desc: data.desc ?? key, value: val };
});
}
refresh();
const id = setInterval(refresh, 1000);
return () => clearInterval(id);
}, [profilerOpen.value]);
// 原生拖拽
useEffect(() => {
const header = panelRef.current?.querySelector('.profiler-drag') as HTMLElement;
if (!header) return;
let startX = 0,
startY = 0,
startPX = 0,
startPY = 0;
let dragging = false;
function onMouseMove(e: MouseEvent) {
if (!dragging) return;
posX.value = startPX + (e.clientX - startX);
posY.value = startPY + (e.clientY - startY);
}
function onMouseUp() {
dragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
function onMouseDown(e: MouseEvent) {
dragging = true;
startX = e.clientX;
startY = e.clientY;
startPX = posX.value;
startPY = posY.value;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
header.addEventListener('mousedown', onMouseDown);
return () => header.removeEventListener('mousedown', onMouseDown);
}, [profilerOpen.value]);
if (!profilerOpen.value) return null;
return (
<div
ref={panelRef}
class="profiler-float"
style={{ left: `${posX.value}px`, top: `${posY.value}px` }}
>
<div class="profiler-drag">
<span>Profiler</span>
<button
class="icon-btn"
onClick={() => {
profilerOpen.value = false;
}}
>
</button>
</div>
<div class="profiler-body">
{items.value.map((item) => (
<div class="profiler-row" key={item.key}>
<span class="profiler-desc">{item.desc}</span>
<span class="profiler-val">{item.value}</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,120 @@
import { useRef, useEffect } from 'preact/hooks';
import { cc } from '../engine';
interface PropItemProps {
model: any;
propName: string;
propKey: string;
}
function getPropType(value: any): string {
if (value === null || value === undefined) return 'unknown';
if (typeof value === 'object' && value.__classname__) return value.__classname__;
return typeof value;
}
function colorToHex(color: any): string {
const hex = color.toHEX() as string;
return `#${hex}`;
}
function hexToColor(hex: string): any {
return new (cc().Color)().fromHEX(hex);
}
function formatNum(v: number): string {
return Number.isInteger(v) ? String(v) : parseFloat(v.toFixed(3)).toString();
}
/**
* 数字输入框:非受控 + onInput 实时写入。
* 用 ref 在外部 tick 变化时手动同步显示值(仅在未聚焦时同步,避免打断输入)。
*/
function NumberInput({ model, propKey }: { model: any; propKey: string }) {
const ref = useRef<HTMLInputElement>(null);
// 同步外部值到输入框(未聚焦时才更新,避免覆盖正在输入的内容)
useEffect(() => {
const el = ref.current;
if (!el || document.activeElement === el) return;
const external = model[propKey];
if (parseFloat(el.value) !== external) {
el.value = formatNum(external);
}
});
return (
<input
ref={ref}
type="number"
class="prop-input"
defaultValue={formatNum(model[propKey])}
step="0.1"
onInput={(e) => {
const v = parseFloat((e.target as HTMLInputElement).value);
if (!isNaN(v)) model[propKey] = v;
}}
/>
);
}
function StringInput({ model, propKey }: { model: any; propKey: string }) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
const el = ref.current;
if (!el || document.activeElement === el) return;
const external = String(model[propKey] ?? '');
if (el.value !== external) el.value = external;
});
return (
<input
ref={ref}
type="text"
class="prop-input"
defaultValue={model[propKey]}
onInput={(e) => {
model[propKey] = (e.target as HTMLInputElement).value;
}}
/>
);
}
export function PropItem({ model, propName, propKey }: PropItemProps) {
const value = model[propKey];
const type = getPropType(value);
return (
<div class="prop-row">
<span class="prop-name">{propName}</span>
<div class="prop-value">
{type === 'number' && <NumberInput model={model} propKey={propKey} />}
{type === 'string' && <StringInput model={model} propKey={propKey} />}
{type === 'boolean' && (
<input
type="checkbox"
class="prop-checkbox"
checked={value}
onChange={(e) => {
model[propKey] = (e.target as HTMLInputElement).checked;
}}
/>
)}
{type === 'cc.Color' && (
<input
type="color"
class="prop-color"
value={colorToHex(value)}
onChange={(e) => {
model[propKey] = hexToColor((e.target as HTMLInputElement).value);
}}
/>
)}
{!['number', 'string', 'boolean', 'cc.Color'].includes(type) && (
<span class="prop-unknown">{String(value)}</span>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import { useComputed } from '@preact/signals';
import { selectedNode, updateTick } from '../store';
import { isValid, getComponents, outputToConsole, drawNodeRect } from '../engine';
import { NodeModel } from '../models/NodeModel';
import { PropItem } from './PropItem';
import { ComponentPanel } from './ComponentPanel';
export function PropPanel() {
const node = useComputed(() => selectedNode.value);
const tick = useComputed(() => updateTick.value);
if (!node.value || !isValid(node.value)) {
return (
<div class="prop-panel">
<div class="panel-header"></div>
<div class="prop-empty"></div>
</div>
);
}
const ccNode = node.value;
const components = getComponents(ccNode);
return (
<div class="prop-panel">
<div class="panel-header"></div>
<div class="prop-scroll">
{/* 节点基础属性 */}
<div class="comp-panel">
<div class="comp-header">
<input
type="checkbox"
class="comp-enabled"
checked={ccNode.active}
onChange={(e) => {
ccNode.active = (e.target as HTMLInputElement).checked;
}}
/>
<span class="comp-name">Node</span>
<div style="flex:1" />
<button class="icon-btn" title="高亮节点" onClick={() => drawNodeRect(ccNode)}>
</button>
<button class="icon-btn" title="输出到控制台" onClick={() => outputToConsole(ccNode)}>
&gt;
</button>
</div>
<div class="comp-props">
{NodeModel.props.map((prop) => (
<PropItem key={prop.key} model={NodeModel} propName={prop.name} propKey={prop.key} />
))}
</div>
</div>
<div class="divider" />
{/* 组件列表 */}
{components.map(({ name, target }) => (
<ComponentPanel key={name} name={name} component={target} updateKey={tick.value} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,275 @@
import { useEffect, useCallback, useRef } from 'preact/hooks';
import { useSignal, useComputed } from '@preact/signals';
import {
treeData,
expandedUuids,
selectedNode,
searchQuery,
updateTick,
type TreeNode,
} from '../store';
import { isReady, getScene, resolveNodeByPath } from '../engine';
// ── 构建树数据 ─────────────────────────────────────────────
function buildTree(children: any[], path: string[]): TreeNode[] {
const result: TreeNode[] = [];
for (const ccNode of children) {
const childPath = [...path, ccNode.uuid];
const node: TreeNode = {
uuid: ccNode.uuid,
name: ccNode.name,
active: ccNode.activeInHierarchy,
children: ccNode.children?.length > 0 ? buildTree(ccNode.children, childPath) : [],
path: childPath,
};
result.push(node);
}
return result;
}
// ── 搜索:收集所有匹配节点(扁平列表)─────────────────────
interface FlatMatch {
node: TreeNode;
depth: number;
}
function collectMatches(nodes: TreeNode[], query: string, depth = 0): FlatMatch[] {
const q = query.toLowerCase();
const result: FlatMatch[] = [];
for (const node of nodes) {
if (node.name.toLowerCase().includes(q)) {
result.push({ node, depth });
}
result.push(...collectMatches(node.children, query, depth + 1));
}
return result;
}
// ── 搜索时自动展开匹配节点的所有祖先 ─────────────────────
function expandAncestors(nodes: TreeNode[], query: string): Set<string> {
const q = query.toLowerCase();
const toExpand = new Set<string>();
function walk(node: TreeNode, ancestors: string[]): boolean {
const matched = node.name.toLowerCase().includes(q);
let childMatched = false;
for (const child of node.children) {
if (walk(child, [...ancestors, node.uuid])) childMatched = true;
}
if (matched || childMatched) {
for (const uuid of ancestors) toExpand.add(uuid);
}
return matched || childMatched;
}
for (const node of nodes) walk(node, []);
return toExpand;
}
// ── 高亮关键词 ────────────────────────────────────────────
function HighlightText({ text, query }: { text: string; query: string }) {
if (!query) return <span class="tree-label">{text}</span>;
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx === -1) return <span class="tree-label">{text}</span>;
return (
<span class="tree-label">
{text.slice(0, idx)}
<mark class="tree-highlight">{text.slice(idx, idx + query.length)}</mark>
{text.slice(idx + query.length)}
</span>
);
}
// ── 搜索结果行 ────────────────────────────────────────────
function SearchResultItem({ node, query }: { node: TreeNode; query: string }) {
const isSelected = useComputed(() => selectedNode.value?.uuid === node.uuid);
const handleClick = useCallback(() => {
const ccNode = resolveNodeByPath(node.path);
selectedNode.value = ccNode ?? null;
}, [node.path]);
return (
<div
class={`tree-row${isSelected.value ? ' selected' : ''}${!node.active ? ' inactive' : ''}`}
style={{ paddingLeft: '8px' }}
onClick={handleClick}
>
<HighlightText text={node.name} query={query} />
</div>
);
}
// ── 单个树节点组件 ─────────────────────────────────────────
interface TreeNodeItemProps {
node: TreeNode;
depth: number;
}
function TreeNodeItem({ node, depth }: TreeNodeItemProps) {
const expanded = useComputed(() => expandedUuids.value.has(node.uuid));
const isSelected = useComputed(() => selectedNode.value?.uuid === node.uuid);
const hasChildren = node.children.length > 0;
const handleClick = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
const ccNode = resolveNodeByPath(node.path);
selectedNode.value = ccNode ?? null;
},
[node.path],
);
const handleToggle = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
const next = new Set(expandedUuids.value);
if (next.has(node.uuid)) {
next.delete(node.uuid);
} else {
next.add(node.uuid);
}
expandedUuids.value = next;
},
[node.uuid],
);
return (
<div class="tree-node">
<div
class={`tree-row${isSelected.value ? ' selected' : ''}${!node.active ? ' inactive' : ''}`}
style={{ paddingLeft: `${depth * 14 + 6}px` }}
onClick={handleClick}
>
<span
class={`tree-arrow${hasChildren ? '' : ' invisible'}${expanded.value ? ' expanded' : ''}`}
onClick={handleToggle}
>
</span>
<span class="tree-label">{node.name}</span>
</div>
{hasChildren && expanded.value && (
<div class="tree-children">
{node.children.map((child) => (
<TreeNodeItem key={child.uuid} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
// ── TreePanel 主组件 ───────────────────────────────────────
export function TreePanel() {
const initialized = useSignal(false);
const inputRef = useRef<HTMLInputElement>(null);
const query = useComputed(() => searchQuery.value.trim());
// 搜索结果(扁平列表)
const searchResults = useComputed(() => {
if (!query.value) return null;
return collectMatches(treeData.value, query.value);
});
// 搜索关键词变化时,自动展开匹配节点的祖先
useEffect(() => {
if (!query.value) return;
const ancestors = expandAncestors(treeData.value, query.value);
if (ancestors.size === 0) return;
expandedUuids.value = new Set([...expandedUuids.value, ...ancestors]);
}, [query.value]);
useEffect(() => {
let rafId: number;
let started = false;
function refreshTree() {
if (isReady()) {
if (!started) {
started = true;
initialized.value = true;
}
treeData.value = buildTree(getScene().children, []);
updateTick.value = -updateTick.value;
}
rafId = requestAnimationFrame(refreshTree);
}
const pollId = setInterval(() => {
if (isReady()) {
clearInterval(pollId);
rafId = requestAnimationFrame(refreshTree);
}
}, 500);
return () => {
clearInterval(pollId);
cancelAnimationFrame(rafId);
};
}, []);
const handleSearchInput = useCallback((e: Event) => {
searchQuery.value = (e.target as HTMLInputElement).value;
}, []);
const handleClear = useCallback(() => {
searchQuery.value = '';
inputRef.current?.focus();
}, []);
// Esc 清空搜索
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
searchQuery.value = '';
}
}, []);
return (
<div class="tree-panel">
<div class="panel-header"></div>
{/* 搜索栏 */}
<div class="tree-search-bar">
<span class="tree-search-icon"></span>
<input
ref={inputRef}
class="tree-search-input"
type="text"
placeholder="搜索节点…"
value={searchQuery.value}
onInput={handleSearchInput}
onKeyDown={handleKeyDown}
/>
{query.value && (
<button class="tree-search-clear" onClick={handleClear} title="清空">
</button>
)}
</div>
<div class="tree-scroll">
{!initialized.value ? (
<div class="tree-empty"></div>
) : searchResults.value !== null ? (
// 搜索模式:扁平结果列表
searchResults.value.length === 0 ? (
<div class="tree-empty"></div>
) : (
<>
<div class="tree-search-count">{searchResults.value.length} </div>
{searchResults.value.map(({ node }) => (
<SearchResultItem key={node.uuid} node={node} query={query.value} />
))}
</>
)
) : treeData.value.length === 0 ? (
<div class="tree-empty"></div>
) : (
// 正常树模式
treeData.value.map((node) => <TreeNodeItem key={node.uuid} node={node} depth={0} />)
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
/** 获取 Cocos 引擎全局对象 */
export const cc = (): any => (window as any)['cc'];
export const getScene = (): any => cc()?.director?.getScene();
export const isReady = (): boolean => !!cc() && !!getScene();
export const isValid = (node: any): boolean => !!node && cc()?.isValid(node);
export const getComponents = (ccNode: any): Array<{ name: string; target: any }> =>
(ccNode?.components ?? []).map((c: any) => ({ name: c.__classname__ as string, target: c }));
export const getSceneChildren = (): any[] => getScene()?.children ?? [];
export const getChildByUuid = (node: any, uuid: string): any => node?.getChildByUuid(uuid) ?? null;
/** 沿 uuid 路径从场景根查找节点 */
export const resolveNodeByPath = (path: string[]): any => {
let node: any = getScene();
for (const uuid of path) {
node = getChildByUuid(node, uuid);
if (!node) return null;
}
return node;
};
/** 将节点/组件输出到 window.temp1、temp2... 方便控制台操作 */
export const outputToConsole = (target: any): void => {
let i = 1;
while ((window as any)['temp' + i] !== undefined) i++;
(window as any)['temp' + i] = target;
console.log('temp' + i, target);
};
/** 在场景中高亮绘制节点包围盒2s 后自动销毁 */
export const drawNodeRect = (target: any): void => {
const c = cc();
if (!c) return;
let rect: any;
const transform = target.getComponent(c.UITransformComponent);
if (transform) {
rect = getSelfBoundingBoxToWorld(transform, c);
} else {
const worldPos = c.v3();
target.getWorldPosition(worldPos);
rect = c.rect(worldPos.x, worldPos.y, 0, 0);
}
const canvasNode = new c.Node('__DevTools_Highlight__');
const scene = getScene();
scene.addChild(canvasNode);
canvasNode.addComponent(c.Canvas);
const bgNode = new c.Node();
const graphics = bgNode.addComponent(c.GraphicsComponent);
const bgTransform = bgNode.addComponent(c.UITransformComponent);
canvasNode.addChild(bgNode);
const centerPos = c.v3(rect.center.x, rect.center.y, 0);
const localPos = c.v3();
canvasNode.getComponent(c.UITransformComponent).convertToNodeSpaceAR(centerPos, localPos);
bgNode.setPosition(localPos);
bgNode.layer = target.layer;
const isZeroSize = rect.width === 0 || rect.height === 0;
if (isZeroSize) {
graphics.circle(0, 0, 100);
graphics.fillColor = c.Color.GREEN;
graphics.fill();
} else {
bgTransform.width = rect.width;
bgTransform.height = rect.height;
graphics.rect(
-bgTransform.width / 2,
-bgTransform.height / 2,
bgTransform.width,
bgTransform.height,
);
graphics.fillColor = new c.Color().fromHEX('#E91E6390');
graphics.fill();
}
setTimeout(() => {
if (c.isValid(canvasNode)) canvasNode.destroy();
}, 2000);
};
function getSelfBoundingBoxToWorld(transform: any, c: any): any {
const _worldMatrix = c.mat4();
if (transform.node.parent) {
transform.node.parent.getWorldMatrix(_worldMatrix);
const parentMat = _worldMatrix;
const _matrix = c.mat4();
c.Mat4.fromRTS(
_matrix,
transform.node.getRotation(),
transform.node.getPosition(),
transform.node.getScale(),
);
const width = transform._contentSize.width;
const height = transform._contentSize.height;
const rect = c.rect(
-transform._anchorPoint.x * width,
-transform._anchorPoint.y * height,
width,
height,
);
c.Mat4.multiply(_worldMatrix, parentMat, _matrix);
rect.transformMat4(_worldMatrix);
return rect;
}
return transform.getBoundingBox();
}

View File

@@ -0,0 +1,17 @@
import { render } from 'preact';
import { App } from './components/App';
import './style.css';
// 等待 DOM 就绪后挂载
function mount() {
const container = document.getElementById('cc-devtools-root');
if (container) {
render(<App />, container);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}

View File

@@ -0,0 +1,119 @@
import type { PropDef } from './NodeModel';
export interface ComponentViewModel {
props: PropDef[];
[key: string]: any;
}
export function getComponentViewModel(
name: string,
componentGetter: () => any,
): ComponentViewModel | null {
switch (name) {
case 'cc.UITransform':
return new CCUITransformModel(componentGetter);
case 'cc.Label':
return new CCLabelModel(componentGetter);
case 'cc.Sprite':
return new CCSpriteModel(componentGetter);
default:
return null;
}
}
class CCUITransformModel implements ComponentViewModel {
props: PropDef[] = [
{ name: 'Width', key: 'width' },
{ name: 'Height', key: 'height' },
{ name: 'Anchor X', key: 'anchorX' },
{ name: 'Anchor Y', key: 'anchorY' },
];
constructor(private getter: () => any) {}
get width(): number {
return this.getter()?.contentSize.width ?? 0;
}
set width(v: number) {
const c = this.getter();
if (!c) return;
c.setContentSize(v, c.contentSize.height);
}
get height(): number {
return this.getter()?.contentSize.height ?? 0;
}
set height(v: number) {
const c = this.getter();
if (!c) return;
c.setContentSize(c.contentSize.width, v);
}
get anchorX(): number {
return this.getter()?.anchorPoint.x ?? 0;
}
set anchorX(v: number) {
const c = this.getter();
if (!c) return;
c.setAnchorPoint(v, c.anchorPoint.y);
}
get anchorY(): number {
return this.getter()?.anchorPoint.y ?? 0;
}
set anchorY(v: number) {
const c = this.getter();
if (!c) return;
c.setAnchorPoint(c.anchorPoint.x, v);
}
}
class CCLabelModel implements ComponentViewModel {
props: PropDef[] = [
{ name: 'String', key: 'string' },
{ name: 'Color', key: 'color' },
{ name: 'Font Size', key: 'fontSize' },
{ name: 'Line Height', key: 'lineHeight' },
];
constructor(private getter: () => any) {}
get string(): string {
return this.getter()?.string ?? '';
}
set string(v: string) {
const c = this.getter();
if (c) c.string = v;
}
get color(): any {
return this.getter()?.color;
}
set color(v: any) {
const c = this.getter();
if (c) c.color = v;
}
get fontSize(): number {
return this.getter()?.fontSize ?? 0;
}
set fontSize(v: number) {
const c = this.getter();
if (c) c.fontSize = v;
}
get lineHeight(): number {
return this.getter()?.lineHeight ?? 0;
}
set lineHeight(v: number) {
const c = this.getter();
if (c) c.lineHeight = v;
}
}
class CCSpriteModel implements ComponentViewModel {
props: PropDef[] = [{ name: 'Color', key: 'color' }];
constructor(private getter: () => any) {}
get color(): any {
return this.getter()?.color;
}
set color(v: any) {
const c = this.getter();
if (c) c.color = v;
}
}

View File

@@ -0,0 +1,83 @@
import { selectedNode } from '../store';
export interface PropDef {
name: string;
key: string;
}
export class NodeModel {
static readonly props: PropDef[] = [
{ name: 'Name', key: 'nodeName' },
{ name: 'X', key: 'x' },
{ name: 'Y', key: 'y' },
{ name: 'Z', key: 'z' },
{ name: 'Scale X', key: 'scaleX' },
{ name: 'Scale Y', key: 'scaleY' },
{ name: 'Scale Z', key: 'scaleZ' },
];
private static get node(): any {
return selectedNode.value;
}
static get nodeName(): string {
return this.node?.name ?? '';
}
static set nodeName(v: string) {
if (this.node) this.node.name = v;
}
static get x(): number {
return this.node?.getPosition().x ?? 0;
}
static set x(v: number) {
if (!this.node) return;
const p = this.node.getPosition();
this.node.setPosition(v, p.y, p.z);
}
static get y(): number {
return this.node?.getPosition().y ?? 0;
}
static set y(v: number) {
if (!this.node) return;
const p = this.node.getPosition();
this.node.setPosition(p.x, v, p.z);
}
static get z(): number {
return this.node?.getPosition().z ?? 0;
}
static set z(v: number) {
if (!this.node) return;
const p = this.node.getPosition();
this.node.setPosition(p.x, p.y, v);
}
static get scaleX(): number {
return this.node?.getScale().x ?? 1;
}
static set scaleX(v: number) {
if (!this.node) return;
const s = this.node.getScale();
this.node.setScale(v, s.y, s.z);
}
static get scaleY(): number {
return this.node?.getScale().y ?? 1;
}
static set scaleY(v: number) {
if (!this.node) return;
const s = this.node.getScale();
this.node.setScale(s.x, v, s.z);
}
static get scaleZ(): number {
return this.node?.getScale().z ?? 1;
}
static set scaleZ(v: number) {
if (!this.node) return;
const s = this.node.getScale();
this.node.setScale(s.x, s.y, v);
}
}

View File

@@ -0,0 +1,40 @@
import { signal, computed } from '@preact/signals';
/** 当前选中的 cc.Node */
export const selectedNode = signal<any>(null);
/** 每帧 toggle1 / -1驱动属性面板重渲 */
export const updateTick = signal<number>(1);
/** DevTools 面板是否展开 */
export const devtoolsOpen = signal<boolean>(!!localStorage.getItem('cc_devtools_show'));
/** Profiler 浮窗是否展开 */
export const profilerOpen = signal<boolean>(false);
/** 节点树数据(每帧重建) */
export interface TreeNode {
uuid: string;
name: string;
active: boolean;
children: TreeNode[];
path: string[];
}
export const treeData = signal<TreeNode[]>([]);
/** 已展开节点的 uuid 集合 */
export const expandedUuids = signal<Set<string>>(new Set());
/** 是否有节点被选中且有效 */
export const hasSelection = computed(() => selectedNode.value !== null);
/** 节点搜索关键词 */
export const searchQuery = signal<string>('');
devtoolsOpen.subscribe((val) => {
if (val) {
localStorage.setItem('cc_devtools_show', '1');
} else {
localStorage.removeItem('cc_devtools_show');
}
});

View File

@@ -0,0 +1,519 @@
/* ── CSS 变量(与 preview-template 主题保持一致) ── */
:root {
--bg-base: #16161e;
--bg-panel: #1c1c28;
--bg-control: #252535;
--bg-hover: #2e2e48;
--bg-selected: rgba(108, 99, 255, 0.22);
--border: rgba(255, 255, 255, 0.08);
--border-focus: rgba(108, 99, 255, 0.6);
--accent: #6c63ff;
--accent-light: #a89dff;
--text-primary: #d0d0f0;
--text-muted: #7878a0;
--text-dim: rgba(208, 208, 240, 0.35);
--radius: 5px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* ── Toolbar 开关按钮 ── */
.ccdev-toggle {
position: fixed;
right: 12px;
transform: translateY(-50%);
z-index: 9999;
width: 28px;
height: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-control);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
transition:
background 0.15s,
border-color 0.15s,
color 0.15s;
}
.ccdev-toggle:hover {
background: var(--bg-hover);
border-color: var(--border-focus);
color: var(--accent-light);
}
.ccdev-toggle.active {
background: rgba(108, 99, 255, 0.18);
border-color: var(--accent);
color: var(--accent-light);
}
/* ── 浮层主容器 ── */
#cc-devtools {
position: fixed;
/* top / height 由 App.tsx 内联 style 动态注入,跟随 toolbar 实际高度 */
top: 42px;
right: 8px;
width: var(--devtools-width, 320px);
height: calc(100vh - 42px);
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: 10px;
display: flex;
flex-direction: column;
z-index: 8888;
font-family: var(--font);
font-size: 12px;
color: var(--text-primary);
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
transition: width 0s;
}
/* ── GitHub 链接 ── */
.ccdev-github {
position: absolute;
top: 6px;
right: 6px;
z-index: 2;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
border-radius: 4px;
transition:
color 0.15s,
background 0.15s;
}
.ccdev-github:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
/* ── 拖拽调整宽度手柄 ── */
#cc-devtools-resize {
position: absolute;
left: 0;
top: 0;
width: 5px;
height: 100%;
cursor: ew-resize;
z-index: 1;
background: transparent;
transition: background 0.15s;
}
#cc-devtools-resize:hover,
#cc-devtools-resize.dragging {
background: rgba(108, 99, 255, 0.4);
}
#cc-devtools * {
box-sizing: border-box;
}
/* ── Panel 标题 ── */
.panel-header {
height: 32px;
line-height: 32px;
padding: 0 10px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
color: var(--text-muted);
font-size: 11px;
letter-spacing: 0.8px;
text-transform: uppercase;
flex-shrink: 0;
}
/* ── 节点树面板 ── */
.tree-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border-bottom: 2px solid var(--border);
}
/* ── 搜索栏 ── */
.tree-search-bar {
display: flex;
align-items: center;
height: 32px;
padding: 0 8px;
gap: 6px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.tree-search-icon {
color: var(--text-muted);
font-size: 15px;
flex-shrink: 0;
line-height: 1;
margin-top: 1px;
}
.tree-search-input {
flex: 1;
height: 22px;
background: var(--bg-control);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 11px;
padding: 0 6px;
outline: none;
transition: border-color 0.15s;
min-width: 0;
}
.tree-search-input::placeholder {
color: var(--text-muted);
}
.tree-search-input:focus {
border-color: var(--border-focus);
}
.tree-search-clear {
flex-shrink: 0;
width: 18px;
height: 18px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
color: var(--text-muted);
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition:
background 0.12s,
color 0.12s;
}
.tree-search-clear:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-search-count {
padding: 4px 10px;
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.3px;
}
.tree-highlight {
background: rgba(108, 99, 255, 0.45);
color: #fff;
border-radius: 2px;
padding: 0 1px;
}
.tree-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.tree-scroll::-webkit-scrollbar {
width: 4px;
}
.tree-scroll::-webkit-scrollbar-track {
background: transparent;
}
.tree-scroll::-webkit-scrollbar-thumb {
background: rgba(108, 99, 255, 0.3);
border-radius: 2px;
}
.tree-empty {
padding: 16px 12px;
color: var(--text-muted);
font-style: italic;
}
.tree-node {
display: block;
}
.tree-row {
display: flex;
align-items: center;
height: 24px;
cursor: pointer;
border-radius: 3px;
margin: 1px 4px;
transition: background 0.12s;
user-select: none;
}
.tree-row:hover {
background: var(--bg-hover);
}
.tree-row.selected {
background: var(--bg-selected);
}
.tree-row.inactive .tree-label {
opacity: 0.35;
}
.tree-arrow {
width: 16px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
flex-shrink: 0;
transition: transform 0.15s;
display: inline-block;
}
.tree-arrow.expanded {
transform: rotate(90deg);
}
.tree-arrow.invisible {
visibility: hidden;
}
.tree-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-primary);
}
/* ── 属性面板 ── */
.prop-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.prop-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.prop-scroll::-webkit-scrollbar {
width: 4px;
}
.prop-scroll::-webkit-scrollbar-track {
background: transparent;
}
.prop-scroll::-webkit-scrollbar-thumb {
background: rgba(108, 99, 255, 0.3);
border-radius: 2px;
}
.prop-empty {
padding: 16px 12px;
color: var(--text-muted);
font-style: italic;
}
.divider {
height: 1px;
background: var(--border);
margin: 2px 0;
}
/* ── 组件折叠块 ── */
.comp-panel {
border-bottom: 1px solid var(--border);
}
.comp-header {
display: flex;
align-items: center;
height: 28px;
padding: 0 8px;
cursor: pointer;
background: var(--bg-panel);
gap: 6px;
user-select: none;
}
.comp-header:hover {
background: var(--bg-hover);
}
.comp-name {
flex: 1;
color: var(--accent-light);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.comp-arrow {
color: var(--text-muted);
font-size: 13px;
width: 14px;
text-align: center;
}
.comp-props {
padding: 4px 0;
}
.comp-empty {
padding: 6px 12px;
color: var(--text-muted);
font-style: italic;
}
.comp-enabled {
accent-color: var(--accent);
cursor: pointer;
}
/* ── 属性行 ── */
.prop-row {
display: flex;
align-items: center;
height: 26px;
padding: 0 8px;
gap: 8px;
}
.prop-row:hover {
background: rgba(255, 255, 255, 0.03);
}
.prop-name {
width: 72px;
flex-shrink: 0;
color: var(--text-muted);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prop-value {
flex: 1;
display: flex;
align-items: center;
}
.prop-input {
width: 100%;
height: 22px;
padding: 0 6px;
background: var(--bg-control);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 11px;
outline: none;
transition: border-color 0.15s;
}
.prop-input:focus {
border-color: var(--border-focus);
}
.prop-checkbox {
accent-color: var(--accent);
cursor: pointer;
}
.prop-color {
width: 100%;
height: 22px;
padding: 1px 3px;
background: var(--bg-control);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
}
.prop-unknown {
color: var(--text-muted);
font-size: 11px;
}
/* ── 通用图标按钮 ── */
.icon-btn {
height: 20px;
min-width: 20px;
padding: 0 5px;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
color: var(--text-muted);
font-size: 11px;
cursor: pointer;
transition:
background 0.12s,
color 0.12s;
flex-shrink: 0;
}
.icon-btn:hover {
background: var(--bg-hover);
border-color: var(--border-focus);
color: var(--accent-light);
}
/* ── Profiler 浮层 ── */
.profiler-float {
position: fixed;
width: 220px;
background: var(--bg-panel);
border: 1px solid rgba(108, 99, 255, 0.4);
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
z-index: 9999;
overflow: hidden;
}
.profiler-drag {
display: flex;
align-items: center;
justify-content: space-between;
height: 30px;
padding: 0 8px;
background: var(--bg-hover);
cursor: move;
user-select: none;
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.profiler-body {
padding: 4px 0;
}
.profiler-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 10px;
font-size: 11px;
}
.profiler-row:hover {
background: rgba(255, 255, 255, 0.04);
}
.profiler-desc {
color: var(--text-muted);
}
.profiler-val {
color: var(--accent-light);
font-family: monospace;
font-size: 12px;
}
/* ── 数字输入框去掉 spin ── */
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}

View File

@@ -0,0 +1,49 @@
<html>
<head>
<link rel="icon" href="./favicon.ico" />
<meta charset="utf-8" />
<title><%=title%></title>
<meta
name="viewport"
content="width=device-width,user-scalable=no,initial-scale=1,minimum-scale=1,maximum-scale=1,minimal-ui=true"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="full-screen" content="yes" />
<meta name="screen-orientation" content="portrait" />
<meta name="x5-fullscreen" content="true" />
<meta name="360-fullscreen" content="true" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="stylesheet" type="text/css" href="./index.css" />
<link rel="stylesheet" href="./devtools/assets/style.css" />
</head>
<body style="overflow: hidden;">
<%- include(cocosToolBar, {config: config}) %>
<div id="content" class="content" style="overflow: hidden;">
<div class="contentWrap">
<div id="GameDiv" class="wrapper">
<div id="Cocos3dGameContainer">
<canvas id="GameCanvas" tabindex="-1" style="background-color: '';"></canvas>
</div>
<div id="splash">
<div class="progress-bar stripes"><span></span></div>
</div>
<div id="bulletin">
<div id="sceneIsEmpty" class="inner"><%=tip_sceneIsEmpty%></div>
</div>
<div class="error" id="error">
<div class="title">Error <i>(Please open the console to see detailed errors)</i></div>
<div class="error-main"></div>
<div class="error-stack"></div>
</div>
</div>
</div>
</div>
<div id="cc-devtools-root"></div>
<%- include(cocosTemplate, {}) %>
<script src="./devtools/assets/index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import { resolve } from 'path';
export default defineConfig({
plugins: [preact()],
base: '/devtools/',
build: {
lib: {
entry: resolve(__dirname, 'src/main.tsx'),
name: 'CCDevTools',
formats: ['iife'],
fileName: () => 'assets/index.js',
},
outDir: 'template/devtools',
emptyOutDir: true,
cssCodeSplit: false,
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[ext]',
},
},
},
});