Răsfoiți Sursa

Merge pull request #184 from ding113/dev

feat(big-screen): 数据大屏
Ding 2 luni în urmă
părinte
comite
71752d4ce5

+ 4 - 0
CHANGELOG.md

@@ -14,6 +14,10 @@ All notable changes to this project will be documented in this file.
 - Merge dev to main with internationalization improvements (Japanese, Russian, Traditional Chinese) and UI enhancements for daily limit dialogs (#182) @ding113
 - Refactor provider quota management page from card layout to compact list layout with circular progress indicators, search, and sorting capabilities (#170) @ding113
 
+### Changed
+
+- Enhance data dashboard with comprehensive optimizations and improvements (#183) @ding113
+
 ### Fixed
 
 - Fix database migration duplicate enum type creation error (#181) @ding113

+ 34 - 0
messages/en/bigScreen.json

@@ -0,0 +1,34 @@
+{
+  "title": "CLAUDE CODE HUB",
+  "subtitle": "REALTIME DATA MONITOR",
+  "metrics": {
+    "concurrent": "Concurrency",
+    "activeSessions": "Active Sessions",
+    "requests": "Requests (24h)",
+    "cost": "Cost (Today)",
+    "latency": "Avg Latency",
+    "errorRate": "Error Rate"
+  },
+  "sections": {
+    "providerRank": "Top Providers",
+    "providerQuotas": "Provider Slot Usage",
+    "requestTrend": "Traffic Trend (24h)",
+    "activity": "Live Stream",
+    "userRank": "Top Users (Realtime)",
+    "modelDist": "Model Distribution"
+  },
+  "headers": {
+    "user": "User",
+    "model": "Model",
+    "provider": "Provider",
+    "latency": "Latency",
+    "cost": "Cost",
+    "status": "Status",
+    "usage": "Usage",
+    "quota": "Quota"
+  },
+  "status": {
+    "normal": "System Normal",
+    "lastUpdate": "Last Update"
+  }
+}

+ 2 - 0
messages/en/index.ts

@@ -1,4 +1,5 @@
 import auth from "./auth.json";
+import bigScreen from "./bigScreen.json";
 import common from "./common.json";
 import customs from "./customs.json";
 import dashboard from "./dashboard.json";
@@ -17,6 +18,7 @@ import internal from "./internal.json";
 
 export default {
   auth,
+  bigScreen,
   common,
   customs,
   dashboard,

+ 34 - 0
messages/ja/bigScreen.json

@@ -0,0 +1,34 @@
+{
+  "title": "CLAUDE CODE HUB",
+  "subtitle": "リアルタイムデータモニター",
+  "metrics": {
+    "concurrent": "同時接続数",
+    "activeSessions": "アクティブセッション",
+    "requests": "今日のリクエスト",
+    "cost": "今日のコスト",
+    "latency": "平均レスポンス",
+    "errorRate": "エラー率"
+  },
+  "sections": {
+    "providerRank": "プロバイダーランキング (TOP 5)",
+    "providerQuotas": "プロバイダー並行スロット状況",
+    "requestTrend": "24時間トラフィック推移",
+    "activity": "リアルタイムリクエスト (Live)",
+    "userRank": "ユーザー消費ランキング (Realtime)",
+    "modelDist": "モデル呼び出し分布"
+  },
+  "headers": {
+    "user": "ユーザー",
+    "model": "モデル",
+    "provider": "プロバイダー",
+    "latency": "レスポンス時間",
+    "cost": "コスト",
+    "status": "ステータス",
+    "usage": "使用量",
+    "quota": "クォータ"
+  },
+  "status": {
+    "normal": "システム正常",
+    "lastUpdate": "データ更新"
+  }
+}

+ 2 - 0
messages/ja/index.ts

@@ -1,4 +1,5 @@
 import auth from "./auth.json";
+import bigScreen from "./bigScreen.json";
 import common from "./common.json";
 import customs from "./customs.json";
 import dashboard from "./dashboard.json";
@@ -17,6 +18,7 @@ import validation from "./validation.json";
 
 export default {
   auth,
+  bigScreen,
   common,
   customs,
   dashboard,

+ 34 - 0
messages/ru/bigScreen.json

@@ -0,0 +1,34 @@
+{
+  "title": "CLAUDE CODE HUB",
+  "subtitle": "МОНИТОР ДАННЫХ В РЕАЛЬНОМ ВРЕМЕНИ",
+  "metrics": {
+    "concurrent": "Параллельные подключения",
+    "activeSessions": "Активные сессии",
+    "requests": "Запросы (сегодня)",
+    "cost": "Стоимость (сегодня)",
+    "latency": "Средний отклик",
+    "errorRate": "Частота ошибок"
+  },
+  "sections": {
+    "providerRank": "Топ провайдеров",
+    "providerQuotas": "Использование слотов провайдера",
+    "requestTrend": "Тренд трафика (24ч)",
+    "activity": "Поток в реальном времени",
+    "userRank": "Топ пользователей (Realtime)",
+    "modelDist": "Распределение моделей"
+  },
+  "headers": {
+    "user": "Пользователь",
+    "model": "Модель",
+    "provider": "Провайдер",
+    "latency": "Задержка",
+    "cost": "Стоимость",
+    "status": "Статус",
+    "usage": "Использование",
+    "quota": "Квота"
+  },
+  "status": {
+    "normal": "Система в норме",
+    "lastUpdate": "Последнее обновление"
+  }
+}

+ 2 - 0
messages/ru/index.ts

@@ -1,4 +1,5 @@
 import auth from "./auth.json";
+import bigScreen from "./bigScreen.json";
 import common from "./common.json";
 import customs from "./customs.json";
 import dashboard from "./dashboard.json";
@@ -17,6 +18,7 @@ import internal from "./internal.json";
 
 export default {
   auth,
+  bigScreen,
   common,
   customs,
   dashboard,

+ 34 - 0
messages/zh-CN/bigScreen.json

@@ -0,0 +1,34 @@
+{
+  "title": "CLAUDE CODE HUB",
+  "subtitle": "实时数据监控中台",
+  "metrics": {
+    "concurrent": "并发数",
+    "activeSessions": "活跃会话",
+    "requests": "今日请求",
+    "cost": "今日成本",
+    "latency": "平均响应",
+    "errorRate": "错误率"
+  },
+  "sections": {
+    "providerRank": "供应商排行 (TOP 5)",
+    "providerQuotas": "供应商并发插槽状态",
+    "requestTrend": "24h 流量趋势",
+    "activity": "实时请求流 (Live)",
+    "userRank": "用户消耗排行 (Realtime)",
+    "modelDist": "模型调用分布"
+  },
+  "headers": {
+    "user": "用户",
+    "model": "模型",
+    "provider": "供应商",
+    "latency": "耗时",
+    "cost": "成本",
+    "status": "状态",
+    "usage": "用量",
+    "quota": "配额"
+  },
+  "status": {
+    "normal": "系统正常",
+    "lastUpdate": "数据更新"
+  }
+}

+ 2 - 0
messages/zh-CN/index.ts

@@ -1,4 +1,5 @@
 import auth from "./auth.json";
+import bigScreen from "./bigScreen.json";
 import common from "./common.json";
 import customs from "./customs.json";
 import dashboard from "./dashboard.json";
@@ -17,6 +18,7 @@ import validation from "./validation.json";
 
 export default {
   auth,
+  bigScreen,
   common,
   customs,
   dashboard,

+ 34 - 0
messages/zh-TW/bigScreen.json

@@ -0,0 +1,34 @@
+{
+  "title": "CLAUDE CODE HUB",
+  "subtitle": "即時資料監控中台",
+  "metrics": {
+    "concurrent": "並發數",
+    "activeSessions": "活躍會話",
+    "requests": "今日請求",
+    "cost": "今日成本",
+    "latency": "平均回應",
+    "errorRate": "錯誤率"
+  },
+  "sections": {
+    "providerRank": "供應商排行 (TOP 5)",
+    "providerQuotas": "供應商並發插槽狀態",
+    "requestTrend": "24h 流量趨勢",
+    "activity": "即時請求流 (Live)",
+    "userRank": "用戶消耗排行 (Realtime)",
+    "modelDist": "模型調用分布"
+  },
+  "headers": {
+    "user": "用戶",
+    "model": "模型",
+    "provider": "供應商",
+    "latency": "耗時",
+    "cost": "成本",
+    "status": "狀態",
+    "usage": "用量",
+    "quota": "配額"
+  },
+  "status": {
+    "normal": "系統正常",
+    "lastUpdate": "資料更新"
+  }
+}

+ 2 - 0
messages/zh-TW/index.ts

@@ -1,4 +1,5 @@
 import auth from "./auth.json";
+import bigScreen from "./bigScreen.json";
 import common from "./common.json";
 import customs from "./customs.json";
 import dashboard from "./dashboard.json";
@@ -17,6 +18,7 @@ import internal from "./internal.json";
 
 export default {
   auth,
+  bigScreen,
   common,
   customs,
   dashboard,

+ 297 - 0
src/actions/dashboard-realtime.ts

@@ -0,0 +1,297 @@
+"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 { getActiveSessions } from "./active-sessions";
+import {
+  findDailyLeaderboard,
+  findDailyProviderLeaderboard,
+  findDailyModelLeaderboard,
+  type LeaderboardEntry,
+  type ProviderLeaderboardEntry,
+  type ModelLeaderboardEntry,
+} from "@/repository/leaderboard";
+import { getProviderSlots, type ProviderSlotInfo } from "./provider-slots";
+import { getUserStatistics } from "./statistics";
+import { findRecentActivityStream } from "@/repository/activity-stream";
+
+/**
+ * 实时活动流条目
+ */
+export interface ActivityStreamEntry {
+  /** 消息 ID */
+  id: string;
+  /** 用户名 */
+  user: string;
+  /** 模型名称 */
+  model: string;
+  /** 供应商名称 */
+  provider: string;
+  /** 响应时间(毫秒) */
+  latency: number;
+  /** HTTP 状态码 */
+  status: number;
+  /** 成本(美元) */
+  cost: number;
+  /** 开始时间 */
+  startTime: number;
+}
+
+/**
+ * 数据大屏完整数据
+ */
+export interface DashboardRealtimeData {
+  /** 核心指标 */
+  metrics: OverviewData;
+
+  /** 实时活动流(最近20条) */
+  activityStream: ActivityStreamEntry[];
+
+  /** 用户排行榜(Top 5) */
+  userRankings: LeaderboardEntry[];
+
+  /** 供应商排行榜(Top 5) */
+  providerRankings: ProviderLeaderboardEntry[];
+
+  /** 供应商并发插槽状态 */
+  providerSlots: ProviderSlotInfo[];
+
+  /** 模型调用分布 */
+  modelDistribution: ModelLeaderboardEntry[];
+
+  /** 24小时趋势数据 */
+  trendData: Array<{
+    hour: number;
+    value: number;
+  }>;
+}
+
+// Constants for data limits
+const ACTIVITY_STREAM_LIMIT = 20;
+const MODEL_DISTRIBUTION_LIMIT = 10;
+
+/**
+ * 获取数据大屏的所有实时数据
+ *
+ * 一次性并行查询所有数据源,包括:
+ * - 核心指标(并发、请求、成本、响应时间、错误率)
+ * - 实时活动流
+ * - 用户/供应商/模型排行榜
+ * - 供应商并发插槽状态
+ * - 24小时趋势
+ *
+ * 权限控制:管理员或 allowGlobalUsageView=true 时可查看
+ */
+export async function getDashboardRealtimeData(): Promise<ActionResult<DashboardRealtimeData>> {
+  try {
+    // 权限检查
+    const session = await getSession();
+    if (!session) {
+      return {
+        ok: false,
+        error: "未登录",
+      };
+    }
+
+    const settings = await getSystemSettings();
+    const isAdmin = session.user.role === "admin";
+    const canViewGlobalData = isAdmin || settings.allowGlobalUsageView;
+
+    if (!canViewGlobalData) {
+      logger.debug("DashboardRealtime: User without global view permission", {
+        userId: session.user.id,
+      });
+      return {
+        ok: false,
+        error: "无权限查看全局数据",
+      };
+    }
+
+    // 并行查询所有数据源(使用 allSettled 以实现部分失败容错)
+    const [
+      overviewResult,
+      activityStreamResult,
+      userRankingsResult,
+      providerRankingsResult,
+      providerSlotsResult,
+      modelRankingsResult,
+      statisticsResult,
+    ] = await Promise.allSettled([
+      getOverviewData(),
+      findRecentActivityStream(ACTIVITY_STREAM_LIMIT), // 使用新的混合数据源
+      findDailyLeaderboard(),
+      findDailyProviderLeaderboard(),
+      getProviderSlots(),
+      findDailyModelLeaderboard(),
+      getUserStatistics("today"),
+    ]);
+
+    // 提取数据并处理错误
+    const overviewData =
+      overviewResult.status === "fulfilled" && overviewResult.value.ok
+        ? overviewResult.value.data
+        : null;
+
+    if (!overviewData) {
+      const errorReason =
+        overviewResult.status === "rejected" ? overviewResult.reason : "Unknown error";
+      logger.error("Failed to get overview data", { reason: errorReason });
+      return {
+        ok: false,
+        error: "获取概览数据失败",
+      };
+    }
+
+    // 提取其他数据,失败时使用空数组作为 fallback
+    const activityStreamItems =
+      activityStreamResult.status === "fulfilled" ? activityStreamResult.value : [];
+
+    const userRankings = userRankingsResult.status === "fulfilled" ? userRankingsResult.value : [];
+
+    const providerRankings =
+      providerRankingsResult.status === "fulfilled" ? providerRankingsResult.value : [];
+
+    const providerSlots =
+      providerSlotsResult.status === "fulfilled" && providerSlotsResult.value.ok
+        ? providerSlotsResult.value.data
+        : [];
+
+    const modelRankings =
+      modelRankingsResult.status === "fulfilled" ? modelRankingsResult.value : [];
+
+    const statisticsData =
+      statisticsResult.status === "fulfilled" && statisticsResult.value.ok
+        ? statisticsResult.value.data
+        : null;
+
+    // 记录部分失败的数据源
+    if (activityStreamResult.status === "rejected" || !activityStreamItems.length) {
+      logger.warn("Failed to get activity stream", {
+        reason:
+          activityStreamResult.status === "rejected" ? activityStreamResult.reason : "empty data",
+      });
+    }
+    if (userRankingsResult.status === "rejected") {
+      logger.warn("Failed to get user rankings", { reason: userRankingsResult.reason });
+    }
+    if (providerRankingsResult.status === "rejected") {
+      logger.warn("Failed to get provider rankings", { reason: providerRankingsResult.reason });
+    }
+    if (providerSlotsResult.status === "rejected" || !providerSlots.length) {
+      logger.warn("Failed to get provider slots", {
+        reason:
+          providerSlotsResult.status === "rejected"
+            ? providerSlotsResult.reason
+            : "empty data or action failed",
+      });
+    }
+    if (modelRankingsResult.status === "rejected") {
+      logger.warn("Failed to get model rankings", { reason: modelRankingsResult.reason });
+    }
+    if (statisticsResult.status === "rejected" || !statisticsData) {
+      logger.warn("Failed to get statistics", {
+        reason:
+          statisticsResult.status === "rejected"
+            ? statisticsResult.reason
+            : "action failed or empty data",
+      });
+    }
+
+    // 处理实时活动流数据(已包含 Redis 活跃 + 数据库最新的混合数据)
+    const now = Date.now();
+    const activityStream: ActivityStreamEntry[] = activityStreamItems.map((item) => {
+      // 计算耗时:
+      // - 如果有 durationMs(已完成的请求),使用实际值
+      // - 如果没有(进行中的请求),计算从开始到现在的耗时
+      const latency = item.durationMs ?? now - item.startTime;
+
+      return {
+        id: item.sessionId ?? `req-${item.id}`, // 使用 sessionId,如果没有则用请求ID
+        user: item.userName,
+        model: item.originalModel ?? item.model ?? "Unknown", // 优先使用计费模型
+        provider: item.providerName ?? "Unknown",
+        latency,
+        status: item.statusCode ?? 200,
+        cost: parseFloat(item.costUsd ?? "0"),
+        startTime: item.startTime,
+      };
+    });
+
+    // 处理供应商插槽数据(合并流量数据 + 过滤未设置限额 + 按占用率排序 + 限制最多3个)
+    const providerSlotsWithVolume: ProviderSlotInfo[] = providerSlots
+      .filter((slot) => slot.totalSlots > 0) // 过滤未设置并发限额的供应商
+      .map((slot) => {
+        const rankingData = providerRankings.find((p) => p.providerId === slot.providerId);
+
+        if (!rankingData) {
+          logger.debug("Provider has slots but no traffic", {
+            providerId: slot.providerId,
+            providerName: slot.name,
+          });
+        }
+
+        return {
+          ...slot,
+          totalVolume: rankingData?.totalTokens ?? 0,
+        };
+      })
+      .sort((a, b) => {
+        // 按占用率降序排序(占用率 = usedSlots / totalSlots)
+        const usageA = a.totalSlots > 0 ? a.usedSlots / a.totalSlots : 0;
+        const usageB = b.totalSlots > 0 ? b.usedSlots / b.totalSlots : 0;
+        return usageB - usageA;
+      })
+      .slice(0, 3); // 只取前3个
+
+    // 处理趋势数据(24小时)- 从 ChartDataItem 正确提取数据
+    const trendData = statisticsData?.chartData
+      ? statisticsData.chartData.map((item) => {
+          const hour = new Date(item.date).getUTCHours();
+          // 聚合所有 *_calls 字段(如 user-1_calls, user-2_calls)
+          const value = Object.keys(item)
+            .filter((key) => key.endsWith("_calls"))
+            .reduce((sum, key) => sum + (Number(item[key]) || 0), 0);
+          return { hour, value };
+        })
+      : Array.from({ length: 24 }, (_, i) => ({ hour: i, value: 0 }));
+
+    logger.debug("DashboardRealtime: Retrieved dashboard data", {
+      userId: session.user.id,
+      concurrentSessions: overviewData.concurrentSessions,
+      activityStreamCount: activityStream.length,
+      userRankingCount: userRankings.length,
+      providerRankingCount: providerRankings.length,
+      providerSlotsCount: providerSlotsWithVolume.length,
+      modelCount: modelRankings.length,
+    });
+
+    // 供应商排行按金额降序排序
+    const sortedProviderRankings = [...providerRankings]
+      .sort((a, b) => b.totalCost - a.totalCost)
+      .slice(0, 5);
+
+    return {
+      ok: true,
+      data: {
+        metrics: overviewData,
+        activityStream,
+        userRankings: userRankings.slice(0, 5),
+        providerRankings: sortedProviderRankings,
+        providerSlots: providerSlotsWithVolume,
+        modelDistribution: modelRankings.slice(0, MODEL_DISTRIBUTION_LIMIT),
+        trendData,
+      },
+    };
+  } catch (error) {
+    logger.error("Failed to get dashboard realtime data:", error);
+    return {
+      ok: false,
+      error: "获取数据大屏数据失败",
+    };
+  }
+}

+ 4 - 0
src/actions/overview.ts

@@ -19,6 +19,8 @@ export interface OverviewData {
   todayCost: number;
   /** 平均响应时间(毫秒) */
   avgResponseTime: number;
+  /** 今日错误率(百分比) */
+  todayErrorRate: number;
 }
 
 /**
@@ -61,6 +63,7 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
           todayRequests: 0, // 无权限时不显示全站请求数
           todayCost: 0, // 无权限时不显示全站消耗
           avgResponseTime: 0, // 无权限时不显示全站平均响应时间
+          todayErrorRate: 0, // 无权限时不显示全站错误率
         },
       };
     }
@@ -82,6 +85,7 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
         todayRequests: metricsData.todayRequests,
         todayCost: metricsData.todayCost,
         avgResponseTime: metricsData.avgResponseTime,
+        todayErrorRate: metricsData.todayErrorRate,
       },
     };
   } catch (error) {

+ 103 - 0
src/actions/provider-slots.ts

@@ -0,0 +1,103 @@
+"use server";
+
+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 { getSystemSettings } from "@/repository/system-config";
+
+/**
+ * 供应商并发插槽信息
+ */
+export interface ProviderSlotInfo {
+  /** 供应商 ID */
+  providerId: number;
+  /** 供应商名称 */
+  name: string;
+  /** 当前已使用插槽数(活跃 Session 数) */
+  usedSlots: number;
+  /** 总插槽数(并发限制) */
+  totalSlots: number;
+  /** 总 Token 流量(从排行榜获取) */
+  totalVolume: number;
+}
+
+/**
+ * 获取所有供应商的并发插槽状态
+ * 用于数据大屏显示供应商实时负载情况
+ *
+ * 权限控制:管理员或 allowGlobalUsageView=true 时可查看
+ */
+export async function getProviderSlots(): Promise<ActionResult<ProviderSlotInfo[]>> {
+  try {
+    // 权限检查
+    const session = await getSession();
+    if (!session) {
+      return {
+        ok: false,
+        error: "未登录",
+      };
+    }
+
+    const settings = await getSystemSettings();
+    const isAdmin = session.user.role === "admin";
+    const canViewGlobalData = isAdmin || settings.allowGlobalUsageView;
+
+    if (!canViewGlobalData) {
+      logger.debug("ProviderSlots: User without global view permission", {
+        userId: session.user.id,
+      });
+      return {
+        ok: false,
+        error: "无权限查看全局数据",
+      };
+    }
+
+    // 查询所有启用的供应商
+    const providerList = await db
+      .select({
+        id: providers.id,
+        name: providers.name,
+        limitConcurrentSessions: providers.limitConcurrentSessions,
+      })
+      .from(providers)
+      .where(and(eq(providers.isEnabled, true), isNull(providers.deletedAt)))
+      .orderBy(providers.priority, providers.id);
+
+    // 并行获取每个供应商的并发数
+    const slotInfoList = await Promise.all(
+      providerList.map(
+        async (provider: { id: number; name: string; limitConcurrentSessions: number | null }) => {
+          const usedSlots = await SessionTracker.getProviderSessionCount(provider.id);
+
+          return {
+            providerId: provider.id,
+            name: provider.name,
+            usedSlots,
+            totalSlots: provider.limitConcurrentSessions ?? 0,
+            totalVolume: 0, // This will be populated by the calling action from leaderboard data.
+          };
+        }
+      )
+    );
+
+    logger.debug("ProviderSlots: Retrieved provider slots", {
+      userId: session.user.id,
+      providerCount: slotInfoList.length,
+    });
+
+    return {
+      ok: true,
+      data: slotInfoList,
+    };
+  } catch (error) {
+    logger.error("Failed to get provider slots:", error);
+    return {
+      ok: false,
+      error: "获取供应商插槽信息失败",
+    };
+  }
+}

+ 11 - 0
src/app/[locale]/internal/dashboard/big-screen/layout.tsx

@@ -0,0 +1,11 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+  title: "实时数据大屏 - Claude Code Hub",
+  description: "Claude Code Hub 实时监控数据大屏",
+};
+
+export default function BigScreenLayout({ children }: { children: React.ReactNode }) {
+  // 全屏布局,移除所有导航栏、侧边栏等元素
+  return <>{children}</>;
+}

+ 917 - 0
src/app/[locale]/internal/dashboard/big-screen/page.tsx

@@ -0,0 +1,917 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+import {
+  Activity,
+  Server,
+  Zap,
+  DollarSign,
+  Clock,
+  AlertTriangle,
+  Globe,
+  Moon,
+  Sun,
+  RefreshCw,
+  ArrowUp,
+  ArrowDown,
+  Wifi,
+  Layers,
+  Shield,
+  User,
+  PieChart as PieIcon,
+} from "lucide-react";
+import {
+  AreaChart,
+  Area,
+  ResponsiveContainer,
+  PieChart,
+  Pie,
+  Cell,
+  Tooltip,
+  Legend,
+  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";
+
+/**
+ * ============================================================================
+ * 配置与常量
+ * ============================================================================
+ */
+const COLORS = {
+  models: ["#ff6b35", "#00d4ff", "#ffd60a", "#00ff88", "#a855f7"],
+};
+
+const THEMES = {
+  dark: {
+    bg: "bg-[#0a0a0f]",
+    text: "text-[#e6e6e6]",
+    card: "bg-[#1a1a2e]/60 backdrop-blur-md border border-white/5",
+    accent: "text-[#ff6b35]",
+    border: "border-white/5",
+  },
+  light: {
+    bg: "bg-[#fafafa]",
+    text: "text-[#1a1a1a]",
+    card: "bg-white/80 backdrop-blur-md border border-black/5 shadow-sm",
+    accent: "text-[#ff5722]",
+    border: "border-black/5",
+  },
+};
+
+/**
+ * ============================================================================
+ * 实用组件:粒子背景 (Canvas)
+ * ============================================================================
+ */
+const ParticleBackground = ({ themeMode }: { themeMode: string }) => {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+
+  useEffect(() => {
+    const canvas = canvasRef.current;
+    if (!canvas) return;
+
+    const ctx = canvas.getContext("2d");
+    if (!ctx) return;
+
+    let animationFrameId: number;
+    let particles: Array<{
+      x: number;
+      y: number;
+      vx: number;
+      vy: number;
+      size: number;
+      alpha: number;
+    }> = [];
+
+    const resize = () => {
+      canvas.width = window.innerWidth;
+      canvas.height = window.innerHeight;
+    };
+    window.addEventListener("resize", resize);
+    resize();
+
+    const createParticles = () => {
+      const count = window.innerWidth < 1000 ? 50 : 80;
+      particles = [];
+      for (let i = 0; i < count; i++) {
+        particles.push({
+          x: Math.random() * canvas.width,
+          y: Math.random() * canvas.height,
+          vx: (Math.random() - 0.5) * 0.3,
+          vy: (Math.random() - 0.5) * 0.3,
+          size: Math.random() * 2 + 0.5,
+          alpha: Math.random() * 0.4 + 0.1,
+        });
+      }
+    };
+    createParticles();
+
+    const render = () => {
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      const particleColor = themeMode === "dark" ? "255, 107, 53" : "2, 119, 189";
+
+      particles.forEach((p) => {
+        p.x += p.vx;
+        p.y += p.vy;
+
+        if (p.x < 0) p.x = canvas.width;
+        if (p.x > canvas.width) p.x = 0;
+        if (p.y < 0) p.y = canvas.height;
+        if (p.y > canvas.height) p.y = 0;
+
+        ctx.beginPath();
+        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
+        ctx.fillStyle = `rgba(${particleColor}, ${p.alpha})`;
+        ctx.fill();
+      });
+
+      animationFrameId = requestAnimationFrame(render);
+    };
+    render();
+
+    return () => {
+      window.removeEventListener("resize", resize);
+      cancelAnimationFrame(animationFrameId);
+    };
+  }, [themeMode]);
+
+  return (
+    <canvas
+      ref={canvasRef}
+      className="absolute top-0 left-0 w-full h-full pointer-events-none z-0"
+    />
+  );
+};
+
+/**
+ * ============================================================================
+ * 实用组件:数字滚动动画
+ * ============================================================================
+ */
+const CountUp = ({
+  value,
+  prefix = "",
+  suffix = "",
+  decimals = 0,
+  className = "",
+}: {
+  value: number;
+  prefix?: string;
+  suffix?: string;
+  decimals?: number;
+  className?: string;
+}) => {
+  const [displayValue, setDisplayValue] = useState(value);
+  useEffect(() => {
+    const start = displayValue;
+    const end = value;
+    if (start === end) return;
+    const duration = 1000;
+    const startTime = performance.now();
+    const animate = (currentTime: number) => {
+      const elapsed = currentTime - startTime;
+      const progress = Math.min(elapsed / duration, 1);
+      const ease = 1 - Math.pow(1 - progress, 4);
+      const current = start + (end - start) * ease;
+      setDisplayValue(current);
+      if (progress < 1) requestAnimationFrame(animate);
+    };
+    requestAnimationFrame(animate);
+  }, [value]);
+  return (
+    <span className={`font-mono ${className}`}>
+      {prefix}
+      {displayValue.toFixed(decimals)}
+      {suffix}
+    </span>
+  );
+};
+
+/**
+ * ============================================================================
+ * 核心业务组件
+ * ============================================================================
+ */
+
+// 1. 顶部核心指标
+const MetricCard = ({
+  title,
+  value,
+  subValue,
+  icon: Icon,
+  type = "neutral",
+  theme,
+}: {
+  title: string;
+  value: React.ReactNode;
+  subValue?: string;
+  icon: React.ComponentType<{ size: number; className?: string }>;
+  type?: string;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+}) => {
+  const isPositive = type === "positive";
+  const isNegative = type === "negative";
+  return (
+    <motion.div
+      initial={{ opacity: 0, scale: 0.95 }}
+      animate={{ opacity: 1, scale: 1 }}
+      className={`relative overflow-hidden rounded-lg p-4 flex flex-col justify-between h-full ${theme.card} hover:border-orange-500/30 transition-colors group`}
+    >
+      <div className="absolute -top-6 -right-6 w-24 h-24 bg-orange-500/10 rounded-full blur-2xl" />
+      <div className="flex justify-between items-start z-10">
+        <span className={`text-xs uppercase tracking-wider font-semibold ${theme.text} opacity-50`}>
+          {title}
+        </span>
+        <Icon size={16} className={`${theme.accent} opacity-80`} />
+      </div>
+      <div className="flex items-end gap-3 mt-1 z-10">
+        <div className="relative">
+          {type === "pulse" && (
+            <span className="absolute -left-2.5 top-1/2 -translate-y-1/2 flex h-1.5 w-1.5">
+              <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
+              <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-orange-500"></span>
+            </span>
+          )}
+          <span className={`text-3xl font-bold font-mono tracking-tight ${theme.text}`}>
+            {value}
+          </span>
+        </div>
+      </div>
+      <div className="mt-1 flex items-center text-[10px] font-medium z-10">
+        {subValue && (
+          <span
+            className={`flex items-center gap-0.5 ${isPositive ? "text-[#00ff88]" : isNegative ? "text-[#ff006e]" : "text-gray-400"}`}
+          >
+            {isPositive ? <ArrowUp size={10} /> : isNegative ? <ArrowDown size={10} /> : null}
+            {subValue}
+          </span>
+        )}
+      </div>
+    </motion.div>
+  );
+};
+
+// 2. 实时活动流
+const ActivityStream = ({
+  activities,
+  theme,
+  t,
+}: {
+  activities: Array<{
+    id: string;
+    user: string;
+    model: string;
+    provider: string;
+    latency: number;
+    status: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+}) => {
+  return (
+    <div className="h-full flex flex-col">
+      <div
+        className={`text-xs font-bold mb-2 flex items-center gap-2 ${theme.text} uppercase tracking-wider px-1`}
+      >
+        <Zap size={12} className="text-yellow-400" />
+        {t("sections.activity")}
+      </div>
+      <div className="flex-1 overflow-hidden relative rounded-lg border border-white/5 bg-black/20">
+        <div
+          className={`grid grid-cols-12 gap-2 text-[10px] font-mono opacity-50 py-2 px-3 border-b border-white/5 ${theme.text}`}
+        >
+          <div className="col-span-2">{t("headers.user")}</div>
+          <div className="col-span-3">{t("headers.model")}</div>
+          <div className="col-span-3">{t("headers.provider")}</div>
+          <div className="col-span-2 text-right">{t("headers.latency")}</div>
+          <div className="col-span-2 text-right">{t("headers.status")}</div>
+        </div>
+
+        <div className="space-y-0.5 relative h-full overflow-y-auto no-scrollbar p-1">
+          <AnimatePresence initial={false}>
+            {activities.map((item) => (
+              <motion.div
+                key={item.id}
+                initial={{ opacity: 0, x: 20, backgroundColor: "rgba(255, 107, 53, 0.2)" }}
+                animate={{ opacity: 1, x: 0, backgroundColor: "rgba(255,255,255,0.02)" }}
+                exit={{ opacity: 0 }}
+                transition={{ duration: 0.3 }}
+                className={`grid grid-cols-12 gap-2 p-2 rounded text-[10px] font-mono items-center hover:bg-white/10 transition-colors`}
+              >
+                <div className={`col-span-2 truncate font-bold text-orange-400`}>{item.user}</div>
+                <div className={`col-span-3 truncate text-gray-300`}>{item.model}</div>
+                <div className={`col-span-3 truncate text-gray-500`}>{item.provider}</div>
+                <div
+                  className={`col-span-2 text-right ${item.latency > 1000 ? "text-red-400" : "text-green-400"}`}
+                >
+                  {item.latency}ms
+                </div>
+                <div className="col-span-2 text-right flex justify-end">
+                  <span
+                    className={`px-1.5 rounded-sm ${
+                      item.status === 200
+                        ? "bg-green-500/10 text-green-500"
+                        : "bg-red-500/10 text-red-500"
+                    }`}
+                  >
+                    {item.status}
+                  </span>
+                </div>
+              </motion.div>
+            ))}
+          </AnimatePresence>
+        </div>
+      </div>
+      <style jsx>{`
+        .no-scrollbar::-webkit-scrollbar {
+          display: none;
+        }
+        .no-scrollbar {
+          -ms-overflow-style: none;
+          scrollbar-width: none;
+        }
+      `}</style>
+    </div>
+  );
+};
+
+// 3. 供应商并发插槽
+const ProviderQuotas = ({
+  providers,
+  theme,
+  t,
+}: {
+  providers: Array<{
+    name: string;
+    usedSlots: number;
+    totalSlots: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+}) => {
+  return (
+    <div className="h-full flex flex-col">
+      <div
+        className={`text-xs font-bold mb-3 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+      >
+        <Server size={12} className="text-blue-400" />
+        {t("sections.providerQuotas")}
+      </div>
+      <div className="flex-1 flex flex-col justify-around gap-2">
+        {providers.map((p, idx) => {
+          const percent = p.totalSlots > 0 ? (p.usedSlots / p.totalSlots) * 100 : 0;
+          const isCritical = percent > 90;
+          const isWarning = percent > 70;
+
+          return (
+            <div key={idx} className="flex flex-col gap-1">
+              <div className="flex justify-between items-end text-[10px]">
+                <span className={`font-mono font-bold ${theme.text}`}>{p.name}</span>
+                <span className={`font-mono ${isCritical ? "text-red-400" : "text-gray-400"}`}>
+                  {p.usedSlots}/{p.totalSlots} Slots
+                </span>
+              </div>
+              <div className="h-2.5 w-full bg-gray-700/30 rounded-sm overflow-hidden relative flex gap-[1px]">
+                <div
+                  className={`h-full absolute left-0 top-0 transition-all duration-1000 ease-out ${
+                    isCritical
+                      ? "bg-gradient-to-r from-red-500 to-red-400"
+                      : isWarning
+                        ? "bg-gradient-to-r from-yellow-500 to-orange-500"
+                        : "bg-gradient-to-r from-blue-600 to-cyan-400"
+                  }`}
+                  style={{ width: `${percent}%` }}
+                />
+                <div className="absolute inset-0 w-full h-full flex">
+                  {Array.from({ length: 20 }).map((_, i) => (
+                    <div key={i} className="flex-1 border-r border-black/20 h-full" />
+                  ))}
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+};
+
+// 4. 用户排行
+const UserRankings = ({
+  users,
+  theme,
+  t,
+}: {
+  users: Array<{
+    userId: number;
+    userName: string;
+    totalCost: number;
+    totalRequests: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+}) => {
+  return (
+    <div className="h-full flex flex-col relative">
+      <div
+        className={`text-xs font-bold mb-3 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+      >
+        <User size={12} className="text-purple-400" />
+        {t("sections.userRank")}
+        <span className="ml-auto flex h-2 w-2">
+          <span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-green-400 opacity-75"></span>
+          <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
+        </span>
+      </div>
+
+      <div className="flex-1 space-y-2 overflow-hidden">
+        {users.slice(0, 5).map((user, index) => (
+          <motion.div
+            key={user.userId}
+            layout
+            transition={{ type: "spring", stiffness: 300, damping: 30 }}
+            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"
+            }`}
+          >
+            <div
+              className={`
+              w-6 h-6 rounded flex items-center justify-center text-xs font-bold
+              ${
+                index === 0
+                  ? "bg-[#ff6b35] text-white shadow-lg shadow-orange-500/50"
+                  : index === 1
+                    ? "bg-gray-400 text-black"
+                    : index === 2
+                      ? "bg-orange-800 text-white"
+                      : "bg-gray-800 text-gray-400"
+              }
+            `}
+            >
+              {index + 1}
+            </div>
+
+            <div className="flex-1 min-w-0">
+              <div className="flex justify-between items-center">
+                <span className={`text-xs font-bold truncate ${theme.text}`}>{user.userName}</span>
+                <span className="text-[10px] text-gray-500 font-mono">
+                  ${user.totalCost.toFixed(2)}
+                </span>
+              </div>
+              <div className="flex justify-between items-center mt-1">
+                <div className="w-16 h-1 bg-gray-700 rounded-full overflow-hidden">
+                  <motion.div
+                    className="h-full bg-purple-500"
+                    initial={{ width: 0 }}
+                    animate={{ width: `${(user.totalCost / (users[0]?.totalCost || 1)) * 100}%` }}
+                  />
+                </div>
+                <span className="text-[9px] text-gray-500">{user.totalRequests} reqs</span>
+              </div>
+            </div>
+          </motion.div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+// 5. 供应商排行
+const ProviderRanking = ({
+  providers,
+  theme,
+  t,
+}: {
+  providers: Array<{
+    providerId: number;
+    providerName: string;
+    totalCost: number;
+    totalTokens: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+}) => {
+  return (
+    <div className="h-full flex flex-col">
+      <div
+        className={`text-xs font-bold mb-3 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+      >
+        <Shield size={12} className="text-green-400" />
+        {t("sections.providerRank")}
+      </div>
+      <div className="flex-1 space-y-2">
+        {providers.map((p, i) => (
+          <div
+            key={p.providerId}
+            className="flex items-center justify-between p-2 rounded bg-white/5 border border-white/5"
+          >
+            <div className="flex items-center gap-2">
+              <span className="text-[10px] text-gray-500 font-mono w-3">0{i + 1}</span>
+              <span className={`text-xs font-semibold ${theme.text}`}>{p.providerName}</span>
+            </div>
+            <div className="text-right">
+              <div className={`text-xs font-mono ${theme.accent}`}>${p.totalCost.toFixed(2)}</div>
+              <div className="text-[9px] text-gray-500">
+                {p.totalTokens.toLocaleString()} Tokens
+              </div>
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+// 6. 流量趋势(24小时)
+const TrafficTrend = ({
+  data,
+  theme,
+  t,
+  currentTime,
+}: {
+  data: Array<{
+    hour: number;
+    value: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+  currentTime: Date;
+}) => {
+  // 只显示到当前小时的数据(截断未来时间)
+  const currentHour = currentTime.getUTCHours();
+  const filteredData = data
+    .filter((item) => item.hour <= currentHour)
+    .map((item) => ({
+      hour: item.hour,
+      value: item.value,
+      hourLabel: `${item.hour}:00`,
+    }));
+
+  return (
+    <div className="h-full flex flex-col">
+      <div
+        className={`text-xs font-bold mb-2 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+      >
+        <Activity size={12} className="text-orange-400" />
+        {t("sections.requestTrend")}
+      </div>
+      <div className="flex-1 min-h-0">
+        <ResponsiveContainer width="100%" height="100%">
+          <AreaChart data={filteredData} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
+            <defs>
+              <linearGradient id="grad1" x1="0" y1="0" x2="0" y2="1">
+                <stop offset="0%" stopColor="#ff6b35" stopOpacity={0.3} />
+                <stop offset="100%" stopColor="#ff6b35" stopOpacity={0} />
+              </linearGradient>
+            </defs>
+            <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
+            <XAxis
+              dataKey="hour"
+              stroke={theme.text === "text-[#e6e6e6]" ? "#666" : "#999"}
+              tick={{ fill: theme.text === "text-[#e6e6e6]" ? "#666" : "#999", fontSize: 10 }}
+              tickFormatter={(value) => `${value}h`}
+            />
+            <YAxis
+              stroke={theme.text === "text-[#e6e6e6]" ? "#666" : "#999"}
+              tick={{ fill: theme.text === "text-[#e6e6e6]" ? "#666" : "#999", fontSize: 10 }}
+              width={30}
+            />
+            <Tooltip
+              contentStyle={{
+                backgroundColor: "#0a0a0f",
+                borderColor: "#333",
+                fontSize: "11px",
+                borderRadius: "6px",
+              }}
+              itemStyle={{ color: "#fff" }}
+              labelFormatter={(value) => `${value}:00`}
+              formatter={(value: number) => [`${value} 请求`, "数量"]}
+            />
+            <Area
+              type="monotone"
+              dataKey="value"
+              stroke="#ff6b35"
+              fill="url(#grad1)"
+              strokeWidth={2}
+              dot={false}
+              activeDot={{ r: 4, fill: "#ff6b35" }}
+            />
+          </AreaChart>
+        </ResponsiveContainer>
+      </div>
+    </div>
+  );
+};
+
+// 7. 模型分布
+const ModelDistribution = ({
+  data,
+  theme,
+  t,
+}: {
+  data: Array<{
+    model: string;
+    totalRequests: number;
+  }>;
+  theme: (typeof THEMES)[keyof typeof THEMES];
+  t: (key: string) => string;
+}) => {
+  const chartData = data.map((item) => ({
+    name: item.model,
+    value: item.totalRequests,
+  }));
+
+  // 处理空数据的情况
+  if (chartData.length === 0) {
+    return (
+      <div className="h-full flex flex-col">
+        <div
+          className={`text-xs font-bold mb-1 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+        >
+          <PieIcon size={12} className="text-indigo-400" />
+          {t("sections.modelDist")}
+        </div>
+        <div className="flex-1 flex items-center justify-center">
+          <span className={`text-xs ${theme.text} opacity-50`}>暂无数据</span>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="h-full flex flex-col">
+      <div
+        className={`text-xs font-bold mb-1 flex items-center gap-2 ${theme.text} uppercase tracking-wider`}
+      >
+        <PieIcon size={12} className="text-indigo-400" />
+        {t("sections.modelDist")}
+      </div>
+      <div className="flex-1 min-h-0 flex items-center">
+        <ResponsiveContainer width="100%" height="100%">
+          <PieChart>
+            <Pie
+              data={chartData}
+              innerRadius={30}
+              outerRadius={50}
+              paddingAngle={2}
+              dataKey="value"
+              stroke="none"
+            >
+              {chartData.map((_, index) => (
+                <Cell key={`cell-${index}`} fill={COLORS.models[index % COLORS.models.length]} />
+              ))}
+            </Pie>
+            <Tooltip
+              contentStyle={{ backgroundColor: "#0a0a0f", borderColor: "#333", fontSize: "12px" }}
+              itemStyle={{ color: "#fff" }}
+            />
+            <Legend
+              layout="vertical"
+              verticalAlign="middle"
+              align="right"
+              iconSize={8}
+              wrapperStyle={{ fontSize: "10px", color: "#999" }}
+            />
+          </PieChart>
+        </ResponsiveContainer>
+      </div>
+    </div>
+  );
+};
+
+/**
+ * ============================================================================
+ * 主应用
+ * ============================================================================
+ */
+export default function BigScreenPage() {
+  const t = useTranslations("bigScreen");
+  const [themeMode, setThemeMode] = useState("dark");
+  const [currentTime, setCurrentTime] = useState(new Date());
+
+  // 语言切换
+  const currentLocale = useLocale() as Locale;
+  const router = useRouter();
+  const pathname = usePathname();
+
+  const handleLocaleSwitch = () => {
+    // 循环切换语言:zh-CN → en → ja → ru → zh-TW → zh-CN
+    const currentIndex = locales.indexOf(currentLocale as Locale);
+    const nextIndex = (currentIndex + 1) % locales.length;
+    const nextLocale = locales[nextIndex];
+
+    router.push(pathname || "/dashboard", { locale: nextLocale });
+  };
+
+  const theme = THEMES[themeMode as keyof typeof THEMES];
+
+  // 时钟
+  useEffect(() => {
+    const timer = setInterval(() => setCurrentTime(new Date()), 1000);
+    return () => clearInterval(timer);
+  }, []);
+
+  // 使用 SWR 获取数据,2秒刷新
+  const { data, error, mutate } = useSWR(
+    "dashboard-realtime",
+    async () => {
+      const result = await getDashboardRealtimeData();
+      if (!result.ok) {
+        throw new Error(result.error || "Failed to fetch data");
+      }
+      return result.data;
+    },
+    {
+      refreshInterval: 2000, // 2秒刷新
+      revalidateOnFocus: false,
+    }
+  );
+
+  // 处理数据
+  const metrics = data?.metrics || {
+    concurrentSessions: 0,
+    todayRequests: 0,
+    todayCost: 0,
+    avgResponseTime: 0,
+    todayErrorRate: 0,
+  };
+
+  const activities = (data?.activityStream || []).map((item) => ({
+    id: item.id,
+    user: item.user,
+    model: item.model,
+    provider: item.provider,
+    latency: item.latency,
+    status: item.status,
+  }));
+
+  const users = data?.userRankings || [];
+  const providers = data?.providerSlots || [];
+  const providerRankings = data?.providerRankings || [];
+  const modelDist = data?.modelDistribution || [];
+  const trendData = data?.trendData || [];
+
+  return (
+    <div
+      className={`relative w-full h-screen overflow-hidden transition-colors duration-500 font-sans selection:bg-orange-500/30 ${theme.bg}`}
+    >
+      <ParticleBackground themeMode={themeMode} />
+
+      <div className="relative z-10 flex flex-col h-full p-4 gap-4 max-w-[1920px] mx-auto">
+        {/* Header */}
+        <header className="flex justify-between items-center pb-2 border-b border-white/5">
+          <div className="flex flex-col">
+            <h1 className={`text-2xl font-bold tracking-widest font-space ${theme.text}`}>
+              {t("title")}
+            </h1>
+            <p className={`text-[10px] tracking-[0.5em] uppercase opacity-50 ${theme.text} mt-1`}>
+              {t("subtitle")}
+            </p>
+          </div>
+
+          <div className="flex items-center gap-6">
+            <div className={`text-right hidden md:block ${theme.text}`}>
+              <div className="text-xl font-mono font-bold tabular-nums">
+                {currentTime.toLocaleTimeString()}
+              </div>
+            </div>
+            <div className="h-6 w-[1px] bg-white/10" />
+            <div className="flex gap-2">
+              <button
+                onClick={handleLocaleSwitch}
+                className={`p-1.5 rounded hover:bg-white/5 ${theme.text} flex items-center gap-1.5 transition-colors`}
+                title={`当前: ${localeLabels[currentLocale]} (点击切��)`}
+              >
+                <Globe size={18} />
+                <span className="text-[10px] font-mono uppercase">
+                  {currentLocale.split("-")[0]}
+                </span>
+              </button>
+              <button
+                onClick={() => setThemeMode(themeMode === "dark" ? "light" : "dark")}
+                className={`p-1.5 rounded hover:bg-white/5 ${theme.text} transition-colors`}
+              >
+                {themeMode === "dark" ? <Moon size={18} /> : <Sun size={18} />}
+              </button>
+              <button
+                onClick={() => mutate()}
+                className={`p-1.5 rounded hover:bg-white/5 ${theme.text} transition-colors`}
+              >
+                <RefreshCw size={18} />
+              </button>
+            </div>
+          </div>
+        </header>
+
+        {/* Top Metrics Row */}
+        <div className="grid grid-cols-5 gap-3 h-[120px]">
+          <MetricCard
+            title={t("metrics.concurrent")}
+            value={metrics.concurrentSessions}
+            subValue={`${activities.length} Recent`}
+            type="pulse"
+            icon={Wifi}
+            theme={theme}
+          />
+          <MetricCard
+            title={t("metrics.requests")}
+            value={<CountUp value={metrics.todayRequests} />}
+            subValue="Today"
+            type="positive"
+            icon={Activity}
+            theme={theme}
+          />
+          <MetricCard
+            title={t("metrics.cost")}
+            value={<CountUp value={metrics.todayCost} prefix="$" decimals={2} />}
+            subValue="Budget"
+            type="neutral"
+            icon={DollarSign}
+            theme={theme}
+          />
+          <MetricCard
+            title={t("metrics.latency")}
+            value={`${metrics.avgResponseTime}ms`}
+            subValue="Avg"
+            type="positive"
+            icon={Clock}
+            theme={theme}
+          />
+          <MetricCard
+            title={t("metrics.errorRate")}
+            value={`${metrics.todayErrorRate.toFixed(2)}%`}
+            subValue={metrics.todayErrorRate > 2 ? "High" : "Normal"}
+            type={metrics.todayErrorRate > 2 ? "negative" : "neutral"}
+            icon={AlertTriangle}
+            theme={theme}
+          />
+        </div>
+
+        {/* Main Content Grid */}
+        <div className="flex-1 grid grid-cols-12 gap-4 min-h-0">
+          {/* LEFT COL */}
+          <div className="col-span-3 flex flex-col gap-4 h-full">
+            <div className={`flex-[3] ${theme.card} rounded-lg p-4 overflow-hidden`}>
+              <UserRankings users={users} theme={theme} t={t} />
+            </div>
+            <div className={`flex-[2] ${theme.card} rounded-lg p-4 overflow-hidden`}>
+              <ProviderRanking providers={providerRankings} theme={theme} t={t} />
+            </div>
+          </div>
+
+          {/* MIDDLE COL */}
+          <div className="col-span-5 flex flex-col gap-4 h-full">
+            <div className={`flex-[2] ${theme.card} rounded-lg p-4`}>
+              <ProviderQuotas providers={providers} theme={theme} t={t} />
+            </div>
+            <div className={`flex-[2] ${theme.card} rounded-lg p-4`}>
+              <TrafficTrend data={trendData} theme={theme} t={t} currentTime={currentTime} />
+            </div>
+            <div className={`flex-[1] ${theme.card} rounded-lg p-4`}>
+              <ModelDistribution data={modelDist} theme={theme} t={t} />
+            </div>
+          </div>
+
+          {/* RIGHT COL */}
+          <div className="col-span-4 h-full">
+            <div className={`h-full ${theme.card} rounded-lg p-0 overflow-hidden flex flex-col`}>
+              <div className="p-3 pb-0">
+                <ActivityStream activities={activities} theme={theme} t={t} />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {/* Footer */}
+        <footer
+          className={`h-6 flex items-center justify-between text-[9px] uppercase tracking-wider opacity-40 ${theme.text}`}
+        >
+          <span>{t("status.normal")}</span>
+          <span>
+            {t("status.lastUpdate")}: {error ? "Error" : "2s ago"}
+          </span>
+        </footer>
+      </div>
+
+      <style jsx global>{`
+        @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Space+Grotesk:wght@400;600;700&display=swap");
+        .font-mono {
+          font-family: "JetBrains Mono", monospace;
+        }
+        .font-space {
+          font-family: "Space Grotesk", sans-serif;
+        }
+      `}</style>
+    </div>
+  );
+}

+ 229 - 0
src/repository/activity-stream.ts

@@ -0,0 +1,229 @@
+"use server";
+
+import { db } from "@/drizzle/db";
+import { messageRequest, users, keys as keysTable, providers } from "@/drizzle/schema";
+import { and, desc, eq, isNull, inArray, notInArray, sql } from "drizzle-orm";
+import { logger } from "@/lib/logger";
+
+/**
+ * 活动流条目(单个请求记录)
+ */
+export interface ActivityStreamItem {
+  /** 请求 ID */
+  id: number;
+  /** Session ID */
+  sessionId: string | null;
+  /** 用户名 */
+  userName: string;
+  /** 用户 ID */
+  userId: number;
+  /** Key ID */
+  keyId: number;
+  /** Key 名称 */
+  keyName: string;
+  /** 供应商 ID */
+  providerId: number | null;
+  /** 供应商名称 */
+  providerName: string | null;
+  /** 模型名称 */
+  model: string | null;
+  /** 原始模型(计费模型) */
+  originalModel: string | null;
+  /** HTTP 状态码 */
+  statusCode: number | null;
+  /** 响应时间(毫秒) */
+  durationMs: number | null;
+  /** 成本(美元) */
+  costUsd: string | null;
+  /** 创建时间(Unix 时间戳,毫秒) */
+  startTime: number;
+  /** Input Tokens */
+  inputTokens: number | null;
+  /** Output Tokens */
+  outputTokens: number | null;
+  /** Cache Creation Tokens */
+  cacheCreationInputTokens: number | null;
+  /** Cache Read Tokens */
+  cacheReadInputTokens: number | null;
+}
+
+/**
+ * 获取最近的活动流(Redis 活跃 session + 数据库最新请求混合)
+ *
+ * 策略:
+ * 1. 从 Redis 获取活跃 session ID 列表
+ * 2. 查询这些 session 的最新请求(每个 session 1条)
+ * 3. 如果不足 limit 条,补充数据库最新请求(排除已包含的 session)
+ * 4. 按创建时间降序排序
+ * 5. 去重并限制数量
+ *
+ * @param limit 最大返回条数(默认 20)
+ */
+export async function findRecentActivityStream(limit = 20): Promise<ActivityStreamItem[]> {
+  try {
+    // 1. 从 Redis 获取活跃 session ID
+    const { SessionTracker } = await import("@/lib/session-tracker");
+    const activeSessionIds = await SessionTracker.getActiveSessions();
+
+    let activityItems: ActivityStreamItem[] = [];
+
+    // 2. 查询活跃 session 的最新请求(每个 session 取最新1条)
+    if (activeSessionIds.length > 0) {
+      // 使用窗口函数获取每个 session 的最新请求
+      const activeSessionRequests = await db
+        .select({
+          id: messageRequest.id,
+          sessionId: messageRequest.sessionId,
+          userName: users.name,
+          userId: messageRequest.userId,
+          keyId: keysTable.id,
+          keyName: keysTable.name,
+          providerId: messageRequest.providerId,
+          providerName: providers.name,
+          model: messageRequest.model,
+          originalModel: messageRequest.originalModel,
+          statusCode: messageRequest.statusCode,
+          durationMs: messageRequest.durationMs,
+          costUsd: messageRequest.costUsd,
+          createdAt: messageRequest.createdAt,
+          inputTokens: messageRequest.inputTokens,
+          outputTokens: messageRequest.outputTokens,
+          cacheCreationInputTokens: messageRequest.cacheCreationInputTokens,
+          cacheReadInputTokens: messageRequest.cacheReadInputTokens,
+          rowNum: sql<number>`ROW_NUMBER() OVER (PARTITION BY ${messageRequest.sessionId} ORDER BY ${messageRequest.createdAt} DESC)`,
+        })
+        .from(messageRequest)
+        .leftJoin(users, eq(messageRequest.userId, users.id))
+        .leftJoin(keysTable, eq(messageRequest.key, keysTable.key))
+        .leftJoin(providers, eq(messageRequest.providerId, providers.id))
+        .where(
+          and(isNull(messageRequest.deletedAt), inArray(messageRequest.sessionId, activeSessionIds))
+        )
+        .orderBy(desc(messageRequest.createdAt))
+        .limit(limit * 2); // 获取足够的数据,后面会过滤
+
+      // 过滤出每个 session 的最新一条(rowNum = 1)
+      const latestPerSession = activeSessionRequests
+        .filter((row) => row.rowNum === 1)
+        .map((row) => ({
+          id: row.id,
+          sessionId: row.sessionId,
+          userName: row.userName || "Unknown",
+          userId: row.userId,
+          keyId: row.keyId ?? 0,
+          keyName: row.keyName || "Unknown",
+          providerId: row.providerId,
+          providerName: row.providerName,
+          model: row.model,
+          originalModel: row.originalModel,
+          statusCode: row.statusCode,
+          durationMs: row.durationMs,
+          costUsd: row.costUsd,
+          startTime: row.createdAt ? new Date(row.createdAt).getTime() : Date.now(),
+          inputTokens: row.inputTokens,
+          outputTokens: row.outputTokens,
+          cacheCreationInputTokens: row.cacheCreationInputTokens,
+          cacheReadInputTokens: row.cacheReadInputTokens,
+        }));
+
+      activityItems = latestPerSession;
+
+      logger.debug("[ActivityStream] Got active session requests", {
+        activeSessionCount: activeSessionIds.length,
+        latestRequestCount: latestPerSession.length,
+      });
+    }
+
+    // 3. 如果不足 limit 条,补充数据库最新请求(排除已包含的 session)
+    if (activityItems.length < limit) {
+      const remaining = limit - activityItems.length;
+      const excludedSessionIds = activityItems
+        .map((item) => item.sessionId)
+        .filter((sid): sid is string => sid !== null);
+
+      const conditions = [isNull(messageRequest.deletedAt)];
+      if (excludedSessionIds.length > 0) {
+        conditions.push(notInArray(messageRequest.sessionId, excludedSessionIds));
+      }
+
+      const recentRequests = await db
+        .select({
+          id: messageRequest.id,
+          sessionId: messageRequest.sessionId,
+          userName: users.name,
+          userId: messageRequest.userId,
+          keyId: keysTable.id,
+          keyName: keysTable.name,
+          providerId: messageRequest.providerId,
+          providerName: providers.name,
+          model: messageRequest.model,
+          originalModel: messageRequest.originalModel,
+          statusCode: messageRequest.statusCode,
+          durationMs: messageRequest.durationMs,
+          costUsd: messageRequest.costUsd,
+          createdAt: messageRequest.createdAt,
+          inputTokens: messageRequest.inputTokens,
+          outputTokens: messageRequest.outputTokens,
+          cacheCreationInputTokens: messageRequest.cacheCreationInputTokens,
+          cacheReadInputTokens: messageRequest.cacheReadInputTokens,
+        })
+        .from(messageRequest)
+        .leftJoin(users, eq(messageRequest.userId, users.id))
+        .leftJoin(keysTable, eq(messageRequest.key, keysTable.key))
+        .leftJoin(providers, eq(messageRequest.providerId, providers.id))
+        .where(and(...conditions))
+        .orderBy(desc(messageRequest.createdAt))
+        .limit(remaining);
+
+      const additionalItems = recentRequests.map((row) => ({
+        id: row.id,
+        sessionId: row.sessionId,
+        userName: row.userName || "Unknown",
+        userId: row.userId,
+        keyId: row.keyId ?? 0,
+        keyName: row.keyName || "Unknown",
+        providerId: row.providerId,
+        providerName: row.providerName,
+        model: row.model,
+        originalModel: row.originalModel,
+        statusCode: row.statusCode,
+        durationMs: row.durationMs,
+        costUsd: row.costUsd,
+        startTime: row.createdAt ? new Date(row.createdAt).getTime() : Date.now(),
+        inputTokens: row.inputTokens,
+        outputTokens: row.outputTokens,
+        cacheCreationInputTokens: row.cacheCreationInputTokens,
+        cacheReadInputTokens: row.cacheReadInputTokens,
+      }));
+
+      activityItems = [...activityItems, ...additionalItems];
+
+      logger.debug("[ActivityStream] Added recent requests", {
+        additionalCount: additionalItems.length,
+        totalCount: activityItems.length,
+      });
+    }
+
+    // 4. 按创建时间降序排序并去重
+    const uniqueItems = new Map<number, ActivityStreamItem>();
+    for (const item of activityItems) {
+      if (!uniqueItems.has(item.id)) {
+        uniqueItems.set(item.id, item);
+      }
+    }
+
+    const sortedItems = Array.from(uniqueItems.values())
+      .sort((a, b) => b.startTime - a.startTime)
+      .slice(0, limit);
+
+    logger.debug("[ActivityStream] Final activity stream", {
+      totalUnique: uniqueItems.size,
+      returned: sortedItems.length,
+    });
+
+    return sortedItems;
+  } catch (error) {
+    logger.error("Failed to get recent activity stream:", error);
+    return [];
+  }
+}

+ 81 - 0
src/repository/leaderboard.ts

@@ -29,6 +29,17 @@ export interface ProviderLeaderboardEntry {
   avgResponseTime: number; // 毫秒
 }
 
+/**
+ * 模型排行榜条目类型
+ */
+export interface ModelLeaderboardEntry {
+  model: string;
+  totalRequests: number;
+  totalCost: number;
+  totalTokens: number;
+  successRate: number; // 0-1 之间的小数,UI 层负责格式化为百分比
+}
+
 /**
  * 查询今日消耗排行榜(不限制数量)
  * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于配置时区(Asia/Shanghai)
@@ -165,3 +176,73 @@ async function findProviderLeaderboardWithTimezone(
     avgResponseTime: entry.avgResponseTime ?? 0,
   }));
 }
+
+/**
+ * 查询今日模型调用排行榜(不限制数量)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"今日"基于配置时区(Asia/Shanghai)
+ */
+export async function findDailyModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
+  const timezone = getEnvConfig().TZ;
+  return findModelLeaderboardWithTimezone("daily", timezone);
+}
+
+/**
+ * 查询本月模型调用排行榜(不限制数量)
+ * 使用 SQL AT TIME ZONE 进行时区转换,确保"本月"基于配置时区(Asia/Shanghai)
+ */
+export async function findMonthlyModelLeaderboard(): Promise<ModelLeaderboardEntry[]> {
+  const timezone = getEnvConfig().TZ;
+  return findModelLeaderboardWithTimezone("monthly", timezone);
+}
+
+/**
+ * 通用模型排行榜查询函数(使用 SQL AT TIME ZONE 确保时区正确)
+ */
+async function findModelLeaderboardWithTimezone(
+  period: "daily" | "monthly",
+  timezone: string
+): Promise<ModelLeaderboardEntry[]> {
+  const rankings = await db
+    .select({
+      // 使用 COALESCE:优先使用 originalModel(计费模型),回退到实际转发的 model
+      // 修复:当 originalModel 为 NULL(未配置模型重定向)时,使用 model 字段
+      model: sql<string>`COALESCE(${messageRequest.originalModel}, ${messageRequest.model})`,
+      totalRequests: sql<number>`count(*)::double precision`,
+      totalCost: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`,
+      totalTokens: sql<number>`COALESCE(
+        sum(
+          ${messageRequest.inputTokens} +
+          ${messageRequest.outputTokens} +
+          COALESCE(${messageRequest.cacheCreationInputTokens}, 0) +
+          COALESCE(${messageRequest.cacheReadInputTokens}, 0)
+        )::double precision,
+        0::double precision
+      )`,
+      successRate: sql<number>`COALESCE(
+        count(CASE WHEN ${messageRequest.errorMessage} IS NULL OR ${messageRequest.errorMessage} = '' THEN 1 END)::double precision
+        / NULLIF(count(*)::double precision, 0),
+        0::double precision
+      )`,
+    })
+    .from(messageRequest)
+    .where(
+      and(
+        isNull(messageRequest.deletedAt),
+        period === "daily"
+          ? sql`(${messageRequest.createdAt} AT TIME ZONE ${timezone})::date = (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date`
+          : sql`date_trunc('month', ${messageRequest.createdAt} AT TIME ZONE ${timezone}) = date_trunc('month', CURRENT_TIMESTAMP AT TIME ZONE ${timezone})`
+      )
+    )
+    .groupBy(sql`COALESCE(${messageRequest.originalModel}, ${messageRequest.model})`) // 修复:GROUP BY 也需要使用相同的 COALESCE
+    .orderBy(desc(sql`count(*)`)); // 按请求数排序
+
+  return rankings
+    .filter((entry) => entry.model !== null && entry.model !== "")
+    .map((entry) => ({
+      model: entry.model as string, // 已过滤 null/空字符串,可安全断言
+      totalRequests: entry.totalRequests,
+      totalCost: parseFloat(entry.totalCost),
+      totalTokens: entry.totalTokens,
+      successRate: entry.successRate ?? 0,
+    }));
+}

+ 17 - 9
src/repository/overview.ts

@@ -2,8 +2,9 @@
 
 import { db } from "@/drizzle/db";
 import { messageRequest } from "@/drizzle/schema";
-import { isNull, and, gte, lt, count, sum, avg } from "drizzle-orm";
+import { isNull, and, count, sum, avg, sql } from "drizzle-orm";
 import { Decimal, toCostDecimal } from "@/lib/utils/currency";
+import { getEnvConfig } from "@/lib/config";
 
 /**
  * 今日概览统计数据
@@ -15,30 +16,30 @@ export interface OverviewMetrics {
   todayCost: number;
   /** 平均响应时间(毫秒) */
   avgResponseTime: number;
+  /** 今日错误率(百分比) */
+  todayErrorRate: number;
 }
 
 /**
  * 获取今日概览统计数据
- * 包括:今日总请求数、今日总消耗、平均响应时间
+ * 包括:今日总请求数、今日总消耗、平均响应时间、今日错误率
+ * 使用 SQL AT TIME ZONE 确保"今日"基于配置时区(TZ 环境变量)
  */
 export async function getOverviewMetrics(): Promise<OverviewMetrics> {
-  const today = new Date();
-  today.setHours(0, 0, 0, 0);
-  const tomorrow = new Date(today);
-  tomorrow.setDate(tomorrow.getDate() + 1);
+  const timezone = getEnvConfig().TZ;
 
   const [result] = await db
     .select({
       requestCount: count(),
       totalCost: sum(messageRequest.costUsd),
       avgDuration: avg(messageRequest.durationMs),
+      errorCount: sql<number>`count(*) FILTER (WHERE ${messageRequest.statusCode} >= 400)`,
     })
     .from(messageRequest)
     .where(
       and(
         isNull(messageRequest.deletedAt),
-        gte(messageRequest.createdAt, today),
-        lt(messageRequest.createdAt, tomorrow)
+        sql`(${messageRequest.createdAt} AT TIME ZONE ${timezone})::date = (CURRENT_TIMESTAMP AT TIME ZONE ${timezone})::date`
       )
     );
 
@@ -49,9 +50,16 @@ export async function getOverviewMetrics(): Promise<OverviewMetrics> {
   // 处理平均响应时间(转换为整数)
   const avgResponseTime = result.avgDuration ? Math.round(Number(result.avgDuration)) : 0;
 
+  // 计算错误率(百分比)
+  const requestCount = Number(result.requestCount || 0);
+  const errorCount = Number(result.errorCount || 0);
+  const todayErrorRate =
+    requestCount > 0 ? parseFloat(((errorCount / requestCount) * 100).toFixed(2)) : 0;
+
   return {
-    todayRequests: Number(result.requestCount || 0),
+    todayRequests: requestCount,
     todayCost,
     avgResponseTime,
+    todayErrorRate,
   };
 }