Przeglądaj źródła

chore: migrate from ESLint + Prettier to Biome

- Install @biomejs/[email protected] as the unified linter and formatter
- Create biome.json with project-specific configuration:
  - Disable a11y rules (not in original ESLint config)
  - Configure rules to match original ESLint behavior
  - Enable Tailwind CSS directive support in CSS parser
  - Add overrides for src/components/ui/** (shadcn components)
- Update package.json scripts: lint, lint:fix, format, format:check
- Update next.config.ts to disable ESLint during builds
- Remove eslint.config.mjs and .prettierrc.json
- Update CONTRIBUTING.md to reference Biome instead of ESLint/Prettier
- Format entire codebase with Biome
- Fix noEmptyPattern error in rate-limit-dashboard.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
ding113 2 miesięcy temu
rodzic
commit
ca835a95cd
100 zmienionych plików z 902 dodań i 1249 usunięć
  1. 0 12
      .prettierrc.json
  2. 2 2
      CONTRIBUTING.md
  3. 96 0
      biome.json
  4. 26 468
      bun.lock
  5. 0 23
      eslint.config.mjs
  6. 1 1
      messages/en/index.ts
  7. 0 1
      messages/ja/dashboard.json
  8. 1 1
      messages/ru/index.ts
  9. 0 1
      messages/zh-CN/dashboard.json
  10. 0 1
      messages/zh-TW/dashboard.json
  11. 1 1
      messages/zh-TW/index.ts
  12. 5 0
      next.config.ts
  13. 5 8
      package.json
  14. 5 5
      scripts/extract-translations.ts
  15. 4 4
      scripts/sync-settings-keys.js
  16. 4 4
      src/actions/active-sessions.ts
  17. 2 2
      src/actions/client-versions.ts
  18. 1 1
      src/actions/concurrent-sessions.ts
  19. 7 8
      src/actions/dashboard-realtime.ts
  20. 4 4
      src/actions/error-rules.ts
  21. 8 8
      src/actions/keys.ts
  22. 6 6
      src/actions/model-prices.ts
  23. 2 2
      src/actions/notifications.ts
  24. 3 3
      src/actions/overview.ts
  25. 4 4
      src/actions/provider-slots.ts
  26. 35 38
      src/actions/providers.ts
  27. 3 3
      src/actions/sensitive-words.ts
  28. 1 1
      src/actions/session-response.ts
  29. 10 10
      src/actions/statistics.ts
  30. 3 3
      src/actions/system-config.ts
  31. 1 1
      src/actions/usage-logs.ts
  32. 18 14
      src/actions/users.ts
  33. 5 6
      src/app/[locale]/dashboard/_components/dashboard-header.tsx
  34. 2 2
      src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx
  35. 4 4
      src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx
  36. 3 3
      src/app/[locale]/dashboard/_components/rate-limit-type-breakdown.tsx
  37. 5 7
      src/app/[locale]/dashboard/_components/statistics/chart.tsx
  38. 1 1
      src/app/[locale]/dashboard/_components/statistics/index.ts
  39. 2 3
      src/app/[locale]/dashboard/_components/statistics/time-range-selector.tsx
  40. 5 5
      src/app/[locale]/dashboard/_components/statistics/wrapper.tsx
  41. 2 2
      src/app/[locale]/dashboard/_components/user-menu.tsx
  42. 3 3
      src/app/[locale]/dashboard/_components/user-quick-overview.tsx
  43. 4 4
      src/app/[locale]/dashboard/_components/user/add-user-dialog.tsx
  44. 4 4
      src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx
  45. 8 8
      src/app/[locale]/dashboard/_components/user/forms/delete-key-confirm.tsx
  46. 7 7
      src/app/[locale]/dashboard/_components/user/forms/delete-user-confirm.tsx
  47. 5 5
      src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx
  48. 8 8
      src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
  49. 4 5
      src/app/[locale]/dashboard/_components/user/key-actions.tsx
  50. 3 3
      src/app/[locale]/dashboard/_components/user/key-limit-usage.tsx
  51. 10 11
      src/app/[locale]/dashboard/_components/user/key-list-header.tsx
  52. 41 44
      src/app/[locale]/dashboard/_components/user/key-list.tsx
  53. 4 4
      src/app/[locale]/dashboard/_components/user/user-actions.tsx
  54. 4 5
      src/app/[locale]/dashboard/_components/user/user-key-manager.tsx
  55. 4 5
      src/app/[locale]/dashboard/_components/user/user-list.tsx
  56. 8 9
      src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  57. 5 6
      src/app/[locale]/dashboard/availability/page.tsx
  58. 4 4
      src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  59. 7 7
      src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  60. 6 6
      src/app/[locale]/dashboard/leaderboard/page.tsx
  61. 130 103
      src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx
  62. 2 3
      src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx
  63. 12 8
      src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
  64. 19 18
      src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  65. 45 32
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  66. 58 51
      src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
  67. 20 13
      src/app/[locale]/dashboard/logs/page.tsx
  68. 6 6
      src/app/[locale]/dashboard/page.tsx
  69. 7 7
      src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx
  70. 7 7
      src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx
  71. 9 9
      src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx
  72. 5 5
      src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx
  73. 1 1
      src/app/[locale]/dashboard/quotas/layout.tsx
  74. 3 3
      src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx
  75. 1 1
      src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-sort-dropdown.tsx
  76. 4 4
      src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx
  77. 5 5
      src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx
  78. 3 3
      src/app/[locale]/dashboard/quotas/providers/page.tsx
  79. 5 5
      src/app/[locale]/dashboard/quotas/users/_components/users-quota-client.tsx
  80. 6 7
      src/app/[locale]/dashboard/quotas/users/page.tsx
  81. 8 8
      src/app/[locale]/dashboard/rate-limits/_components/rate-limit-dashboard.tsx
  82. 7 7
      src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx
  83. 2 2
      src/app/[locale]/dashboard/rate-limits/page.tsx
  84. 23 30
      src/app/[locale]/dashboard/sessions/[sessionId]/messages/page.tsx
  85. 5 5
      src/app/[locale]/dashboard/sessions/_components/active-sessions-table.tsx
  86. 4 5
      src/app/[locale]/dashboard/sessions/_components/session-messages-dialog.tsx
  87. 5 6
      src/app/[locale]/dashboard/sessions/page.tsx
  88. 1 1
      src/app/[locale]/dashboard/users/page.tsx
  89. 5 5
      src/app/[locale]/dashboard/users/users-page-client.tsx
  90. 23 22
      src/app/[locale]/internal/dashboard/big-screen/page.tsx
  91. 5 5
      src/app/[locale]/internal/data-gen/_components/data-generator-page.tsx
  92. 7 7
      src/app/[locale]/layout.tsx
  93. 5 5
      src/app/[locale]/login/page.tsx
  94. 2 3
      src/app/[locale]/settings/_components/settings-nav.tsx
  95. 4 4
      src/app/[locale]/settings/client-versions/_components/client-version-stats-table.tsx
  96. 5 5
      src/app/[locale]/settings/client-versions/_components/client-version-toggle.tsx
  97. 4 4
      src/app/[locale]/settings/client-versions/page.tsx
  98. 4 4
      src/app/[locale]/settings/config/_components/auto-cleanup-form.tsx
  99. 7 7
      src/app/[locale]/settings/config/_components/system-settings-form.tsx
  100. 2 2
      src/app/[locale]/settings/config/page.tsx

+ 0 - 12
.prettierrc.json

@@ -1,12 +0,0 @@
-{
-  "semi": true,
-  "trailingComma": "es5",
-  "singleQuote": false,
-  "printWidth": 100,
-  "tabWidth": 2,
-  "useTabs": false,
-  "arrowParens": "always",
-  "endOfLine": "lf",
-  "bracketSpacing": true,
-  "proseWrap": "preserve"
-}

+ 2 - 2
CONTRIBUTING.md

@@ -97,7 +97,7 @@ Claude Code Hub 是一个面向团队的 AI API 代理平台,支持统一管
 
 ### 7. 代码风格 Code Style
 
-- TypeScript + React 组件遵守 2 空格缩进、单引号和尾随逗号(ESLint/Prettier 默认配置)。
+- TypeScript + React 组件遵守 2 空格缩进、双引号和尾随逗号(Biome 默认配置)。
 - Tailwind CSS 样式与 JSX 同行,沿用 README 中的 emoji 样式和语气。
 - 工具函数保持单一职责,避免重复代码(DRY)。
 - 新增文件前参考 `src/` 下相同模块的实现,保持命名一致。
@@ -225,7 +225,7 @@ Example: `fix: handle redis timeout retry`
 
 ### 7. Code Style
 
-- Respect the shared ESLint + Prettier config (2-space indent, single quotes, trailing commas).
+- Respect the shared Biome config (2-space indent, double quotes, trailing commas).
 - Tailwind classes stay close to the JSX they style, mirroring patterns in `src/app`.
 - Keep utilities single-purpose and reuse helpers from `src/lib` or `src/actions` when possible.
 - Match the conversational tone (emojis + concise explanations) already used in `README.md`.

+ 96 - 0
biome.json

@@ -0,0 +1,96 @@
+{
+  "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
+  "vcs": {
+    "enabled": true,
+    "clientKind": "git",
+    "useIgnoreFile": true
+  },
+  "assist": { "actions": { "source": { "organizeImports": "on" } } },
+  "linter": {
+    "enabled": true,
+    "rules": {
+      "recommended": true,
+      "a11y": {
+        "recommended": false
+      },
+      "correctness": {
+        "noUnusedImports": "error",
+        "noUnusedVariables": "warn",
+        "useExhaustiveDependencies": "warn"
+      },
+      "suspicious": {
+        "noExplicitAny": "off",
+        "noArrayIndexKey": "off",
+        "noImplicitAnyLet": "off",
+        "noTemplateCurlyInString": "off",
+        "useIterableCallbackReturn": "off",
+        "noShadowRestrictedNames": "off",
+        "noAssignInExpressions": "off"
+      },
+      "style": {
+        "noNonNullAssertion": "off"
+      },
+      "complexity": {
+        "noStaticOnlyClass": "off"
+      },
+      "performance": {
+        "noImgElement": "off"
+      }
+    }
+  },
+  "formatter": {
+    "enabled": true,
+    "indentStyle": "space",
+    "indentWidth": 2,
+    "lineWidth": 100
+  },
+  "javascript": {
+    "formatter": {
+      "quoteStyle": "double",
+      "trailingCommas": "es5",
+      "arrowParentheses": "always",
+      "semicolons": "always"
+    }
+  },
+  "css": {
+    "parser": {
+      "cssModules": true,
+      "tailwindDirectives": true
+    },
+    "formatter": {
+      "enabled": true
+    },
+    "linter": {
+      "enabled": false
+    }
+  },
+  "overrides": [
+    {
+      "includes": ["**/src/components/ui/**"],
+      "linter": {
+        "rules": {
+          "suspicious": {
+            "noExplicitAny": "off"
+          },
+          "correctness": {
+            "noUnusedVariables": "off"
+          },
+          "security": {
+            "noDangerouslySetInnerHtml": "off"
+          }
+        }
+      }
+    }
+  ],
+  "files": {
+    "includes": [
+      "**",
+      "!**/node_modules",
+      "!**/.next",
+      "!**/dist",
+      "!**/*.d.ts",
+      "!**/drizzle",
+      "!**/docs-site"
+    ]
+  }
+}

Plik diff jest za duży
+ 26 - 468
bun.lock


+ 0 - 23
eslint.config.mjs

@@ -1,23 +0,0 @@
-import { dirname } from "path";
-import { fileURLToPath } from "url";
-import { FlatCompat } from "@eslint/eslintrc";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-const compat = new FlatCompat({
-  baseDirectory: __dirname,
-});
-
-const eslintConfig = [
-  ...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
-  {
-    files: ["src/components/ui/**"],
-    rules: {
-      "@typescript-eslint/no-explicit-any": "off",
-      "@typescript-eslint/no-unused-vars": "off",
-    },
-  },
-];
-
-export default eslintConfig;

+ 1 - 1
messages/en/index.ts

@@ -5,6 +5,7 @@ import customs from "./customs.json";
 import dashboard from "./dashboard.json";
 import errors from "./errors.json";
 import forms from "./forms.json";
+import internal from "./internal.json";
 import notifications from "./notifications.json";
 import providerChain from "./provider-chain.json";
 import providers from "./providers.json";
@@ -14,7 +15,6 @@ import ui from "./ui.json";
 import usage from "./usage.json";
 import users from "./users.json";
 import validation from "./validation.json";
