commit c5af0cc946625b58e992feebccd958c5f25bed3c Author: shumengya Date: Wed Mar 11 20:41:03 2026 +0800 shumengya mail@smyhub.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52615c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +## Dependencies +**/node_modules/ + +## Build output +**/dist/ + +## Cloudflare / Wrangler +**/.wrangler/ +**/.cache/ + +## Local env / secrets +**/.dev.vars +**/.env +**/.env.* + +## OS / editor +.DS_Store +Thumbs.db +.vscode/ diff --git a/cf-nav-backend/package-lock.json b/cf-nav-backend/package-lock.json new file mode 100644 index 0000000..75a486e --- /dev/null +++ b/cf-nav-backend/package-lock.json @@ -0,0 +1,1504 @@ +{ + "name": "cf-nav-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cf-nav-backend", + "version": "1.0.0", + "devDependencies": { + "wrangler": "^4.68.1" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.14.0.tgz", + "integrity": "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20260218.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260302.0.tgz", + "integrity": "sha512-cGtxPByeVrgoqxbmd8qs631wuGwf8yTm/FY44dEW4HdoXrb5jhlE4oWYHFafedkQCvGjY1Vbs3puAiKnuMxTXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260302.0.tgz", + "integrity": "sha512-WRGqV6RNXM3xoQblJJw1EHKwx9exyhB18cdnToSCUFPObFhk3fzMLoQh7S+nUHUpto6aUrXPVj6R/4G3UPjCxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260302.0.tgz", + "integrity": "sha512-gG423mtUjrmlQT+W2+KisLc6qcGcBLR+QcK5x1gje3bu/dF3oNiYuqY7o58A+sQk6IB849UC4UyNclo1RhP2xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260302.0.tgz", + "integrity": "sha512-7M25noGI4WlSBOhrIaY8xZrnn87OQKtJg9YWAO2EFqGjF1Su5QXGaLlQVF4fAKbqTywbHnI8BAuIsIlUSNkhCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260302.0.tgz", + "integrity": "sha512-jK1L3ADkiWxFzlqZTq2iHW1Bd2Nzu1fmMWCGZw4sMZ2W1B2WCm2wHwO2SX/py4BgylyEN3wuF+5zagbkNKht9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260302.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260302.0.tgz", + "integrity": "sha512-joGFywlo7HdfHXXGOkc6tDCVkwjEncM0mwEsMOLWcl+vDVJPj9HRV7JtEa0+lCpNOLdYw7mZNHYe12xz9KtJOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260302.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260302.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260302.0.tgz", + "integrity": "sha512-FhNdC8cenMDllI6bTktFgxP5Bn5ZEnGtofgKipY6pW9jtq708D1DeGI6vGad78KQLBGaDwFy1eThjCoLYgFfog==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260302.0", + "@cloudflare/workerd-darwin-arm64": "1.20260302.0", + "@cloudflare/workerd-linux-64": "1.20260302.0", + "@cloudflare/workerd-linux-arm64": "1.20260302.0", + "@cloudflare/workerd-windows-64": "1.20260302.0" + } + }, + "node_modules/wrangler": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.68.1.tgz", + "integrity": "sha512-G+TI3k/olEGBAVkPtUlhAX/DIbL/190fv3aK+r+45/wPclNEymjxCc35T8QGTDhc2fEMXiw51L5bH9aNsBg+yQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.14.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260302.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260302.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260302.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/cf-nav-backend/package.json b/cf-nav-backend/package.json new file mode 100644 index 0000000..6a68eee --- /dev/null +++ b/cf-nav-backend/package.json @@ -0,0 +1,12 @@ +{ + "name": "cf-nav-backend", + "version": "1.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev" + }, + "devDependencies": { + "wrangler": "^4.68.1" + } +} diff --git a/cf-nav-backend/worker.js b/cf-nav-backend/worker.js new file mode 100644 index 0000000..46c1ad9 --- /dev/null +++ b/cf-nav-backend/worker.js @@ -0,0 +1,309 @@ +// Cloudflare Worker - 仅 API 后端(前后端分离) +// 部署到 cf-nav-backend,前端静态资源由 Cloudflare Pages 托管 + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', +}; + +function verifyAuth(request, env) { + const auth = request.headers.get('Authorization'); + const token = (env.ADMIN_PASSWORD || env.ADMIN_TOKEN || '').trim(); + if (!token) return false; + const prefix = 'Bearer '; + if (!auth || !auth.startsWith(prefix)) return false; + const provided = auth.slice(prefix.length).trim(); + return provided === token; +} + +export default { + async fetch(request, env) { + if (request.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + const url = new URL(request.url); + const path = url.pathname; + + try { + if (path === '/api/sites') { + return handleSites(request, env, corsHeaders); + } + if (path === '/api/auth/check') { + return handleAuthCheck(request, env, corsHeaders); + } + if (path.match(/^\/api\/sites\/[^/]+\/click$/)) { + const id = path.split('/')[3]; + return handleSiteClick(request, env, id, corsHeaders); + } + if (path.startsWith('/api/sites/')) { + const id = path.split('/')[3]; + return handleSite(request, env, id, corsHeaders); + } + if (path === '/api/categories') { + return handleCategories(request, env, corsHeaders); + } + if (path.startsWith('/api/categories/')) { + const name = decodeURIComponent(path.split('/')[3] || ''); + return handleCategory(request, env, name, corsHeaders); + } + if (path === '/api/favicon') { + return handleFavicon(request, env, corsHeaders); + } + + return new Response('Not Found', { status: 404 }); + } catch (error) { + return new Response('Internal Server Error: ' + error.message, { + status: 500, + headers: corsHeaders, + }); + } + }, +}; + +/** 校验管理员 token,用于前端进入后台时确认链接有效 */ +async function handleAuthCheck(request, env, corsHeaders) { + if (request.method !== 'GET') { + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); + } + const token = (env.ADMIN_PASSWORD || env.ADMIN_TOKEN || '').trim(); + if (!token) { + return new Response(JSON.stringify({ ok: false }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + const auth = request.headers.get('Authorization'); + const prefix = 'Bearer '; + if (!auth || !auth.startsWith(prefix)) { + return new Response(JSON.stringify({ ok: false }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + const provided = auth.slice(prefix.length).trim(); + if (provided !== token) { + return new Response(JSON.stringify({ ok: false }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + return new Response(JSON.stringify({ ok: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); +} + +async function handleFavicon(request, env, corsHeaders) { + const url = new URL(request.url); + const domain = url.searchParams.get('domain'); + if (!domain) { + return new Response('Missing domain parameter', { status: 400, headers: corsHeaders }); + } + const faviconApi = env.FAVICON_API || env.FAVICON; + if (faviconApi && faviconApi !== '') { + const targetUrl = domain.startsWith('http') ? domain : `https://${domain}`; + try { + const res = await fetch(faviconApi + encodeURIComponent(targetUrl), { + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, + cf: { cacheTtl: 86400, cacheEverything: true }, + }); + if (res.ok) { + return new Response(res.body, { + status: res.status, + headers: { + ...corsHeaders, + 'Content-Type': res.headers.get('Content-Type') || 'image/x-icon', + 'Cache-Control': 'public, max-age=86400', + }, + }); + } + } catch (_) { + /* 外置 API 失败时回退到内置源 */ + } + } + const faviconSources = [ + `https://www.google.com/s2/favicons?domain=${domain}&sz=64`, + `https://icons.duckduckgo.com/ip3/${domain}.ico`, + `https://favicon.api.shumengya.top/${domain}`, + ]; + for (const source of faviconSources) { + try { + const response = await fetch(source, { + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, + cf: { cacheTtl: 86400, cacheEverything: true }, + }); + if (response.ok) { + return new Response(response.body, { + status: response.status, + headers: { + ...corsHeaders, + 'Content-Type': response.headers.get('Content-Type') || 'image/x-icon', + 'Cache-Control': 'public, max-age=86400', + }, + }); + } + } catch (_) { + continue; + } + } + return new Response('Favicon not found', { status: 404, headers: corsHeaders }); +} + +async function handleSites(request, env, corsHeaders) { + if (request.method === 'GET') { + const sitesData = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + const normalized = sitesData.map((s) => ({ ...s, clicks: typeof s.clicks === 'number' ? s.clicks : 0 })); + return new Response(JSON.stringify(normalized), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + if (request.method === 'POST') { + if (!verifyAuth(request, env)) { + return new Response('Unauthorized', { status: 401, headers: corsHeaders }); + } + const newSite = await request.json(); + const sites = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + const categories = (await env.NAV_KV.get('categories', { type: 'json' })) || []; + newSite.id = Date.now().toString(); + newSite.clicks = 0; + sites.push(newSite); + if (newSite.category && !categories.includes(newSite.category)) { + categories.push(newSite.category); + await env.NAV_KV.put('categories', JSON.stringify(categories)); + } + await env.NAV_KV.put('sites', JSON.stringify(sites)); + return new Response(JSON.stringify(newSite), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); +} + +async function handleSite(request, env, id, corsHeaders) { + const sites = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + const categories = (await env.NAV_KV.get('categories', { type: 'json' })) || []; + + if (request.method === 'GET') { + const site = sites.find((s) => s.id === id); + if (!site) return new Response('Not Found', { status: 404, headers: corsHeaders }); + const normalized = { ...site, clicks: typeof site.clicks === 'number' ? site.clicks : 0 }; + return new Response(JSON.stringify(normalized), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + if (request.method === 'PUT') { + if (!verifyAuth(request, env)) { + return new Response('Unauthorized', { status: 401, headers: corsHeaders }); + } + const updatedSite = await request.json(); + const index = sites.findIndex((s) => s.id === id); + if (index === -1) return new Response('Not Found', { status: 404, headers: corsHeaders }); + const existingClicks = typeof sites[index].clicks === 'number' ? sites[index].clicks : 0; + sites[index] = { ...sites[index], ...updatedSite, id, clicks: existingClicks }; + if (updatedSite.category && !categories.includes(updatedSite.category)) { + categories.push(updatedSite.category); + await env.NAV_KV.put('categories', JSON.stringify(categories)); + } + await env.NAV_KV.put('sites', JSON.stringify(sites)); + return new Response(JSON.stringify(sites[index]), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + if (request.method === 'DELETE') { + if (!verifyAuth(request, env)) { + return new Response('Unauthorized', { status: 401, headers: corsHeaders }); + } + const index = sites.findIndex((s) => s.id === id); + if (index === -1) return new Response('Not Found', { status: 404, headers: corsHeaders }); + sites.splice(index, 1); + await env.NAV_KV.put('sites', JSON.stringify(sites)); + return new Response(JSON.stringify({ success: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); +} + +async function handleSiteClick(request, env, id, corsHeaders) { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); + } + const sites = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + const index = sites.findIndex((s) => s.id === id); + if (index === -1) { + return new Response('Not Found', { status: 404, headers: corsHeaders }); + } + const site = sites[index]; + const prev = typeof site.clicks === 'number' ? site.clicks : 0; + sites[index] = { ...site, clicks: prev + 1 }; + await env.NAV_KV.put('sites', JSON.stringify(sites)); + return new Response(JSON.stringify({ success: true, clicks: prev + 1 }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); +} + +async function handleCategories(request, env, corsHeaders) { + if (request.method === 'GET') { + let categories = (await env.NAV_KV.get('categories', { type: 'json' })) || []; + if (!categories.length) { + const sites = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + categories = [...new Set(sites.map((s) => s.category).filter(Boolean))]; + if (categories.length) await env.NAV_KV.put('categories', JSON.stringify(categories)); + } + return new Response(JSON.stringify(categories), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + if (request.method === 'POST') { + if (!verifyAuth(request, env)) { + return new Response('Unauthorized', { status: 401, headers: corsHeaders }); + } + const { name } = await request.json(); + if (!name || !name.trim()) { + return new Response('Bad Request', { status: 400, headers: corsHeaders }); + } + const categories = (await env.NAV_KV.get('categories', { type: 'json' })) || []; + if (!categories.includes(name.trim())) { + categories.push(name.trim()); + await env.NAV_KV.put('categories', JSON.stringify(categories)); + } + return new Response(JSON.stringify({ success: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); +} + +async function handleCategory(request, env, name, corsHeaders) { + if (!name) return new Response('Bad Request', { status: 400, headers: corsHeaders }); + if (!verifyAuth(request, env)) { + return new Response('Unauthorized', { status: 401, headers: corsHeaders }); + } + const categories = (await env.NAV_KV.get('categories', { type: 'json' })) || []; + + if (request.method === 'DELETE') { + const filtered = categories.filter((c) => c !== name); + await env.NAV_KV.put('categories', JSON.stringify(filtered)); + return new Response(JSON.stringify({ success: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + if (request.method === 'PUT') { + const body = await request.json(); + const newName = (body?.name || '').trim(); + if (!newName) return new Response('Bad Request', { status: 400, headers: corsHeaders }); + const updated = categories.map((c) => (c === name ? newName : c)); + const unique = Array.from(new Set(updated)); + await env.NAV_KV.put('categories', JSON.stringify(unique)); + const sites = (await env.NAV_KV.get('sites', { type: 'json' })) || []; + const updatedSites = sites.map((site) => + site.category === name ? { ...site, category: newName } : site + ); + await env.NAV_KV.put('sites', JSON.stringify(updatedSites)); + return new Response(JSON.stringify({ success: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + return new Response('Method not allowed', { status: 405, headers: corsHeaders }); +} diff --git a/cf-nav-backend/wrangler.toml b/cf-nav-backend/wrangler.toml new file mode 100644 index 0000000..7a12879 --- /dev/null +++ b/cf-nav-backend/wrangler.toml @@ -0,0 +1,15 @@ +name = "cf-nav-backend" +main = "worker.js" +compatibility_date = "2026-01-01" + +# KV 命名空间(与原先 cf-nav 使用同一套数据可复用同一 id) +[[kv_namespaces]] +binding = "NAV_KV" +id = "a89f429e1a684d2084eae8619755ee11" + +# 管理令牌/密码:与前端 config.js 的 ADMIN_TOKEN 一致;请求头带 Authorization: Bearer <本值> 才能写数据(也可用 wrangler secret put ADMIN_PASSWORD 设置) +[vars] +ADMIN_PASSWORD = "shumengya5201314" +FAVICON_API = "https://cf-favicon.pages.dev/api/favicon?url=" + + diff --git a/cf-nav-frontend/README.md b/cf-nav-frontend/README.md new file mode 100644 index 0000000..6eced1e --- /dev/null +++ b/cf-nav-frontend/README.md @@ -0,0 +1,82 @@ +# 萌芽导航 - 前后端分离版 + +- **前端**:静态 PWA,部署到 **Cloudflare Pages** +- **后端**:API Worker,部署到 **Cloudflare Workers**(目录 `cf-nav-backend`) + +## 一、后端(Cloudflare Worker) + +1. 进入后端目录并安装依赖、部署: + +```bash +cd cf-nav-backend +npm install +npm run deploy +``` + +2. 修改 `cf-nav-backend/wrangler.toml`: + - 如需新 KV:`wrangler kv:namespace create "NAV_KV"`,将返回的 `id` 填入 `[[kv_namespaces]]` 的 `id` + - 修改 `[vars]` 中的 `ADMIN_PASSWORD`,或使用 `wrangler secret put ADMIN_PASSWORD` 设置密钥 + +3. 部署成功后记下 Worker 地址,例如: + `https://cf-nav-backend.你的子域.workers.dev` + +## 二、前端(Cloudflare Pages) + +1. **配置 API 地址** + 编辑项目根目录下的 `config.js`,将 `window.API_BASE` 改为你的后端 Worker 地址,例如: + +```javascript +window.API_BASE = 'https://cf-nav-backend.你的子域.workers.dev'; +``` + +2. **部署到 Pages** + + - **方式 A - Git 推送** + - 在 Cloudflare Dashboard → Pages → 创建项目 → 连接 Git + - 构建:**无**(或留空) + - 输出目录:`/`(根目录) + - 部署后前端即可通过你的 `*.pages.dev` 或自定义域名访问 + + - **方式 B - 直接上传** + - Pages → 创建项目 → 直接上传 + - 将当前仓库根目录下所有前端文件(含 `config.js`、`index.html`、`app.js`、`admin.html`、`admin.js`、`styles.css`、`sw.js`、`manifest.webmanifest`、`logo.png`、`favicon.ico`、`offline.html`、`_redirects`)打包上传 + +3. **本地预览** + +```bash +npm install +npm run dev +``` + +浏览器打开 `http://localhost:3000`。本地未配置同源 API 时,需在 `config.js` 中填写后端 Worker 地址才能正常请求数据。 + +## 三、目录结构说明 + +``` +mengya-nav/ +├── index.html # 首页 +├── admin.html # 后台 +├── app.js # 首页逻辑 +├── admin.js # 后台逻辑 +├── config.js # API 地址(部署前必改) +├── styles.css +├── sw.js # PWA Service Worker +├── manifest.webmanifest +├── offline.html +├── logo.png / favicon.ico +├── _redirects # 后台请直接访问 /admin.html(勿用 /admin 避免重定向) +├── package.json # 前端脚本 +└── cf-nav-backend/ # 后端 Worker + ├── worker.js # 仅 API,无静态资源 + ├── wrangler.toml + └── package.json +``` + +## 四、CORS 与安全 + +- 后端 Worker 已设置 `Access-Control-Allow-Origin: *`,Pages 域名可正常请求。 +- 生产环境建议在 Worker 中把 `Access-Control-Allow-Origin` 改为你的 Pages 域名,并务必修改/保管好 `ADMIN_PASSWORD`。 + +## 五、数据迁移 + +若之前已用旧版单 Worker 部署并使用了 KV,只需在 `cf-nav-backend/wrangler.toml` 中沿用原来的 KV namespace `id`,即可继续使用同一份数据,无需迁移。 diff --git a/cf-nav-frontend/_redirects b/cf-nav-frontend/_redirects new file mode 100644 index 0000000..350a976 --- /dev/null +++ b/cf-nav-frontend/_redirects @@ -0,0 +1 @@ +# 后台请直接访问 /admin.html?token=你的令牌(不要配置 /admin 或 /admin.html 的重定向,避免循环) diff --git a/cf-nav-frontend/admin.html b/cf-nav-frontend/admin.html new file mode 100644 index 0000000..5ea7a82 --- /dev/null +++ b/cf-nav-frontend/admin.html @@ -0,0 +1,417 @@ + + + + + + + + + + 萌芽导航-后台管理 + + + + + + + + + + +
+ + + + +
+
+

萌芽导航-管理后台

+ +
+ +
+
+

📁 分类管理

+
+ + +
+
+
+ +
+
+ +
+ +
+ +
+
+

网站列表

+
+ + +
+
+ + + + + + + + + + + + + + +
名称URL分类描述标签操作
+
+
+ + + + +
+ + 操作成功 +
+
+ + + + + diff --git a/cf-nav-frontend/admin.js b/cf-nav-frontend/admin.js new file mode 100644 index 0000000..7cafceb --- /dev/null +++ b/cf-nav-frontend/admin.js @@ -0,0 +1,420 @@ +// 后台管理 JavaScript(API 地址从 config.js 读取,token 仅从 URL ?token= 传入并由后端校验) +const API_BASE = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : ''; + +// 从 URL 读取 token,不在前端校验,直接交给后端;无 token 则不显示后台 +const urlParams = new URLSearchParams(typeof location !== 'undefined' ? location.search : ''); +const authToken = urlParams.get('token') || null; + +let categories = []; +let allSites = []; + +// 显示提示消息 +function showToast(message, type = 'success') { + const toast = document.getElementById('toast'); + toast.className = `toast ${type} show`; + document.querySelector('.toast-message').textContent = message; + + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); +} + +// 显示无权限提示(无 token 或 token 错误) +function showNoPermission() { + const noPerm = document.getElementById('no-permission'); + const adminEl = document.getElementById('admin-container'); + if (noPerm) noPerm.style.display = 'block'; + if (adminEl) adminEl.style.display = 'none'; +} + +// 退出:跳转到当前页不带 query,下次进入需重新带 token +document.getElementById('logout-btn').addEventListener('click', () => { + if (typeof location !== 'undefined') location.href = location.pathname; +}); + +// 显示管理面板(仅 token 正确时) +function showAdminPanel() { + const noPerm = document.getElementById('no-permission'); + const adminEl = document.getElementById('admin-container'); + if (noPerm) noPerm.style.display = 'none'; + if (adminEl) adminEl.style.display = 'block'; + loadSites(); + loadCategories(); +} + +// 进入页时先向后端校验 token,通过才显示后台 +async function initAdmin() { + if (!authToken) { + showNoPermission(); + return; + } + try { + const response = await fetch(`${API_BASE}/api/auth/check`, { + headers: { 'Authorization': `Bearer ${authToken}` } + }); + if (response.status !== 200) { + showNoPermission(); + return; + } + const data = await response.json().catch(() => ({})); + if (!data || !data.ok) { + showNoPermission(); + return; + } + showAdminPanel(); + } catch (_) { + showNoPermission(); + } +} + +// 加载分类列表 +async function loadCategories() { + try { + const response = await fetch(`${API_BASE}/api/categories`, { + headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {} + }); + if (response.status === 401) { + showNoPermission(); + return; + } + categories = response.ok ? await response.json() : []; + renderCategoryList(); + updateCategorySelect(); + updateSiteCategoryFilterOptions(); + if (allSites.length) renderSites(allSites); + } catch (error) { + categories = []; + renderCategoryList(); + } +} + +// 渲染分类标签 +function renderCategoryList() { + const container = document.getElementById('category-list'); + container.innerHTML = ''; + + if (!categories.length) { + const empty = document.createElement('div'); + empty.style.color = '#64748b'; + empty.textContent = '暂无分类'; + container.appendChild(empty); + return; + } + + categories.forEach(name => { + const item = document.createElement('div'); + item.className = 'category-item'; + item.innerHTML = ` + ${name} + + + + + `; + container.appendChild(item); + }); +} + +// 更新分类下拉 +function updateCategorySelect(selectedValue = '') { + const select = document.getElementById('edit-site-category'); + if (!select) { + return; + } + + const options = ['默认']; + const merged = Array.from(new Set([...options, ...categories].filter(Boolean))); + + select.innerHTML = ''; + merged.forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + if (name === selectedValue) { + option.selected = true; + } + select.appendChild(option); + }); +} + +// 更新「网站列表」上方的分类筛选下拉 +function updateSiteCategoryFilterOptions() { + const select = document.getElementById('site-category-filter'); + if (!select) return; + + const current = select.value; + const fromSites = (allSites || []).map(s => s.category || '默认').filter(Boolean); + const combined = Array.from(new Set(['默认', ...categories, ...fromSites])); + select.innerHTML = ''; + combined.forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + select.appendChild(option); + }); + select.value = current || ''; +} + +// 根据当前筛选条件渲染网站表格 +function renderSites(sites) { + const tbody = document.getElementById('sites-tbody'); + const filterEl = document.getElementById('site-category-filter'); + const categoryFilter = filterEl ? filterEl.value : ''; + + const list = categoryFilter + ? sites.filter(site => (site.category || '默认') === categoryFilter) + : sites; + + tbody.innerHTML = ''; + + list.forEach(site => { + const tr = document.createElement('tr'); + + const tagsHtml = site.tags && site.tags.length > 0 + ? `
${site.tags.map(tag => `${tag}`).join('')}
` + : '-'; + + tr.innerHTML = ` + ${site.name} + ${site.url} + ${site.category} + ${site.description || '-'} + ${tagsHtml} + +
+ + +
+ + `; + + tbody.appendChild(tr); + }); +} + +// 加载网站列表 +async function loadSites() { + try { + const response = await fetch(`${API_BASE}/api/sites`, { + headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {} + }); + if (response.status === 401) { + showNoPermission(); + return; + } + const sites = await response.json(); + allSites = sites || []; + + updateSiteCategoryFilterOptions(); + renderSites(allSites); + } catch (error) { + showToast('加载网站列表失败', 'error'); + } +} + +// 网站列表按分类筛选 +const siteCategoryFilterEl = document.getElementById('site-category-filter'); +if (siteCategoryFilterEl) { + siteCategoryFilterEl.addEventListener('change', () => { + renderSites(allSites); + }); +} + +// 添加新网站 +document.getElementById('add-new-site').addEventListener('click', () => { + if (!authToken) return; + document.getElementById('modal-title').textContent = '添加新网站'; + document.getElementById('edit-site-id').value = ''; + document.getElementById('edit-site-form').reset(); + updateCategorySelect(); + document.getElementById('edit-modal').classList.add('active'); +}); + +// 编辑网站 +async function editSite(id) { + try { + const response = await fetch(`${API_BASE}/api/sites/${id}`); + const site = await response.json(); + + document.getElementById('modal-title').textContent = '编辑网站'; + document.getElementById('edit-site-id').value = site.id; + document.getElementById('edit-site-name').value = site.name; + document.getElementById('edit-site-url').value = site.url; + document.getElementById('edit-site-description').value = site.description || ''; + updateCategorySelect(site.category); + document.getElementById('edit-site-tags').value = site.tags ? site.tags.join(', ') : ''; + + document.getElementById('edit-modal').classList.add('active'); + } catch (error) { + showToast('加载网站信息失败', 'error'); + } +} + +// 删除网站 +async function deleteSite(id) { + if (!confirm('确定要删除这个网站吗?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/sites/${id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + showToast('网站删除成功'); + loadSites(); + } else { + showToast('删除失败', 'error'); + } + } catch (error) { + showToast('删除请求失败', 'error'); + } +} + +// 保存网站(添加或更新) +document.getElementById('edit-site-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const id = document.getElementById('edit-site-id').value; + const site = { + name: document.getElementById('edit-site-name').value.trim(), + url: document.getElementById('edit-site-url').value.trim(), + description: document.getElementById('edit-site-description').value.trim(), + category: document.getElementById('edit-site-category').value, + tags: document.getElementById('edit-site-tags').value + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag) + }; + + // 验证URL + if (!site.url.startsWith('http://') && !site.url.startsWith('https://')) { + site.url = 'https://' + site.url; + } + + try { + const url = id ? `${API_BASE}/api/sites/${id}` : `${API_BASE}/api/sites`; + const method = id ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify(site) + }); + + if (response.ok) { + showToast(id ? '网站更新成功' : '网站添加成功'); + document.getElementById('edit-modal').classList.remove('active'); + loadSites(); + } else { + showToast('保存失败', 'error'); + } + } catch (error) { + showToast('保存请求失败', 'error'); + } +}); + +// 关闭模态框 +document.getElementById('close-edit-modal').addEventListener('click', () => { + document.getElementById('edit-modal').classList.remove('active'); +}); + +document.getElementById('edit-modal').addEventListener('click', (e) => { + if (e.target.id === 'edit-modal') { + document.getElementById('edit-modal').classList.remove('active'); + } +}); + +// 添加分类 +document.getElementById('add-category-btn').addEventListener('click', async () => { + const input = document.getElementById('new-category-name'); + const name = input.value.trim(); + if (!name) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/categories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ name }) + }); + + if (response.ok) { + input.value = ''; + await loadCategories(); + showToast('分类添加成功'); + } else { + showToast('分类添加失败', 'error'); + } + } catch (error) { + showToast('分类添加失败', 'error'); + } +}); + +// 编辑分类 +async function editCategory(oldName) { + const newName = prompt('请输入新的分类名称', oldName); + if (!newName || newName.trim() === oldName) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/categories/${encodeURIComponent(oldName)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ name: newName.trim() }) + }); + + if (response.ok) { + await loadCategories(); + await loadSites(); + showToast('分类更新成功'); + } else { + showToast('分类更新失败', 'error'); + } + } catch (error) { + showToast('分类更新失败', 'error'); + } +} + +// 删除分类 +async function deleteCategory(name) { + if (!confirm(`确定删除分类「${name}」吗?`)) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/categories/${encodeURIComponent(name)}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + await loadCategories(); + showToast('分类删除成功'); + } else { + showToast('分类删除失败', 'error'); + } + } catch (error) { + showToast('分类删除失败', 'error'); + } +} + +// 初始化 +initAdmin(); diff --git a/cf-nav-frontend/app.js b/cf-nav-frontend/app.js new file mode 100644 index 0000000..db2dc1b --- /dev/null +++ b/cf-nav-frontend/app.js @@ -0,0 +1,498 @@ +// API 配置(从 config.js 读取,部署 Pages 时请设置后端 Worker 地址) +const API_BASE = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : ''; + +// 初始数据(示例) +let sites = [ + { + id: '1', + name: '百度', + url: 'https://www.baidu.com', + description: '全球最大的中文搜索引擎', + category: '常用', + tags: ['搜索', '中文'] + }, + { + id: '2', + name: '知乎', + url: 'https://www.zhihu.com', + description: '中文互联网高质量的问答社区', + category: '社交', + tags: ['问答', '知识'] + }, + { + id: '3', + name: 'GitHub', + url: 'https://github.com', + description: '全球最大的代码托管平台', + category: '工作', + tags: ['代码', '开发'] + }, + { + id: '4', + name: 'Bilibili', + url: 'https://www.bilibili.com', + description: '中国年轻世代高度聚集的文化社区', + category: '娱乐', + tags: ['视频', '弹幕'] + }, + { + id: '5', + name: '淘宝', + url: 'https://www.taobao.com', + description: '亚洲最大的购物网站', + category: '购物', + tags: ['电商', '购物'] + } + + +]; + +let deferredInstallPrompt = null; +let installBtn = null; +let hasRefreshing = false; + +function isStandaloneMode() { + return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; +} + +function createInstallButton() { + if (installBtn) { + return installBtn; + } + + installBtn = document.createElement('button'); + installBtn.className = 'install-btn'; + installBtn.type = 'button'; + installBtn.textContent = '📲 安装应用'; + installBtn.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + background: linear-gradient(135deg, #10b981, #059669); + color: #fff; + border: none; + padding: 12px 24px; + border-radius: 50px; + cursor: pointer; + font-weight: 600; + font-size: 0.95rem; + box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3); + display: none; + z-index: 1000; + transition: all 0.2s ease; + `; + + installBtn.addEventListener('mouseenter', () => { + installBtn.style.transform = 'translateY(-2px)'; + installBtn.style.boxShadow = '0 6px 20px rgba(16, 185, 129, 0.4)'; + }); + + installBtn.addEventListener('mouseleave', () => { + installBtn.style.transform = 'translateY(0)'; + installBtn.style.boxShadow = '0 4px 15px rgba(16, 185, 129, 0.3)'; + }); + + installBtn.addEventListener('click', async () => { + if (!deferredInstallPrompt) { + return; + } + + deferredInstallPrompt.prompt(); + const choice = await deferredInstallPrompt.userChoice; + + if (choice.outcome === 'accepted') { + showToast('已触发安装流程'); + } + + deferredInstallPrompt = null; + installBtn.style.display = 'none'; + }); + + document.body.appendChild(installBtn); + return installBtn; +} + +function initInstallPrompt() { + const button = createInstallButton(); + + if (isStandaloneMode()) { + button.style.display = 'none'; + return; + } + + window.addEventListener('beforeinstallprompt', (event) => { + event.preventDefault(); + deferredInstallPrompt = event; + button.style.display = 'block'; + }); + + window.addEventListener('appinstalled', () => { + deferredInstallPrompt = null; + button.style.display = 'none'; + showToast('应用安装成功'); + }); +} + +async function registerServiceWorker() { + if (!('serviceWorker' in navigator)) { + return; + } + + try { + const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); + + if (registration.waiting) { + promptRefresh(registration.waiting); + } + + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing; + if (!newWorker) { + return; + } + + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + promptRefresh(newWorker); + } + }); + }); + + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (hasRefreshing) { + return; + } + hasRefreshing = true; + window.location.reload(); + }); + } catch (error) { + console.error('Service Worker 注册失败:', error); + } +} + +function promptRefresh(worker) { + const shouldRefresh = window.confirm('发现新版本,是否立即刷新?'); + if (shouldRefresh) { + worker.postMessage('SKIP_WAITING'); + } +} + + + +// 加载网站数据 +async function loadSites() { + try { + const response = await fetch(`${API_BASE}/api/sites`); + if (response.ok) { + sites = await response.json(); + updateStats(); + renderSites(); + } + } catch (error) { + console.error('加载网站数据失败:', error); + // 使用默认数据 + updateStats(); + renderSites(); + } +} + +// 更新统计信息 +function updateStats() { + const totalSites = sites.length; + const categories = [...new Set(sites.map(site => site.category))]; + const allTags = sites.flatMap(site => site.tags || []); + const uniqueTags = [...new Set(allTags)]; + + document.getElementById('total-sites').textContent = totalSites; + document.getElementById('total-categories').textContent = categories.length; + document.getElementById('total-tags').textContent = uniqueTags.length; + + // 更新分类过滤器 + updateCategoryFilters(); +} + +// 更新分类过滤器 +function updateCategoryFilters() { + const categoryFilters = document.getElementById('category-filters'); + const categories = ['all', ...new Set(sites.map(site => site.category))]; + + categoryFilters.innerHTML = ''; + + categories.forEach(category => { + const filterBtn = document.createElement('div'); + filterBtn.className = 'category-filter'; + filterBtn.textContent = category === 'all' ? '全部' : category; + filterBtn.dataset.category = category; + + if (category === 'all') { + filterBtn.classList.add('active'); + } + + filterBtn.addEventListener('click', () => { + document.querySelectorAll('.category-filter').forEach(btn => { + btn.classList.remove('active'); + }); + filterBtn.classList.add('active'); + filterSites(); + closeCategorySidebar(); + }); + + categoryFilters.appendChild(filterBtn); + }); +} + +// 渲染网站 +function renderSites(filteredSites = null) { + const container = document.getElementById('categories-container'); + const searchInput = document.getElementById('search-input').value.toLowerCase(); + const activeCategory = document.querySelector('.category-filter.active').dataset.category; + + // 如果没有传入过滤后的网站,则使用全部网站 + const sitesToRender = filteredSites || sites; + + // 如果有搜索关键词,进一步过滤 + let finalSites = sitesToRender; + if (searchInput) { + finalSites = sitesToRender.filter(site => + site.name.toLowerCase().includes(searchInput) || + (site.description && site.description.toLowerCase().includes(searchInput)) || + (site.tags && site.tags.some(tag => tag.toLowerCase().includes(searchInput))) + ); + } + + // 如果有分类过滤 + if (activeCategory !== 'all') { + finalSites = finalSites.filter(site => site.category === activeCategory); + } + + // 按分类分组 + const sitesByCategory = {}; + finalSites.forEach(site => { + if (!sitesByCategory[site.category]) { + sitesByCategory[site.category] = []; + } + sitesByCategory[site.category].push(site); + }); + + // 如果没有网站匹配 + if (Object.keys(sitesByCategory).length === 0) { + container.innerHTML = ` +
+
🔍
+

没有找到匹配的网站

+

尝试调整搜索关键词或分类筛选条件

+
+ `; + return; + } + + // 渲染分类区块 + container.innerHTML = ''; + Object.keys(sitesByCategory).sort().forEach(category => { + const categorySection = document.createElement('div'); + categorySection.className = 'category-section'; + + categorySection.innerHTML = ` +

${category}

+
+ ${sitesByCategory[category].map(site => createSiteCard(site)).join('')} +
+ `; + + container.appendChild(categorySection); + }); + + // 添加点击事件 + document.querySelectorAll('.site-card').forEach(card => { + card.addEventListener('click', (e) => { + if (!e.target.closest('.site-icon') && !e.target.closest('img')) { + window.open(card.dataset.url, '_blank'); + } + }); + }); +} + +// 通过 Worker 代理获取 favicon +function getFaviconUrl(domain) { + return `${API_BASE}/api/favicon?domain=${domain}`; +} + +// 生成favicon HTML,使用 Worker 代理 +function generateFaviconHtml(domain, firstLetter) { + const faviconUrl = getFaviconUrl(domain); + + // Worker 会自动尝试多个源,失败时显示占位符 + const onerrorCode = `this.parentElement.innerHTML='
${firstLetter}
';`; + + return { + src: faviconUrl, + onerror: onerrorCode + }; +} + +// 创建网站卡片HTML +function createSiteCard(site) { + // 从URL提取域名用于获取favicon + const domain = new URL(site.url).hostname.replace('www.', ''); + + // 生成网站名称首字母作为备用图标 + const firstLetter = site.name.charAt(0).toUpperCase(); + + // 获取favicon配置 + const faviconConfig = generateFaviconHtml(domain, firstLetter); + + // 处理标签 + const tagsHtml = site.tags && site.tags.length > 0 + ? site.tags.map(tag => `${tag}`).join('') + : ''; + + // 处理标签文本(用于 title) + const tagsText = site.tags && site.tags.length > 0 + ? site.tags.join('、') + : ''; + + const clickCount = typeof site.clicks === 'number' ? site.clicks : 0; + + return ` + + ${clickCount} +
+ ${site.name}图标 +
+
+
${site.name}
+
${site.description || ''}
+
+
${tagsHtml}
+
+ `; +} + +// 显示提示消息 +function showToast(message, type = 'success') { + const toast = document.getElementById('toast'); + toast.className = `toast ${type} show`; + document.querySelector('.toast-message').textContent = message; + + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); +} + +// 过滤网站(搜索和分类) +function filterSites() { + renderSites(); +} + +// 显示提示消息 +function showToast(message, type = 'success') { + const toast = document.getElementById('toast'); + toast.className = `toast ${type} show`; + document.querySelector('.toast-message').textContent = message; + + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); +} + +// 过滤网站(搜索和分类) +function filterSites() { + renderSites(); +} + +// 网页搜索处理 +function performWebSearch(query, engine) { + const encodedQuery = encodeURIComponent(query); + let searchUrl = ''; + + switch (engine) { + case 'google': + searchUrl = `https://www.google.com/search?q=${encodedQuery}`; + break; + case 'baidu': + searchUrl = `https://www.baidu.com/s?wd=${encodedQuery}`; + break; + case 'bing': + searchUrl = `https://www.bing.com/search?q=${encodedQuery}`; + break; + case 'duckduckgo': + searchUrl = `https://duckduckgo.com/?q=${encodedQuery}`; + break; + case 'yandex': + searchUrl = `https://yandex.com/search/?text=${encodedQuery}`; + break; + default: + searchUrl = `https://www.google.com/search?q=${encodedQuery}`; + } + + window.open(searchUrl, '_blank'); +} + +function openCategorySidebar() { + document.body.classList.add('category-sidebar-open'); + const backdrop = document.getElementById('category-sidebar-backdrop'); + if (backdrop) backdrop.setAttribute('aria-hidden', 'false'); +} + +function closeCategorySidebar() { + document.body.classList.remove('category-sidebar-open'); + const backdrop = document.getElementById('category-sidebar-backdrop'); + if (backdrop) backdrop.setAttribute('aria-hidden', 'true'); +} + +function initCategorySidebar() { + const toggle = document.getElementById('category-sidebar-toggle'); + const closeBtn = document.getElementById('category-sidebar-close'); + const backdrop = document.getElementById('category-sidebar-backdrop'); + if (toggle) toggle.addEventListener('click', openCategorySidebar); + if (closeBtn) closeBtn.addEventListener('click', closeCategorySidebar); + if (backdrop) backdrop.addEventListener('click', closeCategorySidebar); +} + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + initInstallPrompt(); + initCategorySidebar(); + + // 点击卡片时上报访问次数(不阻止跳转) + const container = document.getElementById('categories-container'); + if (container) { + container.addEventListener('click', (e) => { + const card = e.target.closest('.site-card'); + if (!card) return; + const id = card.dataset.siteId; + if (id) { + const base = typeof window !== 'undefined' && window.API_BASE !== undefined ? window.API_BASE : ''; + fetch(base + '/api/sites/' + id + '/click', { method: 'POST', keepalive: true }).catch(() => {}); + } + }); + } + + // 注册 PWA Service Worker + await registerServiceWorker(); + + // 加载网站数据 + await loadSites(); + + // 搜索输入 + document.getElementById('search-input').addEventListener('input', filterSites); + + // 网页搜索表单提交 + const webSearchForm = document.getElementById('web-search-form'); + if (webSearchForm) { + webSearchForm.addEventListener('submit', (e) => { + e.preventDefault(); + const query = document.getElementById('web-search-input').value.trim(); + const engine = document.getElementById('search-engine').value; + + if (query) { + performWebSearch(query, engine); + document.getElementById('web-search-input').value = ''; + } + }); + } +}); diff --git a/cf-nav-frontend/apply-config.js b/cf-nav-frontend/apply-config.js new file mode 100644 index 0000000..5d215d8 --- /dev/null +++ b/cf-nav-frontend/apply-config.js @@ -0,0 +1,89 @@ +/** + * 根据 config.js 应用站点名称到页面标题、meta、标题元素,并动态生成 PWA manifest + * 需在 config.js 之后加载 + */ +(function () { + var name = window.SITE_NAME || '萌芽导航'; + var shortName = window.SITE_SHORT_NAME || '萌芽'; + var desc = window.SITE_DESCRIPTION || (name + ' - 轻量好用的网址导航'); + + document.title = name; + var metaDesc = document.querySelector('meta[name="description"]'); + if (metaDesc) metaDesc.setAttribute('content', desc); + var metaApp = document.querySelector('meta[name="apple-mobile-web-app-title"]'); + if (metaApp) metaApp.setAttribute('content', name); + + var siteTitle = document.getElementById('site-title'); + if (siteTitle) siteTitle.textContent = '\u2728 ' + name; + var adminTitle = document.getElementById('admin-title'); + if (adminTitle) adminTitle.textContent = name + '-管理后台'; + var offlineTitle = document.getElementById('offline-title'); + if (offlineTitle) { + document.title = name + ' - 离线'; + offlineTitle.textContent = name + ' - 离线'; + } + var offlineLogo = document.getElementById('offline-logo'); + if (offlineLogo) offlineLogo.textContent = shortName.charAt(0); + + var glassOpacity = window.SITE_GLASS_OPACITY; + if (typeof glassOpacity === 'number' && glassOpacity >= 0 && glassOpacity <= 1) { + document.documentElement.style.setProperty('--site-glass-opacity', String(glassOpacity)); + } + + var isMobile = window.matchMedia('(max-width: 767px)').matches; + var bgImages = isMobile + ? window.SITE_MOBILE_BACKGROUND_IMAGES + : window.SITE_DESKTOP_BACKGROUND_IMAGES; + if (!Array.isArray(bgImages) || bgImages.length === 0) { + bgImages = isMobile ? window.SITE_DESKTOP_BACKGROUND_IMAGES : window.SITE_MOBILE_BACKGROUND_IMAGES; + } + if (Array.isArray(bgImages) && bgImages.length > 0) { + var imgUrl = bgImages[Math.floor(Math.random() * bgImages.length)]; + var applyBg = function () { + if (!document.body) return; + var bgEl = document.createElement('div'); + bgEl.className = 'site-bg'; + bgEl.setAttribute('aria-hidden', 'true'); + bgEl.style.backgroundImage = 'url(' + imgUrl + ')'; + document.body.insertBefore(bgEl, document.body.firstChild); + }; + if (document.body) { + applyBg(); + } else { + document.addEventListener('DOMContentLoaded', applyBg); + } + } + + var manifest = { + name: name, + short_name: shortName, + description: desc, + lang: 'zh-CN', + dir: 'ltr', + id: '/', + start_url: '/?source=pwa', + scope: '/', + display: 'standalone', + display_override: ['standalone', 'minimal-ui', 'browser'], + orientation: 'portrait-primary', + theme_color: '#10b981', + background_color: '#f8fafc', + categories: ['productivity', 'utilities'], + prefer_related_applications: false, + icons: [ + { src: '/logo.png', type: 'image/png', sizes: '2048x2048', purpose: 'any' }, + { src: '/logo.png', type: 'image/png', sizes: '2048x2048', purpose: 'maskable' }, + { src: '/favicon.ico', type: 'image/x-icon', sizes: '16x16 24x24 32x32 48x48 64x64', purpose: 'any' } + ] + }; + var blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' }); + var url = URL.createObjectURL(blob); + var link = document.querySelector('link[rel="manifest"]'); + if (link) link.href = url; + else { + link = document.createElement('link'); + link.rel = 'manifest'; + link.href = url; + document.head.appendChild(link); + } +})(); diff --git a/cf-nav-frontend/config.js b/cf-nav-frontend/config.js new file mode 100644 index 0000000..6ea3ab9 --- /dev/null +++ b/cf-nav-frontend/config.js @@ -0,0 +1,44 @@ +/** + * 前端配置(前后端分离) + * + * API 地址: + * - 部署到 Cloudflare Pages 时改为你的 Worker 地址,如: + * API_BASE = 'https://cf-nav-backend.你的子域.workers.dev'; + * - 本地同源或未配置时留空 '' + * + * 站点名称(可改为「XX导航」等,别人使用时改成自己的品牌): + * - SITE_NAME: 完整名称,用于标题、PWA 名称等 + * - SITE_SHORT_NAME: 短名称,用于 PWA 桌面图标下方、离线页等 + * - SITE_DESCRIPTION: 站点描述,用于 meta description 和 PWA + */ +window.API_BASE = 'https://cf-nav.api.smyhub.com'; + +// 站点名称配置(可改成你自己的导航站名) +window.SITE_NAME = '萌芽导航'; +window.SITE_SHORT_NAME = '萌芽'; +window.SITE_DESCRIPTION = '萌芽导航 - 轻量好用的网址导航'; + +// 网站背景图(全屏随机一张,带约 30% 高斯模糊)。设为 [] 或不配置则使用默认渐变背景 +// 手机端用 SITE_MOBILE_BACKGROUND_IMAGES,电脑端用 SITE_DESKTOP_BACKGROUND_IMAGES(按视口宽度 768px 区分) +window.SITE_MOBILE_BACKGROUND_IMAGES = [ + 'https://image.smyhub.com/file/手机壁纸/女生/1772108123232_VJ86r.jpg', + 'https://image.smyhub.com/file/手机壁纸/女生/1772108022800_f945774e0a45f7a4afdc3da2b112025f.png', + 'https://image.smyhub.com/file/手机壁纸/女生/1772108024006_3f9030ba77e355869115bc90fe019d53.png', + "https://image.smyhub.com/file/手机壁纸/女生/1772108030393_159bbf61f88b38475ee9144a2e8e4956.png", + "https://image.smyhub.com/file/手机壁纸/女生/1772108021977_8020902a0c8788538eee1cd06e784c6a.png", + "https://image.smyhub.com/file/手机壁纸/女生/1772108021881_44374ab6c1daa54e0204bca48ac382f2.png", +]; + +window.SITE_DESKTOP_BACKGROUND_IMAGES = [ + 'https://image.smyhub.com/file/电脑壁纸/女生/cuSpSkq4.webp', + 'https://image.smyhub.com/file/电脑壁纸/女生/5CrdoShv.webp', + 'https://image.smyhub.com/file/电脑壁纸/女生/xTsVkCli.webp', + "https://image.smyhub.com/file/电脑壁纸/女生/ItOJOHST.webp", + "https://image.smyhub.com/file/电脑壁纸/女生/cUDkKiOf.webp", + "https://image.smyhub.com/file/电脑壁纸/女生/c2HxMuGK.webp", + "https://image.smyhub.com/file/电脑壁纸/女生/L0nQHehz.webp", + "https://image.smyhub.com/file/电脑壁纸/女生/hj64Cqxn.webp", +]; + +// 内容区块(搜索框、分类块、网站卡片、顶栏)的半透明背景透明度,0=全透明 1=不透明,可自行修改 +window.SITE_GLASS_OPACITY = 0.25; \ No newline at end of file diff --git a/cf-nav-frontend/favicon.ico b/cf-nav-frontend/favicon.ico new file mode 100644 index 0000000..b3e38a8 Binary files /dev/null and b/cf-nav-frontend/favicon.ico differ diff --git a/cf-nav-frontend/index.html b/cf-nav-frontend/index.html new file mode 100644 index 0000000..257a567 --- /dev/null +++ b/cf-nav-frontend/index.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + 萌芽导航 + + + + + + + + + +
+
+

萌芽导航

+
+ + + +
+
+
📚 网站总数: 0
+
📁 分类数量: 0
+
🏷️ 标签数量: 0
+
+
+ 🔍 + +
+
+ +
+ +
+
📭
+

暂无网站

+

请在后台管理中添加网站

+
+
+
+ +
+ + 操作成功 +
+ + + + + + + + + diff --git a/cf-nav-frontend/logo.png b/cf-nav-frontend/logo.png new file mode 100644 index 0000000..f719bf9 Binary files /dev/null and b/cf-nav-frontend/logo.png differ diff --git a/cf-nav-frontend/manifest.webmanifest b/cf-nav-frontend/manifest.webmanifest new file mode 100644 index 0000000..a3cd6e3 --- /dev/null +++ b/cf-nav-frontend/manifest.webmanifest @@ -0,0 +1,37 @@ +{ + "name": "萌芽导航", + "short_name": "萌芽导航", + "description": "萌芽导航 - 轻量好用的网址导航", + "lang": "zh-CN", + "dir": "ltr", + "id": "/", + "start_url": "/?source=pwa", + "scope": "/", + "display": "standalone", + "display_override": ["standalone", "minimal-ui", "browser"], + "orientation": "portrait-primary", + "theme_color": "#10b981", + "background_color": "#f8fafc", + "categories": ["productivity", "utilities"], + "prefer_related_applications": false, + "icons": [ + { + "src": "/logo.png", + "type": "image/png", + "sizes": "2048x2048", + "purpose": "any" + }, + { + "src": "/logo.png", + "type": "image/png", + "sizes": "2048x2048", + "purpose": "maskable" + }, + { + "src": "/favicon.ico", + "type": "image/x-icon", + "sizes": "16x16 24x24 32x32 48x48 64x64", + "purpose": "any" + } + ] +} diff --git a/cf-nav-frontend/offline.html b/cf-nav-frontend/offline.html new file mode 100644 index 0000000..f6ed4d8 --- /dev/null +++ b/cf-nav-frontend/offline.html @@ -0,0 +1,113 @@ + + + + + + + 萌芽导航 - 离线 + + + + + +
+ +

