Compare commits
29 Commits
72ea29f435
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da9928de3 | ||
|
|
8985df7625 | ||
|
|
1b3110de87 | ||
| 45267c6391 | |||
| 369799ae4e | |||
| fa7be5f11b | |||
|
|
82f69e2964 | ||
| 6a0ae418ce | |||
| 9df17211a5 | |||
| 80310f710c | |||
| 5617502713 | |||
|
|
15d5dc93c0 | ||
|
|
9fff300a22 | ||
| 8c05d0b332 | |||
|
|
3c38945750 | ||
|
|
8e0c9284a0 | ||
|
|
5d3f1daadf | ||
|
|
b590d5b8c1 | ||
| d798eab0a9 | |||
| 6bbaf9f904 | |||
|
|
010990ea3c | ||
|
|
10395a49c1 | ||
| 6e9e7af780 | |||
|
|
4940e3a9d2 | ||
|
|
e95f9d33f3 | ||
|
|
e5b6099d03 | ||
|
|
969d49ab50 | ||
| e36cfbca83 | |||
| 8239c9fb37 |
11
.env.example
11
.env.example
@@ -1,8 +1,15 @@
|
|||||||
# 数据库连接字符串
|
# 数据库连接字符串
|
||||||
DATABASE_URL=
|
DATABASE_HOST=localhost
|
||||||
|
DATABASE_PORT=23306
|
||||||
|
DATABASE_USERNAME=root
|
||||||
|
DATABASE_PASSWORD=root
|
||||||
|
DATABASE_NAME=app
|
||||||
|
|
||||||
# Redis 连接字符串
|
# Redis 连接字符串
|
||||||
REDIS_URL=
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=26379
|
||||||
|
REDIS_USERNAME=
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
# 京东网关配置
|
# 京东网关配置
|
||||||
JD_BASE=https://smart.jdbox.xyz:58001
|
JD_BASE=https://smart.jdbox.xyz:58001
|
||||||
|
|||||||
44
.gitignore
vendored
44
.gitignore
vendored
@@ -1,7 +1,39 @@
|
|||||||
node_modules
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
src/generated/
|
|
||||||
.next
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
.env
|
.env
|
||||||
deploy.sh
|
|
||||||
.volumes
|
# vercel
|
||||||
.vscode
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# dev
|
||||||
|
.volumes/
|
||||||
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"sqltools.connections": [
|
||||||
|
{
|
||||||
|
"mysqlOptions": {
|
||||||
|
"authProtocol": "default",
|
||||||
|
"enableSsl": "Disabled"
|
||||||
|
},
|
||||||
|
"ssh": "Disabled",
|
||||||
|
"previewLimit": 50,
|
||||||
|
"server": "localhost",
|
||||||
|
"port": 23306,
|
||||||
|
"driver": "MariaDB",
|
||||||
|
"name": "localhost",
|
||||||
|
"database": "app",
|
||||||
|
"username": "root"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
README.md
39
README.md
@@ -1,36 +1,11 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
极狐配置监控项目
|
||||||
|
|
||||||
## Getting Started
|
## 部署方式
|
||||||
|
|
||||||
First, run the development server:
|
见 [tapd文档](https://www.tapd.cn/67163502/documents/show/1167163502001000029) 极狐配置监控部分
|
||||||
|
|
||||||
```bash
|
## 使用方式
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
1. 拉取本项目
|
||||||
|
2. 创建环境变量文件 `.env`,复制 `.env.example` 中的内容到 `.env`,并根据实际情况修改
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
3. 运行 `docker compose up -d`
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
|
|||||||
96
bun.lock
96
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-app",
|
"name": "my-app",
|
||||||
@@ -10,13 +11,14 @@
|
|||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"lucide-react": "^0.541.0",
|
"lucide-react": "^0.541.0",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"next": "15.4.7",
|
"next": "15.4.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@@ -53,7 +55,7 @@
|
|||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
||||||
|
|
||||||
@@ -149,49 +151,55 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="],
|
"@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-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="],
|
"@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-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="],
|
"@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-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="],
|
"@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-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="],
|
"@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-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="],
|
"@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-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="],
|
"@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-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="],
|
"@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-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="],
|
"@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-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="],
|
"@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-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="],
|
"@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-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="],
|
"@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-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="],
|
"@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-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="],
|
"@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-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="],
|
"@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-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="],
|
"@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-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="],
|
"@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-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="],
|
"@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-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="],
|
"@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-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="],
|
"@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-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="],
|
"@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-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="],
|
"@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=="],
|
||||||
|
|
||||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|
||||||
@@ -207,25 +215,25 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@15.4.7", "https://registry.npmmirror.com/@next/env/-/env-15.4.7.tgz", {}, "sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg=="],
|
"@next/env": ["@next/env@15.4.10", "https://registry.npmmirror.com/@next/env/-/env-15.4.10.tgz", {}, "sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.0", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+k83U/fST66eQBjTltX2T9qUYd43ntAe+NZ5qeZVTQyTiFiHvTLtkpLKug4AnZAtuI/lwz5tl/4QDJymjVkybg=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.0", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+k83U/fST66eQBjTltX2T9qUYd43ntAe+NZ5qeZVTQyTiFiHvTLtkpLKug4AnZAtuI/lwz5tl/4QDJymjVkybg=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.7", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.7.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.8", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.8.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A=="],
|
||||||
|
|
||||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.7", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.7.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg=="],
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.8", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.8.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.7", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw=="],
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.8", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.8.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.7", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A=="],
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.8", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.8.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.7", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg=="],
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.8", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.8.tgz", { "os": "linux", "cpu": "x64" }, "sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.7", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg=="],
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.8", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.8.tgz", { "os": "linux", "cpu": "x64" }, "sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA=="],
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.7", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.7.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw=="],
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.8", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.8.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw=="],
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.7", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.7.tgz", { "os": "win32", "cpu": "x64" }, "sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.8", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.8.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
@@ -363,6 +371,10 @@
|
|||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="],
|
||||||
|
|
||||||
|
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "https://registry.npmmirror.com/@tanstack/react-table/-/react-table-8.21.3.tgz", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||||
|
|
||||||
|
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "https://registry.npmmirror.com/@tanstack/table-core/-/table-core-8.21.3.tgz", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
@@ -515,14 +527,10 @@
|
|||||||
|
|
||||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
||||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
@@ -723,8 +731,6 @@
|
|||||||
|
|
||||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||||
|
|
||||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
|
||||||
|
|
||||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||||
|
|
||||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||||
@@ -873,7 +879,7 @@
|
|||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"next": ["next@15.4.7", "https://registry.npmmirror.com/next/-/next-15.4.7.tgz", { "dependencies": { "@next/env": "15.4.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.7", "@next/swc-darwin-x64": "15.4.7", "@next/swc-linux-arm64-gnu": "15.4.7", "@next/swc-linux-arm64-musl": "15.4.7", "@next/swc-linux-x64-gnu": "15.4.7", "@next/swc-linux-x64-musl": "15.4.7", "@next/swc-win32-arm64-msvc": "15.4.7", "@next/swc-win32-x64-msvc": "15.4.7", "sharp": "^0.34.3" }, "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-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg=="],
|
"next": ["next@15.4.10", "https://registry.npmmirror.com/next/-/next-15.4.10.tgz", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "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-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="],
|
||||||
|
|
||||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
@@ -995,7 +1001,7 @@
|
|||||||
|
|
||||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="],
|
"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=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
@@ -1009,8 +1015,6 @@
|
|||||||
|
|
||||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
|
||||||
|
|
||||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
@@ -1121,6 +1125,8 @@
|
|||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||||
@@ -1175,7 +1181,9 @@
|
|||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"sharp/detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"sharp/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
name: jihu-monitor
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
@@ -6,13 +8,13 @@ services:
|
|||||||
MYSQL_ROOT_PASSWORD: root
|
MYSQL_ROOT_PASSWORD: root
|
||||||
MYSQL_DATABASE: app
|
MYSQL_DATABASE: app
|
||||||
ports:
|
ports:
|
||||||
- "23306:3306"
|
- "${DATABASE_PORT}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- .volumes/mysql:/var/lib/mysql
|
- .volumes/mysql:/var/lib/mysql
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:7
|
||||||
ports:
|
ports:
|
||||||
- "26379:6379"
|
- "${REDIS_PORT}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- .volumes/redis:/data
|
- .volumes/redis:/data
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-app",
|
"name": "my-app",
|
||||||
"version": "0.3.0",
|
"version": "0.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
@@ -15,13 +15,14 @@
|
|||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"lucide-react": "^0.541.0",
|
"lucide-react": "^0.541.0",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"next": "15.4.7",
|
"next": "15.4.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@@ -48,5 +49,6 @@
|
|||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
}
|
},
|
||||||
|
"packageManager": "bun@1.3.2"
|
||||||
}
|
}
|
||||||
16
publish.ps1
Normal file
16
publish.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
if (-not $args) {
|
||||||
|
Write-Error "需要指定版本号"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$confrim = Read-Host "构建版本为 [jh-monitor:$($args[0])],是否继续?(y/n)"
|
||||||
|
if ($confrim -ne "y") {
|
||||||
|
Write-Host "已取消构建"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docker build -t 43.226.58.254:53000/wmp/jh-monitor:latest .
|
||||||
|
docker build -t 43.226.58.254:53000/wmp/jh-monitor:$($args[0]) .
|
||||||
|
|
||||||
|
docker push 43.226.58.254:53000/wmp/jh-monitor:latest
|
||||||
|
docker push 43.226.58.254:53000/wmp/jh-monitor:$($args[0])
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import { Page, Res } from '@/lib/api'
|
import { Page, Res } from '@/lib/api'
|
||||||
import drizzle, { change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
|
import drizzle, { and, change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
|
||||||
|
import { cache } from 'react'
|
||||||
|
|
||||||
export type AllocationStatus = {
|
export type AllocationStatus = {
|
||||||
city: string
|
city: string
|
||||||
@@ -62,7 +63,7 @@ export async function getAllocationStatus(hours: number = 24) {
|
|||||||
export type GatewayInfo = {
|
export type GatewayInfo = {
|
||||||
macaddr: string
|
macaddr: string
|
||||||
inner_ip: string
|
inner_ip: string
|
||||||
setid: string
|
setid: number
|
||||||
enable: number
|
enable: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ export async function getGatewayInfo() {
|
|||||||
enable: token.enable,
|
enable: token.enable,
|
||||||
})
|
})
|
||||||
.from(token)
|
.from(token)
|
||||||
.orderBy(token.macaddr)
|
.orderBy(sql`cast(regexp_replace(token.inner_ip, '192.168.50.', '') as unsigned)`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -105,15 +106,37 @@ export type GatewayConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 网关配置
|
// 网关配置
|
||||||
export async function getGatewayConfig(page?: number, mac?: string): Promise<Res<Page<GatewayConfig>>> {
|
export const getGatewayConfig = cache(async (page?: number, filters?: {
|
||||||
|
mac?: string
|
||||||
|
public?: string
|
||||||
|
city?: string
|
||||||
|
user?: string
|
||||||
|
inner_ip?: string
|
||||||
|
}): Promise<Res<Page<GatewayConfig>>> => {
|
||||||
try {
|
try {
|
||||||
if (!page && !mac) {
|
if (!page && !filters?.mac) {
|
||||||
throw new Error('页码和MAC地址不能同时为空')
|
throw new Error('页码和MAC地址不能同时为空')
|
||||||
}
|
}
|
||||||
|
|
||||||
page = mac ? 1 : Math.max(1, page || 1)
|
page = filters?.mac ? 1 : Math.max(1, page || 1)
|
||||||
|
|
||||||
|
const condition = filters ? and(
|
||||||
|
filters.mac ? eq(gateway.macaddr, filters.mac) : undefined,
|
||||||
|
filters.public ? eq(edge.public, filters.public) : undefined,
|
||||||
|
filters.city ? eq(cityhash.city, filters.city) : undefined,
|
||||||
|
filters.user ? eq(gateway.user, filters.user) : undefined,
|
||||||
|
filters.inner_ip ? eq(gateway.network, filters.inner_ip) : undefined,
|
||||||
|
) : undefined
|
||||||
|
|
||||||
const [total, result] = await Promise.all([
|
const [total, result] = await Promise.all([
|
||||||
drizzle.$count(gateway, mac ? eq(gateway.macaddr, mac) : undefined),
|
drizzle
|
||||||
|
.select({
|
||||||
|
value: count(),
|
||||||
|
})
|
||||||
|
.from(gateway)
|
||||||
|
.leftJoin(cityhash, eq(cityhash.hash, gateway.cityhash))
|
||||||
|
.leftJoin(edge, eq(edge.macaddr, gateway.edge))
|
||||||
|
.where(condition),
|
||||||
drizzle
|
drizzle
|
||||||
.select({
|
.select({
|
||||||
city: cityhash.city,
|
city: cityhash.city,
|
||||||
@@ -127,16 +150,15 @@ export async function getGatewayConfig(page?: number, mac?: string): Promise<Res
|
|||||||
.from(gateway)
|
.from(gateway)
|
||||||
.leftJoin(cityhash, eq(cityhash.hash, gateway.cityhash))
|
.leftJoin(cityhash, eq(cityhash.hash, gateway.cityhash))
|
||||||
.leftJoin(edge, eq(edge.macaddr, gateway.edge))
|
.leftJoin(edge, eq(edge.macaddr, gateway.edge))
|
||||||
.where(mac ? eq(gateway.macaddr, mac) : undefined)
|
.where(condition)
|
||||||
.orderBy(gateway.macaddr, sql`cast(regexp_replace(gateway.network, '172.30.168.', '') as unsigned)`)
|
.orderBy(sql`inet_aton(gateway.inner_ip)`)
|
||||||
.offset((page - 1) * 250)
|
.offset((page - 1) * 250)
|
||||||
.limit(250),
|
.limit(250),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
total,
|
total: total[0].value,
|
||||||
page,
|
page,
|
||||||
size: 250,
|
size: 250,
|
||||||
items: result,
|
items: result,
|
||||||
@@ -150,14 +172,14 @@ export async function getGatewayConfig(page?: number, mac?: string): Promise<Res
|
|||||||
error: '查询网关配置失败',
|
error: '查询网关配置失败',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
export type CityNode = {
|
export type CityNode = {
|
||||||
city: string
|
city: string
|
||||||
count: number
|
count: number
|
||||||
hash: string
|
hash: string
|
||||||
label: string
|
label: string | null
|
||||||
offset: string
|
offset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 城市节点数量分布
|
// 城市节点数量分布
|
||||||
@@ -201,14 +223,62 @@ export async function getCityNodeCount() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取节点信息
|
export type Edge = {
|
||||||
export async function getEdgeNodes(page: number, size: number) {
|
id: number
|
||||||
try {
|
macaddr: string
|
||||||
const offset = Math.max(0, (page - 1)) * size
|
city: string | null
|
||||||
const limit = Math.min(100, Math.max(10, size))
|
public: string
|
||||||
|
isp: string
|
||||||
|
single: number | boolean
|
||||||
|
sole: number | boolean
|
||||||
|
arch: number
|
||||||
|
online: number
|
||||||
|
}
|
||||||
|
|
||||||
const [total, data] = await Promise.all([
|
// 获取节点信息
|
||||||
drizzle.$count(edge, eq(edge.active, 1)),
|
export async function getEdgeNodes(page: number, size: number, filters?: {
|
||||||
|
macaddr?: string
|
||||||
|
public?: string
|
||||||
|
city?: string
|
||||||
|
isp?: string
|
||||||
|
}): Promise<Res<Page<Edge>>> {
|
||||||
|
try {
|
||||||
|
page = Math.max(1, page)
|
||||||
|
size = Math.min(100, Math.max(10, size))
|
||||||
|
|
||||||
|
const condition = and(
|
||||||
|
eq(edge.active, 1),
|
||||||
|
filters?.macaddr ? eq(edge.macaddr, filters.macaddr) : undefined,
|
||||||
|
filters?.public ? eq(edge.public, filters.public) : undefined,
|
||||||
|
filters?.city ? eq(cityhash.city, filters.city) : undefined,
|
||||||
|
filters?.isp ? eq(edge.isp, filters.isp) : undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(drizzle
|
||||||
|
.select({
|
||||||
|
id: edge.id,
|
||||||
|
macaddr: edge.macaddr,
|
||||||
|
city: cityhash.city,
|
||||||
|
public: edge.public,
|
||||||
|
isp: edge.isp,
|
||||||
|
single: edge.single,
|
||||||
|
sole: edge.sole,
|
||||||
|
arch: edge.arch,
|
||||||
|
online: edge.online,
|
||||||
|
})
|
||||||
|
.from(edge)
|
||||||
|
.leftJoin(cityhash, eq(cityhash.id, edge.cityId))
|
||||||
|
.where(condition)
|
||||||
|
.orderBy(edge.id)
|
||||||
|
.offset(page * size - size)
|
||||||
|
.limit(size).toSQL().sql)
|
||||||
|
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
drizzle
|
||||||
|
.select({ value: count() })
|
||||||
|
.from(edge)
|
||||||
|
.leftJoin(cityhash, eq(cityhash.id, edge.cityId))
|
||||||
|
.where(condition),
|
||||||
drizzle
|
drizzle
|
||||||
.select({
|
.select({
|
||||||
id: edge.id,
|
id: edge.id,
|
||||||
@@ -223,25 +293,26 @@ export async function getEdgeNodes(page: number, size: number) {
|
|||||||
})
|
})
|
||||||
.from(edge)
|
.from(edge)
|
||||||
.leftJoin(cityhash, eq(cityhash.id, edge.cityId))
|
.leftJoin(cityhash, eq(cityhash.id, edge.cityId))
|
||||||
.where(eq(edge.active, 1))
|
.where(condition)
|
||||||
.orderBy(edge.id)
|
.orderBy(edge.id)
|
||||||
.offset(offset)
|
.offset(page * size - size)
|
||||||
.limit(limit),
|
.limit(size),
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
success: true,
|
||||||
totalCount: total,
|
data: {
|
||||||
currentPage: Math.floor(offset / limit) + 1,
|
total: total[0].value,
|
||||||
totalPages: Math.ceil(total / limit),
|
page,
|
||||||
|
size,
|
||||||
|
items,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Edge nodes query error:', error)
|
console.error('Edge nodes query error:', error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
data: [],
|
|
||||||
error: '查询边缘节点失败',
|
error: '查询边缘节点失败',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Lock, User } from 'lucide-react'
|
import { LockIcon, UserIcon } from 'lucide-react'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { toast, Toaster } from 'sonner'
|
import { toast, Toaster } from 'sonner'
|
||||||
import { login } from '@/actions/auth'
|
import { login } from '@/actions/auth'
|
||||||
@@ -41,7 +41,7 @@ export default function LoginPage() {
|
|||||||
})
|
})
|
||||||
setAuth(true)
|
setAuth(true)
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
router.push('/dashboard')
|
router.push('/gatewayinfo')
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -75,7 +75,7 @@ export default function LoginPage() {
|
|||||||
<FormLabel>账号</FormLabel>
|
<FormLabel>账号</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<UserIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入您的账号"
|
placeholder="请输入您的账号"
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
@@ -95,7 +95,7 @@ export default function LoginPage() {
|
|||||||
<FormLabel>密码</FormLabel>
|
<FormLabel>密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<LockIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input type="password" placeholder="请输入密码" className="pl-8" {...field} />
|
<Input type="password" placeholder="请输入密码" className="pl-8" {...field} />
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
147
src/app/(root)/allocationStatus/page.tsx
Normal file
147
src/app/(root)/allocationStatus/page.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
|
import LoadingCard from '@/components/ui/loadingCard'
|
||||||
|
import ErrorCard from '@/components/ui/errorCard'
|
||||||
|
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Form, FormField } from '@/components/ui/form'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
import { DataTable } from '@/components/data-table'
|
||||||
|
|
||||||
|
const filterSchema = z.object({
|
||||||
|
timeFilter: z.string(),
|
||||||
|
})
|
||||||
|
type FilterSchema = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
|
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
|
||||||
|
const [data, setData] = useState<AllocationStatus[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const form = useForm<FilterSchema>({
|
||||||
|
resolver: zodResolver(filterSchema),
|
||||||
|
defaultValues: {
|
||||||
|
timeFilter: '24',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const timeFilter = form.watch('timeFilter')
|
||||||
|
|
||||||
|
const getTimeHours = useCallback(() => {
|
||||||
|
return parseInt(timeFilter) || 24
|
||||||
|
}, [timeFilter])
|
||||||
|
|
||||||
|
const newData = data.map(item => ({
|
||||||
|
...item,
|
||||||
|
overage: Math.max(0, Number(item.assigned) - Number(item.count)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setError(null)
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const hours = getTimeHours()
|
||||||
|
const result = await getAllocationStatus(hours)
|
||||||
|
|
||||||
|
const validatedData = result.data.map(item => ({
|
||||||
|
city: item.city || '未知',
|
||||||
|
count: item.count,
|
||||||
|
assigned: item.assigned,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setData(validatedData)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to fetch allocation status:', error)
|
||||||
|
setError(error instanceof Error ? error.message : 'Unknown error')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [getTimeHours])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData()
|
||||||
|
}, [fetchData])
|
||||||
|
|
||||||
|
const onSubmit = (data: FilterSchema) => {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <LoadingCard title="节点分配状态" />
|
||||||
|
if (error) return <ErrorCard title="节点分配状态" error={error} onRetry={fetchData} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<h2 className="flex-none text-lg font-semibold mb-4">节点分配状态</h2>
|
||||||
|
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex items-center gap-4">
|
||||||
|
<FormField
|
||||||
|
name="timeFilter"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-sm mr-2">时间筛选:</span>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger className="h-9 w-36">
|
||||||
|
<SelectValue placeholder="选择时间范围" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">最近1小时</SelectItem>
|
||||||
|
<SelectItem value="4">最近4小时</SelectItem>
|
||||||
|
<SelectItem value="12">最近12小时</SelectItem>
|
||||||
|
<SelectItem value="24">最近24小时</SelectItem>
|
||||||
|
<SelectItem value="168">最近7天</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="py-2 bg-blue-600 hover:bg-blue-700">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
data={newData}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: '城市',
|
||||||
|
props: 'city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '可用IP量',
|
||||||
|
props: 'count',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '分配IP量',
|
||||||
|
props: 'assigned',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '超额量',
|
||||||
|
props: 'overage',
|
||||||
|
sortable: true,
|
||||||
|
render: (val) => {
|
||||||
|
const overage = val.overage as number
|
||||||
|
return (
|
||||||
|
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||||
|
{overage}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
src/app/(root)/cityNodeStats/page.tsx
Normal file
77
src/app/(root)/cityNodeStats/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getCityNodeCount, type CityNode } from '@/actions/stats'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
import { DataTable } from '@/components/data-table'
|
||||||
|
|
||||||
|
export default function CityNodeStats() {
|
||||||
|
const [data, setData] = useState<CityNode[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getCityNodeCount()
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '查询城市节点失败')
|
||||||
|
}
|
||||||
|
setData(result.data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取城市节点数据失败:', error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 w-full overflow-hidden">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">城市节点数量分布</h2>
|
||||||
|
<div className="text-gray-600">加载中...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">城市节点数量分布</h2>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
共 {data.length} 个城市
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
data={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: '城市',
|
||||||
|
props: 'city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '节点数量',
|
||||||
|
props: 'count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hash',
|
||||||
|
props: 'hash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '标签',
|
||||||
|
props: 'label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '轮换顺位',
|
||||||
|
props: 'offset',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
263
src/app/(root)/edge/page.tsx
Normal file
263
src/app/(root)/edge/page.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
import { getEdgeNodes, type Edge } from '@/actions/stats'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
import { DataTable } from '@/components/data-table'
|
||||||
|
|
||||||
|
// 定义表单验证规则
|
||||||
|
const filterSchema = z.object({
|
||||||
|
macaddr: z.string(),
|
||||||
|
public: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
isp: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FilterFormValues = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
|
export default function Edge() {
|
||||||
|
const [data, setData] = useState<Edge[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [size, setSize] = useState(100)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
const form = useForm<FilterFormValues>({
|
||||||
|
resolver: zodResolver(filterSchema),
|
||||||
|
defaultValues: {
|
||||||
|
macaddr: '',
|
||||||
|
public: '',
|
||||||
|
city: '',
|
||||||
|
isp: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async (page: number, size: number) => {
|
||||||
|
const filters = form.getValues()
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await getEdgeNodes(page, size, filters)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error)
|
||||||
|
}
|
||||||
|
const data = result.data
|
||||||
|
console.log(data)
|
||||||
|
setData(data.items)
|
||||||
|
setTotal(data.total)
|
||||||
|
setPage(data.page)
|
||||||
|
setSize(data.size)
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
setError('获取边缘节点数据失败' + (error instanceof Error ? `: ${error.message}` : ''))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
fetchData(page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setPage(page)
|
||||||
|
fetchData(page, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每页显示数量变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
setPage(1)
|
||||||
|
setSize(size)
|
||||||
|
fetchData(1, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(page, size)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) return (
|
||||||
|
<div className="bg-white w-full shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
||||||
|
<div className="text-center py-8">加载节点数据中...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) return (
|
||||||
|
<div className="bg-white w-full shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
||||||
|
<div className="text-center py-8 text-red-600">{error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchData(page, size)}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mx-auto block"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page className="gap-3">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="macaddr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>MAC地址</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入MAC地址" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="public"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>公网IP</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入公网IP" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="city"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>城市</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入城市名称" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="isp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>运营商</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入运营商" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="mt-5 py-2 bg-blue-600 hover:bg-blue-700">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" className="mt-5 py-2" onClick={() => form.reset()}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<DataTable
|
||||||
|
data={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: 'MAC地址',
|
||||||
|
props: 'macaddr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '城市',
|
||||||
|
props: 'city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '公网IP',
|
||||||
|
props: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '运营商',
|
||||||
|
render: (val) => {
|
||||||
|
const isp = val.isp as string
|
||||||
|
return (
|
||||||
|
<span className={cn('px-2 py-1 rounded-full text-xs', 'bg-gray-100 text-gray-800',
|
||||||
|
{ 移动: 'bg-blue-100 text-blue-800',
|
||||||
|
电信: 'bg-purple-100 text-purple-800',
|
||||||
|
联通: 'bg-red-100 text-red-800',
|
||||||
|
}[isp])}>
|
||||||
|
{isp}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '多IP节点',
|
||||||
|
render: (val) => {
|
||||||
|
const single = val.single as number
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
single === 1 ? 'bg-red-100 text-red-800' : single === 0 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
||||||
|
{single === 1 ? '是' : single === 0 ? '否' : '未知'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '独享IP',
|
||||||
|
render: (val) => {
|
||||||
|
const sole = val.sole as number
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
sole === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
||||||
|
{sole === 1 ? '是' : '否'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '设备类型',
|
||||||
|
render: (val) => {
|
||||||
|
const arch = val.arch as number
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
arch === 0 ? 'bg-blue-100 text-blue-800'
|
||||||
|
: arch === 1 ? 'bg-green-100 text-green-800'
|
||||||
|
: arch === 2 ? 'bg-purple-100 text-purple-800'
|
||||||
|
: arch === 3 ? 'bg-orange-100 text-orange-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'}`}>
|
||||||
|
{arch === 0 ? '一代' : arch === 1 ? '二代' : arch === 2 ? 'AMD64' : arch === 3 ? 'x86' : `未知 (${arch})`}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '在线时长',
|
||||||
|
render: (val) => {
|
||||||
|
const seconds = val.online as number
|
||||||
|
return seconds < 60 ? `${seconds}秒`
|
||||||
|
: seconds < 3600 ? `${Math.floor(seconds / 60)}分钟`
|
||||||
|
: seconds < 86400 ? `${Math.floor(seconds / 3600)}小时`
|
||||||
|
: `${Math.floor(seconds / 86400)}天`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
size={size}
|
||||||
|
total={total}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
361
src/app/(root)/gatewayConfig/page.tsx
Normal file
361
src/app/(root)/gatewayConfig/page.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState, Suspense, useCallback } from 'react'
|
||||||
|
import { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats'
|
||||||
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Form, FormField, FormItem, FormLabel } from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
import { DataTable } from '@/components/data-table'
|
||||||
|
|
||||||
|
function GatewayConfigContent() {
|
||||||
|
const [data, setData] = useState<GatewayConfig[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [infoData, setInfoData] = useState<GatewayInfo[]>([])
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
// 定义表单验证规则
|
||||||
|
const filterSchema = z.object({
|
||||||
|
macaddr: z.string().optional(),
|
||||||
|
public: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
inner_ip: z.string().optional(),
|
||||||
|
user: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FilterFormValues = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
const form = useForm<FilterFormValues>({
|
||||||
|
resolver: zodResolver(filterSchema),
|
||||||
|
defaultValues: {
|
||||||
|
macaddr: '',
|
||||||
|
public: '',
|
||||||
|
city: '',
|
||||||
|
inner_ip: '',
|
||||||
|
user: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化调用
|
||||||
|
useEffect(() => {
|
||||||
|
const initData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
// 获取网关基本信息
|
||||||
|
const infoResult = await getGatewayInfo()
|
||||||
|
if (!infoResult.success) {
|
||||||
|
throw new Error(infoResult.error || '查询网关信息失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
setInfoData(infoResult.data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error((error as Error).message || '获取数据失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initData()
|
||||||
|
fetchData({}, 1)
|
||||||
|
}, [])
|
||||||
|
const { handleSubmit: formHandleSubmit } = form
|
||||||
|
// 网关配置数据查询函数(用于表单查询)
|
||||||
|
const fetchData = async (filters: {
|
||||||
|
mac?: string
|
||||||
|
public?: string
|
||||||
|
city?: string
|
||||||
|
user?: string
|
||||||
|
inner_ip?: string
|
||||||
|
}, page: number = 1) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await getGatewayConfig(page, filters)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '查询网关配置失败')
|
||||||
|
}
|
||||||
|
const shrink = ['黔东南', '延边']
|
||||||
|
result.data.items.forEach((item) => {
|
||||||
|
shrink.forEach((s) => {
|
||||||
|
if (item.city?.startsWith(s)) {
|
||||||
|
item.city = s
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setData(result.data.items)
|
||||||
|
setTotal(result.data.total)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error((error as Error).message || '获取网关配置失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (data: FilterFormValues) => {
|
||||||
|
setPage(1)
|
||||||
|
const filters = {
|
||||||
|
mac: data.macaddr || '',
|
||||||
|
public: data.public || '',
|
||||||
|
city: data.city || '',
|
||||||
|
user: data.user || '',
|
||||||
|
inner_ip: data.inner_ip || '',
|
||||||
|
}
|
||||||
|
fetchData(filters, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setPage(page)
|
||||||
|
const formValues = form.getValues()
|
||||||
|
const filters = {
|
||||||
|
mac: formValues.macaddr || undefined,
|
||||||
|
public: formValues.public || undefined,
|
||||||
|
city: formValues.city || undefined,
|
||||||
|
user: formValues.user || undefined,
|
||||||
|
inner_ip: formValues.inner_ip || undefined,
|
||||||
|
}
|
||||||
|
fetchData(filters, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每页显示数量变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
setPage(1)
|
||||||
|
const formValues = form.getValues()
|
||||||
|
const filters = {
|
||||||
|
mac: formValues.macaddr || undefined,
|
||||||
|
public: formValues.public || undefined,
|
||||||
|
city: formValues.city || undefined,
|
||||||
|
user: formValues.user || undefined,
|
||||||
|
inner_ip: formValues.inner_ip || undefined,
|
||||||
|
}
|
||||||
|
fetchData(filters, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前选中的mac
|
||||||
|
const [selectedMac, setSelectedMac] = useState<string>('')
|
||||||
|
const handleMacClick = useCallback(async (macaddr: string) => {
|
||||||
|
setSelectedMac(macaddr)
|
||||||
|
await fetchData({ mac: macaddr }, 1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page className="flex flex-row gap-6">
|
||||||
|
{/* 查询表单 */}
|
||||||
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={formHandleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
name="macaddr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1 relative">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索MAC地址..."
|
||||||
|
{...field}
|
||||||
|
className="w-full pr-10"
|
||||||
|
/>
|
||||||
|
<SearchIcon
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(form.getValues())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm font-medium">在线 {infoData.filter(item => item.enable === 1).length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm font-medium">离线 {infoData.filter(item => item.enable === 0).length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-auto border-t flex flex-col">
|
||||||
|
{infoData.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn('p-4 flex-col ', index !== infoData.length - 1 ? 'border-b' : '')}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div
|
||||||
|
className={cn('font-medium cursor-pointer',
|
||||||
|
selectedMac === item.macaddr ? 'text-blue-700' : 'text-gray-900')}
|
||||||
|
onClick={() => handleMacClick(item.macaddr)}
|
||||||
|
>
|
||||||
|
{item.macaddr}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full mr-2',
|
||||||
|
item.enable === 1 ? 'bg-green-500' : 'bg-red-500',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className={cn(
|
||||||
|
'text-xs font-medium',
|
||||||
|
item.enable === 1 ? 'text-green-700' : 'text-red-700',
|
||||||
|
)}>
|
||||||
|
{item.enable === 1 ? '在线' : '离线'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 mb-3">
|
||||||
|
{item.inner_ip || '未配置IP'}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 space-y-1 text-xs text-gray-500">
|
||||||
|
<div>配置版本: {item.setid || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-3 overflow-hidden flex flex-col gap-3">
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="flex gap-4" onSubmit={formHandleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
name="public"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>IP地址</FormLabel>
|
||||||
|
<Input placeholder="输入IP地址" {...field} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="user"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>线路</FormLabel>
|
||||||
|
<Input placeholder="输入线路" {...field} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="inner_ip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>端口号</FormLabel>
|
||||||
|
<Input placeholder="输入端口" {...field} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="city"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>城市</FormLabel>
|
||||||
|
<Input placeholder="输入城市" {...field} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="ml-2 mt-6 px-4 py-2 bg-blue-600 hover:bg-blue-700">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" className="ml-2 mt-6 px-4 py-2" onClick={() => form.reset()}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<DataTable
|
||||||
|
data={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: '端口',
|
||||||
|
props: 'inner_ip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '线路',
|
||||||
|
props: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '城市',
|
||||||
|
props: 'city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '节点MAC',
|
||||||
|
props: 'edge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '节点IP',
|
||||||
|
props: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '配置更新',
|
||||||
|
render: (val) => {
|
||||||
|
const ischange = val.ischange as number
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
ischange === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{ischange === 0 ? '正常' : '更新'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '在用状态',
|
||||||
|
render: (val) => {
|
||||||
|
const isonline = val.isonline as number
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
isonline === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{isonline === 0 ? '空闲' : '在用'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
<Pagination
|
||||||
|
total={total}
|
||||||
|
page={page}
|
||||||
|
size={250}
|
||||||
|
sizeOptions={[250]}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GatewayConfig() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={(
|
||||||
|
<div className="bg-white shadow p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-gray-600">加载搜索参数...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<GatewayConfigContent />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
159
src/app/(root)/gatewayMonitor/page.tsx
Normal file
159
src/app/(root)/gatewayMonitor/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState, Suspense } from 'react'
|
||||||
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
import { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
|
||||||
|
export default function GatewayConfigs() {
|
||||||
|
const [gateways, setGateways] = useState<Map<string, GatewayInfo>>(new Map())
|
||||||
|
const [data, setData] = useState<Map<string, Map<string, GatewayConfig | undefined>>>(new Map())
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = async () => {
|
||||||
|
const now = Date.now()
|
||||||
|
try {
|
||||||
|
// 固定端口信息
|
||||||
|
const slots = new Set<string>()
|
||||||
|
for (let i = 2; i <= 251; i++) {
|
||||||
|
slots.add(`172.30.168.${i}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取网关信息
|
||||||
|
const resp = await getGatewayInfo()
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error(`查询网关信息失败:${resp.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gateways = resp.data
|
||||||
|
const findGateways = gateways.reduce((map, gateway) => {
|
||||||
|
map.set(gateway.macaddr, gateway)
|
||||||
|
return map
|
||||||
|
}, new Map<string, GatewayInfo>())
|
||||||
|
setGateways(findGateways)
|
||||||
|
|
||||||
|
// 获取网关配置
|
||||||
|
const data = new Map<string, Map<string, GatewayConfig | undefined>>()
|
||||||
|
for (const slot of slots) {
|
||||||
|
data.set(slot, new Map<string, GatewayConfig>())
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(gateways.map((gateway, index) => {
|
||||||
|
return new Promise<void>(async (resolve) => {
|
||||||
|
const resp = await getGatewayConfig(1, { mac: gateway.macaddr })
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error(`查询网关 ${gateway.inner_ip} 配置失败:${resp.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs = resp.data.items
|
||||||
|
const findConfig = configs.reduce((map, config) => {
|
||||||
|
map.set(config.inner_ip, config)
|
||||||
|
return map
|
||||||
|
}, new Map<string, GatewayConfig>())
|
||||||
|
|
||||||
|
for (const slot of slots) {
|
||||||
|
data.get(slot)!.set(gateway.macaddr, findConfig.get(slot))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
setData(data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error(`初始化页面数据失败:${(error as Error).message}`)
|
||||||
|
}
|
||||||
|
console.log('初始化数据耗时', Date.now() - now, 'ms')
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page className="gap-3">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm font-medium">在线 {Array.from(gateways.values()).filter(item => item.enable).length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm font-medium">离线 {Array.from(gateways.values()).filter(item => !item.enable).length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-r sticky left-0 bg-gray-50">端口</TableHead>
|
||||||
|
{gateways.values().map((pair, index) => (
|
||||||
|
<TableHead key={index} className="border-r h-auto">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="font-medium">{pair.inner_ip}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">{pair.macaddr}</div>
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
)).toArray()}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.entries().map(([slot, configs], rowIndex) => (
|
||||||
|
<TableRow key={rowIndex}>
|
||||||
|
<TableCell className="border-r sticky left-0 bg-background">{slot}</TableCell>
|
||||||
|
{configs.entries().map(([_, config], colIndex) => {
|
||||||
|
if (!config) {
|
||||||
|
return (
|
||||||
|
<TableCell key={colIndex} className="not-last:border-r">
|
||||||
|
-
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
ischange: config.ischange === 0
|
||||||
|
? { bg: 'bg-green-100', text: 'text-green-800', label: '正常' }
|
||||||
|
: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: '更新' },
|
||||||
|
isonline: config.isonline === 0
|
||||||
|
? { bg: 'bg-green-100', text: 'text-green-800', label: '空闲' }
|
||||||
|
: { bg: 'bg-blue-100', text: 'text-blue-800', label: '在用' },
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TableCell key={`${colIndex}`} className="not-last:border-r">
|
||||||
|
<div key={colIndex} className="flex flex-col gap-1">
|
||||||
|
<div className="text-sm font-medium">{config.public}</div>
|
||||||
|
<div className="text-xs font-medium flex justify-between">
|
||||||
|
<span>{config.user}</span>
|
||||||
|
<span>{shrinkCity(config.city || '?')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.ischange.bg} ${statusConfig.ischange.text}`}>
|
||||||
|
{statusConfig.ischange.label}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.isonline.bg} ${statusConfig.isonline.text}`}>
|
||||||
|
{statusConfig.isonline.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
}).toArray()}
|
||||||
|
</TableRow>
|
||||||
|
)).toArray()}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shrinkCity(city: string) {
|
||||||
|
switch (city) {
|
||||||
|
case '黔东南苗族侗族自治州':
|
||||||
|
return '黔东南'
|
||||||
|
case '延边朝鲜族自治州':
|
||||||
|
return '延边'
|
||||||
|
default:
|
||||||
|
return city
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
||||||
import { Form, FormField } from '@/components/ui/form'
|
import { Form, FormField } from '@/components/ui/form'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { getGatewayInfo, type GatewayInfo } from '@/actions/stats'
|
import { getGatewayInfo, type GatewayInfo } from '@/actions/stats'
|
||||||
|
import { CopyIcon, CheckIcon } from 'lucide-react'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
|
||||||
const filterSchema = z.object({
|
const filterSchema = z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
@@ -16,14 +17,64 @@ const filterSchema = z.object({
|
|||||||
|
|
||||||
type FilterSchema = z.infer<typeof filterSchema>
|
type FilterSchema = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
// IP地址排序函数
|
const SmartCopyButton = ({
|
||||||
const sortByIpAddress = (a: string, b: string): number => {
|
data,
|
||||||
const ipToNumber = (ip: string): number => {
|
mode = 'single',
|
||||||
const parts = ip.split('.').map(part => parseInt(part, 10))
|
}: {
|
||||||
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
|
data: string | GatewayInfo[]
|
||||||
|
mode?: 'single' | 'batch'
|
||||||
|
}) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
let textToCopy: string
|
||||||
|
|
||||||
|
if (mode === 'single' && typeof data === 'string') {
|
||||||
|
textToCopy = data
|
||||||
|
}
|
||||||
|
else if (mode === 'batch' && Array.isArray(data)) {
|
||||||
|
if (data.length === 0) return
|
||||||
|
textToCopy = data.map(item => item.macaddr).join('\n')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(textToCopy)
|
||||||
|
setIsCopied(true)
|
||||||
|
setTimeout(() => setIsCopied(false), 2000)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('复制失败:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipToNumber(a) - ipToNumber(b)
|
const isBatch = mode === 'batch'
|
||||||
|
const disabled = isBatch && Array.isArray(data) && data.length === 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-1 transition-colors
|
||||||
|
${isBatch
|
||||||
|
? 'px-2 py-1 text-xs bg-blue-50 text-blue-600 rounded hover:bg-blue-100'
|
||||||
|
: 'ml-2 p-1 rounded hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
title={isBatch ? '复制所有MAC地址' : '复制MAC地址'}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<CheckIcon className="w-3 h-3 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="w-3 h-3 text-gray-500" />
|
||||||
|
)}
|
||||||
|
{isBatch && <span>{isCopied ? '已复制' : '复制全部'}</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Gatewayinfo() {
|
export default function Gatewayinfo() {
|
||||||
@@ -31,7 +82,6 @@ export default function Gatewayinfo() {
|
|||||||
const [filteredData, setFilteredData] = useState<GatewayInfo[]>([])
|
const [filteredData, setFilteredData] = useState<GatewayInfo[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const form = useForm<FilterSchema>({
|
const form = useForm<FilterSchema>({
|
||||||
resolver: zodResolver(filterSchema),
|
resolver: zodResolver(filterSchema),
|
||||||
@@ -71,11 +121,8 @@ export default function Gatewayinfo() {
|
|||||||
setError('')
|
setError('')
|
||||||
const result = await getGatewayInfo()
|
const result = await getGatewayInfo()
|
||||||
|
|
||||||
const sortedData = result.data.sort((a: GatewayInfo, b: GatewayInfo) =>
|
setData(result.data)
|
||||||
sortByIpAddress(a.inner_ip, b.inner_ip),
|
setFilteredData(result.data) // 初始化时设置filteredData
|
||||||
)
|
|
||||||
setData(sortedData)
|
|
||||||
setFilteredData(sortedData) // 初始化时设置filteredData
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Failed to fetch gateway info:', error)
|
console.error('Failed to fetch gateway info:', error)
|
||||||
@@ -86,19 +133,9 @@ export default function Gatewayinfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusText = (enable: number) => {
|
|
||||||
return enable === 1 ? '启用' : '禁用'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusClass = (enable: number) => {
|
|
||||||
return enable === 1
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: 'bg-red-100 text-red-800'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg p-6 overflow-hidden">
|
<div className="bg-white w-full shadow p-6 overflow-hidden">
|
||||||
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
||||||
<div className="text-center py-8">加载网关信息中...</div>
|
<div className="text-center py-8">加载网关信息中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +144,7 @@ export default function Gatewayinfo() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
<div className="bg-white w-full shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
||||||
<div className="text-center py-8 text-red-600">{error}</div>
|
<div className="text-center py-8 text-red-600">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,9 +152,9 @@ export default function Gatewayinfo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden">
|
<Page>
|
||||||
<div className="flex gap-6">
|
<div className="gap-6 flex">
|
||||||
<div className="flex flex-3 justify-between ">
|
<div className="flex flex-3 justify-between ">
|
||||||
<span className="text-lg pt-2 font-semibold mb-4">网关基本信息</span>
|
<span className="text-lg pt-2 font-semibold mb-4">网关基本信息</span>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form className="flex items-center gap-4">
|
<form className="flex items-center gap-4">
|
||||||
@@ -150,47 +187,45 @@ export default function Gatewayinfo() {
|
|||||||
<div className="flex flex-1"></div>
|
<div className="flex flex-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-6 overflow-hidden">
|
<div className="flex-auto overflow-hidden gap-6 flex">
|
||||||
<div className="flex-3 w-full flex">
|
<div className="flex-3 flex flex-col">
|
||||||
<Table>
|
<DataTable
|
||||||
<TableHeader>
|
data={filteredData}
|
||||||
<TableRow className="bg-gray-50">
|
columns={[
|
||||||
<TableHead className="px-4 py-2 text-left">MAC地址</TableHead>
|
{
|
||||||
<TableHead className="px-4 py-2 text-left">内网IP</TableHead>
|
label: 'MAC地址',
|
||||||
<TableHead className="px-4 py-2 text-left">配置版本</TableHead>
|
render: val => (
|
||||||
<TableHead className="px-4 py-2 text-left">状态</TableHead>
|
<div className="flex items-center gap-2">
|
||||||
</TableRow>
|
{String(val.macaddr)}
|
||||||
</TableHeader>
|
<SmartCopyButton data={String(val.macaddr)} />
|
||||||
<TableBody>
|
</div>
|
||||||
{filteredData.map((item, index) => (
|
),
|
||||||
<TableRow
|
},
|
||||||
key={index}
|
{
|
||||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
label: '内网IP',
|
||||||
>
|
props: 'inner_ip',
|
||||||
<TableCell className="px-4 py-2">
|
},
|
||||||
<button
|
{
|
||||||
onClick={() => {
|
label: '配置版本',
|
||||||
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`)
|
props: 'setid',
|
||||||
}}
|
},
|
||||||
className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer"
|
{
|
||||||
>
|
label: '状态',
|
||||||
{item.macaddr}
|
render: (val) => {
|
||||||
</button>
|
const enable = val.enable as number
|
||||||
</TableCell>
|
return (
|
||||||
<TableCell className="px-4 py-2">{item.inner_ip}</TableCell>
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${enable === 1
|
||||||
<TableCell className="px-4 py-2">{item.setid}</TableCell>
|
? 'bg-green-100 text-green-800'
|
||||||
<TableCell className="px-4 py-2">
|
: 'bg-red-100 text-red-800'}`}>
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.enable)}`}>
|
{enable === 1 ? '启用' : '禁用'}
|
||||||
{getStatusText(item.enable)}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
)
|
||||||
</TableRow>
|
},
|
||||||
))}
|
},
|
||||||
</TableBody>
|
]}
|
||||||
</Table>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 flex-col gap-4 mb-6 flex">
|
||||||
<div className="flex flex-1 flex-col gap-4 mb-6">
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
<div className="text-2xl font-bold text-blue-600">{data.length}</div>
|
<div className="text-2xl font-bold text-blue-600">{data.length}</div>
|
||||||
<div className="text-sm text-blue-800">网关总数</div>
|
<div className="text-sm text-blue-800">网关总数</div>
|
||||||
@@ -209,6 +244,6 @@ export default function Gatewayinfo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
125
src/app/(root)/layout.tsx
Normal file
125
src/app/(root)/layout.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ReactNode, useState } from 'react'
|
||||||
|
import { useRouter, usePathname } from 'next/navigation'
|
||||||
|
import { logout } from '@/actions/auth'
|
||||||
|
import { LayoutDashboardIcon, LogOutIcon, User2Icon } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { DoorClosedIcon } from 'lucide-react'
|
||||||
|
import { DoorClosedLockedIcon } from 'lucide-react'
|
||||||
|
import { DoorOpenIcon } from 'lucide-react'
|
||||||
|
import { MapPinnedIcon } from 'lucide-react'
|
||||||
|
import { ContainerIcon } from 'lucide-react'
|
||||||
|
import { GitForkIcon } from 'lucide-react'
|
||||||
|
import { SettingsIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await logout()
|
||||||
|
if (response) {
|
||||||
|
router.push('/login')
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('退出错误:', error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
return pathname === path
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen flex flex-col">
|
||||||
|
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<header className="flex-none basis-16 border-b">
|
||||||
|
<div className="px-4 sm:px-6">
|
||||||
|
<div className="flex justify-between h-16 items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">网络节点管理系统</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOutIcon className="h-4 w-4" />
|
||||||
|
<span>{isLoading ? '退出中...' : '退出登录'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="flex-auto overflow-hidden flex">
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<nav className="flex-none basis-64 border-r flex flex-col p-3">
|
||||||
|
<NavbarTitle>路由节点</NavbarTitle>
|
||||||
|
<NavbarItem href="/cityNodeStats" active={isActive('/cityNodeStats')}><MapPinnedIcon size={20} />城市信息</NavbarItem>
|
||||||
|
<NavbarTitle>代理网关</NavbarTitle>
|
||||||
|
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}><DoorClosedIcon size={20} />网关列表</NavbarItem>
|
||||||
|
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}><DoorClosedLockedIcon size={20} />网关配置</NavbarItem>
|
||||||
|
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}><LayoutDashboardIcon size={20} />配置总览</NavbarItem>
|
||||||
|
<NavbarTitle>边缘节点</NavbarTitle>
|
||||||
|
<NavbarItem href="/edge" active={isActive('/edge')}><GitForkIcon size={20} />节点列表</NavbarItem>
|
||||||
|
<NavbarItem href="/allocationStatus" active={isActive('/allocationStatus')}><ContainerIcon size={20} />分配状态</NavbarItem>
|
||||||
|
<NavbarTitle>其他</NavbarTitle>
|
||||||
|
<NavbarItem href="/settings" active={isActive('/settings')}><User2Icon size={20} />用户管理</NavbarItem>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<main className="flex-auto overflow-hidden">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavbarTitle(props: {
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<h3 className="text-sm text-weak h-8 flex items-end px-2 pb-1">
|
||||||
|
{props.children}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavbarItem(props: {
|
||||||
|
href: string
|
||||||
|
active: boolean
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={props.href}
|
||||||
|
className={cn(
|
||||||
|
'transition-colors duration-150 ease-in-out',
|
||||||
|
'h-10 rounded-md text-sm flex items-center p-2 gap-2',
|
||||||
|
props.active
|
||||||
|
? 'text-primary bg-primary/10'
|
||||||
|
: 'hover:bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
296
src/app/(root)/settings/page.tsx
Normal file
296
src/app/(root)/settings/page.tsx
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
|
import { UserIcon, LockIcon, SearchIcon, Trash2Icon, PlusIcon, XIcon } from 'lucide-react'
|
||||||
|
import { toast, Toaster } from 'sonner'
|
||||||
|
import { findUsers, createUser, removeUser } from '@/actions/user'
|
||||||
|
import { Page } from '@/components/page'
|
||||||
|
import { DataTable } from '@/components/data-table'
|
||||||
|
|
||||||
|
// 用户类型定义
|
||||||
|
interface UserData {
|
||||||
|
id: number
|
||||||
|
account: string
|
||||||
|
createdAt: Date
|
||||||
|
name: string | null
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
account: z.string().min(3, '账号至少需要3个字符'),
|
||||||
|
password: z.string().min(6, '密码至少需要6个字符'),
|
||||||
|
confirmPassword: z.string().min(6, '密码至少需要6个字符'),
|
||||||
|
}).refine(data => data.password === data.confirmPassword, {
|
||||||
|
message: '密码不匹配',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [users, setUsers] = useState<UserData[]>([])
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [isCreateMode, setIsCreateMode] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const data = await findUsers()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUsers(data.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error('获取用户列表失败', {
|
||||||
|
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await createUser({
|
||||||
|
account: values.account,
|
||||||
|
password: values.password,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toast.success('用户创建成功', {
|
||||||
|
description: '新账户已成功添加',
|
||||||
|
})
|
||||||
|
form.reset()
|
||||||
|
setIsCreateMode(false)
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error('创建用户失败', {
|
||||||
|
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDeleteUser = async (userId: number) => {
|
||||||
|
if (!confirm('确定要删除这个用户吗?此操作不可恢复。')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await removeUser(userId)
|
||||||
|
if (data.success) {
|
||||||
|
toast.success('用户删除成功', {
|
||||||
|
description: '用户账户已删除',
|
||||||
|
})
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
toast.error('删除用户失败', {
|
||||||
|
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newData = users
|
||||||
|
.filter(user => user.account.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
.map(user => ({
|
||||||
|
id: user.id,
|
||||||
|
account: user.account,
|
||||||
|
createdAt: new Date(user.createdAt).toLocaleDateString(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<h1 className="text-3xl font-bold">用户管理</h1>
|
||||||
|
<Button onClick={() => setIsCreateMode(!isCreateMode)}>
|
||||||
|
{isCreateMode ? (
|
||||||
|
<>
|
||||||
|
<XIcon className="mr-2 h-4 w-4" /> 取消
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlusIcon className="mr-2 h-4 w-4" /> 添加用户
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCreateMode && (
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">添加用户账号</CardTitle>
|
||||||
|
<CardDescription>创建新的系统用户账户</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="account"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>账号</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<UserIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="请输入需要添加的账号"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<LockIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>确认密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<LockIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
|
创建中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'创建用户'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsCreateMode(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 用户列表直接显示在页面上 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold">用户列表</h2>
|
||||||
|
<p className="text-muted-foreground">管理系统中的所有用户账户</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-sm mt-4">
|
||||||
|
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
className="pl-8"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
data={newData}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: '账号',
|
||||||
|
props: 'account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
props: 'createdAt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '操作',
|
||||||
|
render: (val) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 border-0 hover:bg-transparent"
|
||||||
|
onClick={() => handleDeleteUser(Number(val.id))}
|
||||||
|
>
|
||||||
|
<Trash2Icon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Toaster richColors />
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
|
||||||
import LoadingCard from '@/components/ui/loadingCard'
|
|
||||||
import ErrorCard from '@/components/ui/errorCard'
|
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
||||||
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
|
|
||||||
|
|
||||||
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
|
|
||||||
const [data, setData] = useState<AllocationStatus[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [timeFilter, setTimeFilter] = useState('24') // 默认24小时
|
|
||||||
const [customHours, setCustomHours] = useState('')
|
|
||||||
|
|
||||||
// 获取时间参数(小时数)
|
|
||||||
const getTimeHours = useCallback(() => {
|
|
||||||
if (timeFilter === 'custom' && customHours) {
|
|
||||||
const hours = parseInt(customHours)
|
|
||||||
return isNaN(hours) ? 24 : Math.max(1, hours) // 默认24小时,最少1小时
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseInt(timeFilter) || 24 // 默认24小时
|
|
||||||
}, [timeFilter, customHours])
|
|
||||||
|
|
||||||
// 计算超额量
|
|
||||||
const calculateOverage = (assigned: number, count: number) => {
|
|
||||||
const overage = assigned - count
|
|
||||||
return Math.max(0, overage)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setError(null)
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
const hours = getTimeHours()
|
|
||||||
const result = await getAllocationStatus(hours)
|
|
||||||
|
|
||||||
// 数据验证
|
|
||||||
const validatedData = (result.data).map(item => ({
|
|
||||||
city: item.city || '未知',
|
|
||||||
count: item.count,
|
|
||||||
assigned: item.assigned,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const sortedData = validatedData.sort((a, b) => b.count - a.count)
|
|
||||||
|
|
||||||
setData(sortedData)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Failed to fetch allocation status:', error)
|
|
||||||
setError(error instanceof Error ? error.message : 'Unknown error')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [getTimeHours])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
}, [fetchData])
|
|
||||||
|
|
||||||
if (loading) return <LoadingCard title="节点分配状态" />
|
|
||||||
if (error) return <ErrorCard title="节点分配状态" error={error} onRetry={fetchData} />
|
|
||||||
|
|
||||||
const problematicCities = data.filter(item => item.assigned > item.count)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden ">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">节点分配状态</h2>
|
|
||||||
|
|
||||||
{/* 时间筛选器 */}
|
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
||||||
<label className="font-medium">时间筛选:</label>
|
|
||||||
<select
|
|
||||||
value={timeFilter}
|
|
||||||
onChange={e => setTimeFilter(e.target.value)}
|
|
||||||
className="border rounded p-2"
|
|
||||||
>
|
|
||||||
<option value="1">最近1小时</option>
|
|
||||||
<option value="6">最近6小时</option>
|
|
||||||
<option value="12">最近12小时</option>
|
|
||||||
<option value="24">最近24小时</option>
|
|
||||||
<option value="168">最近7天</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={fetchData}
|
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
查询
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-6 overflow-hidden">
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="bg-gray-50">
|
|
||||||
<TableHead className="px-4 py-2 text-left">城市</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left">可用IP量</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left">分配IP量</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left">超额量</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((item, index) => {
|
|
||||||
const overage = calculateOverage(Number(item.assigned), Number(item.count))
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={index}
|
|
||||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
|
||||||
>
|
|
||||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">{item.assigned}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">
|
|
||||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
|
||||||
{overage}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
||||||
import { getCityNodeCount, type CityNode } from '@/actions/stats'
|
|
||||||
|
|
||||||
export default function CityNodeStats() {
|
|
||||||
const [data, setData] = useState<CityNode[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const result = await getCityNodeCount()
|
|
||||||
setData(result.data)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('获取城市节点数据失败:', error)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg p-6 overflow-hidden">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">城市节点数量分布</h2>
|
|
||||||
<div className="text-gray-600">加载中...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col bg-white rounded-lg p-6 overflow-hidden">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold">城市节点数量分布</h2>
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
共 {data.length} 个城市
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex overflow-hidden ">
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="bg-gray-50">
|
|
||||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">城市</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">节点数量</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">Hash</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">标签</TableHead>
|
|
||||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">轮换顺位</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<TableRow
|
|
||||||
key={index}
|
|
||||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
|
||||||
>
|
|
||||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">{item.hash}</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">
|
|
||||||
<span className="bg-gray-100 px-2 py-1 rounded text-gray-700">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-4 py-2">{item.offset}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
||||||
import { getEdgeNodes } from '@/actions/stats'
|
|
||||||
|
|
||||||
interface Edge {
|
|
||||||
id: number
|
|
||||||
macaddr: string
|
|
||||||
city: string
|
|
||||||
public: string
|
|
||||||
isp: string
|
|
||||||
single: number | boolean
|
|
||||||
sole: number | boolean
|
|
||||||
arch: number
|
|
||||||
online: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Edge() {
|
|
||||||
const [data, setData] = useState<Edge[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// 分页状态
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(100) // 默认100条
|
|
||||||
const [totalItems, setTotalItems] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
}, [currentPage, itemsPerPage]) // 监听页码和每页数量的变化
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setError(null)
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
// 计算偏移量
|
|
||||||
const offset = (currentPage - 1) * itemsPerPage
|
|
||||||
|
|
||||||
// if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
|
||||||
|
|
||||||
const result = await getEdgeNodes(offset, itemsPerPage)
|
|
||||||
type ResultEdge = {
|
|
||||||
id: number
|
|
||||||
macaddr: string
|
|
||||||
city: string
|
|
||||||
public: string
|
|
||||||
isp: string
|
|
||||||
single: number | boolean
|
|
||||||
sole: number | boolean
|
|
||||||
arch: number
|
|
||||||
online: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatedData = (result.data as ResultEdge[]).map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
macaddr: item.macaddr || '',
|
|
||||||
city: item.city || '',
|
|
||||||
public: item.public || '',
|
|
||||||
isp: item.isp || '',
|
|
||||||
single: item.single,
|
|
||||||
sole: item.sole,
|
|
||||||
arch: item.arch,
|
|
||||||
online: item.online,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setData(validatedData)
|
|
||||||
setTotalItems(result.totalCount || 0)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Failed to fetch edge nodes:', error)
|
|
||||||
setError(error instanceof Error ? error.message : '获取边缘节点数据失败')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 多IP节点格式化
|
|
||||||
const formatMultiIP = (value: number | boolean): string => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
switch (value) {
|
|
||||||
case 1: return '是'
|
|
||||||
case 0: return '否'
|
|
||||||
case -1: return '未知'
|
|
||||||
default: return `未知 (${value})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value ? '是' : '否'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 独享IP节点格式化
|
|
||||||
const formatExclusiveIP = (value: number | boolean): string => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return value === 1 ? '是' : '否'
|
|
||||||
}
|
|
||||||
return value ? '是' : '否'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 多IP节点颜色
|
|
||||||
const getMultiIPColor = (value: number | boolean): string => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
switch (value) {
|
|
||||||
case 1: return 'bg-red-100 text-red-800'
|
|
||||||
case 0: return 'bg-green-100 text-green-800'
|
|
||||||
case -1: return 'bg-gray-100 text-gray-800'
|
|
||||||
default: return 'bg-gray-100 text-gray-800'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 独享IP节点颜色
|
|
||||||
const getExclusiveIPColor = (value: number | boolean): string => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return value === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
||||||
}
|
|
||||||
return value ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatArchType = (arch: number): string => {
|
|
||||||
switch (arch) {
|
|
||||||
case 0: return '一代'
|
|
||||||
case 1: return '二代'
|
|
||||||
case 2: return 'AMD64'
|
|
||||||
case 3: return 'x86'
|
|
||||||
default: return `未知 (${arch})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getArchColor = (arch: number): string => {
|
|
||||||
switch (arch) {
|
|
||||||
case 0: return 'bg-blue-100 text-blue-800'
|
|
||||||
case 1: return 'bg-green-100 text-green-800'
|
|
||||||
case 2: return 'bg-purple-100 text-purple-800'
|
|
||||||
case 3: return 'bg-orange-100 text-orange-800'
|
|
||||||
default: return 'bg-gray-100 text-gray-800'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatOnlineTime = (seconds: number): string => {
|
|
||||||
if (seconds < 60) return `${seconds}秒`
|
|
||||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
|
|
||||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时`
|
|
||||||
return `${Math.floor(seconds / 86400)}天`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理页码变化
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理每页显示数量变化
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
setItemsPerPage(size)
|
|
||||||
setCurrentPage(1) // 重置到第一页
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) return (
|
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
|
||||||
<div className="text-center py-8">加载节点数据中...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) return (
|
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
|
||||||
<div className="text-center py-8 text-red-600">{error}</div>
|
|
||||||
<button
|
|
||||||
onClick={() => fetchData()}
|
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mx-auto block"
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex bg-white flex-col shadow overflow-hidden rounded-lg p-6">
|
|
||||||
{data.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-gray-400 text-4xl mb-4">📋</div>
|
|
||||||
<p className="text-gray-600">暂无节点数据</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex gap-6 overflow-hidden">
|
|
||||||
<div className="flex-3 w-full overflow-y-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="bg-gray-50">
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">MAC地址</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">城市</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">公网IP</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">运营商</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">多IP节点</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">独享IP</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">设备类型</TableHead>
|
|
||||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">在线时长</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<TableRow key={item.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
||||||
<TableCell className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-gray-700">
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
|
||||||
item.isp === '移动'
|
|
||||||
? 'bg-blue-100 text-blue-800'
|
|
||||||
: item.isp === '电信'
|
|
||||||
? 'bg-purple-100 text-purple-800'
|
|
||||||
: item.isp === '联通'
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-gray-100 text-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.isp}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-center">
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMultiIPColor(item.single)}`}>
|
|
||||||
{formatMultiIP(item.single)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-center">
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getExclusiveIPColor(item.sole)}`}>
|
|
||||||
{formatExclusiveIP(item.sole)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-center">
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getArchColor(item.arch)}`}>
|
|
||||||
{formatArchType(item.arch)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
<Pagination
|
|
||||||
page={currentPage}
|
|
||||||
size={itemsPerPage}
|
|
||||||
total={totalItems}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onSizeChange={handleSizeChange}
|
|
||||||
className="mt-4"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useEffect, useState, Suspense } from 'react'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
|
||||||
import { getGatewayConfig, type GatewayConfig } from '@/actions/stats'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
|
|
||||||
function GatewayConfigContent() {
|
|
||||||
const [data, setData] = useState<GatewayConfig[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [macAddress, setMacAddress] = useState('')
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// 分页状态
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [total, setTotal] = useState(0)
|
|
||||||
|
|
||||||
// 监听URL的mac参数变化
|
|
||||||
useEffect(() => {
|
|
||||||
const urlMac = searchParams.get('mac')
|
|
||||||
if (urlMac) {
|
|
||||||
setMacAddress(urlMac)
|
|
||||||
setPage(1) // 重置到第一页
|
|
||||||
fetchData(urlMac, 1)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setMacAddress('')
|
|
||||||
setPage(1) // 重置到第一页
|
|
||||||
fetchData('', 1)
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
|
||||||
|
|
||||||
const fetchData = async (mac: string, page: number = 1) => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
// 计算偏移量
|
|
||||||
const result = await getGatewayConfig(page, mac)
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || '查询网关配置失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const shrink = ['黔东南', '延边']
|
|
||||||
result.data.items.forEach((item) => {
|
|
||||||
shrink.forEach((s) => {
|
|
||||||
if (item.city?.startsWith(s)) {
|
|
||||||
item.city = s
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
setData(result.data.items)
|
|
||||||
setTotal(result.data.total)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error((error as Error).message || '获取网关配置失败')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setPage(1) // 重置到第一页
|
|
||||||
fetchData(macAddress, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理页码变化
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setPage(page)
|
|
||||||
fetchData(macAddress, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理每页显示数量变化
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
setPage(1)
|
|
||||||
fetchData(macAddress, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
|
|
||||||
// 0是正常1是更新,正常(绿)+ 更新(红)
|
|
||||||
return (
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
|
||||||
{value === 0 ? trueText : falseText}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOnlineStatus = (isonline: number) => {
|
|
||||||
// 0是空闲1是在用,在用(红)+ 空闲(绿)
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className={`${isonline === 0 ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
||||||
{getStatusBadge(isonline, '空闲', '在用')}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden">
|
|
||||||
<div className="flex justify-between items-start mb-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-800">网关配置状态</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">查询和管理网关设备的配置信息</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 查询表单 */}
|
|
||||||
<form onSubmit={handleSubmit} className="mb-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={macAddress}
|
|
||||||
onChange={e => setMacAddress(e.target.value)}
|
|
||||||
placeholder="输入MAC地址查询"
|
|
||||||
className="px-4 py-2 h-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="ml-2 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
查询
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-gray-400 text-4xl mb-4">⏳</div>
|
|
||||||
<p className="text-gray-600">正在查询网关配置信息...</p>
|
|
||||||
</div>
|
|
||||||
) : data.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="flex gap-6 overflow-hidden">
|
|
||||||
<div className="flex-3 w-full flex">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="bg-gray-50">
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">端口</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线路</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">城市</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">节点MAC</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">节点IP</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">配置更新</TableHead>
|
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">在用状态</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
||||||
<TableCell>{item.inner_ip}</TableCell>
|
|
||||||
<TableCell>{item.user}</TableCell>
|
|
||||||
<TableCell>{item.city}</TableCell>
|
|
||||||
<TableCell>{item.edge}</TableCell>
|
|
||||||
<TableCell>{item.public}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{getStatusBadge(item.ischange, '正常', '更新')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{getOnlineStatus(item.isonline)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 mb-6">
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
|
|
||||||
<div className="text-2xl font-bold text-blue-600">{total}</div>
|
|
||||||
<div className="text-sm text-blue-800">配置条数</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
|
|
||||||
<div className="text-2xl font-bold text-green-600">
|
|
||||||
{data.filter(item => item.isonline === 1).length}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-green-800">在用节点</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-orange-50 p-4 rounded-lg border border-orange-100">
|
|
||||||
<div className="text-2xl font-bold text-orange-600">
|
|
||||||
{data.filter(item => item.ischange === 1).length}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-orange-800">需要更新</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页组件 */}
|
|
||||||
{!macAddress && (
|
|
||||||
<Pagination
|
|
||||||
total={total}
|
|
||||||
page={page}
|
|
||||||
size={250}
|
|
||||||
sizeOptions={[250]}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onSizeChange={handleSizeChange}
|
|
||||||
className="mt-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-gray-400 text-4xl mb-4">📋</div>
|
|
||||||
<p className="text-gray-600">暂无网关配置数据</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GatewayConfig() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={(
|
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600">加载搜索参数...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<GatewayConfigContent />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import * as z from 'zod'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
||||||
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
|
|
||||||
import { toast, Toaster } from 'sonner'
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
||||||
import { findUsers, createUser, removeUser } from '@/actions/user'
|
|
||||||
|
|
||||||
// 用户类型定义
|
|
||||||
interface UserData {
|
|
||||||
id: number
|
|
||||||
account: string
|
|
||||||
createdAt: Date
|
|
||||||
name: string | null
|
|
||||||
updatedAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
account: z.string().min(3, '账号至少需要3个字符'),
|
|
||||||
password: z.string().min(6, '密码至少需要6个字符'),
|
|
||||||
confirmPassword: z.string().min(6, '密码至少需要6个字符'),
|
|
||||||
}).refine(data => data.password === data.confirmPassword, {
|
|
||||||
message: '密码不匹配',
|
|
||||||
path: ['confirmPassword'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function Settings() {
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [users, setUsers] = useState<UserData[]>([])
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [isCreateMode, setIsCreateMode] = useState(false)
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
account: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取用户列表
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
const data = await findUsers()
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setUsers(data.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error('获取用户列表失败', {
|
|
||||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始加载用户列表
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUsers()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 创建用户
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const data = await createUser({
|
|
||||||
account: values.account,
|
|
||||||
password: values.password,
|
|
||||||
name: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
toast.success('用户创建成功', {
|
|
||||||
description: '新账户已成功添加',
|
|
||||||
})
|
|
||||||
form.reset()
|
|
||||||
setIsCreateMode(false)
|
|
||||||
fetchUsers() // 刷新用户列表
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error('创建用户失败', {
|
|
||||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除用户
|
|
||||||
const handleDeleteUser = async (userId: number) => {
|
|
||||||
if (!confirm('确定要删除这个用户吗?此操作不可恢复。')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await removeUser(userId)
|
|
||||||
if (data.success) {
|
|
||||||
toast.success('用户删除成功', {
|
|
||||||
description: '用户账户已删除',
|
|
||||||
})
|
|
||||||
fetchUsers() // 刷新用户列表
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error('删除用户失败', {
|
|
||||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤用户列表
|
|
||||||
const filteredUsers = users.filter(user =>
|
|
||||||
user.account.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white p-4 md:p-8">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-3xl font-bold">用户管理</h1>
|
|
||||||
<Button onClick={() => setIsCreateMode(!isCreateMode)}>
|
|
||||||
{isCreateMode ? (
|
|
||||||
<>
|
|
||||||
<X className="mr-2 h-4 w-4" /> 取消
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Plus className="mr-2 h-4 w-4" /> 添加用户
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isCreateMode && (
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">添加用户账号</CardTitle>
|
|
||||||
<CardDescription>创建新的系统用户账户</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="account"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>账号</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<User className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="请输入需要添加的账号"
|
|
||||||
className="pl-8"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>密码</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
className="pl-8"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="confirmPassword"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>确认密码</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="请再次输入密码"
|
|
||||||
className="pl-8"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
||||||
创建中...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'创建用户'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsCreateMode(false)}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 用户列表直接显示在页面上 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-semibold">用户列表</h2>
|
|
||||||
<p className="text-muted-foreground">管理系统中的所有用户账户</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-sm mt-4">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索用户..."
|
|
||||||
className="pl-8"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border rounded-lg">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>账号</TableHead>
|
|
||||||
<TableHead>创建时间</TableHead>
|
|
||||||
<TableHead className="text-right">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredUsers.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} className="text-center py-4">
|
|
||||||
暂无用户数据
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
filteredUsers.map(user => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell className="font-medium">{user.account}</TableCell>
|
|
||||||
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 border-0 hover:bg-transparent"
|
|
||||||
onClick={() => handleDeleteUser(Number(user.id))}
|
|
||||||
><Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Toaster richColors />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect, Suspense } from 'react'
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
import Gatewayinfo from './components/gatewayinfo'
|
|
||||||
import GatewayConfig from './components/gatewayConfig'
|
|
||||||
import CityNodeStats from './components/cityNodeStats'
|
|
||||||
import AllocationStatus from './components/allocationStatus'
|
|
||||||
import Settings from './components/settings'
|
|
||||||
import Edge from './components/edge'
|
|
||||||
import { LogOut } from 'lucide-react'
|
|
||||||
import { logout } from '@/actions/auth'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ id: 'gatewayInfo', label: '网关信息' },
|
|
||||||
{ id: 'gateway', label: '网关配置' },
|
|
||||||
{ id: 'city', label: '城市信息' },
|
|
||||||
{ id: 'allocation', label: '分配状态' },
|
|
||||||
{ id: 'edge', label: '节点信息' },
|
|
||||||
{ id: 'setting', label: '设置' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function DashboardContent() {
|
|
||||||
const [activeTab, setActiveTab] = useState('gatewayInfo')
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// 监听URL参数变化
|
|
||||||
useEffect(() => {
|
|
||||||
const urlTab = searchParams.get('tab')
|
|
||||||
if (urlTab && tabs.some(tab => tab.id === urlTab)) {
|
|
||||||
setActiveTab(urlTab)
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
const handleLogout = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const response = await logout()
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
// 退出成功后跳转到登录页
|
|
||||||
router.push('/login')
|
|
||||||
router.refresh()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error('退出失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('退出错误:', error)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTabClick = (tabId: string) => {
|
|
||||||
setActiveTab(tabId)
|
|
||||||
// 更新 URL 参数
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
params.set('tab', tabId)
|
|
||||||
router.push(`/dashboard?${params.toString()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className=" bg-gray-100 w-screen h-screen flex flex-col">
|
|
||||||
<nav className="bg-white flex-none h-16 shadow-sm">
|
|
||||||
<div className="px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="flex justify-between h-16 items-center">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">网络节点管理系统</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 简化的退出按钮 */}
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
<span>{isLoading ? '退出中...' : '退出登录'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="flex flex-3 overflow-hidden px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<div className="border-b border-gray-200 mb-6">
|
|
||||||
<nav className="flex flex-col w-64 -mb-px space-x-8">
|
|
||||||
{tabs.map(tab => (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
onClick={() => handleTabClick(tab.id)}
|
|
||||||
className={`py-2 px-1 h-12 border-b-2 font-medium text-sm ${
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-blue-500 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 flex-auto">
|
|
||||||
{activeTab === 'gatewayInfo' && <Gatewayinfo />}
|
|
||||||
{activeTab === 'gateway' && <GatewayConfig />}
|
|
||||||
{activeTab === 'city' && <CityNodeStats />}
|
|
||||||
{activeTab === 'allocation' && <AllocationStatus detailed />}
|
|
||||||
{activeTab === 'edge' && <Edge />}
|
|
||||||
{activeTab === 'setting' && <Settings />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={(
|
|
||||||
<div className=" bg-gray-100 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600">加载中...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<DashboardContent />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { findConfigs } from '@/actions/config'
|
|
||||||
import { gatewayConfigGet } from '@/actions/remote'
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
type EdgeConfig = {
|
|
||||||
port?: string
|
|
||||||
edge?: string
|
|
||||||
city?: string
|
|
||||||
_index: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DebugConfigPage() {
|
|
||||||
const [macaddr, setMacaddr] = useState('')
|
|
||||||
const [remotes, setRemotes] = useState<EdgeConfig[]>([])
|
|
||||||
const [locals, setLocals] = useState<EdgeConfig[]>([])
|
|
||||||
|
|
||||||
const fetch = async (macaddr: string) => {
|
|
||||||
try {
|
|
||||||
console.log('fetch', macaddr)
|
|
||||||
if (!macaddr) return
|
|
||||||
|
|
||||||
const rawLocal = await findConfigs({ macaddr })
|
|
||||||
console.log('raw local', rawLocal)
|
|
||||||
|
|
||||||
const rawRemote = await gatewayConfigGet({ macaddr })
|
|
||||||
console.log('raw remote', rawRemote)
|
|
||||||
|
|
||||||
setLocals(rawLocal.map(rule => ({
|
|
||||||
port: rule.network,
|
|
||||||
edge: rule.edge,
|
|
||||||
city: rule.cityhash,
|
|
||||||
_index: parseInt(rule.network.split('.')[3] || '0'),
|
|
||||||
})).sort((a, b) => a._index - b._index))
|
|
||||||
|
|
||||||
setRemotes(rawRemote.rules.map((rule) => {
|
|
||||||
const port = rule.network.find(n => !!n)
|
|
||||||
return ({
|
|
||||||
port: port,
|
|
||||||
edge: rule.edge.find(n => !!n),
|
|
||||||
city: rule.cityhash,
|
|
||||||
_index: port ? parseInt(port.split('.')[3]) : 0,
|
|
||||||
})
|
|
||||||
}).sort((a, b) => a._index - b._index))
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('数据获取失败', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-auto overflow-hidden flex flex-col p-6 gap-4.5">
|
|
||||||
<div className="flex-none flex gap-3">
|
|
||||||
<Input type="text" name="macaddr" value={macaddr} onChange={e => setMacaddr(e.target.value)} className="flex-none basis-60" />
|
|
||||||
<Button onClick={() => fetch(macaddr)}>查询</Button>
|
|
||||||
</div>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>端口</TableHead>
|
|
||||||
<TableHead>节点</TableHead>
|
|
||||||
<TableHead>城市</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{!locals.length || !remotes.length ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} className="text-center">获取数据为空</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : locals.length !== remotes.length ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} className="text-center">本地和远程规则数量不匹配</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
locals.map((item, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>
|
|
||||||
{item.port === remotes[index].port ? (
|
|
||||||
<span className="text-green-500">{item.port}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-500">{item.port} : {remotes[index].port}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item.edge === remotes[index].edge ? (
|
|
||||||
<span className="text-green-500">{item.edge}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-500">{item.edge} : {remotes[index].edge}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{item.city === remotes[index].city ? (
|
|
||||||
<span className="text-green-500">{item.city}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-500">{item.city} : {remotes[index].city}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export default async function DebugLayout(props: {
|
|
||||||
children: ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="w-screen h-screen flex flex-col">
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
--color-weak: var(--weak);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
@@ -45,14 +46,19 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
|
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--weak: oklch(0.6 0 0);
|
||||||
|
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
|
--primary: oklch(0.65 0.175 255);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.97 0 0);
|
||||||
|
|||||||
93
src/components/data-table.tsx
Normal file
93
src/components/data-table.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
'use client'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
|
||||||
|
import { ReactNode, useState } from 'react'
|
||||||
|
import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
||||||
|
import { ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type Data = Record<string, unknown>
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
label: string
|
||||||
|
props?: string
|
||||||
|
render?: (val: Data) => ReactNode
|
||||||
|
sortable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<T extends Data>(props: {
|
||||||
|
data: T[]
|
||||||
|
columns: Column[]
|
||||||
|
pinFirst?: boolean
|
||||||
|
}) {
|
||||||
|
const table = useReactTable({
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
data: props.data,
|
||||||
|
columns: props.columns.map(col => ({
|
||||||
|
meta: col,
|
||||||
|
header: col.label,
|
||||||
|
accessorKey: col.props,
|
||||||
|
cell: info => col.render?.(info.row.original) || String(info.getValue()),
|
||||||
|
enableSorting: col.sortable,
|
||||||
|
})) as ColumnDef<T>[],
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
|
||||||
|
{/* 表头行 */}
|
||||||
|
{table.getHeaderGroups().map(group => (
|
||||||
|
<TableRow key={group.id}>
|
||||||
|
|
||||||
|
{/* 表头 */}
|
||||||
|
{group.headers.map((header, index) => (
|
||||||
|
<TableHead
|
||||||
|
key={header.id}
|
||||||
|
className={cn(
|
||||||
|
header.column.columnDef.enableSorting && 'hover:bg-gray-200 transition-colors duration-150 ease-in-out cursor-pointer',
|
||||||
|
header.column.getIsSorted() && 'text-primary',
|
||||||
|
props.pinFirst && index === 0 && 'sticky left-0 bg-gray-50 border-r',
|
||||||
|
)}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
{header.column.columnDef.enableSorting && (
|
||||||
|
header.column.getIsSorted() == 'asc' ? (
|
||||||
|
<ArrowUpIcon className="size-4" />
|
||||||
|
) : header.column.getIsSorted() == 'desc' ? (
|
||||||
|
<ArrowDownIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDownIcon className="size-4" />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
|
||||||
|
{/* 表格行 */}
|
||||||
|
{table.getRowModel().rows.map(row => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
{row.getVisibleCells().map((cell, index) => (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(
|
||||||
|
props.pinFirst && index === 0 && 'sticky left-0 bg-white border-r',
|
||||||
|
)}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/components/page.tsx
Normal file
12
src/components/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { ComponentProps, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export function Page(props: {
|
||||||
|
children: ReactNode
|
||||||
|
} & ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div className={cn('w-full h-full p-6 flex flex-col', props.className)}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ export default function ErrorCard({
|
|||||||
onRetry: () => void
|
onRetry: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg p-6 text-red-600">
|
<div className="bg-white shadow p-6 text-red-600">
|
||||||
<h2 className="text-lg font-semibold mb-2">{title}</h2>
|
<h2 className="text-lg font-semibold mb-2">{title}</h2>
|
||||||
<p>加载失败: {error}</p>
|
<p>加载失败: {error}</p>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function LoadingCard({ title }: { title: string }) {
|
export default function LoadingCard({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
<div className="bg-white w-full shadow p-6">
|
||||||
<div className="animate-pulse">
|
<div className="animate-pulse">
|
||||||
<div className="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
|
<div className="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function Pagination({
|
|||||||
const paginationItems = generatePaginationItems()
|
const paginationItems = generatePaginationItems()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-wrap items-center justify-end gap-4 ${className || ''}`}>
|
<div className={`flex flex-wrap items-center gap-4 ${className || ''}`}>
|
||||||
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
共
|
共
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="table-container"
|
data-slot="table-container"
|
||||||
className="relative w-full overflow-x-auto"
|
className="rounded-md border overflow-auto"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
data-slot="table"
|
data-slot="table"
|
||||||
className={cn('w-full caption-bottom text-sm ', className)}
|
className={cn('w-full caption-bottom text-sm border-separate border-spacing-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
data-slot="table-header"
|
data-slot="table-header"
|
||||||
className={cn('[&_tr]:border-b sticky top-0', className)}
|
className={cn('sticky top-0 bg-gray-50 z-10', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -33,7 +33,7 @@ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|||||||
return (
|
return (
|
||||||
<tbody
|
<tbody
|
||||||
data-slot="table-body"
|
data-slot="table-body"
|
||||||
className={cn('[&_tr:last-child]:border-0', className)}
|
className={cn('[&>tr:last-child>td]:border-b-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|||||||
<tr
|
<tr
|
||||||
data-slot="table-row"
|
data-slot="table-row"
|
||||||
className={cn(
|
className={cn(
|
||||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors h-10',
|
'hover:data-[state=selected]:bg-muted border-border/50 transition-colors',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -71,6 +71,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
'text-sm text-gray-500 border-b',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -83,7 +84,7 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|||||||
<td
|
<td
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 h-10 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
'p-2 h-10 border-b align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import 'dotenv/config'
|
import 'server-only'
|
||||||
import { drizzle as client } from 'drizzle-orm/mysql2'
|
import { drizzle as client, type MySql2Database } from 'drizzle-orm/mysql2'
|
||||||
import * as schema from './schema'
|
import * as schema from './schema'
|
||||||
|
|
||||||
declare global {
|
const globalForDrizzle = globalThis as { drizzle?: MySql2Database<typeof schema> }
|
||||||
var drizzle: ReturnType<typeof client<typeof schema>> | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const { DATABASE_URL } = process.env
|
const { DATABASE_HOST, DATABASE_PORT, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME } = process.env
|
||||||
if (!DATABASE_URL) {
|
const proxy = new Proxy({} as MySql2Database<typeof schema>, {
|
||||||
throw new Error('DATABASE_URL is not set')
|
get(_, prop) {
|
||||||
}
|
if (!globalForDrizzle.drizzle) {
|
||||||
|
globalForDrizzle.drizzle = client(
|
||||||
|
`mysql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}`,
|
||||||
|
{ mode: 'default', schema })
|
||||||
|
}
|
||||||
|
|
||||||
const drizzle = global.drizzle || client(DATABASE_URL, { mode: 'default', schema })
|
const drizzle = globalForDrizzle.drizzle
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
return drizzle[prop as keyof typeof drizzle]
|
||||||
global.drizzle = drizzle
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
export default drizzle
|
export default proxy
|
||||||
export * from './schema'
|
export * from './schema'
|
||||||
export * from 'drizzle-orm'
|
export * from 'drizzle-orm'
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import 'server-only'
|
import 'server-only'
|
||||||
import { createClient } from 'redis'
|
import { createClient, type RedisClientType } from 'redis'
|
||||||
|
|
||||||
const client = createClient({
|
const globalForRedis = globalThis as { redis?: RedisClientType }
|
||||||
url: process.env.REDIS_URL,
|
|
||||||
})
|
const { REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD } = process.env
|
||||||
|
if (!globalForRedis.redis) {
|
||||||
|
const url = (REDIS_USERNAME || REDIS_PASSWORD)
|
||||||
|
? `redis://${REDIS_USERNAME}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}`
|
||||||
|
: `redis://${REDIS_HOST}:${REDIS_PORT}`
|
||||||
|
console.log('test url', url)
|
||||||
|
globalForRedis.redis = createClient({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
const redis = globalForRedis.redis
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
await redis.connect()
|
||||||
|
}
|
||||||
|
|
||||||
const redis = await client.connect()
|
|
||||||
export default redis
|
export default redis
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
// 给没有页面的路径添加跳转页面
|
// 给没有页面的路径添加跳转页面
|
||||||
if (request.nextUrl.pathname === '/') {
|
if (request.nextUrl.pathname === '/') {
|
||||||
return NextResponse.redirect(new URL('/dashboard', request.url))
|
return NextResponse.redirect(new URL('/gatewayinfo', request.url))
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
|
|||||||
Reference in New Issue
Block a user