-import internal from "./internal.json";
 
 export default {
   auth,

+ 0 - 1
messages/ja/dashboard.json

@@ -57,7 +57,6 @@
       "provider": "プロバイダー",
       "model": "モデル",
       "endpoint": "エンドポイント",
-      "endpoint": "エンドポイント",
       "status": "ステータス",
       "timeRange": "時間範囲",
       "startTime": "開始時間",

+ 1 - 1
messages/ru/index.ts

@@ -5,6 +5,7 @@ import customs from "./customs.json";
 import dashboard from "./dashboard.json";
 import errors from "./errors.json";
 import forms from "./forms.json";
+import internal from "./internal.json";
 import notifications from "./notifications.json";
 import providerChain from "./provider-chain.json";
 import providers from "./providers.json";
@@ -14,7 +15,6 @@ import ui from "./ui.json";
 import usage from "./usage.json";
 import users from "./users.json";
 import validation from "./validation.json";
-import internal from "./internal.json";
 
 export default {
   auth,

+ 0 - 1
messages/zh-CN/dashboard.json

@@ -57,7 +57,6 @@
       "provider": "供应商",
       "model": "模型",
       "endpoint": "端点",
-      "endpoint": "端点",
       "status": "状态",
       "timeRange": "时间范围",
       "startTime": "开始时间",

+ 0 - 1
messages/zh-TW/dashboard.json

@@ -57,7 +57,6 @@
       "provider": "供應商",
       "model": "模型",
       "endpoint": "端點",
-      "endpoint": "端點",
       "status": "狀態",
       "timeRange": "時間範圍",
       "startTime": "開始時間",

+ 1 - 1
messages/zh-TW/index.ts

@@ -5,6 +5,7 @@ import customs from "./customs.json";
 import dashboard from "./dashboard.json";
 import errors from "./errors.json";
 import forms from "./forms.json";
+import internal from "./internal.json";
 import notifications from "./notifications.json";
 import providerChain from "./provider-chain.json";
 import providers from "./providers.json";
@@ -14,7 +15,6 @@ import ui from "./ui.json";
 import usage from "./usage.json";
 import users from "./users.json";
 import validation from "./validation.json";
-import internal from "./internal.json";
 
 export default {
   auth,

+ 5 - 0
next.config.ts

@@ -7,6 +7,11 @@ const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
 const nextConfig: NextConfig = {
   output: "standalone",
 
+  // Disable ESLint during builds (using Biome instead)
+  eslint: {
+    ignoreDuringBuilds: true,
+  },
+
   // 转译 ESM 模块(@lobehub/icons 需要)
   transpilePackages: ["@lobehub/icons"],
 

+ 5 - 8
package.json

@@ -6,10 +6,11 @@
     "dev": "next dev --port 13500 --turbo",
     "build": "next build && cp VERSION .next/standalone/VERSION",
     "start": "next start",
-    "lint": "next lint",
+    "lint": "biome check .",
+    "lint:fix": "biome check --write .",
     "typecheck": "tsc -p tsconfig.json --noEmit",
-    "format": "prettier --write .",
-    "format:check": "prettier --check .",
+    "format": "biome format --write .",
+    "format:check": "biome format .",
     "cui": "npx cui-server --host 0.0.0.0 --port 30000 --token a7564bc8882aa9a2d25d8b4ea6ea1e2e",
     "db:generate": "drizzle-kit generate && node scripts/validate-migrations.js",
     "db:migrate": "drizzle-kit migrate",
@@ -76,7 +77,7 @@
     "zod": "^4.1.12"
   },
   "devDependencies": {
-    "@eslint/eslintrc": "^3.3.1",
+    "@biomejs/biome": "^2.3.8",
     "@tailwindcss/postcss": "^4.1.16",
     "@types/ioredis": "^5.0.0",
     "@types/node": "^20.19.13",
@@ -85,10 +86,6 @@
     "@types/react-dom": "^19.2.2",
     "bun-types": "^1.3.2",
     "drizzle-kit": "^0.31.5",
-    "eslint": "^9.38.0",
-    "eslint-config-next": "15.5.6",
-    "eslint-config-prettier": "^10.1.8",
-    "prettier": "^3.6.2",
     "tailwindcss": "^4.1.16",
     "typescript": "^5.9.3"
   },

+ 5 - 5
scripts/extract-translations.ts

@@ -14,8 +14,8 @@
  *   --target     Target directory to scan (default: src/app/[locale])
  */
 
-import * as fs from "fs";
-import * as path from "path";
+import * as fs from "node:fs";
+import * as path from "node:path";
 
 // Translation key naming convention: namespace.section.key
 // Example: dashboard.stats.totalRequests, settings.providers.addButton
@@ -38,7 +38,7 @@ interface ExtractionReport {
 }
 
 // Chinese character detection regex
-const CHINESE_REGEX = /[\u4e00-\u9fa5]+/g;
+const _CHINESE_REGEX = /[\u4e00-\u9fa5]+/g;
 
 // Namespace mapping based on file path
 const NAMESPACE_MAP: Record<string, string> = {
@@ -163,7 +163,7 @@ function generateKey(
 
   // Use Pinyin-like mapping for key (simplified)
   const keyPart = Array.from(safeKey)
-    .map((char, i) => `key${i}`)
+    .map((_char, i) => `key${i}`)
     .join("");
 
   return {
@@ -272,7 +272,7 @@ function generateTranslationFiles(strings: ExtractedString[], dryRun: boolean):
     }
 
     if (!dryRun) {
-      fs.writeFileSync(translationFile, JSON.stringify(translations, null, 2) + "\n", "utf-8");
+      fs.writeFileSync(translationFile, `${JSON.stringify(translations, null, 2)}\n`, "utf-8");
       console.log(`✓ Updated ${namespace}.json with ${items.length} strings`);
     } else {
       console.log(`[DRY RUN] Would update ${namespace}.json with ${items.length} strings`);

+ 4 - 4
scripts/sync-settings-keys.js

@@ -5,8 +5,8 @@
  * - Fills missing keys with zh-CN text as placeholder
  * - Drops extra keys not present in zh-CN (notably for en, but applies consistently)
  */
-const fs = require("fs");
-const path = require("path");
+const fs = require("node:fs");
+const path = require("node:path");
 
 const ROOT = process.cwd();
 const MESSAGES_DIR = path.join(ROOT, "messages");
@@ -37,7 +37,7 @@ function mergeWithCanonical(cn, target) {
       result[k] = mergeWithCanonical(v, tchild);
     } else {
       // Canonical expects a leaf (string/number/bool/array/null). If target is an object, ignore it.
-      if (Object.prototype.hasOwnProperty.call(target || {}, k) && !isObject(tVal)) {
+      if (Object.hasOwn(target || {}, k) && !isObject(tVal)) {
         result[k] = tVal;
       } else {
         result[k] = v;
@@ -61,7 +61,7 @@ function loadJSON(p) {
 }
 
 function saveJSON(p, data) {
-  fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf8");
+  fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
 }
 
 function ensureSettings(locale) {

+ 4 - 4
src/actions/active-sessions.ts

@@ -1,15 +1,15 @@
 "use server";
 
-import { logger } from "@/lib/logger";
 import { getSession } from "@/lib/auth";
-import type { ActionResult } from "./types";
-import type { ActiveSessionInfo } from "@/types/session";
 import {
   getActiveSessionsCache,
-  setActiveSessionsCache,
   getSessionDetailsCache,
+  setActiveSessionsCache,
   setSessionDetailsCache,
 } from "@/lib/cache/session-cache";
+import { logger } from "@/lib/logger";
+import type { ActiveSessionInfo } from "@/types/session";
+import type { ActionResult } from "./types";
 
 /**
  * 获取所有活跃 session 的详细信息(使用聚合数据 + 批量查询 + 缓存)

+ 2 - 2
src/actions/client-versions.ts

@@ -1,8 +1,8 @@
 "use server";
 
-import { logger } from "@/lib/logger";
-import { ClientVersionChecker, type ClientVersionStats } from "@/lib/client-version-checker";
 import { getSession } from "@/lib/auth";
+import { ClientVersionChecker, type ClientVersionStats } from "@/lib/client-version-checker";
+import { logger } from "@/lib/logger";
 import type { ActionResult } from "./types";
 
 /**

+ 1 - 1
src/actions/concurrent-sessions.ts

@@ -1,7 +1,7 @@
 "use server";
 
-import { getActiveConcurrentSessions } from "@/lib/redis";
 import { logger } from "@/lib/logger";
+import { getActiveConcurrentSessions } from "@/lib/redis";
 import type { ActionResult } from "./types";
 
 /**

+ 7 - 8
src/actions/dashboard-realtime.ts

@@ -1,23 +1,22 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
 import { logger } from "@/lib/logger";
-import type { ActionResult } from "./types";
-
-// 导入已有的接口和方法
-import { getOverviewData, type OverviewData } from "./overview";
+import { findRecentActivityStream } from "@/repository/activity-stream";
 import {
   findDailyLeaderboard,
-  findDailyProviderLeaderboard,
   findDailyModelLeaderboard,
+  findDailyProviderLeaderboard,
   type LeaderboardEntry,
-  type ProviderLeaderboardEntry,
   type ModelLeaderboardEntry,
+  type ProviderLeaderboardEntry,
 } from "@/repository/leaderboard";
+import { getSystemSettings } from "@/repository/system-config";
+// 导入已有的接口和方法
+import { getOverviewData, type OverviewData } from "./overview";
 import { getProviderSlots, type ProviderSlotInfo } from "./provider-slots";
 import { getUserStatistics } from "./statistics";
-import { findRecentActivityStream } from "@/repository/activity-stream";
+import type { ActionResult } from "./types";
 
 /**
  * 实时活动流条目

+ 4 - 4
src/actions/error-rules.ts

@@ -1,12 +1,12 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
-import * as repo from "@/repository/error-rules";
-import { errorRuleDetector } from "@/lib/error-rule-detector";
-import { logger } from "@/lib/logger";
+import safeRegex from "safe-regex";
 import { getSession } from "@/lib/auth";
 import { validateErrorOverrideResponse } from "@/lib/error-override-validator";
-import safeRegex from "safe-regex";
+import { errorRuleDetector } from "@/lib/error-rule-detector";
+import { logger } from "@/lib/logger";
+import * as repo from "@/repository/error-rules";
 import type { ActionResult } from "./types";
 
 /** 覆写状态码最小值 */

+ 8 - 8
src/actions/keys.ts

@@ -1,23 +1,23 @@
 "use server";
 
+import { randomBytes } from "node:crypto";
 import { revalidatePath } from "next/cache";
+import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
-import { randomBytes } from "node:crypto";
 import { KeyFormSchema } from "@/lib/validation/schemas";
+import type { KeyStatistics } from "@/repository/key";
 import {
+  countActiveKeysByUser,
   createKey,
-  updateKey,
   deleteKey,
   findActiveKeyByUserIdAndName,
   findKeyById,
-  countActiveKeysByUser,
-  findKeysWithStatistics,
   findKeyList,
+  findKeysWithStatistics,
+  updateKey,
 } from "@/repository/key";
-import { getSession } from "@/lib/auth";
-import type { ActionResult } from "./types";
-import type { KeyStatistics } from "@/repository/key";
 import type { Key } from "@/types/key";
+import type { ActionResult } from "./types";
 
 // 添加密钥
 // 说明:为提升前端可控性,避免直接抛错,返回判别式结果。
@@ -117,7 +117,7 @@ export async function addKey(data: {
       };
     }
 
-    const generatedKey = "sk-" + randomBytes(16).toString("hex");
+    const generatedKey = `sk-${randomBytes(16).toString("hex")}`;
 
     // 转换 expiresAt: undefined → null(永不过期),string → Date(设置日期)
     const expiresAt =

+ 6 - 6
src/actions/model-prices.ts

@@ -1,25 +1,25 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
-import { logger } from "@/lib/logger";
 import { getSession } from "@/lib/auth";
+import { logger } from "@/lib/logger";
+import { getPriceTableJson } from "@/lib/price-sync";
 import {
-  findLatestPriceByModel,
   createModelPrice,
   findAllLatestPrices,
   findAllLatestPricesPaginated,
+  findLatestPriceByModel,
   hasAnyPriceRecords,
-  type PaginationParams,
   type PaginatedResult,
+  type PaginationParams,
 } from "@/repository/model-price";
 import type {
-  PriceTableJson,
-  PriceUpdateResult,
   ModelPrice,
   ModelPriceData,
+  PriceTableJson,
+  PriceUpdateResult,
 } from "@/types/model-price";
 import type { ActionResult } from "./types";
-import { getPriceTableJson } from "@/lib/price-sync";
 
 /**
  * 检查价格数据是否相同

+ 2 - 2
src/actions/notifications.ts

@@ -1,12 +1,12 @@
 "use server";
 
-import { WeChatBot } from "@/lib/wechat/bot";
 import { logger } from "@/lib/logger";
+import { WeChatBot } from "@/lib/wechat/bot";
 import {
   getNotificationSettings,
-  updateNotificationSettings,
   type NotificationSettings,
   type UpdateNotificationSettingsInput,
+  updateNotificationSettings,
 } from "@/repository/notifications";
 
 /**

+ 3 - 3
src/actions/overview.ts

@@ -1,10 +1,10 @@
 "use server";
 
-import { getOverviewMetrics as getOverviewMetricsFromDB } from "@/repository/overview";
-import { getConcurrentSessions as getConcurrentSessionsCount } from "./concurrent-sessions";
 import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
 import { logger } from "@/lib/logger";
+import { getOverviewMetrics as getOverviewMetricsFromDB } from "@/repository/overview";
+import { getSystemSettings } from "@/repository/system-config";
+import { getConcurrentSessions as getConcurrentSessionsCount } from "./concurrent-sessions";
 import type { ActionResult } from "./types";
 
 /**

+ 4 - 4
src/actions/provider-slots.ts

@@ -1,13 +1,13 @@
 "use server";
 
+import { and, eq, isNull } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { providers } from "@/drizzle/schema";
-import { eq, isNull, and } from "drizzle-orm";
-import { SessionTracker } from "@/lib/session-tracker";
-import { logger } from "@/lib/logger";
-import type { ActionResult } from "./types";
 import { getSession } from "@/lib/auth";
+import { logger } from "@/lib/logger";
+import { SessionTracker } from "@/lib/session-tracker";
 import { getSystemSettings } from "@/repository/system-config";
+import type { ActionResult } from "./types";
 
 /**
  * 供应商并发插槽信息

+ 35 - 38
src/actions/providers.ts

@@ -1,41 +1,41 @@
 "use server";
 
-import {
-  findProviderList,
-  createProvider,
-  updateProvider,
-  deleteProvider,
-  getProviderStatistics,
-  findProviderById,
-} from "@/repository/provider";
 import { revalidatePath } from "next/cache";
-import { logger } from "@/lib/logger";
-import { type ProviderDisplay, type ProviderType } from "@/types/provider";
-import { maskKey } from "@/lib/utils/validation";
+import { GeminiAuth } from "@/app/v1/_lib/gemini/auth";
+import { isClientAbortError } from "@/app/v1/_lib/proxy/errors";
 import { getSession } from "@/lib/auth";
-import { CreateProviderSchema, UpdateProviderSchema } from "@/lib/validation/schemas";
-import type { ActionResult } from "./types";
-import { getAllHealthStatus, resetCircuit, clearConfigCache } from "@/lib/circuit-breaker";
-import {
-  saveProviderCircuitConfig,
-  deleteProviderCircuitConfig,
-} from "@/lib/redis/circuit-breaker-config";
-import {
-  createProxyAgentForProvider,
-  isValidProxyUrl,
-  type ProviderProxyConfig,
-} from "@/lib/proxy-agent";
+import { clearConfigCache, getAllHealthStatus, resetCircuit } from "@/lib/circuit-breaker";
 import { CodexInstructionsCache } from "@/lib/codex-instructions-cache";
-import { isClientAbortError } from "@/app/v1/_lib/proxy/errors";
 import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
-import { GeminiAuth } from "@/app/v1/_lib/gemini/auth";
+import { logger } from "@/lib/logger";
 import {
   executeProviderTest,
   type ProviderTestConfig,
   type TestStatus,
   type TestSubStatus,
 } from "@/lib/provider-testing";
-import { getPresetsForProvider, type PresetConfig } from "@/lib/provider-testing/presets";
+import { getPresetsForProvider } from "@/lib/provider-testing/presets";
+import {
+  createProxyAgentForProvider,
+  isValidProxyUrl,
+  type ProviderProxyConfig,
+} from "@/lib/proxy-agent";
+import {
+  deleteProviderCircuitConfig,
+  saveProviderCircuitConfig,
+} from "@/lib/redis/circuit-breaker-config";
+import { maskKey } from "@/lib/utils/validation";
+import { CreateProviderSchema, UpdateProviderSchema } from "@/lib/validation/schemas";
+import {
+  createProvider,
+  deleteProvider,
+  findProviderById,
+  findProviderList,
+  getProviderStatistics,
+  updateProvider,
+} from "@/repository/provider";
+import type { ProviderDisplay, ProviderType } from "@/types/provider";
+import type { ActionResult } from "./types";
 
 const API_TEST_TIMEOUT_LIMITS = {
   DEFAULT: 15000,
@@ -139,7 +139,7 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
           } else {
             // 尝试将其他类型转换为 Date
             const date = new Date(stats.last_call_time as string | number);
-            if (!isNaN(date.getTime())) {
+            if (!Number.isNaN(date.getTime())) {
               lastCallTimeStr = date.toISOString();
             }
           }
@@ -430,7 +430,7 @@ export async function editProvider(
     const validated = UpdateProviderSchema.parse(data);
 
     // 如果 website_url 被更新,重新生成 favicon URL
-    let faviconUrl: string | null | undefined = undefined; // undefined 表示不更新
+    let faviconUrl: string | null | undefined; // undefined 表示不更新
     if (validated.website_url !== undefined) {
       if (validated.website_url) {
         try {
@@ -1172,11 +1172,8 @@ function sanitizeErrorTextForLogging(text: string, maxLength = 500): string {
   let sanitized = text;
   sanitized = sanitized.replace(/\b(?:sk|rk|pk)-[a-zA-Z0-9]{16,}\b/giu, "[REDACTED_KEY]");
   sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[EMAIL]");
-  sanitized = sanitized.replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, "Bearer [REDACTED]");
-  sanitized = sanitized.replace(
-    /(password|token|secret)\s*[:=]\s*['\"]?[^'"\s]+['\"]?/gi,
-    "$1:***"
-  );
+  sanitized = sanitized.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]");
+  sanitized = sanitized.replace(/(password|token|secret)\s*[:=]\s*['"]?[^'"\s]+['"]?/gi, "$1:***");
   sanitized = sanitized.replace(/\/[\w.-]+\.(?:env|ya?ml|json|conf|ini)/gi, "[PATH]");
 
   if (sanitized.length > maxLength) {
@@ -1273,8 +1270,8 @@ function detectCloudflareGatewayError(response: Response): boolean {
   const headerIndicatesCloudflare = Boolean(
     cfRay ||
       cfCacheStatus ||
-      (server && server.toLowerCase().includes("cloudflare")) ||
-      (via && via.toLowerCase().includes("cloudflare"))
+      server?.toLowerCase().includes("cloudflare") ||
+      via?.toLowerCase().includes("cloudflare")
   );
 
   return headerIndicatesCloudflare && CLOUDFLARE_ERROR_STATUS_CODES.has(response.status);
@@ -1639,7 +1636,7 @@ function mergeStreamChunks(chunks: ProviderApiResponse[]): ProviderApiResponse {
         (base as OpenAIResponsesResponse).output = [
           {
             type: "message",
-            id: firstOutput?.id || "msg_" + Date.now(),
+            id: firstOutput?.id || `msg_${Date.now()}`,
             status: firstOutput?.status || "completed",
             role: "assistant",
             content: [{ type: "output_text", text: mergedText }],
@@ -2331,7 +2328,7 @@ export async function testProviderGemini(
           "x-goog-api-client": "google-genai-sdk/1.30.0 gl-node/v24.11.0",
         };
         if (isJsonCreds) {
-          headers["Authorization"] = `Bearer ${apiKey}`;
+          headers.Authorization = `Bearer ${apiKey}`;
         } else {
           headers["x-goog-api-key"] = apiKey;
         }
@@ -2429,7 +2426,7 @@ export async function testProviderGemini(
       ok: true,
       data: {
         ...secondResult.data,
-        message: secondResult.data.message + " [FALLBACK:URL_PARAM]",
+        message: `${secondResult.data.message} [FALLBACK:URL_PARAM]`,
       },
     };
   }

+ 3 - 3
src/actions/sensitive-words.ts

@@ -1,10 +1,10 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
-import * as repo from "@/repository/sensitive-words";
-import { sensitiveWordDetector } from "@/lib/sensitive-word-detector";
-import { logger } from "@/lib/logger";
 import { getSession } from "@/lib/auth";
+import { logger } from "@/lib/logger";
+import { sensitiveWordDetector } from "@/lib/sensitive-word-detector";
+import * as repo from "@/repository/sensitive-words";
 import type { ActionResult } from "./types";
 
 /**

+ 1 - 1
src/actions/session-response.ts

@@ -1,8 +1,8 @@
 "use server";
 
-import { SessionManager } from "@/lib/session-manager";
 import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
+import { SessionManager } from "@/lib/session-manager";
 
 /**
  * 获取 session 响应体内容

+ 10 - 10
src/actions/statistics.ts

@@ -2,27 +2,27 @@
 
 import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
+import { formatCostForStorage } from "@/lib/utils/currency";
 import {
-  getUserStatisticsFromDB,
+  getActiveKeysForUserFromDB,
   getActiveUsersFromDB,
   getKeyStatisticsFromDB,
-  getActiveKeysForUserFromDB,
   getMixedStatisticsFromDB,
+  getUserStatisticsFromDB,
 } from "@/repository/statistics";
 import { getSystemSettings } from "@/repository/system-config";
 import type {
-  TimeRange,
-  UserStatisticsData,
+  ChartDataItem,
+  DatabaseKey,
+  DatabaseKeyStatRow,
   DatabaseStatRow,
   DatabaseUser,
-  ChartDataItem,
   StatisticsUser,
-  DatabaseKeyStatRow,
-  DatabaseKey,
+  TimeRange,
+  UserStatisticsData,
 } from "@/types/statistics";
-import { TIME_RANGE_OPTIONS, DEFAULT_TIME_RANGE } from "@/types/statistics";
+import { DEFAULT_TIME_RANGE, TIME_RANGE_OPTIONS } from "@/types/statistics";
 import type { ActionResult } from "./types";
-import { formatCostForStorage } from "@/lib/utils/currency";
 
 /**
  * 生成图表数据使用的用户键,避免名称碰撞
@@ -162,7 +162,7 @@ export async function getUserStatistics(
 
     return {
       ok: false,
-      error: "获取统计数据失败:" + errorMessage,
+      error: `获取统计数据失败:${errorMessage}`,
     };
   }
 }

+ 3 - 3
src/actions/system-config.ts

@@ -1,12 +1,12 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
-import { logger } from "@/lib/logger";
-import { getSystemSettings, updateSystemSettings } from "@/repository/system-config";
 import { getSession } from "@/lib/auth";
+import { logger } from "@/lib/logger";
 import { UpdateSystemSettingsSchema } from "@/lib/validation/schemas";
-import type { ActionResult } from "./types";
+import { getSystemSettings, updateSystemSettings } from "@/repository/system-config";
 import type { SystemSettings } from "@/types/system-config";
+import type { ActionResult } from "./types";
 
 export async function fetchSystemSettings(): Promise<ActionResult<SystemSettings>> {
   try {

+ 1 - 1
src/actions/usage-logs.ts

@@ -4,9 +4,9 @@ import { getSession } from "@/lib/auth";
 import { logger } from "@/lib/logger";
 import {
   findUsageLogsWithDetails,
+  getUsedEndpoints,
   getUsedModels,
   getUsedStatusCodes,
-  getUsedEndpoints,
   type UsageLogFilters,
   type UsageLogsResult,
 } from "@/repository/usage-logs";

+ 18 - 14
src/actions/users.ts

@@ -1,21 +1,25 @@
 "use server";
 
-import { findUserList, createUser, updateUser, deleteUser, findUserById } from "@/repository/user";
-import { logger } from "@/lib/logger";
-import { findKeyList, findKeyUsageToday, findKeysWithStatistics } from "@/repository/key";
-import { revalidatePath } from "next/cache";
 import { randomBytes } from "node:crypto";
-import { type UserDisplay } from "@/types/user";
-import { maskKey } from "@/lib/utils/validation";
-import { CreateUserSchema, UpdateUserSchema } from "@/lib/validation/schemas";
-import { USER_DEFAULTS } from "@/lib/constants/user.constants";
-import { createKey } from "@/repository/key";
+import { revalidatePath } from "next/cache";
+import { getLocale, getTranslations } from "next-intl/server";
 import { getSession } from "@/lib/auth";
-import type { ActionResult } from "./types";
+import { USER_DEFAULTS } from "@/lib/constants/user.constants";
+import { logger } from "@/lib/logger";
+import { getUnauthorizedFields } from "@/lib/permissions/user-field-permissions";
 import { ERROR_CODES } from "@/lib/utils/error-messages";
+import { maskKey } from "@/lib/utils/validation";
 import { formatZodError } from "@/lib/utils/zod-i18n";
-import { getTranslations, getLocale } from "next-intl/server";
-import { getUnauthorizedFields } from "@/lib/permissions/user-field-permissions";
+import { CreateUserSchema, UpdateUserSchema } from "@/lib/validation/schemas";
+import {
+  createKey,
+  findKeyList,
+  findKeysWithStatistics,
+  findKeyUsageToday,
+} from "@/repository/key";
+import { createUser, deleteUser, findUserById, findUserList, updateUser } from "@/repository/user";
+import type { UserDisplay } from "@/types/user";
+import type { ActionResult } from "./types";
 
 // 获取用户数据
 export async function getUsers(): Promise<UserDisplay[]> {
@@ -198,7 +202,7 @@ export async function addUser(data: {
     });
 
     // 为新用户创建默认密钥
-    const generatedKey = "sk-" + randomBytes(16).toString("hex");
+    const generatedKey = `sk-${randomBytes(16).toString("hex")}`;
     await createKey({
       user_id: newUser.id,
       name: "default",
@@ -268,7 +272,7 @@ export async function editUser(
     if (unauthorizedFields.length > 0) {
       return {
         ok: false,
-        error: tError("PERMISSION_DENIED") + `: ${unauthorizedFields.join(", ")}`,
+        error: `${tError("PERMISSION_DENIED")}: ${unauthorizedFields.join(", ")}`,
         errorCode: ERROR_CODES.PERMISSION_DENIED,
       };
     }

+ 5 - 6
src/app/[locale]/dashboard/_components/dashboard-header.tsx

@@ -1,13 +1,12 @@
+import { useTranslations } from "next-intl";
+import { VersionUpdateNotifier } from "@/components/customs/version-update-notifier";
+import { Button } from "@/components/ui/button";
+import { LanguageSwitcher } from "@/components/ui/language-switcher";
+import { ThemeSwitcher } from "@/components/ui/theme-switcher";
 import { Link } from "@/i18n/routing";
-
 import type { AuthSession } from "@/lib/auth";
-import { Button } from "@/components/ui/button";
 import { DashboardNav, type DashboardNavItem } from "./dashboard-nav";
 import { UserMenu } from "./user-menu";
-import { VersionUpdateNotifier } from "@/components/customs/version-update-notifier";
-import { LanguageSwitcher } from "@/components/ui/language-switcher";
-import { ThemeSwitcher } from "@/components/ui/theme-switcher";
-import { useTranslations } from "next-intl";
 
 interface DashboardHeaderProps {
   session: AuthSession | null;

+ 2 - 2
src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx

@@ -1,11 +1,11 @@
 "use client";
 
+import { useTranslations } from "next-intl";
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
+import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
 import type { EventTimeline } from "@/types/statistics";
-import { useTranslations } from "next-intl";
 
 export interface RateLimitEventsChartProps {
   data: EventTimeline[];

+ 4 - 4
src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx

@@ -1,6 +1,10 @@
 "use client";
 
+import { ArrowUpDown } from "lucide-react";
+import { useTranslations } from "next-intl";
 import * as React from "react";
+import { getUsers } from "@/actions/users";
+import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import {
   Table,
@@ -10,10 +14,6 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { getUsers } from "@/actions/users";
-import { useTranslations } from "next-intl";
-import { ArrowUpDown } from "lucide-react";
-import { Button } from "@/components/ui/button";
 
 export interface RateLimitTopUsersProps {
   data: Record<number, number>;

+ 3 - 3
src/app/[locale]/dashboard/_components/rate-limit-type-breakdown.tsx

@@ -1,11 +1,11 @@
 "use client";
 
+import { useTranslations } from "next-intl";
 import * as React from "react";
-import { Pie, PieChart, Cell, Legend } from "recharts";
+import { Cell, Legend, Pie, PieChart } from "recharts";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
+import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart";
 import type { RateLimitType } from "@/types/statistics";
-import { useTranslations } from "next-intl";
 
 export interface RateLimitTypeBreakdownProps {
   data: Record<RateLimitType, number>;

+ 5 - 7
src/app/[locale]/dashboard/_components/statistics/chart.tsx

@@ -1,16 +1,14 @@
 "use client";
 
+import { useTranslations } from "next-intl";
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
-
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
+import { type ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/components/ui/chart";
 import type { CurrencyCode } from "@/lib/utils";
-import { ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/components/ui/chart";
-
-import type { UserStatisticsData, TimeRange } from "@/types/statistics";
+import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
+import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import { TimeRangeSelector } from "./time-range-selector";
-import { useTranslations } from "next-intl";
 
 // 固定的调色盘,确保新增用户也能获得可辨识的颜色
 const USER_COLOR_PALETTE = [
@@ -70,7 +68,7 @@ export function UserStatisticsChart({
   // 重置选择状态(当 data.users 变化时)
   React.useEffect(() => {
     setSelectedUserIds(new Set(data.users.map((u) => u.id)));
-  }, [data.users, t]);
+  }, [data.users]);
 
   const isAdminMode = data.mode === "users";
   const enableUserFilter = isAdminMode && data.users.length > 1;

+ 1 - 1
src/app/[locale]/dashboard/_components/statistics/index.ts

@@ -1,3 +1,3 @@
-export { StatisticsWrapper } from "./wrapper";
 export { UserStatisticsChart } from "./chart";
 export { TimeRangeSelector } from "./time-range-selector";
+export { StatisticsWrapper } from "./wrapper";

+ 2 - 3
src/app/[locale]/dashboard/_components/statistics/time-range-selector.tsx

@@ -1,9 +1,8 @@
 "use client";
 
-import * as React from "react";
-import { TIME_RANGE_OPTIONS, type TimeRange } from "@/types/statistics";
-import { cn } from "@/lib/utils";
 import { useTranslations } from "next-intl";
+import { cn } from "@/lib/utils";
+import { TIME_RANGE_OPTIONS, type TimeRange } from "@/types/statistics";
 
 interface TimeRangeSelectorProps {
   value: TimeRange;

+ 5 - 5
src/app/[locale]/dashboard/_components/statistics/wrapper.tsx

@@ -1,14 +1,14 @@
 "use client";
 
-import * as React from "react";
 import { useQuery } from "@tanstack/react-query";
-import { UserStatisticsChart } from "./chart";
+import { useTranslations } from "next-intl";
+import * as React from "react";
+import { toast } from "sonner";
 import { getUserStatistics } from "@/actions/statistics";
-import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import type { CurrencyCode } from "@/lib/utils";
+import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import { DEFAULT_TIME_RANGE } from "@/types/statistics";
-import { toast } from "sonner";
-import { useTranslations } from "next-intl";
+import { UserStatisticsChart } from "./chart";
 
 interface StatisticsWrapperProps {
   initialData?: UserStatisticsData;

+ 2 - 2
src/app/[locale]/dashboard/_components/user-menu.tsx

@@ -1,10 +1,10 @@
 "use client";
 
+import { LogOut } from "lucide-react";
 import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
 import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 import { Button } from "@/components/ui/button";
-import { LogOut } from "lucide-react";
-import { useTranslations } from "next-intl";
 
 interface UserMenuProps {
   user: {

+ 3 - 3
src/app/[locale]/dashboard/_components/user-quick-overview.tsx

@@ -1,12 +1,12 @@
 "use client";
 
+import { ArrowRight, Clock, Key, TrendingUp, Users } from "lucide-react";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
-import { ArrowRight, Users, Key, TrendingUp, Clock } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Link } from "@/i18n/routing";
-import type { UserDisplay } from "@/types/user";
 import { formatRelativeTime } from "@/lib/utils/date";
+import type { UserDisplay } from "@/types/user";
 
 interface UserQuickOverviewProps {
   users: UserDisplay[];

+ 4 - 4
src/app/[locale]/dashboard/_components/user/add-user-dialog.tsx

@@ -1,11 +1,11 @@
 "use client";
-import { useState, type ComponentProps } from "react";
+import { ListPlus } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { type ComponentProps, useState } from "react";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
 import { Button } from "@/components/ui/button";
 import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { ListPlus } from "lucide-react";
 import { UserForm } from "./forms/user-form";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-import { useTranslations } from "next-intl";
 
 type ButtonProps = ComponentProps<typeof Button>;
 

+ 4 - 4
src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx

@@ -1,13 +1,12 @@
 "use client";
-import { useTransition } from "react";
 import { useRouter } from "next/navigation";
-import { toast } from "sonner";
 import { useTranslations } from "next-intl";
+import { useTransition } from "react";
+import { toast } from "sonner";
 import { addKey } from "@/actions/keys";
+import { DateField, NumberField, TextField } from "@/components/form/form-field";
 import { DialogFormLayout, FormGrid } from "@/components/form/form-layout";
-import { TextField, DateField, NumberField } from "@/components/form/form-field";
 import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
 import {
   Select,
   SelectContent,
@@ -15,6 +14,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
 import { useZodForm } from "@/lib/hooks/use-zod-form";
 import { KeyFormSchema } from "@/lib/validation/schemas";
 import type { User } from "@/types/user";

+ 8 - 8
src/app/[locale]/dashboard/_components/user/forms/delete-key-confirm.tsx

@@ -1,17 +1,17 @@
 "use client";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
 import { useTransition } from "react";
+import { toast } from "sonner";
+import { removeKey } from "@/actions/keys";
+import { Button } from "@/components/ui/button";
 import {
-  DialogHeader,
-  DialogTitle,
+  DialogClose,
   DialogDescription,
   DialogFooter,
-  DialogClose,
+  DialogHeader,
+  DialogTitle,
 } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { useRouter } from "next/navigation";
-import { useTranslations } from "next-intl";
-import { removeKey } from "@/actions/keys";
-import { toast } from "sonner";
 
 interface DeleteKeyConfirmProps {
   keyData?: {

+ 7 - 7
src/app/[locale]/dashboard/_components/user/forms/delete-user-confirm.tsx

@@ -1,16 +1,16 @@
 "use client";
+import { useRouter } from "next/navigation";
 import { useTransition } from "react";
+import { toast } from "sonner";
+import { removeUser } from "@/actions/users";
+import { Button } from "@/components/ui/button";
 import {
-  DialogHeader,
-  DialogTitle,
+  DialogClose,
   DialogDescription,
   DialogFooter,
-  DialogClose,
+  DialogHeader,
+  DialogTitle,
 } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { useRouter } from "next/navigation";
-import { removeUser } from "@/actions/users";
-import { toast } from "sonner";
 
 interface DeleteUserConfirmProps {
   user?: {

+ 5 - 5
src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx

@@ -1,11 +1,12 @@
 "use client";
-import { useTransition } from "react";
 import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useTransition } from "react";
+import { toast } from "sonner";
 import { editKey } from "@/actions/keys";
+import { DateField, NumberField, TextField } from "@/components/form/form-field";
 import { DialogFormLayout, FormGrid } from "@/components/form/form-layout";
-import { TextField, DateField, NumberField } from "@/components/form/form-field";
 import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
 import {
   Select,
   SelectContent,
@@ -13,10 +14,9 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
 import { useZodForm } from "@/lib/hooks/use-zod-form";
 import { KeyFormSchema } from "@/lib/validation/schemas";
-import { toast } from "sonner";
-import { useTranslations } from "next-intl";
 import type { User } from "@/types/user";
 
 interface EditKeyFormProps {

+ 8 - 8
src/app/[locale]/dashboard/_components/user/forms/user-form.tsx

@@ -1,16 +1,16 @@
 "use client";
-import { useTransition, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
+import { useEffect, useTransition } from "react";
+import { toast } from "sonner";
 import { addUser, editUser } from "@/actions/users";
+import { TagInputField, TextField } from "@/components/form/form-field";
 import { DialogFormLayout, FormGrid } from "@/components/form/form-layout";
-import { TextField, TagInputField } from "@/components/form/form-field";
-import { useZodForm } from "@/lib/hooks/use-zod-form";
-import { CreateUserSchema } from "@/lib/validation/schemas";
 import { USER_DEFAULTS, USER_LIMITS } from "@/lib/constants/user.constants";
-import { toast } from "sonner";
-import { setZodErrorMap } from "@/lib/utils/zod-i18n";
+import { useZodForm } from "@/lib/hooks/use-zod-form";
 import { getErrorMessage } from "@/lib/utils/error-messages";
+import { setZodErrorMap } from "@/lib/utils/zod-i18n";
+import { CreateUserSchema } from "@/lib/validation/schemas";
 
 interface UserFormProps {
   user?: {
@@ -65,7 +65,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
         try {
           let res;
           if (isEdit) {
-            res = await editUser(user!.id, {
+            res = await editUser(user?.id, {
               name: data.name,
               note: data.note,
               rpm: data.rpm,
@@ -149,7 +149,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
         maxTagLength={50}
         placeholder={tForm("providerGroup.placeholder")}
         description={tForm("providerGroup.description")}
-        onInvalidTag={(tag, reason) => {
+        onInvalidTag={(_tag, reason) => {
           const messages: Record<string, string> = {
             empty: tUI("emptyTag"),
             duplicate: tUI("duplicateTag"),

+ 4 - 5
src/app/[locale]/dashboard/_components/user/key-actions.tsx

@@ -1,13 +1,12 @@
 "use client";
-import { useState } from "react";
 import { SquarePen, Trash2 } from "lucide-react";
 import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
 import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { EditKeyForm } from "./forms/edit-key-form";
+import type { User, UserKeyDisplay } from "@/types/user";
 import { DeleteKeyConfirm } from "./forms/delete-key-confirm";
-import type { UserKeyDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
+import { EditKeyForm } from "./forms/edit-key-form";
 
 interface KeyActionsProps {
   keyData: UserKeyDisplay;

+ 3 - 3
src/app/[locale]/dashboard/_components/user/key-limit-usage.tsx

@@ -1,12 +1,12 @@
 "use client";
 
-import { useEffect, useState } from "react";
+import { AlertCircle, Loader2 } from "lucide-react";
 import { useTranslations } from "next-intl";
+import { useEffect, useState } from "react";
 import { getKeyLimitUsage } from "@/actions/keys";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { Loader2, AlertCircle } from "lucide-react";
 import { Progress } from "@/components/ui/progress";
 import { cn } from "@/lib/utils";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
 
 interface KeyLimitUsageProps {
   keyId: number;

+ 10 - 11
src/app/[locale]/dashboard/_components/user/key-list-header.tsx

@@ -1,5 +1,10 @@
 "use client";
-import { useMemo, useState, useEffect } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { CheckCircle, Copy, Eye, EyeOff, ListPlus } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useEffect, useMemo, useState } from "react";
+import { getProxyStatus } from "@/actions/proxy-status";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
 import { Button } from "@/components/ui/button";
 import {
   Dialog,
@@ -10,18 +15,12 @@ import {
   DialogTitle,
   DialogTrigger,
 } from "@/components/ui/dialog";
-import { ListPlus, Copy, CheckCircle, Eye, EyeOff } from "lucide-react";
-import { isClipboardSupported, copyToClipboard } from "@/lib/utils/clipboard";
-import { useTranslations } from "next-intl";
+import { copyToClipboard, isClipboardSupported } from "@/lib/utils/clipboard";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
+import type { ProxyStatusResponse } from "@/types/proxy-status";
+import type { User, UserDisplay } from "@/types/user";
 import { AddKeyForm } from "./forms/add-key-form";
 import { UserActions } from "./user-actions";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { useQuery } from "@tanstack/react-query";
-import { getProxyStatus } from "@/actions/proxy-status";
-import type { ProxyStatusResponse } from "@/types/proxy-status";
 
 const PROXY_STATUS_REFRESH_INTERVAL = 2000;
 

+ 41 - 44
src/app/[locale]/dashboard/_components/user/key-list.tsx

@@ -1,16 +1,11 @@
 "use client";
-import { useState, useEffect } from "react";
-import { DataTable, TableColumnTypes } from "@/components/ui/data-table";
+import { Check, ChevronDown, ChevronRight, Copy, ExternalLink, Eye, EyeOff } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useEffect, useState } from "react";
 import { Button } from "@/components/ui/button";
-import { Copy, Check, ExternalLink, ChevronDown, ChevronRight, Eye, EyeOff } from "lucide-react";
-import { KeyActions } from "./key-actions";
-import { KeyLimitUsage } from "./key-limit-usage";
-import type { UserKeyDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { RelativeTime } from "@/components/ui/relative-time";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { Link } from "@/i18n/routing";
 import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { DataTable, TableColumnTypes } from "@/components/ui/data-table";
+import { RelativeTime } from "@/components/ui/relative-time";
 import {
   Table,
   TableBody,
@@ -19,8 +14,12 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { useTranslations } from "next-intl";
-import { isClipboardSupported, copyToClipboard } from "@/lib/utils/clipboard";
+import { Link } from "@/i18n/routing";
+import { copyToClipboard, isClipboardSupported } from "@/lib/utils/clipboard";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
+import type { User, UserKeyDisplay } from "@/types/user";
+import { KeyActions } from "./key-actions";
+import { KeyLimitUsage } from "./key-limit-usage";
 
 interface KeyListProps {
   keys: UserKeyDisplay[];
@@ -183,37 +182,35 @@ export function KeyList({
         return (
           <div className="group inline-flex items-center gap-1">
             <div className={`font-mono ${isVisible ? "select-all" : "truncate"}`}>{displayKey}</div>
-            {record.canCopy && record.fullKey && (
-              <>
-                {clipboardAvailable ? (
-                  // HTTPS 环境:显示复制按钮
-                  <Button
-                    variant="ghost"
-                    size="sm"
-                    onClick={() => handleCopyKey(record)}
-                    className="h-5 w-5 p-0 hover:bg-muted flex-shrink-0"
-                    title={t("copyKeyTooltip")}
-                  >
-                    {copiedKeyId === record.id ? (
-                      <Check className="h-3 w-3 text-green-600" />
-                    ) : (
-                      <Copy className="h-3 w-3" />
-                    )}
-                  </Button>
-                ) : (
-                  // HTTP 环境:显示显示/隐藏按钮
-                  <Button
-                    variant="ghost"
-                    size="sm"
-                    onClick={() => toggleKeyVisibility(record.id)}
-                    className="h-5 w-5 p-0 hover:bg-muted flex-shrink-0"
-                    title={isVisible ? t("hideKeyTooltip") : t("showKeyTooltip")}
-                  >
-                    {isVisible ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
-                  </Button>
-                )}
-              </>
-            )}
+            {record.canCopy &&
+              record.fullKey &&
+              (clipboardAvailable ? (
+                // HTTPS 环境:显示复制按钮
+                <Button
+                  variant="ghost"
+                  size="sm"
+                  onClick={() => handleCopyKey(record)}
+                  className="h-5 w-5 p-0 hover:bg-muted flex-shrink-0"
+                  title={t("copyKeyTooltip")}
+                >
+                  {copiedKeyId === record.id ? (
+                    <Check className="h-3 w-3 text-green-600" />
+                  ) : (
+                    <Copy className="h-3 w-3" />
+                  )}
+                </Button>
+              ) : (
+                // HTTP 环境:显示显示/隐藏按钮
+                <Button
+                  variant="ghost"
+                  size="sm"
+                  onClick={() => toggleKeyVisibility(record.id)}
+                  className="h-5 w-5 p-0 hover:bg-muted flex-shrink-0"
+                  title={isVisible ? t("hideKeyTooltip") : t("showKeyTooltip")}
+                >
+                  {isVisible ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
+                </Button>
+              ))}
           </div>
         );
       },
@@ -251,7 +248,7 @@ export function KeyList({
         </div>
       ),
     }),
-    TableColumnTypes.actions<UserKeyDisplay>(t("columns.actions"), (value, record) => (
+    TableColumnTypes.actions<UserKeyDisplay>(t("columns.actions"), (_value, record) => (
       <div className="flex items-center gap-1">
         <Link href={`/dashboard/logs?keyId=${record.id}`}>
           <Button variant="ghost" size="sm" className="h-7 text-xs" title={t("viewLogsTooltip")}>

+ 4 - 4
src/app/[locale]/dashboard/_components/user/user-actions.tsx

@@ -1,12 +1,12 @@
 "use client";
-import { useState } from "react";
 import { SquarePen, Trash } from "lucide-react";
 import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
 import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { UserForm } from "./forms/user-form";
+import type { User, UserDisplay } from "@/types/user";
 import { DeleteUserConfirm } from "./forms/delete-user-confirm";
-import type { UserDisplay, User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
+import { UserForm } from "./forms/user-form";
 
 interface UserActionsProps {
   user: UserDisplay;

+ 4 - 5
src/app/[locale]/dashboard/_components/user/user-key-manager.tsx

@@ -1,11 +1,10 @@
 "use client";
 import { useState } from "react";
-import { UserList } from "./user-list";
-import { KeyListHeader } from "./key-list-header";
-import { KeyList } from "./key-list";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
 import type { CurrencyCode } from "@/lib/utils/currency";
+import type { User, UserDisplay } from "@/types/user";
+import { KeyList } from "./key-list";
+import { KeyListHeader } from "./key-list-header";
+import { UserList } from "./user-list";
 
 interface UserKeyManagerProps {
   users: UserDisplay[];

+ 4 - 5
src/app/[locale]/dashboard/_components/user/user-list.tsx

@@ -1,10 +1,9 @@
 "use client";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { ListContainer, ListItem, ListItemData } from "@/components/ui/list";
-import { AddUserDialog } from "./add-user-dialog";
-import { useTranslations } from "next-intl";
 import { Users } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { ListContainer, ListItem, type ListItemData } from "@/components/ui/list";
+import type { User, UserDisplay } from "@/types/user";
+import { AddUserDialog } from "./add-user-dialog";
 
 interface UserListProps {
   users: UserDisplay[];

+ 8 - 9
src/app/[locale]/dashboard/availability/_components/availability-view.tsx

@@ -1,8 +1,11 @@
 "use client";
 
-import { useState, useEffect, useCallback, useMemo } from "react";
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Activity, CheckCircle2, HelpCircle, RefreshCw, XCircle } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import {
   Select,
   SelectContent,
@@ -10,17 +13,14 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Badge } from "@/components/ui/badge";
 import { Skeleton } from "@/components/ui/skeleton";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
-import { RefreshCw, Activity, CheckCircle2, XCircle, HelpCircle } from "lucide-react";
-import { useTranslations } from "next-intl";
-import { cn } from "@/lib/utils";
 import type {
   AvailabilityQueryResult,
   ProviderAvailabilitySummary,
   TimeBucketMetrics,
 } from "@/lib/availability";
+import { cn } from "@/lib/utils";
 
 type TimeRangeOption = "15min" | "1h" | "6h" | "24h" | "7d";
 type SortOption = "availability" | "name" | "requests";
@@ -92,7 +92,7 @@ function formatBucketTime(isoString: string, bucketSizeMinutes: number): string
 /**
  * Format bucket size for display
  */
-function formatBucketSizeDisplay(minutes: number): string {
+function _formatBucketSizeDisplay(minutes: number): string {
   if (minutes >= 60) {
     const hours = minutes / 60;
     return hours === 1 ? "1 hour" : `${hours.toFixed(1)} hours`;
@@ -216,7 +216,6 @@ export function AvailabilityView() {
         case "red":
           // 错误 - 红色
           return "bg-red-100 text-red-700 border-red-300 dark:bg-red-900/30 dark:text-red-400 dark:border-red-700";
-        case "unknown":
         default:
           // 未知 - 灰色
           return "bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600";
@@ -432,7 +431,7 @@ export function AvailabilityView() {
                               <TooltipContent side="top" className="max-w-xs">
                                 <div className="text-sm space-y-1">
                                   <div className="font-medium">
-                                    {formatBucketTime(bucketStart, data!.bucketSizeMinutes)}
+                                    {formatBucketTime(bucketStart, data?.bucketSizeMinutes)}
                                   </div>
                                   {hasData && bucket ? (
                                     <>

+ 5 - 6
src/app/[locale]/dashboard/availability/page.tsx

@@ -1,11 +1,10 @@
-import { Section } from "@/components/section";
-import { AvailabilityView } from "./_components/availability-view";
-import { getSession } from "@/lib/auth";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { AlertCircle } from "lucide-react";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { Link } from "@/i18n/routing";
 import { getTranslations } from "next-intl/server";
+import { Section } from "@/components/section";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { getSession } from "@/lib/auth";
+import { AvailabilityView } from "./_components/availability-view";
 
 export const dynamic = "force-dynamic";
 

+ 4 - 4
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx

@@ -1,5 +1,9 @@
 "use client";
 
+import { Award, Medal, Trophy } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent } from "@/components/ui/card";
 import {
   Table,
   TableBody,
@@ -8,10 +12,6 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { Card, CardContent } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { Trophy, Medal, Award } from "lucide-react";
-import { useTranslations } from "next-intl";
 
 // 支持动态列定义
 export interface ColumnDef<T> {

+ 7 - 7
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx

@@ -1,13 +1,13 @@
 "use client";
 
-import { useState, useEffect } from "react";
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { useSearchParams } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useEffect, useState } from "react";
 import { Card, CardContent } from "@/components/ui/card";
-import { LeaderboardTable, type ColumnDef } from "./leaderboard-table";
-import type { LeaderboardEntry, ProviderLeaderboardEntry } from "@/repository/leaderboard";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { formatTokenAmount } from "@/lib/utils";
-import { useTranslations } from "next-intl";
-import { useSearchParams } from "next/navigation";
+import type { LeaderboardEntry, ProviderLeaderboardEntry } from "@/repository/leaderboard";
+import { type ColumnDef, LeaderboardTable } from "./leaderboard-table";
 
 interface LeaderboardViewProps {
   isAdmin: boolean;
@@ -47,7 +47,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
     }
     // 移除 scope 和 period 从依赖数组,避免无限循环
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isAdmin, searchParams]);
+  }, [isAdmin, searchParams, period, scope]);
 
   useEffect(() => {
     let cancelled = false;

+ 6 - 6
src/app/[locale]/dashboard/leaderboard/page.tsx

@@ -1,12 +1,12 @@
-import { Section } from "@/components/section";
-import { LeaderboardView } from "./_components/leaderboard-view";
-import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { AlertCircle } from "lucide-react";
+import { getTranslations } from "next-intl/server";
+import { Section } from "@/components/section";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Link } from "@/i18n/routing";
-import { getTranslations } from "next-intl/server";
+import { getSession } from "@/lib/auth";
+import { getSystemSettings } from "@/repository/system-config";
+import { LeaderboardView } from "./_components/leaderboard-view";
 
 export const dynamic = "force-dynamic";
 

+ 130 - 103
src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx

@@ -1,8 +1,11 @@
 "use client";
 
-import { useState, useEffect } from "react";
+import { AlertCircle, ArrowRight, CheckCircle, ExternalLink, Loader2, Monitor } from "lucide-react";
 import { useTranslations } from "next-intl";
-import { Link } from "@/i18n/routing";
+import { useEffect, useState } from "react";
+import { hasSessionMessages } from "@/actions/active-sessions";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
 import {
   Dialog,
   DialogContent,
@@ -11,14 +14,11 @@ import {
   DialogTitle,
   DialogTrigger,
 } from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { AlertCircle, ArrowRight, CheckCircle, ExternalLink, Loader2, Monitor } from "lucide-react";
-import type { ProviderChainItem } from "@/types/message";
-import { hasSessionMessages } from "@/actions/active-sessions";
+import { Link } from "@/i18n/routing";
+import { cn } from "@/lib/utils";
 import { formatProviderTimeline } from "@/lib/utils/provider-chain-formatter";
+import type { ProviderChainItem } from "@/types/message";
 import type { BillingModelSource } from "@/types/system-config";
-import { cn } from "@/lib/utils";
 
 interface ErrorDetailsDialogProps {
   statusCode: number | null;
@@ -77,7 +77,8 @@ export function ErrorDetailsDialog({
   const isBlocked = !!blockedBy; // 是否被拦截
 
   // 解析 blockedReason JSON
-  let parsedBlockedReason: { word?: string; matchType?: string; matchedText?: string } | null = null;
+  let parsedBlockedReason: { word?: string; matchType?: string; matchedText?: string } | null =
+    null;
   if (blockedReason) {
     try {
       parsedBlockedReason = JSON.parse(blockedReason);
@@ -97,7 +98,7 @@ export function ErrorDetailsDialog({
           }
         })
         .catch((err) => {
-          console.error('Failed to check session messages:', err);
+          console.error("Failed to check session messages:", err);
         })
         .finally(() => {
           setCheckingMessages(false);
@@ -114,8 +115,8 @@ export function ErrorDetailsDialog({
     if (open && scrollToRedirect) {
       // 等待 DOM 渲染完成后滚动
       const timer = setTimeout(() => {
-        const element = document.getElementById('model-redirect-section');
-        element?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+        const element = document.getElementById("model-redirect-section");
+        element?.scrollIntoView({ behavior: "smooth", block: "start" });
       }, 100);
       return () => clearTimeout(timer);
     }
@@ -169,10 +170,7 @@ export function ErrorDetailsDialog({
   return (
     <Dialog open={open} onOpenChange={setOpen}>
       <DialogTrigger asChild>
-        <Button
-          variant="ghost"
-          className="h-auto p-0 font-normal hover:bg-transparent"
-        >
+        <Button variant="ghost" className="h-auto p-0 font-normal hover:bg-transparent">
           <Badge variant="outline" className={getStatusBadgeClassName()}>
             {isInProgress ? t("logs.details.inProgress") : statusCode}
           </Badge>
@@ -189,15 +187,17 @@ export function ErrorDetailsDialog({
               <AlertCircle className="h-5 w-5 text-destructive" />
             )}
             {t("logs.details.statusTitle", {
-              status: isInProgress ? t("logs.details.inProgress") : statusCode || t("logs.details.unknown")
+              status: isInProgress
+                ? t("logs.details.inProgress")
+                : statusCode || t("logs.details.unknown"),
             })}
           </DialogTitle>
           <DialogDescription>
             {isInProgress
               ? t("logs.details.processing")
               : isSuccess
-              ? t("logs.details.success")
-              : t("logs.details.error")}
+                ? t("logs.details.success")
+                : t("logs.details.error")}
           </DialogDescription>
         </DialogHeader>
 
@@ -215,7 +215,9 @@ export function ErrorDetailsDialog({
                     {t("logs.details.blocked.type")}:
                   </span>
                   <Badge variant="outline" className="border-orange-600 text-orange-600">
-                    {blockedBy === 'sensitive_word' ? t("logs.details.blocked.sensitiveWord") : blockedBy}
+                    {blockedBy === "sensitive_word"
+                      ? t("logs.details.blocked.sensitiveWord")
+                      : blockedBy}
                   </Badge>
                 </div>
                 {parsedBlockedReason && (
@@ -236,9 +238,12 @@ export function ErrorDetailsDialog({
                           {t("logs.details.blocked.matchType")}:
                         </span>
                         <span className="text-orange-800 dark:text-orange-200">
-                          {parsedBlockedReason.matchType === 'contains' && t("logs.details.blocked.matchTypeContains")}
-                          {parsedBlockedReason.matchType === 'exact' && t("logs.details.blocked.matchTypeExact")}
-                          {parsedBlockedReason.matchType === 'regex' && t("logs.details.blocked.matchTypeRegex")}
+                          {parsedBlockedReason.matchType === "contains" &&
+                            t("logs.details.blocked.matchTypeContains")}
+                          {parsedBlockedReason.matchType === "exact" &&
+                            t("logs.details.blocked.matchTypeExact")}
+                          {parsedBlockedReason.matchType === "regex" &&
+                            t("logs.details.blocked.matchTypeRegex")}
                         </span>
                       </div>
                     )}
@@ -264,9 +269,7 @@ export function ErrorDetailsDialog({
               <h4 className="font-semibold text-sm">{t("logs.details.sessionId")}</h4>
               <div className="flex items-center gap-3">
                 <div className="flex-1 rounded-md border bg-muted/50 p-3">
-                  <code className="text-xs font-mono break-all">
-                    {sessionId}
-                  </code>
+                  <code className="text-xs font-mono break-all">{sessionId}</code>
                 </div>
                 {hasMessages && !checkingMessages && (
                   <Link href={`/dashboard/sessions/${sessionId}/messages`}>
@@ -287,7 +290,8 @@ export function ErrorDetailsDialog({
               <div className="rounded-md border bg-muted/50 p-3">
                 <div className="text-sm">
                   <span className="font-medium">{t("logs.details.messagesLabel")}:</span>{" "}
-                  <code className="text-base font-mono font-semibold">{messagesCount}</code> {t("logs.details.messagesUnit")}
+                  <code className="text-base font-mono font-semibold">{messagesCount}</code>{" "}
+                  {t("logs.details.messagesUnit")}
                 </div>
               </div>
             </div>
@@ -301,9 +305,7 @@ export function ErrorDetailsDialog({
                 {t("logs.details.clientInfo")}
               </h4>
               <div className="rounded-md border bg-muted/50 p-3">
-                <code className="text-xs font-mono break-all">
-                  {userAgent}
-                </code>
+                <code className="text-xs font-mono break-all">{userAgent}</code>
               </div>
             </div>
           )}
@@ -313,9 +315,7 @@ export function ErrorDetailsDialog({
             <div className="space-y-2">
               <h4 className="font-semibold text-sm">{t("logs.columns.endpoint")}</h4>
               <div className="rounded-md border bg-muted/50 p-3">
-                <code className="text-xs font-mono break-all">
-                  {endpoint}
-                </code>
+                <code className="text-xs font-mono break-all">{endpoint}</code>
               </div>
             </div>
           )}
@@ -329,27 +329,33 @@ export function ErrorDetailsDialog({
               </h4>
               <div className="rounded-md border bg-blue-50 dark:bg-blue-950/20 px-3 py-2">
                 <div className="flex items-center gap-2 text-sm flex-wrap">
-                  <code className={cn(
-                    "px-1.5 py-0.5 rounded text-xs",
-                    billingModelSource === "original"
-                      ? "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 ring-1 ring-green-300 dark:ring-green-700"
-                      : "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"
-                  )}>
+                  <code
+                    className={cn(
+                      "px-1.5 py-0.5 rounded text-xs",
+                      billingModelSource === "original"
+                        ? "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 ring-1 ring-green-300 dark:ring-green-700"
+                        : "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"
+                    )}
+                  >
                     {originalModel}
                   </code>
                   <ArrowRight className="h-3.5 w-3.5 text-blue-500 flex-shrink-0" />
-                  <code className={cn(
-                    "px-1.5 py-0.5 rounded text-xs",
-                    billingModelSource === "redirected"
-                      ? "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 ring-1 ring-green-300 dark:ring-green-700"
-                      : "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"
-                  )}>
+                  <code
+                    className={cn(
+                      "px-1.5 py-0.5 rounded text-xs",
+                      billingModelSource === "redirected"
+                        ? "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 ring-1 ring-green-300 dark:ring-green-700"
+                        : "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"
+                    )}
+                  >
                     {currentModel}
                   </code>
                   <span className="text-xs text-muted-foreground ml-1">
-                    ({billingModelSource === "original"
+                    (
+                    {billingModelSource === "original"
                       ? t("logs.details.modelRedirect.billingOriginal")
-                      : t("logs.details.modelRedirect.billingRedirected")})
+                      : t("logs.details.modelRedirect.billingRedirected")}
+                    )
                   </span>
                 </div>
               </div>
@@ -370,30 +376,41 @@ export function ErrorDetailsDialog({
                   const error = JSON.parse(errorMessage);
 
                   // 检查是否是限流错误
-                  if (error.code === 'rate_limit_exceeded' || error.code === 'circuit_breaker_open' || error.code === 'mixed_unavailable') {
+                  if (
+                    error.code === "rate_limit_exceeded" ||
+                    error.code === "circuit_breaker_open" ||
+                    error.code === "mixed_unavailable"
+                  ) {
                     return (
                       <div className="rounded-md border bg-orange-50 dark:bg-orange-950/20 p-4 space-y-3">
                         <div className="font-semibold text-orange-900 dark:text-orange-100">
                           💰 {error.message}
                         </div>
-                        {error.details?.filteredProviders && error.details.filteredProviders.length > 0 && (
-                          <div className="space-y-2">
-                            <div className="text-sm font-medium text-orange-900 dark:text-orange-100">
-                              {t("logs.details.filteredProviders")}:
+                        {error.details?.filteredProviders &&
+                          error.details.filteredProviders.length > 0 && (
+                            <div className="space-y-2">
+                              <div className="text-sm font-medium text-orange-900 dark:text-orange-100">
+                                {t("logs.details.filteredProviders")}:
+                              </div>
+                              <ul className="text-sm space-y-1">
+                                {error.details.filteredProviders
+                                  .filter(
+                                    (p: { reason: string }) =>
+                                      p.reason === "rate_limited" || p.reason === "circuit_open"
+                                  )
+                                  .map((p: { id: number; name: string; details: string }) => (
+                                    <li
+                                      key={p.id}
+                                      className="text-orange-800 dark:text-orange-200 flex items-center gap-2"
+                                    >
+                                      <span className="text-orange-600">•</span>
+                                      <span className="font-medium">{p.name}</span>
+                                      <span className="text-xs">({p.details})</span>
+                                    </li>
+                                  ))}
+                              </ul>
                             </div>
-                            <ul className="text-sm space-y-1">
-                              {error.details.filteredProviders
-                                .filter((p: { reason: string }) => p.reason === 'rate_limited' || p.reason === 'circuit_open')
-                                .map((p: { id: number; name: string; details: string }) => (
-                                  <li key={p.id} className="text-orange-800 dark:text-orange-200 flex items-center gap-2">
-                                    <span className="text-orange-600">•</span>
-                                    <span className="font-medium">{p.name}</span>
-                                    <span className="text-xs">({p.details})</span>
-                                  </li>
-                                ))}
-                            </ul>
-                          </div>
-                        )}
+                          )}
                       </div>
                     );
                   }
@@ -421,43 +438,53 @@ export function ErrorDetailsDialog({
           )}
 
           {/* 被过滤的供应商(仅在成功请求时显示) */}
-          {isSuccess && providerChain && providerChain.length > 0 && (() => {
-            // 从决策链中提取被过滤的供应商
-            const filteredProviders = providerChain
-              .flatMap(item => item.decisionContext?.filteredProviders || [])
-              .filter(p => p.reason === 'rate_limited' || p.reason === 'circuit_open');
-
-            if (filteredProviders.length === 0) return null;
-
-            return (
-              <div className="space-y-2">
-                <h4 className="font-semibold text-sm flex items-center gap-2">
-                  <AlertCircle className="h-4 w-4 text-orange-600" />
-                  {t("logs.details.filteredProviders")}
-                </h4>
-                <div className="rounded-md border bg-orange-50 dark:bg-orange-950/20 p-4">
-                  <ul className="text-sm space-y-2">
-                    {filteredProviders.map((p, index) => (
-                      <li key={`${p.id}-${index}`} className="text-orange-800 dark:text-orange-200 flex items-start gap-2">
-                        <span className="text-orange-600 mt-0.5">💰</span>
-                        <div className="flex-1">
-                          <span className="font-medium">{p.name}</span>
-                          <span className="text-xs ml-2">
-                            ({t(`logs.details.reasons.${p.reason === 'rate_limited' ? 'rateLimited' : 'circuitOpen'}`)})
-                          </span>
-                          {p.details && (
-                            <div className="text-xs text-orange-700 dark:text-orange-300 mt-0.5">
-                              {p.details}
-                            </div>
-                          )}
-                        </div>
-                      </li>
-                    ))}
-                  </ul>
+          {isSuccess &&
+            providerChain &&
+            providerChain.length > 0 &&
+            (() => {
+              // 从决策链中提取被过滤的供应商
+              const filteredProviders = providerChain
+                .flatMap((item) => item.decisionContext?.filteredProviders || [])
+                .filter((p) => p.reason === "rate_limited" || p.reason === "circuit_open");
+
+              if (filteredProviders.length === 0) return null;
+
+              return (
+                <div className="space-y-2">
+                  <h4 className="font-semibold text-sm flex items-center gap-2">
+                    <AlertCircle className="h-4 w-4 text-orange-600" />
+                    {t("logs.details.filteredProviders")}
+                  </h4>
+                  <div className="rounded-md border bg-orange-50 dark:bg-orange-950/20 p-4">
+                    <ul className="text-sm space-y-2">
+                      {filteredProviders.map((p, index) => (
+                        <li
+                          key={`${p.id}-${index}`}
+                          className="text-orange-800 dark:text-orange-200 flex items-start gap-2"
+                        >
+                          <span className="text-orange-600 mt-0.5">💰</span>
+                          <div className="flex-1">
+                            <span className="font-medium">{p.name}</span>
+                            <span className="text-xs ml-2">
+                              (
+                              {t(
+                                `logs.details.reasons.${p.reason === "rate_limited" ? "rateLimited" : "circuitOpen"}`
+                              )}
+                              )
+                            </span>
+                            {p.details && (
+                              <div className="text-xs text-orange-700 dark:text-orange-300 mt-0.5">
+                                {p.details}
+                              </div>
+                            )}
+                          </div>
+                        </li>
+                      ))}
+                    </ul>
+                  </div>
                 </div>
-              </div>
-            );
-          })()}
+              );
+            })()}
 
           {/* 供应商决策链时间线 */}
           {providerChain && providerChain.length > 0 && (
@@ -491,8 +518,8 @@ export function ErrorDetailsDialog({
               {isInProgress
                 ? t("logs.details.noError.processing")
                 : isSuccess
-                ? t("logs.details.noError.success")
-                : t("logs.details.noError.default")}
+                  ? t("logs.details.noError.success")
+                  : t("logs.details.noError.default")}
             </div>
           )}
         </div>

+ 2 - 3
src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx

@@ -1,7 +1,7 @@
 "use client";
 
-import { Badge } from "@/components/ui/badge";
 import { ArrowRight } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
 import type { BillingModelSource } from "@/types/system-config";
 
 interface ModelDisplayWithRedirectProps {
@@ -18,8 +18,7 @@ export function ModelDisplayWithRedirect({
   onRedirectClick,
 }: ModelDisplayWithRedirectProps) {
   // 判断是否发生重定向
-  const isRedirected =
-    originalModel && currentModel && originalModel !== currentModel;
+  const isRedirected = originalModel && currentModel && originalModel !== currentModel;
 
   // 根据计费模型来源配置决定显示哪个模型
   const billingModel = billingModelSource === "original" ? originalModel : currentModel;

+ 12 - 8
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx

@@ -1,12 +1,12 @@
 "use client";
 
+import { InfoIcon } from "lucide-react";
 import { useTranslations } from "next-intl";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
-import { InfoIcon } from "lucide-react";
-import type { ProviderChainItem } from "@/types/message";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import { formatProviderDescription } from "@/lib/utils/provider-chain-formatter";
+import type { ProviderChainItem } from "@/types/message";
 
 interface ProviderChainPopoverProps {
   chain: ProviderChainItem[];
@@ -18,13 +18,13 @@ interface ProviderChainPopoverProps {
  */
 function isActualRequest(item: ProviderChainItem): boolean {
   // 并发限制失败:算作一次尝试
-  if (item.reason === 'concurrent_limit_failed') return true;
+  if (item.reason === "concurrent_limit_failed") return true;
 
   // 失败记录
-  if (item.reason === 'retry_failed' || item.reason === 'system_error') return true;
+  if (item.reason === "retry_failed" || item.reason === "system_error") return true;
 
   // 成功记录:必须有 statusCode
-  if ((item.reason === 'request_success' || item.reason === 'retry_success') && item.statusCode) {
+  if ((item.reason === "request_success" || item.reason === "retry_success") && item.statusCode) {
     return true;
   }
 
@@ -51,7 +51,8 @@ export function ProviderChainPopover({ chain, finalProvider }: ProviderChainPopo
           <span className="flex items-center gap-1">
             {finalProvider}
             <Badge variant="secondary" className="ml-1">
-              {requestCount}{t("logs.table.times")}
+              {requestCount}
+              {t("logs.table.times")}
             </Badge>
             <InfoIcon className="h-3 w-3 text-muted-foreground" />
           </span>
@@ -62,7 +63,10 @@ export function ProviderChainPopover({ chain, finalProvider }: ProviderChainPopo
         <div className="space-y-3">
           <div className="flex items-center justify-between">
             <h4 className="font-semibold text-sm">{t("logs.providerChain.decisionChain")}</h4>
-            <Badge variant="outline">{requestCount}{t("logs.table.times")}</Badge>
+            <Badge variant="outline">
+              {requestCount}
+              {t("logs.table.times")}
+            </Badge>
           </div>
 
           <div className="rounded-md border bg-muted/50 p-4 max-h-[300px] overflow-y-auto overflow-x-hidden">

+ 19 - 18
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx

@@ -2,10 +2,12 @@
 
 import { useTranslations } from "next-intl";
 
-import { useState, useEffect } from "react";
-import { Label } from "@/components/ui/label";
+import { useEffect, useState } from "react";
+import { getKeys } from "@/actions/keys";
+import { getEndpointList, getModelList, getStatusCodeList } from "@/actions/usage-logs";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
 import {
   Select,
   SelectContent,
@@ -13,11 +15,9 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { getEndpointList, getModelList, getStatusCodeList } from "@/actions/usage-logs";
-import { getKeys } from "@/actions/keys";
-import type { UserDisplay } from "@/types/user";
-import type { ProviderDisplay } from "@/types/provider";
 import type { Key } from "@/types/key";
+import type { ProviderDisplay } from "@/types/provider";
+import type { UserDisplay } from "@/types/user";
 
 interface UsageLogsFiltersProps {
   isAdmin: boolean;
@@ -112,7 +112,7 @@ export function UsageLogsFilters({
 
   // 处理用户选择变更
   const handleUserChange = async (userId: string) => {
-    const newUserId = userId ? parseInt(userId) : undefined;
+    const newUserId = userId ? parseInt(userId, 10) : undefined;
     const newFilters = { ...localFilters, userId: newUserId, keyId: undefined };
     setLocalFilters(newFilters);
 
@@ -173,10 +173,7 @@ export function UsageLogsFilters({
         {isAdmin && (
           <div className="space-y-2 lg:col-span-4">
             <Label>{t("logs.filters.user")}</Label>
-            <Select
-              value={localFilters.userId?.toString() || ""}
-              onValueChange={handleUserChange}
-            >
+            <Select value={localFilters.userId?.toString() || ""} onValueChange={handleUserChange}>
               <SelectTrigger>
                 <SelectValue placeholder={t("logs.filters.allUsers")} />
               </SelectTrigger>
@@ -199,13 +196,19 @@ export function UsageLogsFilters({
             onValueChange={(value: string) =>
               setLocalFilters({
                 ...localFilters,
-                keyId: value ? parseInt(value) : undefined,
+                keyId: value ? parseInt(value, 10) : undefined,
               })
             }
             disabled={isAdmin && !localFilters.userId && keys.length === 0}
           >
             <SelectTrigger>
-              <SelectValue placeholder={isAdmin && !localFilters.userId && keys.length === 0 ? t("logs.filters.selectUserFirst") : t("logs.filters.allKeys")} />
+              <SelectValue
+                placeholder={
+                  isAdmin && !localFilters.userId && keys.length === 0
+                    ? t("logs.filters.selectUserFirst")
+                    : t("logs.filters.allKeys")
+                }
+              />
             </SelectTrigger>
             <SelectContent>
               {keys.map((key) => (
@@ -226,7 +229,7 @@ export function UsageLogsFilters({
               onValueChange={(value: string) =>
                 setLocalFilters({
                   ...localFilters,
-                  providerId: value ? parseInt(value) : undefined,
+                  providerId: value ? parseInt(value, 10) : undefined,
                 })
               }
             >
@@ -296,9 +299,7 @@ export function UsageLogsFilters({
               ))}
             </SelectContent>
           </Select>
-          {endpointError && (
-            <p className="text-xs text-destructive">{endpointError}</p>
-          )}
+          {endpointError && <p className="text-xs text-destructive">{endpointError}</p>}
         </div>
 
         {/* 状态码选择 */}
@@ -309,7 +310,7 @@ export function UsageLogsFilters({
             onValueChange={(value: string) =>
               setLocalFilters({
                 ...localFilters,
-                statusCode: value ? parseInt(value) : undefined,
+                statusCode: value ? parseInt(value, 10) : undefined,
               })
             }
           >

+ 45 - 32
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -1,7 +1,10 @@
 "use client";
 
-import { useState } from "react";
 import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { RelativeTime } from "@/components/ui/relative-time";
 import {
   Table,
   TableBody,
@@ -10,24 +13,16 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import {
-  Tooltip,
-  TooltipContent,
-  TooltipProvider,
-  TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import type { UsageLogRow } from "@/repository/usage-logs";
-import { RelativeTime } from "@/components/ui/relative-time";
-import { ProviderChainPopover } from "./provider-chain-popover";
-import { ErrorDetailsDialog } from "./error-details-dialog";
-import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter";
-import { ModelDisplayWithRedirect } from "./model-display-with-redirect";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn, formatTokenAmount } from "@/lib/utils";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
-import { cn, formatTokenAmount } from "@/lib/utils";
+import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter";
+import type { UsageLogRow } from "@/repository/usage-logs";
 import type { BillingModelSource } from "@/types/system-config";
+import { ErrorDetailsDialog } from "./error-details-dialog";
+import { ModelDisplayWithRedirect } from "./model-display-with-redirect";
+import { ProviderChainPopover } from "./provider-chain-popover";
 
 const NON_BILLING_ENDPOINT = "/v1/messages/count_tokens";
 
@@ -37,7 +32,7 @@ const NON_BILLING_ENDPOINT = "/v1/messages/count_tokens";
  * - 1000ms 以下显示为毫秒(如 "850ms")
  */
 function formatDuration(durationMs: number | null): string {
-  if (!durationMs) return '-';
+  if (!durationMs) return "-";
 
   // 1000ms 以上转换为秒
   if (durationMs >= 1000) {
@@ -144,7 +139,9 @@ export function UsageLogsTable({
                                   <ProviderChainPopover
                                     chain={log.providerChain}
                                     finalProvider={
-                                      log.providerChain[log.providerChain.length - 1].name || log.providerName || tChain("circuit.unknown")
+                                      log.providerChain[log.providerChain.length - 1].name ||
+                                      log.providerName ||
+                                      tChain("circuit.unknown")
                                     }
                                   />
                                 </div>
@@ -158,7 +155,11 @@ export function UsageLogsTable({
                                             {formatProviderSummary(log.providerChain, tChain)}
                                           </span>
                                         </TooltipTrigger>
-                                        <TooltipContent side="bottom" align="start" className="max-w-[500px]">
+                                        <TooltipContent
+                                          side="bottom"
+                                          align="start"
+                                          className="max-w-[500px]"
+                                        >
                                           <p className="text-xs whitespace-normal break-words font-mono">
                                             {formatProviderSummary(log.providerChain, tChain)}
                                           </p>
@@ -175,18 +176,22 @@ export function UsageLogsTable({
                           {/* 显示供应商倍率 Badge(不为 1.0 时) */}
                           {(() => {
                             // 从决策链中找到最后一个成功的供应商,使用它的倍率
-                            const successfulProvider = log.providerChain && log.providerChain.length > 0
-                              ? [...log.providerChain]
-                                  .reverse()
-                                  .find(item =>
-                                    item.reason === 'request_success' ||
-                                    item.reason === 'retry_success'
-                                  )
-                              : null;
+                            const successfulProvider =
+                              log.providerChain && log.providerChain.length > 0
+                                ? [...log.providerChain]
+                                    .reverse()
+                                    .find(
+                                      (item) =>
+                                        item.reason === "request_success" ||
+                                        item.reason === "retry_success"
+                                    )
+                                : null;
 
-                            const actualCostMultiplier = successfulProvider?.costMultiplier ?? log.costMultiplier;
+                            const actualCostMultiplier =
+                              successfulProvider?.costMultiplier ?? log.costMultiplier;
 
-                            return actualCostMultiplier && parseFloat(String(actualCostMultiplier)) !== 1.0 ? (
+                            return actualCostMultiplier &&
+                              parseFloat(String(actualCostMultiplier)) !== 1.0 ? (
                               <Badge
                                 variant="outline"
                                 className={
@@ -211,7 +216,9 @@ export function UsageLogsTable({
                                 originalModel={log.originalModel}
                                 currentModel={log.model}
                                 billingModelSource={billingModelSource}
-                                onRedirectClick={() => setDialogState({ logId: log.id, scrollToRedirect: true })}
+                                onRedirectClick={() =>
+                                  setDialogState({ logId: log.id, scrollToRedirect: true })
+                                }
                               />
                             </div>
                           </TooltipTrigger>
@@ -234,7 +241,11 @@ export function UsageLogsTable({
                       {formatTokenAmount(log.cacheReadInputTokens)}
                     </TableCell>
                     <TableCell className="text-right font-mono text-xs">
-                      {isNonBilling ? "-" : log.costUsd ? formatCurrency(log.costUsd, currencyCode, 6) : "-"}
+                      {isNonBilling
+                        ? "-"
+                        : log.costUsd
+                          ? formatCurrency(log.costUsd, currencyCode, 6)
+                          : "-"}
                     </TableCell>
                     <TableCell className="text-right font-mono text-xs">
                       {formatDuration(log.durationMs)}
@@ -257,7 +268,9 @@ export function UsageLogsTable({
                         onExternalOpenChange={(open) => {
                           if (!open) setDialogState({ logId: null, scrollToRedirect: false });
                         }}
-                        scrollToRedirect={dialogState.logId === log.id && dialogState.scrollToRedirect}
+                        scrollToRedirect={
+                          dialogState.logId === log.id && dialogState.scrollToRedirect
+                        }
                       />
                     </TableCell>
                   </TableRow>

+ 58 - 51
src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx

@@ -1,22 +1,22 @@
 "use client";
 
-import { useState, useTransition, useEffect, useRef, useCallback } from "react";
+import { Pause, Play, RefreshCw } from "lucide-react";
 import { useRouter, useSearchParams } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useCallback, useEffect, useRef, useState, useTransition } from "react";
 import { getUsageLogs } from "@/actions/usage-logs";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
-import { RefreshCw, Pause, Play } from "lucide-react";
-import { UsageLogsFilters } from "./usage-logs-filters";
-import { UsageLogsTable } from "./usage-logs-table";
-import type { UsageLogsResult } from "@/repository/usage-logs";
-import type { UserDisplay } from "@/types/user";
-import type { ProviderDisplay } from "@/types/provider";
-import type { Key } from "@/types/key";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { formatTokenAmount } from "@/lib/utils";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
-import { formatTokenAmount } from "@/lib/utils";
-import { useTranslations } from "next-intl";
+import type { UsageLogsResult } from "@/repository/usage-logs";
+import type { Key } from "@/types/key";
+import type { ProviderDisplay } from "@/types/provider";
 import type { BillingModelSource } from "@/types/system-config";
+import type { UserDisplay } from "@/types/user";
+import { UsageLogsFilters } from "./usage-logs-filters";
+import { UsageLogsTable } from "./usage-logs-table";
 
 interface UsageLogsViewProps {
   isAdmin: boolean;
@@ -49,7 +49,7 @@ export function UsageLogsView({
   // 追踪新增记录(用于动画高亮)
   const [newLogIds, setNewLogIds] = useState<Set<number>>(new Set());
   const previousLogsRef = useRef<Map<number, boolean>>(new Map());
-  const previousParamsRef = useRef<string>('');
+  const previousParamsRef = useRef<string>("");
 
   // 从 URL 参数解析筛选条件
   // 注意:时间使用字符串传递,避免 Date 序列化导致的时区问题
@@ -64,16 +64,20 @@ export function UsageLogsView({
     endpoint?: string;
     page: number;
   } = {
-    userId: searchParams.userId ? parseInt(searchParams.userId as string) : undefined,
-    keyId: searchParams.keyId ? parseInt(searchParams.keyId as string) : undefined,
-    providerId: searchParams.providerId ? parseInt(searchParams.providerId as string) : undefined,
+    userId: searchParams.userId ? parseInt(searchParams.userId as string, 10) : undefined,
+    keyId: searchParams.keyId ? parseInt(searchParams.keyId as string, 10) : undefined,
+    providerId: searchParams.providerId
+      ? parseInt(searchParams.providerId as string, 10)
+      : undefined,
     // 直接传递本地时间字符串,不转换为 Date
     startDateLocal: searchParams.startDate as string | undefined,
     endDateLocal: searchParams.endDate as string | undefined,
-    statusCode: searchParams.statusCode ? parseInt(searchParams.statusCode as string) : undefined,
+    statusCode: searchParams.statusCode
+      ? parseInt(searchParams.statusCode as string, 10)
+      : undefined,
     model: searchParams.model as string | undefined,
     endpoint: searchParams.endpoint as string | undefined,
-    page: searchParams.page ? parseInt(searchParams.page as string) : 1,
+    page: searchParams.page ? parseInt(searchParams.page as string, 10) : 1,
   };
 
   // 使用 ref 来存储最新的值,避免闭包陷阱
@@ -87,37 +91,38 @@ export function UsageLogsView({
 
   // 加载数据
   // shouldDetectNew: 是否检测新增记录(只在刷新时为 true,筛选/翻页时为 false)
-  const loadData = useCallback(async (shouldDetectNew = false) => {
-    startTransition(async () => {
-      const result = await getUsageLogs(filtersRef.current);
-      if (result.ok && result.data) {
-        // 只在刷新时检测新增(非筛选/翻页)
-        if (shouldDetectNew && previousLogsRef.current.size > 0) {
-          const newIds = result.data.logs
-            .filter(log => !previousLogsRef.current.has(log.id))
-            .map(log => log.id)
-            .slice(0, 10); // 限制最多高亮 10 条
+  const loadData = useCallback(
+    async (shouldDetectNew = false) => {
+      startTransition(async () => {
+        const result = await getUsageLogs(filtersRef.current);
+        if (result.ok && result.data) {
+          // 只在刷新时检测新增(非筛选/翻页)
+          if (shouldDetectNew && previousLogsRef.current.size > 0) {
+            const newIds = result.data.logs
+              .filter((log) => !previousLogsRef.current.has(log.id))
+              .map((log) => log.id)
+              .slice(0, 10); // 限制最多高亮 10 条
 
-          if (newIds.length > 0) {
-            setNewLogIds(new Set(newIds));
-            // 800ms 后清除高亮
-            setTimeout(() => setNewLogIds(new Set()), 800);
+            if (newIds.length > 0) {
+              setNewLogIds(new Set(newIds));
+              // 800ms 后清除高亮
+              setTimeout(() => setNewLogIds(new Set()), 800);
+            }
           }
-        }
 
-        // 更新记录缓存
-        previousLogsRef.current = new Map(
-          result.data.logs.map(log => [log.id, true])
-        );
+          // 更新记录缓存
+          previousLogsRef.current = new Map(result.data.logs.map((log) => [log.id, true]));
 
-        setData(result.data);
-        setError(null);
-      } else {
-        setError(!result.ok && 'error' in result ? result.error : t("logs.error.loadFailed"));
-        setData(null);
-      }
-    });
-  }, [startTransition, t]);
+          setData(result.data);
+          setError(null);
+        } else {
+          setError(!result.ok && "error" in result ? result.error : t("logs.error.loadFailed"));
+          setData(null);
+        }
+      });
+    },
+    [t]
+  );
 
   // 手动刷新(检测新增)
   const handleManualRefresh = async () => {
@@ -153,10 +158,10 @@ export function UsageLogsView({
     }, 3000); // 3 秒间隔
 
     return () => clearInterval(intervalId);
-  }, [isAutoRefresh, loadData]);  
+  }, [isAutoRefresh, loadData]);
 
   // 处理筛选条件变更
-  const handleFilterChange = (newFilters: Omit<typeof filters, 'page'>) => {
+  const handleFilterChange = (newFilters: Omit<typeof filters, "page">) => {
     const query = new URLSearchParams();
 
     if (newFilters.userId) query.set("userId", newFilters.userId.toString());
@@ -212,11 +217,15 @@ export function UsageLogsView({
             <CardContent className="text-xs text-muted-foreground space-y-1">
               <div className="flex justify-between">
                 <span>{t("logs.stats.input")}:</span>
-                <span className="font-mono">{formatTokenAmount(data.summary.totalInputTokens)}</span>
+                <span className="font-mono">
+                  {formatTokenAmount(data.summary.totalInputTokens)}
+                </span>
               </div>
               <div className="flex justify-between">
                 <span>{t("logs.stats.output")}:</span>
-                <span className="font-mono">{formatTokenAmount(data.summary.totalOutputTokens)}</span>
+                <span className="font-mono">
+                  {formatTokenAmount(data.summary.totalOutputTokens)}
+                </span>
               </div>
             </CardContent>
           </Card>
@@ -280,9 +289,7 @@ export function UsageLogsView({
                 disabled={isPending}
                 className="gap-2"
               >
-                <RefreshCw
-                  className={`h-4 w-4 ${isManualRefreshing ? 'animate-spin' : ''}`}
-                />
+                <RefreshCw className={`h-4 w-4 ${isManualRefreshing ? "animate-spin" : ""}`} />
                 {t("logs.actions.refresh")}
               </Button>
 

+ 20 - 13
src/app/[locale]/dashboard/logs/page.tsx

@@ -1,14 +1,14 @@
+import { getTranslations } from "next-intl/server";
 import { Suspense } from "react";
+import { getKeys } from "@/actions/keys";
+import { getProviders } from "@/actions/providers";
+import { getUsers } from "@/actions/users";
+import { ActiveSessionsPanel } from "@/components/customs/active-sessions-panel";
+import { Section } from "@/components/section";
 import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { Section } from "@/components/section";
-import { UsageLogsView } from "./_components/usage-logs-view";
-import { ActiveSessionsPanel } from "@/components/customs/active-sessions-panel";
-import { getUsers } from "@/actions/users";
-import { getProviders } from "@/actions/providers";
-import { getKeys } from "@/actions/keys";
 import { getSystemSettings } from "@/repository/system-config";
-import { getTranslations } from "next-intl/server";
+import { UsageLogsView } from "./_components/usage-logs-view";
 
 export const dynamic = "force-dynamic";
 
@@ -34,7 +34,13 @@ export default async function UsageLogsPage({
   // 管理员:获取用户和供应商列表
   // 非管理员:获取当前用户的 Keys 列表
   const [users, providers, initialKeys, resolvedSearchParams, systemSettings] = isAdmin
-    ? await Promise.all([getUsers(), getProviders(), Promise.resolve({ ok: true, data: [] }), searchParams, getSystemSettings()])
+    ? await Promise.all([
+        getUsers(),
+        getProviders(),
+        Promise.resolve({ ok: true, data: [] }),
+        searchParams,
+        getSystemSettings(),
+      ])
     : await Promise.all([
         Promise.resolve([]),
         Promise.resolve([]),
@@ -47,11 +53,12 @@ export default async function UsageLogsPage({
     <div className="space-y-6">
       <ActiveSessionsPanel currencyCode={systemSettings.currencyDisplay} />
 
-      <Section
-        title={t("title.usageLogs")}
-        description={t("title.usageLogsDescription")}
-      >
-        <Suspense fallback={<div className="text-center py-8 text-muted-foreground">{t("logs.stats.loading")}</div>}>
+      <Section title={t("title.usageLogs")} description={t("title.usageLogsDescription")}>
+        <Suspense
+          fallback={
+            <div className="text-center py-8 text-muted-foreground">{t("logs.stats.loading")}</div>
+          }
+        >
           <UsageLogsView
             isAdmin={isAdmin}
             users={users}

+ 6 - 6
src/app/[locale]/dashboard/page.tsx

@@ -1,14 +1,14 @@
+import { getTranslations } from "next-intl/server";
+import { hasPriceTable } from "@/actions/model-prices";
+import { getUserStatistics } from "@/actions/statistics";
+import { getUsers } from "@/actions/users";
+import { OverviewPanel } from "@/components/customs/overview-panel";
 import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { getUserStatistics } from "@/actions/statistics";
-import { hasPriceTable } from "@/actions/model-prices";
 import { getSystemSettings } from "@/repository/system-config";
-import { getUsers } from "@/actions/users";
+import { DEFAULT_TIME_RANGE } from "@/types/statistics";
 import { StatisticsWrapper } from "./_components/statistics";
-import { OverviewPanel } from "@/components/customs/overview-panel";
 import { UserQuickOverview } from "./_components/user-quick-overview";
-import { DEFAULT_TIME_RANGE } from "@/types/statistics";
-import { getTranslations } from "next-intl/server";
 
 export const dynamic = "force-dynamic";
 

+ 7 - 7
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx

@@ -1,7 +1,12 @@
 "use client";
 
-import { useState, useTransition } from "react";
+import { Loader2, Settings } from "lucide-react";
 import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useState, useTransition } from "react";
+import { toast } from "sonner";
+import { editKey } from "@/actions/keys";
+import { Button } from "@/components/ui/button";
 import {
   Dialog,
   DialogContent,
@@ -11,14 +16,8 @@ import {
   DialogTitle,
   DialogTrigger,
 } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Settings, Loader2 } from "lucide-react";
-import { editKey } from "@/actions/keys";
-import { toast } from "sonner";
-import { type CurrencyCode, CURRENCY_CONFIG } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
 import {
   Select,
   SelectContent,
@@ -26,6 +25,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { CURRENCY_CONFIG, type CurrencyCode } from "@/lib/utils/currency";
 
 interface KeyQuota {
   cost5h: { current: number; limit: number | null };

+ 7 - 7
src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx

@@ -1,7 +1,12 @@
 "use client";
 
-import { useState, useTransition } from "react";
+import { Loader2, Settings } from "lucide-react";
 import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useState, useTransition } from "react";
+import { toast } from "sonner";
+import { editUser } from "@/actions/users";
+import { Button } from "@/components/ui/button";
 import {
   Dialog,
   DialogContent,
@@ -11,14 +16,9 @@ import {
   DialogTitle,
   DialogTrigger,
 } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Settings, Loader2 } from "lucide-react";
-import { editUser } from "@/actions/users";
-import { toast } from "sonner";
-import { type CurrencyCode, CURRENCY_CONFIG } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import { CURRENCY_CONFIG, type CurrencyCode } from "@/lib/utils/currency";
 
 interface UserQuota {
   rpm: { current: number; limit: number; window: "per_minute" };

+ 9 - 9
src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-client.tsx

@@ -1,6 +1,14 @@
 "use client";
 
+import { Settings } from "lucide-react";
+import { useTranslations } from "next-intl";
 import { useState } from "react";
+import { QuotaCountdownCompact } from "@/components/quota/quota-countdown";
+import { QuotaProgress } from "@/components/quota/quota-progress";
+import { QuotaWindowType } from "@/components/quota/quota-window-type";
+import { UserQuotaHeader } from "@/components/quota/user-quota-header";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
 import { Card, CardContent } from "@/components/ui/card";
 import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
 import {
@@ -11,18 +19,10 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Settings } from "lucide-react";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
-import { UserQuotaHeader } from "@/components/quota/user-quota-header";
-import { QuotaProgress } from "@/components/quota/quota-progress";
-import { QuotaWindowType } from "@/components/quota/quota-window-type";
-import { QuotaCountdownCompact } from "@/components/quota/quota-countdown";
-import { hasKeyQuotaSet, isUserExceeded, getUsageRate } from "@/lib/utils/quota-helpers";
+import { getUsageRate, hasKeyQuotaSet, isUserExceeded } from "@/lib/utils/quota-helpers";
 import { EditKeyQuotaDialog } from "./edit-key-quota-dialog";
-import { useTranslations } from "next-intl";
 
 interface KeyQuota {
   cost5h: { current: number; limit: number | null; resetAt?: Date };

+ 5 - 5
src/app/[locale]/dashboard/quotas/keys/_components/keys-quota-manager.tsx

@@ -1,7 +1,8 @@
 "use client";
 
-import { useState, useMemo } from "react";
-import { KeysQuotaClient } from "./keys-quota-client";
+import { Search } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
 import { Input } from "@/components/ui/input";
 import {
   Select,
@@ -10,10 +11,9 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Search } from "lucide-react";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import { hasKeyQuotaSet, isWarning, isExceeded } from "@/lib/utils/quota-helpers";
-import { useTranslations } from "next-intl";
+import { hasKeyQuotaSet, isExceeded, isWarning } from "@/lib/utils/quota-helpers";
+import { KeysQuotaClient } from "./keys-quota-client";
 
 interface KeyQuota {
   cost5h: { current: number; limit: number | null };

+ 1 - 1
src/app/[locale]/dashboard/quotas/layout.tsx

@@ -1,6 +1,6 @@
+import { getTranslations } from "next-intl/server";
 import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Link } from "@/i18n/routing";
-import { getTranslations } from "next-intl/server";
 
 export default async function QuotasLayout({ children }: { children: React.ReactNode }) {
   const t = await getTranslations("quota.layout");

+ 3 - 3
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx

@@ -1,13 +1,13 @@
 "use client";
 
 import { CheckCircle, XCircle } from "lucide-react";
+import { useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
 import { CircularProgress } from "@/components/ui/circular-progress";
 import { CountdownTimer } from "@/components/ui/countdown-timer";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { getProviderTypeConfig } from "@/lib/provider-type-utils";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
 import type { ProviderType } from "@/types/provider";
 
 interface ProviderQuota {
@@ -68,7 +68,7 @@ export function ProviderQuotaListItem({
               {resetAt ? (
                 <CountdownTimer
                   targetDate={resetAt}
-                  prefix={t("list.resetIn") + " "}
+                  prefix={`${t("list.resetIn")} `}
                   className="text-[10px] text-muted-foreground"
                 />
               ) : resetInfo ? (

+ 1 - 1
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-sort-dropdown.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { ArrowUpDown } from "lucide-react";
+import { useTranslations } from "next-intl";
 import {
   Select,
   SelectContent,
@@ -8,7 +9,6 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { useTranslations } from "next-intl";
 
 export type QuotaSortKey = "name" | "priority" | "weight" | "usage";
 

+ 4 - 4
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx

@@ -1,13 +1,13 @@
 "use client";
 
-import { useMemo, useState } from "react";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
 import { ChevronDown, Globe } from "lucide-react";
 import { useTranslations } from "next-intl";
-import type { ProviderType } from "@/types/provider";
+import { useMemo, useState } from "react";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import type { QuotaSortKey } from "./provider-quota-sort-dropdown";
+import type { ProviderType } from "@/types/provider";
 import { ProviderQuotaListItem } from "./provider-quota-list-item";
+import type { QuotaSortKey } from "./provider-quota-sort-dropdown";
 
 interface ProviderQuota {
   cost5h: { current: number; limit: number | null; resetInfo: string };

+ 5 - 5
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx

@@ -1,15 +1,15 @@
 "use client";
 
-import { useState, useMemo } from "react";
 import { Search, X } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
 import { ProviderTypeFilter } from "@/app/[locale]/settings/providers/_components/provider-type-filter";
-import { ProviderQuotaSortDropdown, type QuotaSortKey } from "./provider-quota-sort-dropdown";
-import { ProvidersQuotaClient } from "./providers-quota-client";
 import { Input } from "@/components/ui/input";
 import { useDebounce } from "@/lib/hooks/use-debounce";
-import type { ProviderType } from "@/types/provider";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import type { ProviderType } from "@/types/provider";
+import { ProviderQuotaSortDropdown, type QuotaSortKey } from "./provider-quota-sort-dropdown";
+import { ProvidersQuotaClient } from "./providers-quota-client";
 
 interface ProviderQuota {
   cost5h: { current: number; limit: number | null; resetInfo: string };

+ 3 - 3
src/app/[locale]/dashboard/quotas/providers/page.tsx

@@ -1,7 +1,7 @@
-import { getProviders, getProviderLimitUsageBatch } from "@/actions/providers";
-import { ProvidersQuotaManager } from "./_components/providers-quota-manager";
-import { getSystemSettings } from "@/repository/system-config";
 import { getTranslations } from "next-intl/server";
+import { getProviderLimitUsageBatch, getProviders } from "@/actions/providers";
+import { getSystemSettings } from "@/repository/system-config";
+import { ProvidersQuotaManager } from "./_components/providers-quota-manager";
 
 // 强制动态渲染 (此页面需要实时数据和认证)
 export const dynamic = "force-dynamic";

+ 5 - 5
src/app/[locale]/dashboard/quotas/users/_components/users-quota-client.tsx

@@ -1,13 +1,13 @@
 "use client";
 
+import { useLocale, useTranslations } from "next-intl";
 import { useMemo } from "react";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
 import { QuotaProgress } from "@/components/quota/quota-progress";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { formatDateDistance } from "@/lib/utils/date-format";
-import { useLocale, useTranslations } from "next-intl";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Progress } from "@/components/ui/progress";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
+import { formatDateDistance } from "@/lib/utils/date-format";
 
 interface UserQuota {
   rpm: { current: number; limit: number; window: "per_minute" };

+ 6 - 7
src/app/[locale]/dashboard/quotas/users/page.tsx

@@ -1,12 +1,11 @@
-import { getUsers } from "@/actions/users";
-import { getUserLimitUsage } from "@/actions/users";
-import { QuotaToolbar } from "@/components/quota/quota-toolbar";
-import { UsersQuotaClient } from "./_components/users-quota-client";
-import { getSystemSettings } from "@/repository/system-config";
-import { getTranslations } from "next-intl/server";
-import { Link } from "@/i18n/routing";
 import { Info } from "lucide-react";
+import { getTranslations } from "next-intl/server";
+import { getUserLimitUsage, getUsers } from "@/actions/users";
+import { QuotaToolbar } from "@/components/quota/quota-toolbar";
 import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Link } from "@/i18n/routing";
+import { getSystemSettings } from "@/repository/system-config";
+import { UsersQuotaClient } from "./_components/users-quota-client";
 
 // 强制动态渲染 (此页面需要实时数据和认证)
 export const dynamic = "force-dynamic";

+ 8 - 8
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-dashboard.tsx

@@ -1,15 +1,15 @@
 "use client";
 
+import { Loader2 } from "lucide-react";
+import { useTranslations } from "next-intl";
 import * as React from "react";
-import { RateLimitFilters } from "./rate-limit-filters";
-import { RateLimitEventsChart } from "../../_components/rate-limit-events-chart";
-import { RateLimitTypeBreakdown } from "../../_components/rate-limit-type-breakdown";
-import { RateLimitTopUsers } from "../../_components/rate-limit-top-users";
 import { getRateLimitStats } from "@/actions/rate-limit-stats";
-import type { RateLimitEventFilters, RateLimitEventStats } from "@/types/statistics";
 import type { CurrencyCode } from "@/lib/utils";
-import { Loader2 } from "lucide-react";
-import { useTranslations } from "next-intl";
+import type { RateLimitEventFilters, RateLimitEventStats } from "@/types/statistics";
+import { RateLimitEventsChart } from "../../_components/rate-limit-events-chart";
+import { RateLimitTopUsers } from "../../_components/rate-limit-top-users";
+import { RateLimitTypeBreakdown } from "../../_components/rate-limit-type-breakdown";
+import { RateLimitFilters } from "./rate-limit-filters";
 
 export interface RateLimitDashboardProps {
   currencyCode?: CurrencyCode;
@@ -19,7 +19,7 @@ export interface RateLimitDashboardProps {
  * 限流事件统计仪表板
  * 包含过滤器和三个可视化组件
  */
-export function RateLimitDashboard({}: RateLimitDashboardProps) {
+export function RateLimitDashboard(_props: RateLimitDashboardProps) {
   const t = useTranslations("dashboard.rateLimits");
   const [loading, setLoading] = React.useState(true);
   const [error, setError] = React.useState<string | null>(null);

+ 7 - 7
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-filters.tsx

@@ -1,7 +1,14 @@
 "use client";
 
+import { format } from "date-fns";
+import { Calendar, X } from "lucide-react";
+import { useTranslations } from "next-intl";
 import * as React from "react";
+import { getProviders } from "@/actions/providers";
+import { getUsers } from "@/actions/users";
 import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
 import {
   Select,
   SelectContent,
@@ -9,14 +16,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
 import type { RateLimitEventFilters, RateLimitType } from "@/types/statistics";
-import { getUsers } from "@/actions/users";
-import { getProviders } from "@/actions/providers";
-import { useTranslations } from "next-intl";
-import { Calendar, X } from "lucide-react";
-import { format } from "date-fns";
 
 export interface RateLimitFiltersProps {
   initialFilters: RateLimitEventFilters;

+ 2 - 2
src/app/[locale]/dashboard/rate-limits/page.tsx

@@ -1,9 +1,9 @@
+import { getTranslations } from "next-intl/server";
+import { Section } from "@/components/section";
 import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { Section } from "@/components/section";
 import { getSystemSettings } from "@/repository/system-config";
 import { RateLimitDashboard } from "./_components/rate-limit-dashboard";
-import { getTranslations } from "next-intl/server";
 
 export const dynamic = "force-dynamic";
 

+ 23 - 30
src/app/[locale]/dashboard/sessions/[sessionId]/messages/page.tsx

@@ -1,18 +1,17 @@
 "use client";
 
-import * as React from "react";
-import { useRouter } from "@/i18n/routing";
+import { useQuery } from "@tanstack/react-query";
+import { ArrowLeft, Check, Copy, Download, Hash, Monitor } from "lucide-react";
 import { useParams } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { ArrowLeft, Copy, Download, Check, Monitor, Hash } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useEffect, useState } from "react";
 import { getSessionDetails } from "@/actions/active-sessions";
-import { useState, useEffect } from "react";
 import { Section } from "@/components/section";
 import { Badge } from "@/components/ui/badge";
-import { useQuery } from "@tanstack/react-query";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { useRouter } from "@/i18n/routing";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
 
 async function fetchSystemSettings(): Promise<{ currencyDisplay: CurrencyCode }> {
   const response = await fetch("/api/system-settings");
@@ -189,14 +188,12 @@ export default function SessionMessagesPage() {
             {/* User-Agent 信息 */}
             {sessionStats?.userAgent && (
               <Section title={t("details.clientInfo")} description={tDesc("clientInfo")}>
-                <>
-                  <div className="rounded-md border bg-muted/50 p-4">
-                    <div className="flex items-start gap-3">
-                      <Monitor className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
-                      <code className="text-sm font-mono break-all">{sessionStats.userAgent}</code>
-                    </div>
+                <div className="rounded-md border bg-muted/50 p-4">
+                  <div className="flex items-start gap-3">
+                    <Monitor className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
+                    <code className="text-sm font-mono break-all">{sessionStats.userAgent}</code>
                   </div>
-                </>
+                </div>
               </Section>
             )}
 
@@ -206,13 +203,11 @@ export default function SessionMessagesPage() {
                 title={t("details.requestMessages")}
                 description={t("details.requestMessagesDescription")}
               >
-                <>
-                  <div className="rounded-md border bg-muted/50 p-6">
-                    <pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words font-mono">
-                      {JSON.stringify(messages, null, 2)}
-                    </pre>
-                  </div>
-                </>
+                <div className="rounded-md border bg-muted/50 p-6">
+                  <pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words font-mono">
+                    {JSON.stringify(messages, null, 2)}
+                  </pre>
+                </div>
               </Section>
             )}
 
@@ -242,13 +237,11 @@ export default function SessionMessagesPage() {
                   </Button>
                 }
               >
-                <>
-                  <div className="rounded-md border bg-muted/50 p-6 max-h-[600px] overflow-auto">
-                    <pre className="text-xs whitespace-pre-wrap break-words font-mono">
-                      {formatResponse(response)}
-                    </pre>
-                  </div>
-                </>
+                <div className="rounded-md border bg-muted/50 p-6 max-h-[600px] overflow-auto">
+                  <pre className="text-xs whitespace-pre-wrap break-words font-mono">
+                    {formatResponse(response)}
+                  </pre>
+                </div>
               </Section>
             )}
 

+ 5 - 5
src/app/[locale]/dashboard/sessions/_components/active-sessions-table.tsx

@@ -1,5 +1,9 @@
 "use client";
 
+import { Eye } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
 import {
   Table,
   TableBody,
@@ -8,15 +12,11 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Eye } from "lucide-react";
 import { Link } from "@/i18n/routing";
 import { cn } from "@/lib/utils";
-import type { ActiveSessionInfo } from "@/types/session";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import type { ActiveSessionInfo } from "@/types/session";
 
 interface ActiveSessionsTableProps {
   sessions: ActiveSessionInfo[];

+ 4 - 5
src/app/[locale]/dashboard/sessions/_components/session-messages-dialog.tsx

@@ -1,6 +1,9 @@
 "use client";
 
-import * as React from "react";
+import { Eye } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { getSessionMessages } from "@/actions/active-sessions";
 import { Button } from "@/components/ui/button";
 import {
   Dialog,
@@ -10,10 +13,6 @@ import {
   DialogTitle,
   DialogTrigger,
 } from "@/components/ui/dialog";
-import { Eye } from "lucide-react";
-import { getSessionMessages } from "@/actions/active-sessions";
-import { useState } from "react";
-import { useTranslations } from "next-intl";
 
 interface SessionMessagesDialogProps {
   sessionId: string;

+ 5 - 6
src/app/[locale]/dashboard/sessions/page.tsx

@@ -1,16 +1,15 @@
 "use client";
 
-import * as React from "react";
 import { useQuery } from "@tanstack/react-query";
+import { ArrowLeft } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { getAllSessions } from "@/actions/active-sessions";
 import { Section } from "@/components/section";
 import { Button } from "@/components/ui/button";
-import { ArrowLeft } from "lucide-react";
 import { useRouter } from "@/i18n/routing";
-import { getAllSessions } from "@/actions/active-sessions";
-import { ActiveSessionsTable } from "./_components/active-sessions-table";
-import type { ActiveSessionInfo } from "@/types/session";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
+import type { ActiveSessionInfo } from "@/types/session";
+import { ActiveSessionsTable } from "./_components/active-sessions-table";
 
 const REFRESH_INTERVAL = 3000; // 3秒刷新一次
 

+ 1 - 1
src/app/[locale]/dashboard/users/page.tsx

@@ -1,6 +1,6 @@
+import { redirect } from "next/navigation";
 import { getUsers } from "@/actions/users";
 import { getSession } from "@/lib/auth";
-import { redirect } from "next/navigation";
 import { UsersPageClient } from "./users-page-client";
 
 export default async function UsersPage() {

+ 5 - 5
src/app/[locale]/dashboard/users/users-page-client.tsx

@@ -1,7 +1,8 @@
 "use client";
 
-import { useState, useMemo } from "react";
-import { UserKeyManager } from "../_components/user/user-key-manager";
+import { Search } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
 import { Input } from "@/components/ui/input";
 import {
   Select,
@@ -10,9 +11,8 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Search } from "lucide-react";
-import { useTranslations } from "next-intl";
-import type { UserDisplay, User } from "@/types/user";
+import type { User, UserDisplay } from "@/types/user";
+import { UserKeyManager } from "../_components/user/user-key-manager";
 
 interface UsersPageClientProps {
   users: UserDisplay[];

+ 23 - 22
src/app/[locale]/internal/dashboard/big-screen/page.tsx

@@ -1,43 +1,44 @@
 "use client";
 
-import React, { useState, useEffect, useRef } from "react";
+import { AnimatePresence, motion } from "framer-motion";
 import {
   Activity,
-  Server,
-  Zap,
-  DollarSign,
-  Clock,
   AlertTriangle,
+  ArrowDown,
+  ArrowUp,
+  Clock,
+  DollarSign,
   Globe,
   Moon,
-  Sun,
+  PieChart as PieIcon,
   RefreshCw,
-  ArrowUp,
-  ArrowDown,
-  Wifi,
+  Server,
   Shield,
+  Sun,
   User,
-  PieChart as PieIcon,
+  Wifi,
+  Zap,
 } from "lucide-react";
+import { useLocale, useTranslations } from "next-intl";
+import type React from "react";
+import { useEffect, useRef, useState } from "react";
 import {
-  AreaChart,
   Area,
-  ResponsiveContainer,
-  PieChart,
-  Pie,
+  AreaChart,
+  CartesianGrid,
   Cell,
-  Tooltip,
   Legend,
+  Pie,
+  PieChart,
+  ResponsiveContainer,
+  Tooltip,
   XAxis,
   YAxis,
-  CartesianGrid,
 } from "recharts";
-import { motion, AnimatePresence } from "framer-motion";
 import useSWR from "swr";
-import { useTranslations, useLocale } from "next-intl";
-import { useRouter, usePathname } from "@/i18n/routing";
-import { locales, localeLabels, type Locale } from "@/i18n/config";
 import { getDashboardRealtimeData } from "@/actions/dashboard-realtime";
+import { type Locale, localeLabels, locales } from "@/i18n/config";
+import { usePathname, useRouter } from "@/i18n/routing";
 
 /**
  * ============================================================================
@@ -180,7 +181,7 @@ const CountUp = ({
     const animate = (currentTime: number) => {
       const elapsed = currentTime - startTime;
       const progress = Math.min(elapsed / duration, 1);
-      const ease = 1 - Math.pow(1 - progress, 4);
+      const ease = 1 - (1 - progress) ** 4;
       const current = start + (end - start) * ease;
       setDisplayValue(current);
       if (progress < 1) requestAnimationFrame(animate);
@@ -442,7 +443,7 @@ const UserRankings = ({
             className={`flex items-center gap-3 p-2 rounded border ${
               index === 0
                 ? "bg-gradient-to-r from-orange-500/20 to-transparent border-orange-500/30"
-                : theme.border + " bg-white/5"
+                : `${theme.border} bg-white/5`
             }`}
           >
             <div

+ 5 - 5
src/app/[locale]/internal/data-gen/_components/data-generator-page.tsx

@@ -1,13 +1,13 @@
 "use client";
 
-import { useState, useMemo } from "react";
+import { AlertCircle, Download, FileDown, Loader2, Settings } from "lucide-react";
 import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
+import { Alert, AlertDescription } from "@/components/ui/alert";
 import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Switch } from "@/components/ui/switch";
 import {
   Table,
@@ -17,7 +17,7 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
-import { AlertCircle, Download, FileDown, Loader2, Settings } from "lucide-react";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import type { GeneratorResult, UserBreakdownResult } from "@/lib/data-generator/types";
 
 export function DataGeneratorPage() {

+ 7 - 7
src/app/[locale]/layout.tsx

@@ -1,14 +1,14 @@
 import type { Metadata } from "next";
 import "../globals.css";
-import { Toaster } from "@/components/ui/sonner";
-import { Footer } from "@/components/customs/footer";
-import { AppProviders } from "../providers";
-import { getSystemSettings } from "@/repository/system-config";
-import { logger } from "@/lib/logger";
+import { notFound } from "next/navigation";
 import { NextIntlClientProvider } from "next-intl";
 import { getMessages } from "next-intl/server";
-import { locales, type Locale } from "@/i18n/config";
-import { notFound } from "next/navigation";
+import { Footer } from "@/components/customs/footer";
+import { Toaster } from "@/components/ui/sonner";
+import { type Locale, locales } from "@/i18n/config";
+import { logger } from "@/lib/logger";
+import { getSystemSettings } from "@/repository/system-config";
+import { AppProviders } from "../providers";
 
 const FALLBACK_TITLE = "Claude Code Hub";
 

+ 5 - 5
src/app/[locale]/login/page.tsx

@@ -1,16 +1,16 @@
 "use client";
 
-import { Suspense, useState, useEffect } from "react";
-import { useRouter, Link } from "@/i18n/routing";
+import { AlertTriangle, Book, Key, Loader2 } from "lucide-react";
 import { useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Suspense, useEffect, useState } from "react";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { Key, Loader2, AlertTriangle, Book } from "lucide-react";
 import { LanguageSwitcher } from "@/components/ui/language-switcher";
+import { Link, useRouter } from "@/i18n/routing";
 
 export default function LoginPage() {
   return (

+ 2 - 3
src/app/[locale]/settings/_components/settings-nav.tsx

@@ -1,11 +1,10 @@
 "use client";
 
+import { useTranslations } from "next-intl";
+import { ThemeSwitcher } from "@/components/ui/theme-switcher";
 import { Link, usePathname } from "@/i18n/routing";
-
 import { cn } from "@/lib/utils";
 import type { SettingsNavItem } from "../_lib/nav-items";
-import { ThemeSwitcher } from "@/components/ui/theme-switcher";
-import { useTranslations } from "next-intl";
 
 interface SettingsNavProps {
   items: SettingsNavItem[];

+ 4 - 4
src/app/[locale]/settings/client-versions/_components/client-version-stats-table.tsx

@@ -1,9 +1,8 @@
 "use client";
 
-import type { ClientVersionStats } from "@/lib/client-version-checker";
-import { getClientTypeDisplayName } from "@/lib/ua-parser";
+import { AlertTriangle, Check, Code2, HelpCircle, Package, Terminal } from "lucide-react";
+import { useLocale, useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
-import { Code2, Terminal, HelpCircle, Package, Check, AlertTriangle } from "lucide-react";
 import {
   Table,
   TableBody,
@@ -12,8 +11,9 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
+import type { ClientVersionStats } from "@/lib/client-version-checker";
+import { getClientTypeDisplayName } from "@/lib/ua-parser";
 import { formatDateDistance } from "@/lib/utils/date-format";
-import { useLocale, useTranslations } from "next-intl";
 
 interface ClientVersionStatsTableProps {
   data: ClientVersionStats[];

+ 5 - 5
src/app/[locale]/settings/client-versions/_components/client-version-toggle.tsx

@@ -1,13 +1,13 @@
 "use client";
 
-import { useState, useTransition } from "react";
+import { AlertCircle } from "lucide-react";
 import { useTranslations } from "next-intl";
+import { useState, useTransition } from "react";
+import { toast } from "sonner";
+import { saveSystemSettings } from "@/actions/system-config";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { AlertCircle } from "lucide-react";
-import { saveSystemSettings } from "@/actions/system-config";
-import { toast } from "sonner";
 
 interface ClientVersionToggleProps {
   enabled: boolean;

+ 4 - 4
src/app/[locale]/settings/client-versions/page.tsx

@@ -1,12 +1,12 @@
 import { getTranslations } from "next-intl/server";
-import { redirect } from "@/i18n/routing";
-import { getSession } from "@/lib/auth";
 import { fetchClientVersionStats } from "@/actions/client-versions";
 import { fetchSystemSettings } from "@/actions/system-config";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { redirect } from "@/i18n/routing";
+import { getSession } from "@/lib/auth";
 import { SettingsPageHeader } from "../_components/settings-page-header";
-import { ClientVersionToggle } from "./_components/client-version-toggle";
 import { ClientVersionStatsTable } from "./_components/client-version-stats-table";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { ClientVersionToggle } from "./_components/client-version-toggle";
 
 export default async function ClientVersionsPage({
   params,

+ 4 - 4
src/app/[locale]/settings/config/_components/auto-cleanup-form.tsx

@@ -1,16 +1,16 @@
 "use client";
 
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Loader2 } from "lucide-react";
+import { useTranslations } from "next-intl";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
+import { toast } from "sonner";
 import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useTranslations } from "next-intl";
-import { Loader2 } from "lucide-react";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
-import { toast } from "sonner";
 import type { SystemSettings } from "@/types/system-config";
 
 /**

+ 7 - 7
src/app/[locale]/settings/config/_components/system-settings-form.tsx

@@ -1,11 +1,12 @@
 "use client";
 
-import { useState, useTransition } from "react";
 import { useTranslations } from "next-intl";
+import { useState, useTransition } from "react";
+import { toast } from "sonner";
+import { saveSystemSettings } from "@/actions/system-config";
+import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { Button } from "@/components/ui/button";
 import {
   Select,
   SelectContent,
@@ -13,11 +14,10 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { saveSystemSettings } from "@/actions/system-config";
-import { toast } from "sonner";
-import { CURRENCY_CONFIG } from "@/lib/utils";
-import type { SystemSettings, BillingModelSource } from "@/types/system-config";
+import { Switch } from "@/components/ui/switch";
 import type { CurrencyCode } from "@/lib/utils";
+import { CURRENCY_CONFIG } from "@/lib/utils";
+import type { BillingModelSource, SystemSettings } from "@/types/system-config";
 
 interface SystemSettingsFormProps {
   initialSettings: Pick<

+ 2 - 2
src/app/[locale]/settings/config/page.tsx

@@ -1,9 +1,9 @@
 import { getTranslations } from "next-intl/server";
 import { Section } from "@/components/section";
-import { SettingsPageHeader } from "../_components/settings-page-header";
 import { getSystemSettings } from "@/repository/system-config";
-import { SystemSettingsForm } from "./_components/system-settings-form";
+import { SettingsPageHeader } from "../_components/settings-page-header";
 import { AutoCleanupForm } from "./_components/auto-cleanup-form";
+import { SystemSettingsForm } from "./_components/system-settings-form";
 
 export const dynamic = "force-dynamic";
 

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików