初始化管理后台仓库

This commit is contained in:
2025-12-29 10:41:23 +08:00
commit f9a8df0fe5
27 changed files with 2481 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
API_BASE_URL=http://192.168.3.42:8080
CLIENT_ID=admin
CLIENT_SECRET=admin

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# editor
.idea/

15
.zed/settings.json Normal file
View File

@@ -0,0 +1,15 @@
{
"language_servers": ["...", "!typescript-language-server", "!eslint"],
"languages": {
"TypeScript": {
"formatter": { "language_server": { "name": "biome" } },
"code_actions_on_format": {
"source.fixAll.biome": true,
"source.organizeImports.biome": true,
},
},
"JSON": {
"formatter": { "language_server": { "name": "biome" } },
},
},
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
## todo
仪表盘,欢迎组件有点丑,可以优化一下

43
biome.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useNodejsImportProtocol": "off"
}
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

295
bun.lock Normal file
View File

@@ -0,0 +1,295 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "lanhu-admin",
"dependencies": {
"@hookform/resolvers": "^4.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.479.0",
"next": "^16.0.10",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.68.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.25.76",
"zustand": "^5.0.9",
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@tailwindcss/postcss": "^4.1.17",
"@types/bun": "^1.3.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/types": ["@babel/types@7.28.5", "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.10", "https://registry.npmmirror.com/@biomejs/biome/-/biome-2.3.10.tgz", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "https://registry.npmmirror.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.7.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@hookform/resolvers": ["@hookform/resolvers@4.1.3", "https://registry.npmmirror.com/@hookform/resolvers/-/resolvers-4.1.3.tgz", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="],
"@img/colour": ["@img/colour@1.0.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@next/env": ["@next/env@16.1.1", "https://registry.npmmirror.com/@next/env/-/env-16.1.1.tgz", {}, "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.1", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.1", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.1", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.1", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.1", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.1", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.1", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.1", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.18.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.18.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.1.18.tgz", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
"@types/bun": ["@types/bun@1.3.5", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.5.tgz", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/node": ["@types/node@20.17.24", "https://registry.npmmirror.com/@types/node/-/node-20.17.24.tgz", { "dependencies": { "undici-types": "6.19.8" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
"@types/react": ["@types/react@19.2.7", "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "https://registry.npmmirror.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
"bun-types": ["bun-types@1.3.5", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.5.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001703", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", {}, "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"date-fns": ["date-fns@4.1.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"lightningcss": ["lightningcss@1.30.2", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.2.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lucide-react": ["lucide-react@0.479.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.479.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"nanoid": ["nanoid@3.3.9", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.9.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg=="],
"next": ["next@16.1.1", "https://registry.npmmirror.com/next/-/next-16.1.1.tgz", { "dependencies": { "@next/env": "16.1.1", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.1", "@next/swc-darwin-x64": "16.1.1", "@next/swc-linux-arm64-gnu": "16.1.1", "@next/swc-linux-arm64-musl": "16.1.1", "@next/swc-linux-x64-gnu": "16.1.1", "@next/swc-linux-x64-musl": "16.1.1", "@next/swc-win32-arm64-msvc": "16.1.1", "@next/swc-win32-x64-msvc": "16.1.1", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w=="],
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.3", "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", { "dependencies": { "nanoid": "3.3.9", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"react": ["react@19.2.3", "https://registry.npmmirror.com/react/-/react-19.2.3.tgz", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
"react-dom": ["react-dom@19.2.3", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
"react-hook-form": ["react-hook-form@7.69.0", "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.69.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="],
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.3", "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "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" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"styled-jsx": ["styled-jsx@5.1.6", "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": "19.0.0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwindcss": ["tailwindcss@4.1.18", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.18.tgz", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
"tapable": ["tapable@2.2.1", "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.19.8", "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zustand": ["zustand@5.0.9", "https://registry.npmmirror.com/zustand/-/zustand-5.0.9.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.7.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.7.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"next/postcss": ["postcss@8.4.31", "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", { "dependencies": { "nanoid": "3.3.9", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
}
}

13
next.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
output: "standalone",
cacheComponents: true,
reactCompiler: true,
experimental: {
turbopackFileSystemCacheForDev: true,
},
allowedDevOrigins: ["192.168.3.42", "192.168.3.14"],
}
export default nextConfig

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "lanhu-admin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -H 0.0.0.0 --turbopack",
"build": "next build --turbopack",
"lint": "eslint --fix"
},
"dependencies": {
"lucide-react": "^0.479.0",
"next": "^16.0.10",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.68.0",
"@hookform/resolvers": "^4.1.3",
"zod": "^3.25.76",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.9",
"date-fns": "^4.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@tailwindcss/postcss": "^4.1.17",
"@types/bun": "^1.3.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3"
},
"packageManager": "bun@1.3.2"
}

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,434 @@
import Link from 'next/link'
export type DashboardPageProps = {}
export default function DashboardPage(props: DashboardPageProps) {
return (
<div className="space-y-5">
{/* 欢迎区域 - 全宽 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="flex items-center justify-between p-5">
<div>
<h1 className="text-xl font-bold text-gray-800">IP代理管理控制台</h1>
<p className="text-gray-500 mt-1">: {new Date().toLocaleString('zh-CN')}</p>
</div>
<div className="flex space-x-3">
<button
className="px-4 py-2 bg-gray-100 text-gray-700 border border-gray-200 rounded-md hover:bg-gray-200 transition-colors text-sm font-medium">
使
</button>
<button
className="px-4 py-2 bg-blue-600 text-white border border-blue-700 rounded-md hover:bg-blue-700 transition-colors text-sm font-medium">
</button>
</div>
</div>
</div>
{/* 主体内容 - 双栏布局 */}
<div className="flex flex-col lg:flex-row space-y-5 lg:space-y-0 lg:space-x-5">
{/* 左侧栏 - 占比较大 */}
<div className="w-full lg:w-8/12 space-y-5">
{/* 代理资源统计卡片组 */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-5">
<DataCard
title="在线代理"
value="14,283"
change="+12.5%"
isIncrease={true}
icon={
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7"/>
</svg>
}
/>
<DataCard
title="总请求数"
value="851,492"
change="+8.2%"
isIncrease={true}
icon={
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
}
/>
<DataCard
title="成功率"
value="98.5%"
change="+2.4%"
isIncrease={true}
icon={
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
}
/>
<DataCard
title="平均响应时间"
value="0.82s"
change="-12.3%"
isIncrease={true}
icon={
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
}
/>
</div>
{/* 代理使用图表 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="flex justify-between items-center p-5 border-b border-gray-200">
<h2 className="font-bold text-gray-800">使</h2>
<div className="flex items-center space-x-2">
<select className="text-sm border border-gray-200 rounded-md px-3 py-1 bg-white">
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</div>
</div>
<div className="h-80 bg-white p-5 flex items-center justify-center text-gray-400 border-b border-gray-200">
</div>
<div className="flex justify-between p-5 text-sm">
<div className="text-gray-500">
<span className="inline-block w-3 h-3 rounded-full bg-blue-500 mr-1"></span>
</div>
<div className="text-gray-500">
<span className="inline-block w-3 h-3 rounded-full bg-green-500 mr-1"></span>
</div>
<div className="text-gray-500">
<span className="inline-block w-3 h-3 rounded-full bg-orange-500 mr-1"></span>
</div>
</div>
</div>
{/* IP代理列表 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="p-5 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="font-bold text-gray-800">IP</h2>
<Link href="/proxies" className="text-blue-600 text-sm hover:underline"></Link>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
className="bg-gray-50 text-left text-xs text-gray-500 uppercase tracking-wider border-b border-gray-200">
<th className="px-5 py-3">IP地址</th>
<th className="px-5 py-3"></th>
<th className="px-5 py-3"></th>
<th className="px-5 py-3"></th>
<th className="px-5 py-3"></th>
<th className="px-5 py-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{[1, 2, 3, 4, 5].map((item) => (
<ContentRow
key={item}
ip={`192.168.${item}.${item * 10}`}
location={item % 2 === 0 ? '中国' : '美国'}
status={item % 3 === 0 ? 'error' : item % 2 === 0 ? 'warning' : 'active'}
requests={Math.floor(Math.random() * 10000)}
successRate={`${95 + Math.floor(Math.random() * 5)}%`}
/>
))}
</tbody>
</table>
</div>
<div className="p-3 bg-gray-50 border-t border-gray-200 flex justify-end">
<button className="px-3 py-1 bg-white border border-gray-200 rounded-md text-sm mr-2"></button>
<button className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm"></button>
</div>
</div>
</div>
{/* 右侧栏 - 占比较小 */}
<div className="w-full lg:w-4/12 space-y-5">
{/* 代理资源分布 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="p-5 border-b border-gray-200">
<h2 className="font-bold text-gray-800"></h2>
</div>
<div className="h-64 bg-white p-5 flex items-center justify-center text-gray-400">
IP地区分布饼图
</div>
<div className="grid grid-cols-2 gap-3 p-5 border-t border-gray-200">
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
<span className="text-xs text-gray-600"></span>
<span className="text-xs font-medium">42%</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
<span className="text-xs text-gray-600"></span>
<span className="text-xs font-medium">28%</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
<span className="text-xs text-gray-600"></span>
<span className="text-xs font-medium">16%</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
<span className="text-xs text-gray-600"></span>
<span className="text-xs font-medium">14%</span>
</div>
</div>
</div>
{/* 系统告警 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="p-5 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="font-bold text-gray-800"></h2>
<span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">3 </span>
</div>
</div>
<div className="p-3 space-y-3">
<AlertItem
title="IP不足告警"
severity="high"
time="10分钟前"
message="特定地区(美国加州)代理IP资源不足影响用户请求。"
/>
<AlertItem
title="响应延迟"
severity="medium"
time="30分钟前"
message="欧洲区域代理响应时间超过阈值(1.5s),请检查网络状况。"
/>
<AlertItem
title="异常请求"
severity="low"
time="2小时前"
message="检测到异常请求模式,可能存在爬虫攻击行为。"
/>
</div>
<div className="p-4 border-t border-gray-200">
<button
className="w-full py-2 bg-gray-100 text-gray-600 border border-gray-200 rounded-md text-sm hover:bg-gray-200 transition-colors">
</button>
</div>
</div>
{/* 系统状态 */}
<div className="bg-white border border-gray-200 rounded-md">
<div className="p-5 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="font-bold text-gray-800"></h2>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full font-medium"></span>
</div>
</div>
<div className="p-3 space-y-3">
<StatusBar
title="代理服务器负载"
value={28}
status="normal"
/>
<StatusBar
title="带宽使用率"
value={65}
status="normal"
/>
<StatusBar
title="存储空间"
value={82}
status="warning"
/>
<StatusBar
title="API请求队列"
value={45}
status="normal"
/>
</div>
<div className="p-4 border-t border-gray-200">
<Link
href="/system/status"
className="block w-full py-2 text-center bg-gray-100 text-gray-600 border border-gray-200 rounded-md text-sm hover:bg-gray-200 transition-colors">
</Link>
</div>
</div>
</div>
</div>
</div>
)
}
// 数据卡片组件 - 显示关键指标
function DataCard({
title,
value,
change,
isIncrease,
icon,
}: {
title: string;
value: string;
change: string;
isIncrease: boolean;
icon: React.ReactNode;
}) {
return (
<div className="bg-white border border-gray-200 rounded-md p-5">
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm text-gray-500">{title}</h3>
<p className="text-xl font-bold mt-1 text-gray-800">{value}</p>
<div className="flex items-center mt-2">
<span className={`text-xs font-medium ${isIncrease ? 'text-green-600' : 'text-red-600'}`}>
{change}
</span>
<span className="text-xs text-gray-400 ml-1"></span>
</div>
</div>
<div
className={`p-3 rounded-md ${isIncrease ? 'bg-blue-50 text-blue-600' : 'bg-orange-50 text-orange-500'} border ${isIncrease ? 'border-blue-100' : 'border-orange-100'}`}>
{icon}
</div>
</div>
</div>
)
}
// 代理IP内容行组件
function ContentRow({
ip,
location,
status,
requests,
successRate,
}: {
ip: string;
location: string;
status: 'active' | 'warning' | 'error';
requests: number;
successRate: string;
}) {
const statusConfig = {
active: {color: 'bg-green-100 text-green-800 border-green-200', label: '在线'},
warning: {color: 'bg-yellow-100 text-yellow-800 border-yellow-200', label: '不稳定'},
error: {color: 'bg-red-100 text-red-800 border-red-200', label: '离线'},
}
return (
<tr className="hover:bg-gray-50">
<td className="px-5 py-4">
<div className="font-mono text-sm">{ip}</div>
</td>
<td className="px-5 py-4">
<div className="text-sm text-gray-700">{location}</div>
</td>
<td className="px-5 py-4">
<span className={`px-2 py-1 text-xs rounded-md ${statusConfig[status].color} border`}>
{statusConfig[status].label}
</span>
</td>
<td className="px-5 py-4 text-sm text-gray-700">
{requests.toLocaleString()}
</td>
<td className="px-5 py-4 text-sm text-gray-700">
{successRate}
</td>
<td className="px-5 py-4">
<div className="flex space-x-2">
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
<svg className="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
<svg className="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
<button className="p-1 border border-red-200 rounded-md hover:bg-red-50">
<svg className="h-4 w-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</td>
</tr>
)
}
// 告警通知组件
function AlertItem({
title,
severity,
time,
message,
}: {
title: string;
severity: 'high' | 'medium' | 'low';
time: string;
message: string;
}) {
const severityConfig = {
high: {color: 'bg-red-50 border-red-200', dot: 'bg-red-500'},
medium: {color: 'bg-yellow-50 border-yellow-200', dot: 'bg-yellow-500'},
low: {color: 'bg-blue-50 border-blue-200', dot: 'bg-blue-500'},
}
return (
<div className={`p-3 rounded-md ${severityConfig[severity].color} border`}>
<div className="flex justify-between items-start">
<div className="flex items-center">
<span className={`h-2 w-2 rounded-full ${severityConfig[severity].dot} mr-2`}></span>
<span className="font-medium text-gray-800 text-sm">{title}</span>
</div>
<span className="text-xs text-gray-500">{time}</span>
</div>
<p className="mt-2 text-xs text-gray-600">{message}</p>
</div>
)
}
// 系统状态条组件
function StatusBar({
title,
value,
status,
}: {
title: string;
value: number;
status: 'normal' | 'warning' | 'error';
}) {
const statusConfig = {
normal: {color: 'bg-green-500', bgColor: 'bg-green-100'},
warning: {color: 'bg-yellow-500', bgColor: 'bg-yellow-100'},
error: {color: 'bg-red-500', bgColor: 'bg-red-100'},
}
return (
<div className="p-3 border border-gray-200 rounded-md">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-700">{title}</span>
<span className="text-sm font-medium">{value}%</span>
</div>
<div className={`w-full h-2 ${statusConfig[status].bgColor} rounded-full`}>
<div
className={`h-2 ${statusConfig[status].color} rounded-full`}
style={{width: `${value}%`}}
></div>
</div>
</div>
)
}

276
src/app/(root)/appbar.tsx Normal file
View File

@@ -0,0 +1,276 @@
'use client'
import {useState, useRef, useEffect} from 'react'
import Image from 'next/image'
import Link from 'next/link'
import {usePathname} from 'next/navigation'
export type AppbarProps = {}
export default function Appbar(props: AppbarProps) {
const [currentUser] = useState({
name: '张三',
avatar: '/avatar.png',
role: '管理员',
})
const [showDropdown, setShowDropdown] = useState(false)
const [showNotifications, setShowNotifications] = useState(false)
const [notifications] = useState([
{id: 1, title: '系统通知', content: '您有新的待审核内容', time: '10分钟前', read: false},
{id: 2, title: '安全提醒', content: '您的账号于昨天登录了新设备', time: '1小时前', read: true},
{id: 3, title: '系统更新', content: '系统将在今晚进行例行维护', time: '2小时前', read: true},
])
const pathname = usePathname()
const dropdownRef = useRef<HTMLDivElement>(null)
const notificationRef = useRef<HTMLDivElement>(null)
// 处理点击外部关闭下拉菜单
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropdown(false)
}
if (notificationRef.current && !notificationRef.current.contains(event.target as Node)) {
setShowNotifications(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// 根据路径生成面包屑
const generateBreadcrumbs = () => {
const paths = pathname.split('/').filter(Boolean)
const breadcrumbs = [
{path: '/', label: '首页'},
...paths.map((path, index) => {
const url = `/${paths.slice(0, index + 1).join('/')}`
const label = getBreadcrumbLabel(path)
return {path: url, label}
}),
]
return breadcrumbs
}
const getBreadcrumbLabel = (path: string) => {
const labels: Record<string, string> = {
'dashboard': '控制台',
'content': '内容管理',
'articles': '文章管理',
'media': '媒体库',
'users': '用户管理',
'roles': '角色权限',
'settings': '系统设置',
'logs': '系统日志',
}
return labels[path] || path
}
const breadcrumbs = generateBreadcrumbs()
const unreadCount = notifications.filter(n => !n.read).length
return (
<header className="bg-white h-16 border-b border-gray-200 flex items-center justify-between px-6">
{/* 面包屑导航 */}
<div className="flex items-center text-sm">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.path} className="flex items-center">
{index > 0 && (
<svg className="mx-2 h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7"/>
</svg>
)}
<Link
href={crumb.path}
className={index === breadcrumbs.length - 1
? 'text-gray-800 font-medium'
: 'text-gray-500 hover:text-gray-700'
}
>
{crumb.label}
</Link>
</div>
))}
</div>
{/* 右侧用户信息和工具栏 */}
<div className="flex items-center space-x-4">
{/* 搜索框 */}
<div className="hidden md:block relative">
<input
type="text"
placeholder="搜索..."
className="pl-10 pr-4 py-2 bg-gray-100 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-56"
/>
<svg
className="h-4 w-4 text-gray-400 absolute left-3 top-2.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
{/* 通知图标 */}
<div className="relative" ref={notificationRef}>
<button
onClick={() => setShowNotifications(!showNotifications)}
className="relative p-2 rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition-colors"
aria-label="通知"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{unreadCount > 0 && (
<span
className="absolute top-1 right-1 h-4 w-4 text-xs flex items-center justify-center rounded-full bg-red-500 text-white">{unreadCount}</span>
)}
</button>
{/* 通知下拉面板 */}
{showNotifications && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border border-gray-200">
<div className="px-4 py-2 border-b border-gray-100 flex justify-between items-center">
<h3 className="font-medium text-gray-800"></h3>
<button className="text-xs text-blue-600 hover:text-blue-800">
</button>
</div>
<div className="max-h-72 overflow-y-auto">
{notifications.length > 0 ? (
notifications.map((notification) => (
<div
key={notification.id}
className={`px-4 py-3 border-b border-gray-100 hover:bg-gray-50 ${
notification.read ? 'bg-white' : 'bg-blue-50'
}`}
>
<div className="flex justify-between items-start">
<h4 className="text-sm font-medium text-gray-800">{notification.title}</h4>
<span className="text-xs text-gray-500">{notification.time}</span>
</div>
<p className="text-xs text-gray-600 mt-1">{notification.content}</p>
</div>
))
) : (
<div className="py-8 px-4 text-center">
<svg className="w-12 h-12 text-gray-300 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p className="mt-2 text-sm text-gray-500"></p>
</div>
)}
</div>
<div className="border-t border-gray-100 p-2 text-center">
<Link href="/notifications" className="text-xs text-blue-600 hover:text-blue-800">
</Link>
</div>
</div>
)}
</div>
{/* 分隔线 */}
<div className="hidden md:block h-8 w-px bg-gray-200"></div>
{/* 用户下拉菜单 */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center space-x-2 rounded-lg hover:bg-gray-100 p-2 transition-colors"
aria-label="用户菜单"
>
<div
className="h-8 w-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
<Image
src={currentUser.avatar}
alt="用户头像"
width={32}
height={32}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
target.parentElement!.innerHTML = currentUser.name.charAt(0)
}}
/>
</div>
<div className="hidden md:block text-left">
<p className="text-sm font-medium text-gray-800">{currentUser.name}</p>
<p className="text-xs text-gray-500">{currentUser.role}</p>
</div>
<svg
className="h-4 w-4 text-gray-400 hidden md:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
</svg>
</button>
{/* 用户下拉内容 */}
{showDropdown && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 z-20 border border-gray-200">
<div className="px-4 py-2 border-b border-gray-100 md:hidden">
<p className="font-medium text-gray-800">{currentUser.name}</p>
<p className="text-xs text-gray-500">{currentUser.role}</p>
</div>
<div className="py-1">
<Link href="/profile" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</Link>
<Link
href="/settings/account"
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</Link>
<Link
href="/system/help" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</Link>
</div>
<div className="border-t border-gray-100 mt-1">
<Link href="/login" className="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<svg className="mr-3 h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
退
</Link>
</div>
</div>
)}
</div>
</div>
</header>
)
}

27
src/app/(root)/layout.tsx Normal file
View File

@@ -0,0 +1,27 @@
import {ReactNode} from 'react'
import Appbar from '@/app/(root)/appbar'
import Navigation from '@/app/(root)/navigation'
export type RootLayoutProps = {
children: ReactNode
}
export default function RootLayout({children}: RootLayoutProps) {
return (
<div className="flex h-screen bg-gray-100">
{/* 侧边栏 */}
<Navigation/>
{/* 主内容区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 顶部导航栏 */}
<Appbar/>
{/* 内容区域 */}
<main className="flex-1 overflow-auto p-6">
{children}
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,191 @@
'use client'
import {useState} from 'react'
import Link from 'next/link'
import {usePathname} from 'next/navigation'
export type NavigationProps = {}
// 菜单组接口
interface MenuGroup {
title: string;
items: MenuItem[];
}
// 菜单项接口
interface MenuItem {
path: string;
icon: string;
label: string;
}
// 定义菜单组
const menuGroups: MenuGroup[] = [
{
title: '概览',
items: [
{
path: '/',
icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
label: '首页',
},
{
path: '/statistics',
icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
label: '数据统计',
},
],
},
{
title: 'IP 资源',
items: [
{
path: '/proxy/nodes',
icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9',
label: '节点列表',
},
{
path: '/proxy/pools',
icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01',
label: 'IP池管理',
},
{
path: '/proxy/sources',
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
label: '代理源管理',
},
],
},
{
title: '客户',
items: [
{
path: '/clients',
icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z',
label: '客户管理',
},
{path: '/packages', icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10', label: '套餐管理'},
{
path: '/orders',
icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2',
label: '订单管理',
},
],
},
{
title: '运营',
items: [
{path: '/api/management', icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4', label: 'API管理'},
{
path: '/traffic',
icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
label: '流量监控',
},
{
path: '/billing',
icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
label: '计费系统',
},
],
},
{
title: '系统',
items: [
{
path: '/settings',
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
label: '系统设置',
},
{
path: '/security',
icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
label: '安全管理',
},
{
path: '/logs',
icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
label: '系统日志',
},
],
},
]
export default function Navigation(props: NavigationProps) {
const [collapsed, setCollapsed] = useState(false)
const pathname = usePathname()
const isActive = (path: string) => {
return path === '/'
? pathname === path
: pathname.startsWith(path)
}
return (
<aside
className={`bg-white border-r border-gray-200 transition-all duration-300 ease-in-out flex flex-col ${collapsed ? 'w-20' : 'w-64'}`}>
{/* Logo */}
<div className="h-16 flex items-center px-5 border-b border-gray-200">
{!collapsed ? (
<span className="text-xl font-bold tracking-wide text-gray-800"></span>
) : (
<span className="text-xl font-bold mx-auto text-gray-800"></span>
)}
</div>
{/* 导航菜单 */}
<nav className="flex-1 py-4 overflow-y-auto">
<div className="space-y-4">
{menuGroups.map((group, groupIndex) => (
<div key={groupIndex} className="px-3">
{!collapsed && (
<h3 className="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
{group.title}
</h3>
)}
<ul className={`mt-${collapsed ? '0' : '2'} space-y-1`}>
{group.items.map((item) => (
<li key={item.path}>
<Link
href={item.path}
className={`flex items-center px-3 py-2 rounded-md transition-colors ${
isActive(item.path)
? 'bg-blue-50 text-blue-700'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<svg
className={`h-5 w-5 ${isActive(item.path) ? 'text-blue-600' : 'text-gray-500'}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon}/>
</svg>
{!collapsed && <span className="ml-3 font-medium text-sm">{item.label}</span>}
</Link>
</li>
))}
</ul>
{!collapsed && groupIndex < menuGroups.length - 1 && (
<div className="my-4 border-t border-gray-200"></div>
)}
</div>
))}
</div>
</nav>
{/* 侧边栏底部按钮 */}
<div className="p-4 border-t border-gray-200 mt-auto">
<button
onClick={() => setCollapsed(!collapsed)}
className="flex items-center justify-center w-full p-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d={collapsed ? 'M13 5l7 7-7 7M5 5l7 7-7 7' : 'M11 19l-7-7 7-7m8 14l-7-7 7-7'}/>
</svg>
{!collapsed && <span className="ml-2 text-sm"></span>}
</button>
</div>
</aside>
)
}

View File

@@ -0,0 +1,495 @@
'use client'
import {useState, useEffect} from 'react'
import Link from 'next/link'
// 定义节点数据接口
interface ProxyNode {
id: string;
ipAddress: string;
location: {
country: string;
region: string;
};
type: string;
status: 'online' | 'offline' | 'warning';
responseTime: number;
lastCheckTime: string;
pool: string;
isStatic: boolean;
}
export type ProxyNodesPageProps = {}
export default function ProxyNodesPage(props: ProxyNodesPageProps) {
const [loading, setLoading] = useState<boolean>(true)
const [nodes, setNodes] = useState<ProxyNode[]>([])
const [searchTerm, setSearchTerm] = useState<string>('')
const [filterStatus, setFilterStatus] = useState<string>('all')
const [filterType, setFilterType] = useState<string>('all')
const [filterPool, setFilterPool] = useState<string>('all')
// 模拟数据加载
useEffect(() => {
setTimeout(() => {
setNodes([
{
id: 'ip-1',
ipAddress: '203.45.167.82',
location: {country: '美国', region: '纽约'},
type: '数据中心',
status: 'online',
responseTime: 126,
lastCheckTime: '2024-05-10 15:30:22',
pool: '北美专用池',
isStatic: true,
},
{
id: 'ip-2',
ipAddress: '185.72.193.54',
location: {country: '德国', region: '法兰克福'},
type: '住宅',
status: 'online',
responseTime: 158,
lastCheckTime: '2024-05-10 15:28:45',
pool: '欧洲高速池',
isStatic: false,
},
{
id: 'ip-3',
ipAddress: '118.96.244.105',
location: {country: '新加坡', region: '中心区'},
type: '移动',
status: 'warning',
responseTime: 312,
lastCheckTime: '2024-05-10 15:25:12',
pool: '亚太地区池',
isStatic: false,
},
{
id: 'ip-4',
ipAddress: '45.178.29.6',
location: {country: '加拿大', region: '多伦多'},
type: '数据中心',
status: 'online',
responseTime: 143,
lastCheckTime: '2024-05-10 15:23:08',
pool: '北美专用池',
isStatic: false,
},
{
id: 'ip-5',
ipAddress: '79.114.83.201',
location: {country: '英国', region: '伦敦'},
type: '住宅',
status: 'offline',
responseTime: 0,
lastCheckTime: '2024-05-10 15:18:33',
pool: '欧洲高速池',
isStatic: false,
},
{
id: 'ip-6',
ipAddress: '164.83.219.47',
location: {country: '日本', region: '东京'},
type: '住宅',
status: 'online',
responseTime: 87,
lastCheckTime: '2024-05-10 15:15:21',
pool: '亚太地区池',
isStatic: true,
},
{
id: 'ip-7',
ipAddress: '221.67.93.143',
location: {country: '中国', region: '上海'},
type: '移动',
status: 'online',
responseTime: 104,
lastCheckTime: '2024-05-10 15:10:46',
pool: '亚太地区池',
isStatic: false,
},
{
id: 'ip-8',
ipAddress: '37.209.148.72',
location: {country: '法国', region: '巴黎'},
type: '数据中心',
status: 'warning',
responseTime: 276,
lastCheckTime: '2024-05-10 15:05:19',
pool: '欧洲高速池',
isStatic: false,
},
])
setLoading(false)
}, 800)
}, [])
// 过滤节点数据
const filteredNodes = nodes.filter(node => {
return (
(searchTerm === '' ||
node.ipAddress.includes(searchTerm) ||
node.location.country.includes(searchTerm) ||
node.pool.includes(searchTerm)) &&
(filterStatus === 'all' ||
(filterStatus === 'online' && node.status === 'online') ||
(filterStatus === 'offline' && node.status === 'offline') ||
(filterStatus === 'warning' && node.status === 'warning')) &&
(filterType === 'all' || node.type === filterType) &&
(filterPool === 'all' || node.pool === filterPool)
)
})
return (
<div className="space-y-5">
{/* 概览区域 - 使用色块和留白风格 */}
<div className="border border-gray-200 rounded-lg overflow-hidden">
{/* 标题区域 - 简洁风格 */}
<div className="bg-white px-5 py-4">
<div className="flex justify-between items-center">
<h1 className="text-lg font-bold text-gray-900"></h1>
<div className="flex gap-2">
<button
className="bg-gray-50 border border-gray-200 text-gray-700 px-3 py-1.5 rounded-md text-sm font-medium flex items-center hover:bg-gray-100">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg>
</button>
<button
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md text-sm font-medium flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</button>
</div>
</div>
<p className="mt-1 text-sm text-gray-500">IP资源</p>
</div>
{/* 统计信息区域 - 色块风格 */}
<div className="grid grid-cols-4 gap-px bg-gray-100">
{/* 总IP数量 */}
<div className="bg-white p-4">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center bg-blue-50 rounded-md">
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/>
</svg>
</div>
<div className="ml-3">
<p className="text-xs font-medium text-gray-500">IP数量</p>
<div className="text-lg font-semibold text-gray-900">152,487</div>
</div>
</div>
</div>
{/* 在线IP */}
<div className="bg-white p-4">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center bg-green-50 rounded-md">
<svg className="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="ml-3">
<div className="flex items-center">
<p className="text-xs font-medium text-gray-500">线IP</p>
<span className="ml-2 text-xs font-medium text-green-600">91%</span>
</div>
<div className="text-lg font-semibold text-gray-900">138,954</div>
</div>
</div>
</div>
{/* IP池分布 */}
<div className="bg-white p-4">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center bg-indigo-50 rounded-md">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
</div>
<div className="ml-3">
<div className="flex items-center">
<p className="text-xs font-medium text-gray-500">IP池分布</p>
<span className="ml-2 text-xs font-medium text-gray-500">5</span>
</div>
<div className="text-lg font-semibold text-gray-900">12</div>
</div>
</div>
</div>
{/* 异常IP */}
<div className="bg-white p-4">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center bg-red-50 rounded-md">
<svg className="h-5 w-5 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div className="ml-3">
<div className="flex items-center">
<p className="text-xs font-medium text-gray-500">IP</p>
<span className="ml-2 text-xs font-medium text-red-600"></span>
</div>
<div className="text-lg font-semibold text-gray-900">1,205</div>
</div>
</div>
</div>
</div>
</div>
{/* 数据展示 */}
<div className="border border-gray-200 rounded-lg overflow-hidden">
{/* 筛选搜索区域 */}
<div className="bg-white p-4 border-b border-gray-200">
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
{/* 搜索框 */}
<div className="relative md:col-span-5">
<input
type="text"
placeholder="搜索IP地址、地区或标签..."
className="w-full px-3 py-2 bg-gray-50 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
<svg
className="absolute right-3 top-2.5 h-4 w-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{/* 筛选区域 */}
<div className="flex space-x-3 md:col-span-7">
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
>
<option value="all"></option>
<option value="online">线</option>
<option value="offline">线</option>
<option value="warning"></option>
</select>
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
>
<option value="all"></option>
<option value="数据中心"></option>
<option value="住宅"></option>
<option value="移动"></option>
</select>
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
value={filterPool}
onChange={(e) => setFilterPool(e.target.value)}
>
<option value="all"></option>
<option value="北美专用池"></option>
<option value="欧洲高速池"></option>
<option value="亚太地区池"></option>
</select>
</div>
</div>
</div>
{/* IP表格区域 */}
{loading ? (
<div className="p-12 flex justify-center items-center bg-white">
<svg
className="animate-spin h-6 w-6 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
) : (
<div className="overflow-x-auto bg-white">
<table className="min-w-full">
<thead>
<tr>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
IP地址
</th>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
<th scope="col" className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredNodes.map((node, index) => (
<tr key={node.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<div className="flex items-center">
<span className={`w-2 h-2 rounded-full mr-2 ${
node.status === 'online' ? 'bg-green-500' :
node.status === 'offline' ? 'bg-red-500' : 'bg-yellow-500'
}`}></span>
<span className="font-medium text-gray-900">{node.ipAddress}</span>
{node.isStatic && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
</span>
)}
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<div className="text-gray-900">{node.location.country}</div>
<div className="text-xs text-gray-500">{node.location.region}</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
node.type === '数据中心' ? 'bg-purple-100 text-purple-800' :
node.type === '住宅' ? 'bg-blue-100 text-blue-800' : 'bg-indigo-100 text-indigo-800'
}`}>
{node.type}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
node.status === 'online' ? 'bg-green-100 text-green-800' :
node.status === 'offline' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{node.status === 'online' ? '在线' : node.status === 'offline' ? '离线' : '异常'}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{node.responseTime > 0 ? (
<div className="flex items-center">
<span className={`font-medium ${
node.responseTime < 150 ? 'text-green-600' :
node.responseTime < 250 ? 'text-yellow-600' : 'text-red-600'
}`}>
{node.responseTime} ms
</span>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{node.pool}
</td>
<td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium space-x-2">
<Link href={`/proxy/nodes/${node.id}`} className="text-blue-600 hover:text-blue-900">
</Link>
<button className="text-gray-600 hover:text-gray-900">
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* 分页控制 */}
<div className="bg-gray-50 px-5 py-3 border-t border-gray-200">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700">
<span className="font-medium">1</span> <span className="font-medium">8</span> <span
className="font-medium">152,487</span>
</div>
<div>
<nav className="relative z-0 inline-flex -space-x-px">
<button
className="relative inline-flex items-center px-2 py-1.5 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"/>
</svg>
</button>
<button
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium hover:bg-gray-50 text-blue-600 bg-blue-50 border-blue-300">
1
</button>
<button
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
2
</button>
<button
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
3
</button>
<span
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700">
...
</span>
<button
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
152
</button>
<button
className="relative inline-flex items-center px-2 py-1.5 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"/>
</svg>
</button>
</nav>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,421 @@
'use client'
import {useState, useEffect} from 'react'
import Link from 'next/link'
export type ProxyPoolsPageProps = {}
// 定义IP池接口
interface ProxyPool {
id: string;
name: string;
description: string;
ips: number;
activeIps: number;
region: string;
type: string;
createdAt: string;
status: 'active' | 'inactive' | 'maintenance';
}
export default function ProxyPoolsPage(props: ProxyPoolsPageProps) {
const [pools, setPools] = useState<ProxyPool[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [searchTerm, setSearchTerm] = useState<string>('')
const [filterStatus, setFilterStatus] = useState<string>('all')
const [filterRegion, setFilterRegion] = useState<string>('all')
// 模拟数据加载
useEffect(() => {
// 实际项目中替换为API调用
setTimeout(() => {
setPools([
{
id: 'pool-1',
name: '全球通用池',
description: '包含全球多个地区的高质量IP',
ips: 5000,
activeIps: 4328,
region: '全球',
type: '住宅IP',
createdAt: '2023-10-15',
status: 'active',
},
{
id: 'pool-2',
name: '北美专用池',
description: '美国和加拿大地区专用IP池',
ips: 3200,
activeIps: 2950,
region: '北美',
type: '数据中心IP',
createdAt: '2023-11-02',
status: 'active',
},
{
id: 'pool-3',
name: '欧洲高速池',
description: '欧洲地区高速稳定IP',
ips: 2800,
activeIps: 2180,
region: '欧洲',
type: '住宅IP',
createdAt: '2023-09-28',
status: 'active',
},
{
id: 'pool-4',
name: '亚太地区池',
description: '亚洲和太平洋地区IP',
ips: 4200,
activeIps: 3890,
region: '亚太',
type: '移动IP',
createdAt: '2023-12-05',
status: 'maintenance',
},
{
id: 'pool-5',
name: '电商专用池',
description: '适用于电商平台的IP池',
ips: 1500,
activeIps: 1200,
region: '全球',
type: '住宅IP',
createdAt: '2024-01-10',
status: 'inactive',
},
])
setLoading(false)
}, 800)
}, [])
// 过滤IP池
const filteredPools = pools.filter(pool => {
return (
(searchTerm === '' ||
pool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
pool.description.toLowerCase().includes(searchTerm.toLowerCase())) &&
(filterStatus === 'all' || pool.status === filterStatus) &&
(filterRegion === 'all' || pool.region === filterRegion)
)
})
// 获取状态颜色和文本
const getStatusInfo = (status: string) => {
switch (status) {
case 'active':
return {color: 'bg-green-100 text-green-800', text: '运行中'}
case 'inactive':
return {color: 'bg-gray-100 text-gray-800', text: '未启用'}
case 'maintenance':
return {color: 'bg-yellow-100 text-yellow-800', text: '维护中'}
default:
return {color: 'bg-gray-100 text-gray-800', text: '未知'}
}
}
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">IP池管理</h1>
<p className="mt-1 text-sm text-gray-500">IP池</p>
</div>
<button
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium flex items-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
IP池
</button>
</div>
{/* 筛选和搜索工具栏 */}
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="relative">
<input
type="text"
placeholder="搜索IP池..."
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<svg
className="absolute right-3 top-2.5 h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div className="flex space-x-4">
<div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
>
<option value="all"></option>
<option value="active"></option>
<option value="inactive"></option>
<option value="maintenance"></option>
</select>
</div>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
value={filterRegion}
onChange={(e) => setFilterRegion(e.target.value)}
>
<option value="all"></option>
<option value="全球"></option>
<option value="北美"></option>
<option value="欧洲"></option>
<option value="亚太"></option>
</select>
</div>
</div>
</div>
{/* IP池列表 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
{loading ? (
<div className="p-8 flex justify-center">
<svg
className="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
) : filteredPools.length === 0 ? (
<div className="p-8 text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p className="mt-4 text-gray-500 text-lg">IP池</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
IP池名称
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
IP概况
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
IP类型
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th
scope="col"
className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredPools.map((pool) => {
const statusInfo = getStatusInfo(pool.status)
return (
<tr key={pool.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900">{pool.name}</div>
<div className="text-sm text-gray-500 max-w-xs truncate">{pool.description}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">IP: {pool.ips.toLocaleString()}</div>
<div className="text-sm text-gray-500">IP: {pool.activeIps.toLocaleString()}</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-1.5">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{width: `${(pool.activeIps / pool.ips) * 100}%`}}
></div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{pool.region}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{pool.type}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusInfo.color}`}>
{statusInfo.text}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{pool.createdAt}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link href={`/proxy/pools/${pool.id}`} className="text-blue-600 hover:text-blue-900 mr-4">
</Link>
<Link
href={`/proxy/pools/${pool.id}/edit`} className="text-indigo-600 hover:text-indigo-900 mr-4">
</Link>
<button className="text-red-600 hover:text-red-900">
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
{/* 数据卡片概览 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="flex items-center">
<div className="p-3 rounded-full bg-blue-100 text-blue-600">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
</svg>
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">IP池</h3>
<div className="mt-1 text-3xl font-semibold">{pools.length}</div>
</div>
</div>
<div className="mt-4">
<div className="flex justify-between text-sm">
<span className="text-gray-500">IP池</span>
<span className="font-medium text-gray-900">{pools.filter(p => p.status === 'active').length}</span>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="flex items-center">
<div className="p-3 rounded-full bg-green-100 text-green-600">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">IP数量</h3>
<div className="mt-1 text-3xl font-semibold">
{pools.reduce((sum, pool) => sum + pool.ips, 0).toLocaleString()}
</div>
</div>
</div>
<div className="mt-4">
<div className="flex justify-between text-sm">
<span className="text-gray-500">IP</span>
<span className="font-medium text-gray-900">
{pools.reduce((sum, pool) => sum + pool.activeIps, 0).toLocaleString()}
</span>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="flex items-center">
<div className="p-3 rounded-full bg-purple-100 text-purple-600">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">IP利用率</h3>
<div className="mt-1 text-3xl font-semibold">
{Math.round((pools.reduce((sum, pool) => sum + pool.activeIps, 0) /
pools.reduce((sum, pool) => sum + pool.ips, 0)) * 100)}%
</div>
</div>
</div>
<div className="mt-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full"
style={{
width: `${(pools.reduce((sum, pool) => sum + pool.activeIps, 0) /
pools.reduce((sum, pool) => sum + pool.ips, 0)) * 100}%`,
}}
></div>
</div>
</div>
</div>
</div>
{/* 操作指南 */}
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700">
IP池是管理IP资源的基础单位IP池访
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<a href="/help/proxy-pools" className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600">
<span aria-hidden="true">&rarr;</span>
</a>
</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import {ReactNode} from 'react'
export type ProxySourcesPageProps = {
}
export default function ProxySourcesPage(props: ProxySourcesPageProps) {
return (
)
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
src/app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

16
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout(props: { children: ReactNode }) {
return (
<html lang="zh-CN">
<body>{props.children}</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
export const BASE_URL = process.env.API_BASE_URL
export const CLIENT_ID = process.env.CLIENT_ID
export const CLIENT_SECRET = process.env.CLIENT_SECRET

85
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,85 @@
export type LoginPageProps = {};
export default function LoginPage(props: LoginPageProps) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center p-4">
{/* 登录卡片 */}
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-8">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-800"></h1>
<p className="text-sm text-gray-500 mt-2">访</p>
</div>
<form className="space-y-6">
<div>
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700"
>
</label>
<input
type="text"
id="username"
className="mt-1 block w-full px-4 py-3 border border-gray-200 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="请输入用户名"
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
</label>
<input
type="password"
id="password"
className="mt-1 block w-full px-4 py-3 border border-gray-200 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="请输入密码"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-700"
>
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-blue-600 hover:text-blue-500"
>
</a>
</div>
</div>
<div>
<button
type="submit"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
</button>
</div>
</form>
<div className="mt-6 text-center text-xs text-gray-500">
© {new Date().getFullYear()} - 访
</div>
</div>
</div>
);
}

33
tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next",
},
],
"paths": {
"@/*": ["./src/*"],
},
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
],
"exclude": ["node_modules"],
}