迁移 orm 依赖到 drizzle

This commit is contained in:
2025-09-27 10:37:27 +08:00
parent 7fa2fe67ca
commit 72ea29f435
19 changed files with 538 additions and 523 deletions

View File

@@ -14,7 +14,6 @@ WORKDIR /app
COPY --from=dep /app/node_modules ./node_modules
COPY . .
RUN bun prisma generate
RUN bun run build
# 生产阶段

View File

@@ -13,7 +13,9 @@
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.5",
"lucide-react": "^0.541.0",
"mysql2": "^3.15.1",
"next": "15.4.7",
"next-themes": "^0.4.6",
"react": "19.1.0",
@@ -26,16 +28,16 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@prisma/client": "^6.16.2",
"@stylistic/eslint-plugin": "^5.4.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"drizzle-kit": "^0.31.4",
"eslint": "^9",
"eslint-config-next": "15.5.0",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.6",
"prisma": "^6.16.1",
"tailwindcss": "^4",
"tsx": "^4.20.4",
"tw-animate-css": "^1.3.7",
@@ -47,12 +49,18 @@
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@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/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "https://registry.npmmirror.com/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
@@ -463,6 +471,8 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -475,6 +485,8 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -541,6 +553,8 @@
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"denque": ["denque@2.1.0", "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
@@ -551,6 +565,10 @@
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"drizzle-kit": ["drizzle-kit@0.31.4", "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.4.tgz", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="],
"drizzle-orm": ["drizzle-orm@0.44.5", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.44.5.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="],
@@ -579,6 +597,8 @@
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"esbuild-register": ["esbuild-register@3.6.0", "https://registry.npmmirror.com/esbuild-register/-/esbuild-register-3.6.0.tgz", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.33.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA=="],
@@ -591,6 +611,8 @@
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
"eslint-plugin-drizzle": ["eslint-plugin-drizzle@0.2.3", "https://registry.npmmirror.com/eslint-plugin-drizzle/-/eslint-plugin-drizzle-0.2.3.tgz", { "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-BO+ymHo33IUNoJlC0rbd7HP9EwwpW4VIp49R/tWQF/d2E1K2kgTf0tCXT0v9MSiBr6gGR1LtPwMLapTKEWSg9A=="],
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
@@ -649,6 +671,8 @@
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"generate-function": ["generate-function@2.3.1", "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
@@ -687,6 +711,8 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"iconv-lite": ["iconv-lite@0.7.0", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -731,6 +757,8 @@
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-property": ["is-property@1.0.2", "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@@ -805,8 +833,14 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@7.18.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"lru.min": ["lru.min@1.1.2", "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
"lucide-react": ["lucide-react@0.541.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -829,6 +863,10 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mysql2": ["mysql2@3.15.1", "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.1.tgz", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw=="],
"named-placeholders": ["named-placeholders@1.1.3", "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="],
@@ -943,10 +981,14 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seq-queue": ["seq-queue@0.0.5", "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -971,8 +1013,14 @@
"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-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"sqlstring": ["sqlstring@2.3.3", "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -1067,6 +1115,8 @@
"zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
@@ -1127,6 +1177,50 @@
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@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-arm64": ["@esbuild/android-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],

View File

@@ -2,6 +2,7 @@ import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
import stylistic from '@stylistic/eslint-plugin'
import drizzle from 'eslint-plugin-drizzle'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
@@ -23,6 +24,14 @@ const eslintConfig = [
'@typescript-eslint/no-unused-vars': 'off',
},
},
{
plugins: {
drizzle,
},
rules: {
...drizzle.configs.recommended.rules,
},
},
]
export default eslintConfig

0
localhost.session.sql Normal file
View File

View File

@@ -18,7 +18,9 @@
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.5",
"lucide-react": "^0.541.0",
"mysql2": "^3.15.1",
"next": "15.4.7",
"next-themes": "^0.4.6",
"react": "19.1.0",
@@ -31,16 +33,16 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@prisma/client": "^6.16.2",
"@stylistic/eslint-plugin": "^5.4.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"drizzle-kit": "^0.31.4",
"eslint": "^9",
"eslint-config-next": "15.5.0",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.6",
"prisma": "^6.16.1",
"tailwindcss": "^4",
"tsx": "^4.20.4",
"tw-animate-css": "^1.3.7",

View File

@@ -1,29 +0,0 @@
-- jdbox.sessions definition
CREATE TABLE `sessions` (
`id` varchar(191) NOT NULL,
`expires` datetime(3) NOT NULL,
`createdAt` datetime(3) NOT NULL DEFAULT current_timestamp(3),
`userId` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_userId_idx` (`userId`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- jdbox.users definition
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(191) DEFAULT NULL,
`createdAt` datetime(3) NOT NULL DEFAULT current_timestamp(3),
`password` varchar(191) NOT NULL,
`account` varchar(191) NOT NULL,
`updatedAt` datetime(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_phone_key` (`phone`)
) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- 插入初始用户
INSERT INTO users(phone, password, name)
VALUES(
'admin',
'$2a$10$k.p3.s28OdLmGCMtuvBoqOxABp03h0Zhmop4eqqlR8sIjkThCcsnS',
'管理员'
);

View File

@@ -1,115 +0,0 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model change {
id Int @id @default(autoincrement())
time DateTime? @db.Timestamp(0)
city Int?
macaddr String @db.VarChar(20)
edge_new String @db.VarChar(20)
edge_old String? @db.VarChar(20)
info String @db.VarChar(500)
network String @db.VarChar(20)
createtime DateTime @default(now()) @db.DateTime(0)
@@index([edge_new], map: "edge_new")
@@index([time], map: "change_time_index")
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model cityhash {
id Int @id @default(autoincrement()) @db.UnsignedInt
macaddr String? @db.VarChar(20)
city String @db.VarChar(20)
num Int
hash String @db.VarChar(100)
label String? @db.VarChar(20)
count Int @default(0)
offset Int @default(0)
createtime DateTime @default(now()) @db.DateTime(0)
updatetime DateTime @default(now()) @db.DateTime(0)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model edge {
id Int @id @default(autoincrement())
macaddr String @unique(map: "edge_macaddr_idx") @db.VarChar(17)
public String @db.VarChar(255)
isp String @db.VarChar(255)
single Boolean
sole Boolean
arch Boolean
online Int @default(0)
city_id Int
active Boolean
@@index([active], map: "edge_active_index")
@@index([city_id])
@@index([isp], map: "edge_isp_index")
@@index([public], map: "edge_public_index")
}
model gateway {
id Int @id @default(autoincrement()) @db.UnsignedInt
macaddr String @db.VarChar(20)
table Int
edge String @db.VarChar(20)
network String @db.VarChar(20)
cityhash String @db.VarChar(100)
label String? @db.VarChar(20)
user String? @db.VarChar(20)
inner_ip String? @db.VarChar(20)
ischange Int @default(0) @db.TinyInt
isonline Int @default(0) @db.TinyInt
onlinenum Int @default(0)
createtime DateTime @default(now()) @db.DateTime(0)
updatetime DateTime @default(now()) @db.DateTime(0)
@@index([inner_ip], map: "inner_ip")
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model token {
id Int @id @default(autoincrement()) @db.UnsignedInt
setid Int @default(1)
change_count Int
limit_count Int @default(32000)
token String @db.VarChar(1000)
macaddr String @db.VarChar(100)
token_time DateTime @db.DateTime(0)
inner_ip String? @db.VarChar(20)
l2ip String? @db.VarChar(20)
enable Boolean @default(true)
createtime DateTime @default(now()) @db.DateTime(0)
updatetime DateTime @default(now()) @db.DateTime(0)
}
model User {
id Int @id @default(autoincrement())
account String @unique
password String
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
@@map("users")
}
model Session {
id String @id
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expires DateTime
createdAt DateTime @default(now())
@@index([userId])
@@map("sessions")
}

View File

@@ -1,58 +1,57 @@
'use server'
import prisma, { User } from '@/lib/prisma' // 使用统一的prisma实例
import drizzle, { eq, sessions, users } from '@/lib/drizzle'
import { compare } from 'bcryptjs'
import { randomUUID } from 'crypto'
import { cookies } from 'next/headers'
import { z } from 'zod'
const loginSchema = z.object({
account: z.string().min(3, '账号至少需要3个字符'),
password: z.string().min(6, '密码至少需要6个字符'),
const loginProps = z.object({
account: z.string().min(3, '账号至少需要3个字符').trim(),
password: z.string().min(6, '密码至少需要6个字符').trim(),
})
export async function login(props: {
account: string
password: string
}) {
export async function login(rawParams: z.infer<typeof loginProps>) {
try {
const result = loginSchema.parse(props)
const params = loginProps.parse(rawParams)
const user: User | null = await prisma.user.findFirst({
where: {
OR: [
{ account: result.account.trim() },
{ password: result.password.trim() },
],
},
// 查找用户
const user = await drizzle.query.users.findFirst({
where: eq(users.account, params.account),
})
if (!user) {
return {
success: false,
error: '用户不存在或密码未设置',
error: '用户不存在或密码错误',
}
}
// 验证密码
const passwordMatch = await compare(result.password, user.password || '')
const passwordMatch = await compare(params.password, user.password || '')
if (!passwordMatch) {
return {
success: false,
error: '密码错误',
error: '用户不存在或密码错误',
}
}
// 创建会话
const sessionToken = crypto.randomUUID()
await prisma.session.create({
data: {
const sessionToken = randomUUID()
await drizzle.insert(sessions).values({
id: sessionToken,
userId: user.id,
userid: user.id,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
})
// 设置cookie
const response = {
const cookieStore = await cookies()
cookieStore.set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
})
return {
success: true,
user: {
id: user.id,
@@ -60,15 +59,6 @@ export async function login(props: {
name: user.name,
},
}
const cookieStore = await cookies()
cookieStore.set('session', sessionToken, {
httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
})
return response
}
catch (error) {
console.error('登录错误:', error)
@@ -86,24 +76,19 @@ export async function logout() {
// 删除数据库中的session如果存在
if (sessionToken) {
await prisma.session.deleteMany({
where: { id: sessionToken },
}).catch(() => {
// 忽略删除错误确保cookie被清除
})
await drizzle.delete(sessions).where(eq(sessions.id, sessionToken))
}
// 清除cookie
const response = { success: true }
cookieStore.set('session', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // 立即过期
path: '/',
})
return response
return {
success: true,
}
}
catch (error) {
console.error('退出错误:', error)

View File

@@ -1,18 +1,47 @@
'use server'
import prisma from '@/lib/prisma'
import { log } from 'console'
import drizzle, { eq, gateway } from '@/lib/drizzle'
import z from 'zod'
export async function findConfigs(params: {
macaddr: string
}) {
try {
return await prisma.gateway.findMany({
where: {
macaddr: params.macaddr,
},
return await drizzle.query.gateway.findMany({
where: eq(gateway.macaddr, params.macaddr),
})
}
catch (e) {
throw new Error('查询配置失败: ' + (e as Error).message)
}
}
const pageConfigsParams = z.object({
macaddr: z.string().min(1, 'MAC地址不能为空').trim(),
page: z.number().min(1).default(1),
size: z.number().min(10).max(250).default(100),
})
export async function pageConfigs(rawParams: z.infer<typeof pageConfigsParams>) {
try {
const params = pageConfigsParams.parse(rawParams)
const offset = (params.page - 1) * params.size
const limit = params.size
const [data, total] = await Promise.all([
drizzle.select().from(gateway).where(eq(gateway.macaddr, params.macaddr)).offset(offset).limit(limit),
drizzle.$count(gateway, eq(gateway.macaddr, params.macaddr)),
])
return {
success: true,
data,
total,
page: params.page,
size: params.size,
}
}
catch (e) {
throw new Error('获取配置失败: ' + (e as Error).message)
}
}

View File

@@ -1,49 +1,48 @@
'use server'
import prisma from '@/lib/prisma'
import { Page, Res } from '@/lib/api'
import drizzle, { change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
export type AllocationStatus = {
city: string
count: bigint
assigned: bigint
count: number
assigned: number
}
// 城市分配状态
export async function getAllocationStatus(hours: number) {
export async function getAllocationStatus(hours: number = 24) {
try {
const hoursNum = hours || 24
// 使用参数化查询防止SQL注入
const result: AllocationStatus[] = await prisma.$queryRaw`
SELECT
city,
c1.count AS count,
c2.assigned AS assigned
FROM
cityhash
LEFT JOIN (
SELECT
city_id,
COUNT(*) AS count
FROM
edge
WHERE
active = 1
GROUP BY
city_id
) c1 ON c1.city_id = cityhash.id
LEFT JOIN (
SELECT
city AS city_id,
COUNT(*) AS assigned
FROM
\`change\`
WHERE
time > NOW() - INTERVAL ${hoursNum} HOUR
GROUP BY
city
) c2 ON c2.city_id = cityhash.id
WHERE
cityhash.macaddr IS NOT NULL;
`
const c1 = drizzle
.select({
cityId: edge.cityId,
count: count().as('count'),
})
.from(edge)
.where(eq(edge.active, 1))
.groupBy(edge.cityId)
.as('c1')
const c2 = drizzle
.select({
cityId: change.city,
assigned: count().as('assigned'),
})
.from(change)
.where(sql`time > NOW() - INTERVAL ${hours} HOUR`)
.groupBy(change.city)
.as('c2')
const result = await drizzle
.select({
city: cityhash.city,
count: sql<number>`c1.count`,
assigned: sql<number>`ifnull(c2.assigned, 0)`,
})
.from(cityhash)
.leftJoin(c1, eq(c1.cityId, cityhash.id))
.leftJoin(c2, eq(c2.cityId, cityhash.id))
.where(sql`cityhash.macaddr is not null`)
return {
success: true,
data: result,
@@ -70,11 +69,16 @@ export type GatewayInfo = {
// 获取网关基本信息
export async function getGatewayInfo() {
try {
const result: GatewayInfo[] = await prisma.$queryRaw`
SELECT macaddr, inner_ip, setid, enable
FROM token
ORDER BY macaddr
`
const result = await drizzle
.select({
macaddr: token.macaddr,
inner_ip: token.innerIp,
setid: token.setid,
enable: token.enable,
})
.from(token)
.orderBy(token.macaddr)
return {
success: true,
data: result,
@@ -91,88 +95,63 @@ export async function getGatewayInfo() {
}
export type GatewayConfig = {
city: string
city: string | null
edge: string
user: string
public: string
public: string | null
inner_ip: string
ischange: number
isonline: number
}
// 网关配置
export async function getGatewayConfig(off: number, itemsPerPage: number, mac?: string) {
export async function getGatewayConfig(page?: number, mac?: string): Promise<Res<Page<GatewayConfig>>> {
try {
const offset = off || 0
const limit = itemsPerPage || 100
// 获取总数
let totalCountQuery = ''
let totalCountParams: (string | number)[] = []
if (mac) {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
LEFT JOIN edge ON edge.macaddr = gateway.edge
WHERE gateway.macaddr = ?
`
totalCountParams = [mac]
}
else {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
`
if (!page && !mac) {
throw new Error('页码和MAC地址不能同时为空')
}
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
totalCountQuery,
...totalCountParams,
)
const totalCount = Number(totalCountResult[0]?.total || 0)
// 获取分页数据
let query = `
select edge, city, user, public, inner_ip, ischange, isonline
from
gateway
left join cityhash
on cityhash.hash = gateway.cityhash
left join edge
on edge.macaddr = gateway.edge
`
let params: (string | number)[] = []
if (mac) {
query += ' WHERE gateway.macaddr = ?'
params = [mac]
}
else {
query += ' LIMIT ? OFFSET ?'
params.push(limit, offset)
}
// 指定返回类型
const result: GatewayConfig[] = await prisma.$queryRawUnsafe<GatewayConfig[]>(query, ...params)
page = mac ? 1 : Math.max(1, page || 1)
const [total, result] = await Promise.all([
drizzle.$count(gateway, mac ? eq(gateway.macaddr, mac) : undefined),
drizzle
.select({
city: cityhash.city,
edge: gateway.edge,
user: gateway.user,
public: edge.public,
inner_ip: gateway.network,
ischange: gateway.ischange,
isonline: gateway.isonline,
})
.from(gateway)
.leftJoin(cityhash, eq(cityhash.hash, gateway.cityhash))
.leftJoin(edge, eq(edge.macaddr, gateway.edge))
.where(mac ? eq(gateway.macaddr, mac) : undefined)
.orderBy(gateway.macaddr, sql`cast(regexp_replace(gateway.network, '172.30.168.', '') as unsigned)`)
.offset((page - 1) * 250)
.limit(250),
])
return {
data: result,
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit),
success: true,
data: {
total,
page,
size: 250,
items: result,
},
}
}
catch (error) {
console.error('Gateway config query error:', error)
return {
success: false,
data: [],
error: '查询网关配置失败',
}
}
}
export type CityNode = {
city: string
count: number
@@ -184,25 +163,29 @@ export type CityNode = {
// 城市节点数量分布
export async function getCityNodeCount() {
try {
const result: CityNode[] = await prisma.$queryRaw`
select c.city, c.hash, c.label, e.count, c.\`offset\`
from
cityhash c
left join (
select city_id, count(*) as count
from
edge
where
edge.active is true
group by
city_id
) e
on c.id = e.city_id
group by
c.hash
order by
count desc
`
const e = drizzle
.select({
cityId: edge.cityId,
count: count().as('count'),
})
.from(edge)
.where(eq(edge.active, 1))
.groupBy(edge.cityId)
.as('e')
const result = await drizzle
.select({
city: cityhash.city,
hash: cityhash.hash,
label: cityhash.label,
count: sql<number>`ifnull(e.count, 0)`,
offset: cityhash.offset,
})
.from(cityhash)
.leftJoin(e, eq(e.cityId, cityhash.id))
.groupBy(cityhash.hash)
.orderBy(desc(sql`e.count`))
return {
success: true,
data: result,
@@ -219,32 +202,39 @@ export async function getCityNodeCount() {
}
// 获取节点信息
export async function getEdgeNodes(off: number, itemsPerPage: number) {
export async function getEdgeNodes(page: number, size: number) {
try {
const offset = off || 0
const limit = itemsPerPage || 100
// 获取总数 - 使用类型断言
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
SELECT COUNT(*) as total
FROM edge
WHERE active = true
`
const totalCount = Number(totalCountResult[0]?.total || 0)
const offset = Math.max(0, (page - 1)) * size
const limit = Math.min(100, Math.max(10, size))
const [total, data] = await Promise.all([
drizzle.$count(edge, eq(edge.active, 1)),
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(eq(edge.active, 1))
.orderBy(edge.id)
.offset(offset)
.limit(limit),
])
// 获取分页数据
const result = await prisma.$queryRaw`
SELECT edge.id, edge.macaddr, city, public, isp, single, sole, arch, online
FROM edge
LEFT JOIN cityhash ON cityhash.id = edge.city_id
WHERE edge.active = true
ORDER BY edge.id
LIMIT ${limit} OFFSET ${offset}
`
return {
data: result,
totalCount: totalCount,
data,
totalCount: total,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit),
totalPages: Math.ceil(total / limit),
}
}
catch (error) {

View File

@@ -1,5 +1,6 @@
'use server'
import prisma from '@/lib/prisma'
import drizzle, { desc, eq, users } from '@/lib/drizzle'
import { first } from '@/lib/utils'
import { hash } from 'bcryptjs'
type User = {
@@ -17,22 +18,20 @@ export async function findUsers(): Promise<{
error: string
}> {
try {
const users = await prisma.user.findMany({
select: {
id: true,
account: true,
name: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc',
},
const result = await drizzle
.select({
id: users.id,
account: users.account,
name: users.name,
createdAt: users.createdat,
updatedAt: users.updatedat,
})
.from(users)
.orderBy(desc(users.createdat))
return {
success: true,
data: users,
data: result,
error: '',
}
}
@@ -54,11 +53,10 @@ export async function createUser(params: {
}) {
try {
// 检查用户是否已存在
const existingUser = await prisma.user.findUnique({
where: { account: params.account },
const user = await drizzle.query.users.findFirst({
where: eq(users.account, params.account),
})
if (existingUser) {
if (user) {
return {
success: false,
error: '用户账号已存在',
@@ -69,20 +67,31 @@ export async function createUser(params: {
const hashedPassword = await hash(params.password, 10)
// 创建用户
const user = await prisma.user.create({
data: {
const id = first(
await drizzle
.insert(users)
.values({
account: params.account,
password: hashedPassword,
name: params.name || params.account,
},
})
}).$returningId(),
r => r.id,
)
if (!id) {
return {
success: false,
error: '创建用户失败',
}
}
// 不返回密码字段
const { password: _, ...userWithoutPassword } = user
return {
success: true,
user: userWithoutPassword,
user: {
id,
account: params.account,
name: params.name || params.account,
},
}
}
catch (error) {
@@ -97,38 +106,10 @@ export async function createUser(params: {
// 删除用户
export async function removeUser(id: number) {
try {
if (!id) {
return {
success: false,
error: '用户ID不能为空',
}
}
await drizzle
.delete(users)
.where(eq(users.id, id))
// const userId = parseInt(id)
if (isNaN(id)) {
return {
success: false,
error: '无效的用户ID',
}
}
// 检查用户是否存在
const existingUser = await prisma.user.findUnique({
where: { id: id },
})
if (!existingUser) {
return {
status: 404,
success: false,
error: '用户不存在',
}
}
// 删除用户
await prisma.user.delete({
where: { id: id },
})
return {
success: true,
message: '用户删除成功',

View File

@@ -40,11 +40,11 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
// 数据验证
const validatedData = (result.data).map(item => ({
city: item.city || '未知',
count: item.count || BigInt(0),
assigned: item.assigned || BigInt(0),
count: item.count,
assigned: item.assigned,
}))
const sortedData = validatedData.sort((a, b) => Number(b.count - a.count))
const sortedData = validatedData.sort((a, b) => b.count - a.count)
setData(sortedData)
}

View File

@@ -1,7 +1,6 @@
'use client'
import { useEffect, useState } from 'react'
import { validateNumber } from '@/lib/utils'
import { Pagination } from '@/components/ui/pagination'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { getEdgeNodes } from '@/actions/stats'
@@ -56,15 +55,15 @@ export default function Edge() {
}
const validatedData = (result.data as ResultEdge[]).map(item => ({
id: validateNumber(item.id),
id: item.id,
macaddr: item.macaddr || '',
city: item.city || '',
public: item.public || '',
isp: item.isp || '',
single: item.single,
sole: item.sole,
arch: validateNumber(item.arch),
online: validateNumber(item.online),
arch: item.arch,
online: item.online,
}))
setData(validatedData)

View File

@@ -4,79 +4,56 @@ 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'
// interface GatewayConfig {
// city: string
// edge: string
// user: string
// public: string
// inner_ip: string
// ischange: number
// isonline: number
// }
import { toast } from 'sonner'
function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([])
const [loading, setLoading] = useState(false)
const [macAddress, setMacAddress] = useState('')
const [error, setError] = useState('')
const searchParams = useSearchParams()
// 分页状态
const [currentPage, setCurrentPage] = useState(1)
const [itemsPerPage, setItemsPerPage] = useState(100)
const [totalItems, setTotalItems] = useState(0)
// 判断是否为MAC地址查询用于控制分页显示
const isMacQuery = !!macAddress
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
// 监听URL的mac参数变化
useEffect(() => {
const urlMac = searchParams.get('mac')
if (urlMac) {
setMacAddress(urlMac)
setCurrentPage(1) // 重置到第一页
fetchData(urlMac, 1, itemsPerPage)
setPage(1) // 重置到第一页
fetchData(urlMac, 1)
}
else {
setMacAddress('')
setCurrentPage(1) // 重置到第一页
fetchData('', 1, itemsPerPage)
setPage(1) // 重置到第一页
fetchData('', 1)
}
}, [searchParams])
const fetchData = async (mac: string, page: number = 1, limit: number = itemsPerPage) => {
const fetchData = async (mac: string, page: number = 1) => {
setLoading(true)
setError('')
try {
// 计算偏移量
const offset = (page - 1) * limit
const result = await getGatewayConfig(offset, limit, mac)
// 检查返回的数据是否有效
if (!result.data || result.data.length === 0) {
if (mac.trim()) {
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
}
else {
setError('未找到任何网关配置信息')
}
setData([])
setTotalItems(0)
return
const result = await getGatewayConfig(page, mac)
if (!result.success) {
throw new Error(result.error || '查询网关配置失败')
}
setData(result.data)
const shrink = ['黔东南', '延边']
result.data.items.forEach((item) => {
shrink.forEach((s) => {
if (item.city?.startsWith(s)) {
item.city = s
}
})
})
setTotalItems(result.totalCount || 0)
setData(result.data.items)
setTotal(result.data.total)
}
catch (error) {
console.error('获取网关配置失败:', error)
setError(error instanceof Error ? error.message : '获取网关配置失败')
setData([])
setTotalItems(0)
toast.error((error as Error).message || '获取网关配置失败')
}
finally {
setLoading(false)
@@ -85,28 +62,26 @@ function GatewayConfigContent() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setCurrentPage(1) // 重置到第一页
fetchData(macAddress, 1, itemsPerPage)
setPage(1) // 重置到第一页
fetchData(macAddress, 1)
}
// 处理页码变化
const handlePageChange = (page: number) => {
setCurrentPage(page)
fetchData(macAddress, page, itemsPerPage)
setPage(page)
fetchData(macAddress, page)
}
// 处理每页显示数量变化
const handleSizeChange = (size: number) => {
setItemsPerPage(size)
setCurrentPage(1)
fetchData(macAddress, 1, size)
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'}`}>
<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>
)
@@ -139,7 +114,7 @@ function GatewayConfigContent() {
value={macAddress}
onChange={e => setMacAddress(e.target.value)}
placeholder="输入MAC地址查询"
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
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"
@@ -148,17 +123,6 @@ function GatewayConfigContent() {
</button>
</div>
{error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
<div className="flex items-center text-red-800">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
{error}
</div>
</div>
)}
</form>
{loading ? (
@@ -173,11 +137,11 @@ function GatewayConfigContent() {
<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">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>
<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>
@@ -185,21 +149,11 @@ function GatewayConfigContent() {
<TableBody>
{data.map((item, index) => (
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<TableCell>
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
{item.city}
</span>
</TableCell>
<TableCell>
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
</TableCell>
<TableCell>
<div className="font-mono text-sm text-green-600">{item.public}</div>
</TableCell>
<TableCell className="text-sm text-gray-900">{item.user}</TableCell>
<TableCell>
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
</TableCell>
<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>
@@ -213,7 +167,7 @@ function GatewayConfigContent() {
</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">{totalItems}</div>
<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">
@@ -231,12 +185,13 @@ function GatewayConfigContent() {
</div>
</div>
{/* 分页组件 - 仅在非MAC查询时显示 */}
{!isMacQuery && (
{/* 分页组件 */}
{!macAddress && (
<Pagination
page={currentPage}
size={itemsPerPage}
total={totalItems}
total={total}
page={page}
size={250}
sizeOptions={[250]}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
className="mt-4"

16
src/lib/api.ts Normal file
View File

@@ -0,0 +1,16 @@
export type Res<T> = {
success: true
data: T
} | {
success: false
error: string
}
export type Page<T> = {
total: number
page: number
size: number
items: T[]
}
export type ResData<T extends (...args: never) => unknown> = Awaited<ReturnType<T>> extends Res<infer D> ? D : never

21
src/lib/drizzle/index.ts Normal file
View File

@@ -0,0 +1,21 @@
import 'dotenv/config'
import { drizzle as client } from 'drizzle-orm/mysql2'
import * as schema from './schema'
declare global {
var drizzle: ReturnType<typeof client<typeof schema>> | undefined
}
const { DATABASE_URL } = process.env
if (!DATABASE_URL) {
throw new Error('DATABASE_URL is not set')
}
const drizzle = global.drizzle || client(DATABASE_URL, { mode: 'default', schema })
if (process.env.NODE_ENV !== 'production') {
global.drizzle = drizzle
}
export default drizzle
export * from './schema'
export * from 'drizzle-orm'

95
src/lib/drizzle/schema.ts Normal file
View File

@@ -0,0 +1,95 @@
import { mysqlTable, int, timestamp, varchar, datetime, text, tinyint } from 'drizzle-orm/mysql-core'
export const change = mysqlTable('change', {
id: int().autoincrement().notNull().primaryKey(),
time: timestamp({ mode: 'date' }),
city: int(),
macaddr: varchar({ length: 20 }).notNull(),
edgeNew: varchar('edge_new', { length: 20 }).notNull(),
edgeOld: varchar('edge_old', { length: 20 }),
info: varchar({ length: 500 }).notNull(),
network: varchar({ length: 20 }).notNull(),
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
})
export const cityhash = mysqlTable('cityhash', {
id: int().autoincrement().notNull().primaryKey(),
index: int(),
macaddr: varchar({ length: 20 }),
city: varchar({ length: 20 }).notNull(),
num: int().notNull(),
hash: varchar({ length: 100 }).notNull(),
label: varchar({ length: 20 }),
count: int().default(0).notNull(),
offset: int().default(0).notNull(),
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
})
export const edge = mysqlTable('edge', {
id: int().autoincrement().notNull().primaryKey(),
macaddr: varchar({ length: 17 }).notNull(),
public: varchar({ length: 255 }).notNull(),
isp: varchar({ length: 255 }).notNull(),
single: tinyint().notNull(),
sole: tinyint().notNull(),
arch: tinyint().notNull(),
online: int().default(0).notNull(),
cityId: int('city_id').notNull(),
active: tinyint().notNull(),
})
export const gateway = mysqlTable('gateway', {
id: int().autoincrement().notNull().primaryKey(),
macaddr: varchar({ length: 20 }).notNull(),
table: int().notNull(),
edge: varchar({ length: 20 }).notNull(),
network: varchar({ length: 20 }).notNull(),
cityhash: varchar({ length: 100 }).notNull(),
label: varchar({ length: 20 }),
user: varchar({ length: 20 }).notNull(),
innerIp: varchar('inner_ip', { length: 20 }).notNull(),
ischange: tinyint().default(0).notNull(),
isonline: tinyint().default(0).notNull(),
onlinenum: int().default(0).notNull(),
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
})
export const sessions = mysqlTable('sessions', {
id: varchar({ length: 191 }).notNull().primaryKey(),
expires: datetime({ mode: 'date' }).notNull(),
userid: int().notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
createdat: datetime({ mode: 'date' }).default(new Date()).notNull(),
})
export const submit = mysqlTable('submit', {
id: int().autoincrement().notNull().primaryKey(),
time: datetime({ mode: 'date' }).notNull(),
gateway: varchar({ length: 20 }).notNull(),
config: text(),
})
export const token = mysqlTable('token', {
id: int().autoincrement().notNull().primaryKey(),
setid: int().default(1).notNull(),
changeCount: int('change_count').notNull(),
limitCount: int('limit_count').default(32000).notNull(),
token: varchar({ length: 1000 }).notNull(),
macaddr: varchar({ length: 100 }).notNull(),
tokenTime: datetime('token_time', { mode: 'date' }).notNull(),
innerIp: varchar('inner_ip', { length: 20 }).notNull(),
l2Ip: varchar({ length: 20 }),
enable: tinyint().default(1).notNull(),
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
})
export const users = mysqlTable('users', {
id: int().autoincrement().notNull().primaryKey(),
name: varchar({ length: 191 }),
password: varchar({ length: 191 }).notNull(),
account: varchar({ length: 191 }).notNull(),
createdat: datetime({ mode: 'date' }).default(new Date()).notNull(),
updatedat: datetime({ mode: 'date' }).default(new Date()).notNull(),
})

View File

@@ -1,15 +0,0 @@
import 'server-only'
import { PrismaClient } from '@/generated/prisma/client'
const globalForPrisma = global as unknown as {
prisma: PrismaClient | undefined
}
const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
export default prisma
export * from '@/generated/prisma/client'

View File

@@ -6,12 +6,11 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 数据验证函数
export const validateNumber = (value: unknown): number => {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const num = parseInt(value)
return isNaN(num) ? 0 : num
}
return 0
// 取数组第一个元素,支持映射
export function first<T>(array: T[]): T | undefined
export function first<T, I>(array: T[], select: (item: T) => I): I | undefined
export function first<T, I>(array: T[], select?: (item: T) => I) {
const item = array.length > 0 ? array[0] : undefined
return select && item ? select(item) : item
}