当前离线,已切换离线页面

+

网络恢复后,刷新页面即可继续访问最新数据。已缓存的页面资源可以继续使用。

+
+ + +
+
+ + diff --git a/cf-nav-frontend/package-lock.json b/cf-nav-frontend/package-lock.json new file mode 100644 index 0000000..6e8ebe6 --- /dev/null +++ b/cf-nav-frontend/package-lock.json @@ -0,0 +1,1060 @@ +{ + "name": "mengya-nav-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mengya-nav-frontend", + "version": "1.0.0", + "devDependencies": { + "serve": "^14.0.0" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serve": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.6", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + } + } +} diff --git a/cf-nav-frontend/package.json b/cf-nav-frontend/package.json new file mode 100644 index 0000000..46d72cf --- /dev/null +++ b/cf-nav-frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "mengya-nav-frontend", + "version": "1.0.0", + "description": "萌芽导航 - 前端 PWA(Cloudflare Pages)", + "private": true, + "scripts": { + "build": "echo Static site - no build required", + "dev": "npx serve . -l 3000", + "preview": "npx serve . -l 3000" + }, + "devDependencies": { + "serve": "^14.0.0" + } +} diff --git a/cf-nav-frontend/styles.css b/cf-nav-frontend/styles.css new file mode 100644 index 0000000..f2a7689 --- /dev/null +++ b/cf-nav-frontend/styles.css @@ -0,0 +1,930 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #4361ee; + --secondary-color: #3f37c9; + --success-color: #4cc9f0; + --dark-color: #1e293b; + --light-color: #f8fafc; + --gray-color: #64748b; + --border-radius: 10px; + --box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08); + --transition: all 0.25s ease; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); + color: var(--dark-color); + line-height: 1.5; + padding: 12px; + min-height: 100vh; +} + +/* 全屏背景图层:仅当 config 中 SITE_BACKGROUND_IMAGES 非空时由 JS 插入,约 30% 高斯模糊 */ +.site-bg { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + filter: blur(10px); + opacity: 0.85; +} + +.container { + max-width: 1500px; + margin: 0 auto; +} + +header { + text-align: center; + margin-bottom: 16px; + padding: 10px 0; +} + +.main-header { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 0; + margin-bottom: 16px; +} + +.main-header h1 { + margin: 0; + flex: 1; + text-align: center; +} + +/* 左上角固定「分类」按钮:不随页面滚动 */ +.category-toggle-fixed { + position: fixed; + left: 16px; + top: 16px; + z-index: 998; +} + +body.category-sidebar-open .category-toggle-fixed { + z-index: 997; +} + +.category-toggle-btn { + padding: 8px 14px; + border-radius: 8px; + border: 1px solid rgba(67, 97, 238, 0.35); + background: white; + color: var(--primary-color); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + box-shadow: 0 2px 8px rgba(67, 97, 238, 0.1); +} + +.category-toggle-btn:hover { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); + box-shadow: 0 4px 12px rgba(67, 97, 238, 0.25); +} + +/* 分类侧边栏:固定不随页面滚动 */ +.category-sidebar-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.35); + z-index: 999; + opacity: 0; + visibility: hidden; + transition: opacity 0.25s ease, visibility 0.25s ease; +} + +body.category-sidebar-open .category-sidebar-backdrop { + opacity: 1; + visibility: visible; +} + +.category-sidebar { + position: fixed; + top: 0; + left: 0; + width: 280px; + max-width: 85vw; + height: 100vh; + background: white; + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.12); + z-index: 1000; + display: flex; + flex-direction: column; + transform: translateX(-100%); + transition: transform 0.3s ease; + overflow: hidden; +} + +body.category-sidebar-open .category-sidebar { + transform: translateX(0); +} + +body.category-sidebar-open { + overflow: hidden; +} + +.category-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 18px; + border-bottom: 1px solid #f1f5f9; + flex-shrink: 0; +} + +.category-sidebar-header h2 { + font-size: 1.15rem; + font-weight: 700; + color: var(--dark-color); + margin: 0; +} + +.category-sidebar-close { + width: 36px; + height: 36px; + border: none; + background: #f1f5f9; + border-radius: 50%; + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + color: var(--gray-color); + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition); +} + +.category-sidebar-close:hover { + background: #e2e8f0; + color: var(--dark-color); +} + +.category-sidebar-list { + flex: 1; + overflow-y: auto; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.category-sidebar-list .category-filter { + display: block; + width: 100%; + padding: 12px 14px; + text-align: left; + border-radius: 10px; + font-size: 0.9rem; +} + +.category-sidebar-list .category-filter:hover:not(.active) { + background: #f8fafc; +} + + +/* 隐藏整个页面的滚动条 */ +html { + /* 隐藏IE/Edge滚动条 */ + -ms-overflow-style: none; + /* 隐藏Firefox滚动条 */ + scrollbar-width: none; +} + +/* 隐藏Chrome/Safari/Opera滚动条 */ +html::-webkit-scrollbar { + display: none; +} + +/* 确保滚动功能正常 */ +body { + /* 允许滚动 */ + overflow-y: scroll; + /* 防止内容抖动,保持滚动条空间 */ + overflow-x: hidden; + /* 兼容WebKit内核浏览器 */ + -webkit-overflow-scrolling: touch; +} + + + + +h1 { + font-size: 2.1rem; + margin-bottom: 6px; + background: linear-gradient(45deg, var(--primary-color), var(--success-color)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-weight: 800; + letter-spacing: -0.5px; +} + +.subtitle { + display: none; +} + +.web-search-box { + background: rgba(255, 255, 255, var(--site-glass-opacity, 0.78)); + border-radius: 14px; + padding: 20px 22px; + margin-bottom: 20px; + box-shadow: 0 8px 32px rgba(67, 97, 238, 0.18), 0 4px 12px rgba(0, 0, 0, 0.06); + border: 2px solid rgba(67, 97, 238, 0.2); +} + +#web-search-form { + width: 100%; +} + +.search-wrapper { + display: flex; + align-items: center; + gap: 12px; + position: relative; +} + +#web-search-input { + flex: 1; + min-width: 0; + padding: 16px 18px 16px 46px; + border: 2px solid rgba(67, 97, 238, 0.3); + border-radius: 50px; + font-size: 1.05rem; + transition: var(--transition); + background: linear-gradient(135deg, #f8faff 0%, #f0f4ff 100%); + box-shadow: 0 4px 12px rgba(67, 97, 238, 0.12), inset 0 1px 0 rgba(255,255,255,0.8); +} + +#web-search-input::placeholder { + color: var(--gray-color); +} + +#web-search-input:hover { + border-color: var(--primary-color); + background: #fff; + box-shadow: 0 4px 16px rgba(67, 97, 238, 0.15); +} + +#web-search-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 5px rgba(67, 97, 238, 0.25); + background: #fff; +} + +.search-wrapper .search-icon { + position: absolute; + left: 18px; + font-size: 1.35rem; + color: var(--primary-color); + opacity: 0.95; +} + +.search-select { + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 8px; + background: white; + cursor: pointer; + font-size: 0.82rem; + transition: var(--transition); + font-weight: 500; + flex-shrink: 0; + max-width: 100px; +} + +.search-select:hover { + border-color: var(--primary-color); +} + +.search-select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15); +} + +@media (max-width: 768px) { + .web-search-box { + padding: 16px 14px; + margin-bottom: 16px; + } + + /* 移动端:搜索框与搜索引擎下拉并排同一行(并排 = side-by-side) */ + .search-wrapper { + flex-direction: row; + align-items: center; + gap: 8px; + } + + #web-search-input { + flex: 1; + min-width: 0; + width: auto; + padding: 14px 12px 14px 42px; + font-size: 1rem; + } + + .search-wrapper .search-icon { + left: 14px; + font-size: 1.2rem; + } + + .search-select { + width: auto; + min-width: 72px; + max-width: 36%; + padding: 8px 8px; + font-size: 0.78rem; + } +} + +.add-site-btn { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: white; + border: none; + padding: 9px 18px; + font-size: 0.95rem; + border-radius: 50px; + cursor: pointer; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(67, 97, 238, 0.28); + transition: var(--transition); + font-weight: 600; + letter-spacing: 0.2px; + white-space: nowrap; +} + +.add-site-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(67, 97, 238, 0.4); +} + +.add-site-btn:active { + transform: translateY(0); +} + +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: white; + border-radius: var(--border-radius); + width: 92%; + max-width: 560px; + box-shadow: var(--box-shadow); + animation: modalOpen 0.35s ease-out; + overflow: hidden; +} + +@keyframes modalOpen { + from { opacity: 0; transform: translateY(-16px); } + to { opacity: 1; transform: translateY(0); } +} + +.modal-header { + padding: 16px 20px; + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: white; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h2 { + font-size: 1.25rem; + font-weight: 600; +} + +.close-modal { + background: none; + border: none; + color: white; + font-size: 1.6rem; + cursor: pointer; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition); +} + +.close-modal:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.modal-body { + padding: 18px 20px 20px; +} + +.form-group { + margin-bottom: 14px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--dark-color); +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 0.95rem; + transition: var(--transition); +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.18); +} + +.form-group textarea { + min-height: 70px; + resize: vertical; +} + +.form-row { + display: flex; + gap: 12px; +} + +.form-row .form-group { + flex: 1; +} + +.submit-btn { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: white; + border: none; + padding: 11px 18px; + font-size: 0.98rem; + border-radius: 8px; + cursor: pointer; + width: 100%; + font-weight: 600; + letter-spacing: 0.3px; + transition: var(--transition); + margin-top: 6px; +} + +.submit-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(67, 97, 238, 0.25); +} + +.categories-container { + display: grid; + gap: 16px; +} + +.category-section { + background: rgba(255, 255, 255, var(--site-glass-opacity, 0.78)); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + padding: 16px; + transition: var(--transition); +} + +.category-section:hover { + transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); +} + +.category-title { + font-size: 1.2rem; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 2px solid #f1f5f9; + display: flex; + align-items: center; + color: var(--dark-color); + font-weight: 700; +} + +.category-title::before { + content: ""; + display: inline-block; + width: 6px; + height: 18px; + background: var(--primary-color); + border-radius: 4px; + margin-right: 10px; +} + +.sites-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 10px; +} + +.site-card { + background: rgba(255, 255, 255, var(--site-glass-opacity, 0.78)); + border-radius: 12px; + padding: 14px 10px; + text-align: center; + transition: var(--transition); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + border: 1px solid transparent; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + min-height: 140px; + text-decoration: none; + color: var(--dark-color); + position: relative; +} + +/* 卡片右上角点击次数,可与内容重叠 */ +.site-card-clicks { + position: absolute; + top: 2px; + right: 4px; + font-size: 9px; + line-height: 1; + color: #94a3b8; + z-index: 2; + pointer-events: none; +} + +/* 图标固定在最上方 */ +.site-icon { + width: 64px; + height: 64px; + border-radius: 12px; + background-color: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; + overflow: hidden; + flex-shrink: 0; +} + +.site-icon img { + width: 48px; + height: 48px; + object-fit: contain; +} + +/* 中间区域:网站名称+描述,弹性占据剩余空间 */ +.site-card-body { + flex: 1; + min-height: 0; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; +} + +.site-name { + font-weight: 600; + font-size: 0.9rem; + margin-bottom: 4px; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3; + flex-shrink: 0; +} + +.site-description { + font-size: 0.75rem; + color: var(--gray-color); + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3; + flex: 1; + min-height: 0; +} + +/* 标签固定在最下方,不受名称和描述影响 */ +.site-tags { + width: 100%; + margin-top: auto; + flex-shrink: 0; + display: flex; + flex-wrap: nowrap; + gap: 4px; + justify-content: center; + overflow: hidden; +} + +.site-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + border-color: rgba(67, 97, 238, 0.2); +} + +.tag { + background: #eef2ff; + color: var(--primary-color); + font-size: 0.65rem; + padding: 2px 6px; + border-radius: 10px; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} + +.empty-state { + text-align: center; + padding: 30px 20px; + color: var(--gray-color); +} + +.empty-state p { + margin-top: 8px; + font-size: 1rem; +} + +.empty-state button { + margin-top: 12px; +} + +.top-bar { + background: rgba(255, 255, 255, var(--site-glass-opacity, 0.78)); + border-radius: var(--border-radius); + padding: 8px 14px; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: var(--box-shadow); +} + +.stats-group { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.search-container { + max-width: 560px; + margin: 0 auto 16px; + position: relative; +} + +.search-container.compact { + margin: 0; + max-width: 500px; + flex: 1; +} + +.search-container input { + width: 100%; + padding: 9px 16px 9px 36px; + border-radius: 50px; + border: 1px solid #ddd; + font-size: 0.93rem; + box-shadow: var(--box-shadow); + transition: var(--transition); +} + +.search-container input:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.18); +} + +.search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--gray-color); + font-size: 1.1rem; +} + + + +.stats-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.85rem; + font-weight: 500; + white-space: nowrap; +} + +.stats-item span { + font-weight: 700; + color: var(--primary-color); +} + +@media (max-width: 1024px) { + .sites-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } +} + +@media (max-width: 768px) { + body { + padding: 10px; + } + + h1 { + font-size: 1.85rem; + } + + .sites-grid { + grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); + } + + .form-row { + flex-direction: column; + gap: 0; + } + + .top-bar { + flex-direction: column; + gap: 10px; + } + + .stats-group { + width: 100%; + justify-content: space-around; + flex-wrap: wrap; + gap: 8px; + } + + .search-container.compact { + width: 100%; + max-width: none; + } + + .add-site-btn { + width: 100%; + } +} + +@media (max-width: 480px) { + .sites-grid { + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: 8px; + } + + .site-card { + padding: 10px 6px; + min-height: 120px; + } + + .site-icon { + width: 48px; + height: 48px; + } + + .site-icon img { + width: 36px; + height: 36px; + } + + .site-name { + font-size: 0.85rem; + } + + .site-description { + font-size: 0.7rem; + } + + .favicon-placeholder { + font-size: 1.1rem; + } + + .modal-content { + width: 96%; + margin: 16px; + } + + .add-site-btn { + padding: 8px 16px; + font-size: 0.92rem; + } +} + +.toast { + position: fixed; + bottom: 20px; + right: 20px; + background: white; + color: var(--dark-color); + padding: 12px 20px; + border-radius: 50px; + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.15); + display: flex; + align-items: center; + gap: 10px; + z-index: 2000; + transform: translateX(320px); + transition: transform 0.25s ease; + font-weight: 500; +} + +.toast.show { + transform: translateX(0); +} + +.toast.success { + border-left: 4px solid var(--success-color); +} + +.toast.error { + border-left: 4px solid #f72585; +} + +.toast-icon { + font-size: 1.2rem; +} + +.category-selector { + display: none; +} + +.category-filter { + padding: 5px 12px; + border-radius: 30px; + background: white; + border: 1px solid #ddd; + cursor: pointer; + transition: var(--transition); + font-weight: 500; + font-size: 0.85rem; +} + +.category-filter.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.category-filter:hover:not(.active) { + background: #f8fafc; + border-color: var(--primary-color); +} + +.no-results { + text-align: center; + padding: 30px 20px; + color: var(--gray-color); + grid-column: 1 / -1; +} + +.no-results p { + font-size: 1.05rem; + margin-top: 12px; +} + +.favicon-placeholder { + background: linear-gradient(45deg, #4361ee, #3f37c9); + color: white; + font-weight: bold; + font-size: 1.4rem; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} diff --git a/cf-nav-frontend/sw.js b/cf-nav-frontend/sw.js new file mode 100644 index 0000000..9475042 --- /dev/null +++ b/cf-nav-frontend/sw.js @@ -0,0 +1,182 @@ +const CACHE_VERSION = 'v2'; +const STATIC_CACHE = `mengya-static-${CACHE_VERSION}`; +const RUNTIME_CACHE = `mengya-runtime-${CACHE_VERSION}`; +const API_CACHE = `mengya-api-${CACHE_VERSION}`; + +const OFFLINE_FALLBACK = '/offline.html'; +const APP_SHELL = [ + '/', + '/index.html', + '/styles.css', + '/app.js', + '/manifest.webmanifest', + '/logo.png', + '/favicon.ico', + OFFLINE_FALLBACK, +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(STATIC_CACHE).then((cache) => cache.addAll(APP_SHELL)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames + .filter((name) => ![STATIC_CACHE, RUNTIME_CACHE, API_CACHE].includes(name)) + .map((name) => caches.delete(name)) + ); + + if ('navigationPreload' in self.registration) { + await self.registration.navigationPreload.enable(); + } + + await self.clients.claim(); + })() + ); +}); + +self.addEventListener('message', (event) => { + if (event.data === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') { + return; + } + + const url = new URL(request.url); + + if (request.mode === 'navigate') { + event.respondWith(handleNavigationRequest(event)); + return; + } + + if (url.origin !== self.location.origin) { + return; + } + + if (url.pathname.startsWith('/api/')) { + event.respondWith(networkFirst(request, API_CACHE)); + return; + } + + if (isStaticAssetRequest(request, url)) { + event.respondWith(staleWhileRevalidate(request, RUNTIME_CACHE)); + return; + } + + event.respondWith(cacheFirst(request, RUNTIME_CACHE)); +}); + +async function handleNavigationRequest(event) { + try { + const preloadResponse = await event.preloadResponse; + if (preloadResponse) { + return preloadResponse; + } + + const networkResponse = await fetch(event.request); + if (networkResponse && networkResponse.ok) { + const cache = await caches.open(RUNTIME_CACHE); + cache.put(event.request, networkResponse.clone()); + } + return networkResponse; + } catch (error) { + const cachedPage = await caches.match(event.request); + if (cachedPage) { + return cachedPage; + } + + const offlineResponse = await caches.match(OFFLINE_FALLBACK); + return offlineResponse || new Response('Offline', { status: 503 }); + } +} + +async function networkFirst(request, cacheName) { + const cache = await caches.open(cacheName); + + try { + const response = await fetch(request); + if (shouldCacheResponse(response)) { + cache.put(request, response.clone()); + } + return response; + } catch (error) { + const cached = await cache.match(request); + if (cached) { + return cached; + } + + if (request.mode === 'navigate') { + const offlineResponse = await caches.match(OFFLINE_FALLBACK); + if (offlineResponse) { + return offlineResponse; + } + } + + return new Response(JSON.stringify({ message: 'Network unavailable' }), { + status: 503, + headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + }); + } +} + +async function staleWhileRevalidate(request, cacheName) { + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + + const networkPromise = fetch(request) + .then((response) => { + if (shouldCacheResponse(response)) { + cache.put(request, response.clone()); + } + return response; + }) + .catch(() => undefined); + + return cached || networkPromise || new Response('Not found', { status: 404 }); +} + +async function cacheFirst(request, cacheName) { + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + if (cached) { + return cached; + } + + const response = await fetch(request); + if (shouldCacheResponse(response)) { + cache.put(request, response.clone()); + } + return response; +} + +function isStaticAssetRequest(request, url) { + if (request.destination === 'script' || request.destination === 'style' || request.destination === 'image' || request.destination === 'font') { + return true; + } + + return ( + url.pathname.endsWith('.css') || + url.pathname.endsWith('.js') || + url.pathname.endsWith('.png') || + url.pathname.endsWith('.jpg') || + url.pathname.endsWith('.jpeg') || + url.pathname.endsWith('.svg') || + url.pathname.endsWith('.ico') || + url.pathname.endsWith('.webmanifest') + ); +} + +function shouldCacheResponse(response) { + return Boolean(response && (response.status === 200 || response.type === 'opaque')); +}