Kaynağa Gözat

refactor: extract reusable ActiveSessionsList component

重构活跃 Session 展示逻辑,提取可复用组件以提升代码复用性和可维护性:

- 新增 ActiveSessionsList 组件,支持自定义显示数量和样式
- 新增 SessionListItem 组件,统一 Session 列表项展示逻辑
- 简化 ActiveSessionsPanel,使用新组件实现(保持向后兼容)
- 优化 OverviewPanel,移除 recentSessions 数据依赖
- 简化 overview action,移除不必要的 Session 查询
- 修复价格上传对话框翻译 key 引用

技术改进:
- 组件解耦:Session 列表逻辑独立,可在多个页面复用
- 性能优化:减少不必要的数据查询
- 代码复用:统一 Session 展示格式和交互逻辑

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

Co-Authored-By: Claude <[email protected]>
ding113 3 ay önce
ebeveyn
işleme
ece321a614

+ 2 - 13
src/actions/overview.ts

@@ -2,12 +2,10 @@
 
 import { getOverviewMetrics as getOverviewMetricsFromDB } from "@/repository/overview";
 import { getConcurrentSessions as getConcurrentSessionsCount } from "./concurrent-sessions";
-import { getActiveSessions as getActiveSessionsFromManager } from "./active-sessions";
 import { getSession } from "@/lib/auth";
 import { getSystemSettings } from "@/repository/system-config";
 import { logger } from "@/lib/logger";
 import type { ActionResult } from "./types";
-import type { ActiveSessionInfo } from "@/types/session";
 
 /**
  * 概览数据(包含并发数和今日统计)
@@ -21,8 +19,6 @@ export interface OverviewData {
   todayCost: number;
   /** 平均响应时间(毫秒) */
   avgResponseTime: number;
-  /** 最近活跃的Session列表(用于滚动展示) */
-  recentSessions: ActiveSessionInfo[];
 }
 
 /**
@@ -45,21 +41,17 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
     const canViewGlobalData = isAdmin || settings.allowGlobalUsageView;
 
     // 并行查询所有数据
-    const [concurrentResult, metricsData, sessionsResult] = await Promise.all([
+    const [concurrentResult, metricsData] = await Promise.all([
       getConcurrentSessionsCount(),
       getOverviewMetricsFromDB(),
-      getActiveSessionsFromManager(),
     ]);
 
     // 根据权限决定显示范围
     if (!canViewGlobalData) {
-      // 普通用户且无权限:仅显示自己的活跃 Session,全站指标设为 0
-      const recentSessions = sessionsResult.ok ? sessionsResult.data.slice(0, 10) : [];
-
+      // 普通用户且无权限:全站指标设为 0
       logger.debug("Overview: User without global view permission", {
         userId: session.user.id,
         userName: session.user.name,
-        ownSessionsCount: recentSessions.length,
       });
 
       return {
@@ -69,14 +61,12 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
           todayRequests: 0, // 无权限时不显示全站请求数
           todayCost: 0, // 无权限时不显示全站消耗
           avgResponseTime: 0, // 无权限时不显示全站平均响应时间
-          recentSessions, // 仅显示自己的活跃 Session(getActiveSessions 已做权限过滤)
         },
       };
     }
 
     // 管理员或有权限:显示全站数据
     const concurrentSessions = concurrentResult.ok ? concurrentResult.data : 0;
-    const recentSessions = sessionsResult.ok ? sessionsResult.data.slice(0, 10) : [];
 
     logger.debug("Overview: User with global view permission", {
       userId: session.user.id,
@@ -92,7 +82,6 @@ export async function getOverviewData(): Promise<ActionResult<OverviewData>> {
         todayRequests: metricsData.todayRequests,
         todayCost: metricsData.todayCost,
         avgResponseTime: metricsData.avgResponseTime,
-        recentSessions,
       },
     };
   } catch (error) {

+ 3 - 2
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx

@@ -58,6 +58,7 @@ export function UploadPriceDialog({
   isRequired = false,
 }: UploadPriceDialogProps) {
   const t = useTranslations("settings.prices");
+  const tCommon = useTranslations("settings.common");
   const router = useRouter();
   const [open, setOpen] = useState(defaultOpen);
   const [uploading, setUploading] = useState(false);
@@ -283,8 +284,8 @@ export function UploadPriceDialog({
 
               <Button onClick={handleClose} className="w-full">
                 {isRequired && result && (result.added.length > 0 || result.updated.length > 0)
-                  ? t("common.confirm")
-                  : t("common.completed")}
+                  ? tCommon("confirm")
+                  : tCommon("completed")}
               </Button>
             </div>
           )}

+ 114 - 0
src/components/customs/active-sessions-list.tsx

@@ -0,0 +1,114 @@
+"use client";
+
+import * as React from "react";
+import { useRouter } from "next/navigation";
+import { useQuery } from "@tanstack/react-query";
+import { Activity, Loader2 } from "lucide-react";
+import { getActiveSessions } from "@/actions/active-sessions";
+import type { ActiveSessionInfo } from "@/types/session";
+import type { CurrencyCode } from "@/lib/utils/currency";
+import { useTranslations } from "next-intl";
+import { SessionListItem } from "./session-list-item";
+
+const REFRESH_INTERVAL = 5000; // 5秒刷新一次
+
+async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
+  const result = await getActiveSessions();
+  if (!result.ok) {
+    throw new Error(result.error || "获取活跃 Session 失败");
+  }
+  return result.data;
+}
+
+interface ActiveSessionsListProps {
+  /** 货币代码 */
+  currencyCode?: CurrencyCode;
+  /** 最大显示数量,默认显示全部 */
+  maxItems?: number;
+  /** 是否显示标题栏 */
+  showHeader?: boolean;
+  /** 容器最大高度 */
+  maxHeight?: string;
+  /** 自定义类名 */
+  className?: string;
+}
+
+/**
+ * 活跃 Session 列表组件
+ * 可复用组件,支持自定义最大显示数量
+ *
+ * 注意:计数始终显示实际的活跃 session 数量,而不是显示的数量
+ */
+export function ActiveSessionsList({
+  currencyCode = "USD",
+  maxItems,
+  showHeader = true,
+  maxHeight = "200px",
+  className = "",
+}: ActiveSessionsListProps) {
+  const router = useRouter();
+  const tu = useTranslations("ui");
+  const tc = useTranslations("customs");
+
+  const { data = [], isLoading } = useQuery<ActiveSessionInfo[], Error>({
+    queryKey: ["active-sessions"],
+    queryFn: fetchActiveSessions,
+    refetchInterval: REFRESH_INTERVAL,
+  });
+
+  // 实际显示的 session 列表(限制数量)
+  const displaySessions = maxItems ? data.slice(0, maxItems) : data;
+  // 实际的活跃 session 总数(用于计数显示)
+  const totalCount = data.length;
+
+  return (
+    <div className={`border rounded-lg bg-card ${className}`}>
+      {showHeader && (
+        <div className="px-4 py-3 border-b flex items-center justify-between">
+          <div className="flex items-center gap-2">
+            <Activity className="h-4 w-4 text-primary" />
+            <h3 className="font-semibold text-sm">{tc("activeSessions.title")}</h3>
+            <span className="text-xs text-muted-foreground">
+              {tc("activeSessions.summary", { count: totalCount, minutes: 5 })}
+            </span>
+          </div>
+          <button
+            onClick={() => router.push("/dashboard/sessions")}
+            className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+          >
+            {tc("activeSessions.viewAll")} →
+          </button>
+        </div>
+      )}
+
+      <div style={{ maxHeight }} className="overflow-y-auto">
+        {isLoading && displaySessions.length === 0 ? (
+          <div
+            className="flex items-center justify-center text-muted-foreground text-sm"
+            style={{ height: maxHeight }}
+          >
+            <Loader2 className="h-4 w-4 animate-spin mr-2" />
+            {tu("common.loading")}
+          </div>
+        ) : displaySessions.length === 0 ? (
+          <div
+            className="flex items-center justify-center text-muted-foreground text-sm"
+            style={{ height: maxHeight }}
+          >
+            {tc("activeSessions.empty")}
+          </div>
+        ) : (
+          <div className="divide-y">
+            {displaySessions.map((session) => (
+              <SessionListItem
+                key={session.sessionId}
+                session={session}
+                currencyCode={currencyCode}
+              />
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}

+ 5 - 186
src/components/customs/active-sessions-panel.tsx

@@ -1,197 +1,16 @@
 "use client";
 
 import * as React from "react";
-import { useRouter } from "next/navigation";
-import { useQuery } from "@tanstack/react-query";
-import { Activity, User, Key, Cpu, Clock, CheckCircle, XCircle, Loader2 } from "lucide-react";
-import { getActiveSessions } from "@/actions/active-sessions";
-import { cn, formatTokenAmount } from "@/lib/utils";
-import type { ActiveSessionInfo } from "@/types/session";
-import { Link } from "@/i18n/routing";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import { formatCurrency } from "@/lib/utils/currency";
-import { useTranslations } from "next-intl";
-
-const REFRESH_INTERVAL = 5000; // 5秒刷新一次
-
-async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
-  const result = await getActiveSessions();
-  if (!result.ok) {
-    throw new Error(result.error || "获取活跃 Session 失败");
-  }
-  return result.data;
-}
-
-/**
- * 格式化持续时长
- */
-function formatDuration(durationMs: number | undefined): string {
-  if (!durationMs) return "-";
-
-  if (durationMs < 1000) {
-    return `${durationMs}ms`;
-  } else if (durationMs < 60000) {
-    return `${(durationMs / 1000).toFixed(1)}s`;
-  } else {
-    const minutes = Math.floor(durationMs / 60000);
-    const seconds = Math.floor((durationMs % 60000) / 1000);
-    return `${minutes}m ${seconds}s`;
-  }
-}
-
-/**
- * 获取状态图标和颜色
- */
-function getStatusIcon(status: "in_progress" | "completed" | "error", statusCode?: number) {
-  if (status === "in_progress") {
-    return { icon: Loader2, className: "text-blue-500 animate-spin" };
-  } else if (status === "error" || (statusCode && statusCode >= 400)) {
-    return { icon: XCircle, className: "text-red-500" };
-  } else {
-    return { icon: CheckCircle, className: "text-green-500" };
-  }
-}
-
-/**
- * 简洁的 Session 列表项
- */
-function SessionListItem({
-  session,
-  currencyCode = "USD",
-}: {
-  session: ActiveSessionInfo;
-  currencyCode?: CurrencyCode;
-}) {
-  const statusInfo = getStatusIcon(session.status, session.statusCode);
-  const StatusIcon = statusInfo.icon;
-  const inputTokensDisplay =
-    session.inputTokens !== undefined ? formatTokenAmount(session.inputTokens) : null;
-  const outputTokensDisplay =
-    session.outputTokens !== undefined ? formatTokenAmount(session.outputTokens) : null;
-
-  return (
-    <Link
-      href={`/dashboard/sessions/${session.sessionId}/messages`}
-      className="block hover:bg-muted/50 transition-colors rounded-md px-3 py-2 group"
-    >
-      <div className="flex items-center gap-2 text-sm">
-        {/* 状态图标 */}
-        <StatusIcon className={cn("h-3.5 w-3.5 flex-shrink-0", statusInfo.className)} />
-
-        {/* 用户信息 */}
-        <div className="flex items-center gap-1.5 min-w-0">
-          <User className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span className="truncate font-medium max-w-[100px]" title={session.userName}>
-            {session.userName}
-          </span>
-        </div>
-
-        {/* 密钥 */}
-        <div className="flex items-center gap-1 min-w-0">
-          <Key className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span
-            className="truncate text-muted-foreground text-xs font-mono max-w-[80px]"
-            title={session.keyName}
-          >
-            {session.keyName}
-          </span>
-        </div>
-
-        {/* 模型和供应商 */}
-        <div className="flex items-center gap-1 min-w-0">
-          <Cpu className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span
-            className="truncate text-xs font-mono max-w-[120px]"
-            title={`${session.model} @ ${session.providerName}`}
-          >
-            {session.model}
-            {session.providerName && (
-              <span className="text-muted-foreground"> @ {session.providerName}</span>
-            )}
-          </span>
-        </div>
-
-        {/* 时长 */}
-        <div className="flex items-center gap-1 ml-auto flex-shrink-0">
-          <Clock className="h-3 w-3 text-muted-foreground" />
-          <span className="text-xs font-mono text-muted-foreground">
-            {formatDuration(session.durationMs)}
-          </span>
-        </div>
-
-        {/* Token 和成本 */}
-        <div className="flex items-center gap-2 text-xs font-mono flex-shrink-0">
-          {(inputTokensDisplay || outputTokensDisplay) && (
-            <span className="text-muted-foreground">
-              {inputTokensDisplay && `↑${inputTokensDisplay}`}
-              {inputTokensDisplay && outputTokensDisplay && " "}
-              {outputTokensDisplay && `↓${outputTokensDisplay}`}
-            </span>
-          )}
-          {session.costUsd && (
-            <span className="font-medium">{formatCurrency(session.costUsd, currencyCode, 4)}</span>
-          )}
-        </div>
-      </div>
-    </Link>
-  );
-}
+import { ActiveSessionsList } from "./active-sessions-list";
 
 /**
  * 活跃 Session 面板
  * 显示最近 5 分钟内的活跃 session 列表(简洁文字+图标形式)
+ *
+ * 注意:此组件现在是 ActiveSessionsList 的简单包装
+ * 保留此组件是为了保持向后兼容性
  */
 export function ActiveSessionsPanel({ currencyCode = "USD" }: { currencyCode?: CurrencyCode }) {
-  const router = useRouter();
-  const tu = useTranslations("ui");
-  const tc = useTranslations("customs");
-
-  const { data = [], isLoading } = useQuery<ActiveSessionInfo[], Error>({
-    queryKey: ["active-sessions"],
-    queryFn: fetchActiveSessions,
-    refetchInterval: REFRESH_INTERVAL,
-  });
-
-  return (
-    <div className="border rounded-lg bg-card">
-      <div className="px-4 py-3 border-b flex items-center justify-between">
-        <div className="flex items-center gap-2">
-          <Activity className="h-4 w-4 text-primary" />
-          <h3 className="font-semibold text-sm">{tc("activeSessions.title")}</h3>
-          <span className="text-xs text-muted-foreground">
-            {tc("activeSessions.summary", { count: data.length, minutes: 5 })}
-          </span>
-        </div>
-        <button
-          onClick={() => router.push("/dashboard/sessions")}
-          className="text-xs text-muted-foreground hover:text-foreground transition-colors"
-        >
-          {tc("activeSessions.viewAll")} →
-        </button>
-      </div>
-
-      <div className="max-h-[200px] overflow-y-auto">
-        {isLoading && data.length === 0 ? (
-          <div className="flex items-center justify-center h-[200px] text-muted-foreground text-sm">
-            <Loader2 className="h-4 w-4 animate-spin mr-2" />
-            {tu("common.loading")}
-          </div>
-        ) : data.length === 0 ? (
-          <div className="flex items-center justify-center h-[200px] text-muted-foreground text-sm">
-            {tc("activeSessions.empty")}
-          </div>
-        ) : (
-          <div className="divide-y">
-            {data.map((session) => (
-              <SessionListItem
-                key={session.sessionId}
-                session={session}
-                currencyCode={currencyCode}
-              />
-            ))}
-          </div>
-        )}
-      </div>
-    </div>
-  );
+  return <ActiveSessionsList currencyCode={currencyCode} showHeader={true} maxHeight="200px" />;
 }

+ 10 - 173
src/components/customs/overview-panel.tsx

@@ -3,27 +3,14 @@
 import * as React from "react";
 import { useRouter } from "next/navigation";
 import { useQuery } from "@tanstack/react-query";
-import {
-  Activity,
-  TrendingUp,
-  DollarSign,
-  Clock,
-  User,
-  Key,
-  Cpu,
-  CheckCircle,
-  XCircle,
-  Loader2,
-} from "lucide-react";
+import { Activity, TrendingUp, DollarSign, Clock } from "lucide-react";
 import { getOverviewData } from "@/actions/overview";
 import { MetricCard } from "./metric-card";
 import { formatCurrency } from "@/lib/utils/currency";
-import { cn, formatTokenAmount } from "@/lib/utils";
 import type { OverviewData } from "@/actions/overview";
-import type { ActiveSessionInfo } from "@/types/session";
 import type { CurrencyCode } from "@/lib/utils";
-import { Link } from "@/i18n/routing";
 import { useTranslations } from "next-intl";
+import { ActiveSessionsList } from "./active-sessions-list";
 
 const REFRESH_INTERVAL = 5000; // 5秒刷新一次
 
@@ -35,121 +22,6 @@ async function fetchOverviewData(): Promise<OverviewData> {
   return result.data;
 }
 
-/**
- * 格式化持续时长
- */
-function formatDuration(durationMs: number | undefined): string {
-  if (!durationMs) return "-";
-
-  if (durationMs < 1000) {
-    return `${durationMs}ms`;
-  } else if (durationMs < 60000) {
-    return `${(durationMs / 1000).toFixed(1)}s`;
-  } else {
-    const minutes = Math.floor(durationMs / 60000);
-    const seconds = Math.floor((durationMs % 60000) / 1000);
-    return `${minutes}m ${seconds}s`;
-  }
-}
-
-/**
- * 获取状态图标和颜色
- */
-function getStatusIcon(status: "in_progress" | "completed" | "error", statusCode?: number) {
-  if (status === "in_progress") {
-    return { icon: Loader2, className: "text-blue-500 animate-spin" };
-  } else if (status === "error" || (statusCode && statusCode >= 400)) {
-    return { icon: XCircle, className: "text-red-500" };
-  } else {
-    return { icon: CheckCircle, className: "text-green-500" };
-  }
-}
-
-/**
- * 简洁的 Session 列表项
- */
-function SessionListItem({
-  session,
-  currencyCode = "USD",
-}: {
-  session: ActiveSessionInfo;
-  currencyCode?: CurrencyCode;
-}) {
-  const statusInfo = getStatusIcon(session.status, session.statusCode);
-  const StatusIcon = statusInfo.icon;
-  const inputTokensDisplay =
-    session.inputTokens !== undefined ? formatTokenAmount(session.inputTokens) : null;
-  const outputTokensDisplay =
-    session.outputTokens !== undefined ? formatTokenAmount(session.outputTokens) : null;
-
-  return (
-    <Link
-      href={`/dashboard/sessions/${session.sessionId}/messages`}
-      className="block hover:bg-muted/50 transition-colors rounded-md px-3 py-2 group"
-    >
-      <div className="flex items-center gap-2 text-sm">
-        {/* 状态图标 */}
-        <StatusIcon className={cn("h-3.5 w-3.5 flex-shrink-0", statusInfo.className)} />
-
-        {/* 用户信息 */}
-        <div className="flex items-center gap-1.5 min-w-0">
-          <User className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span className="truncate font-medium max-w-[100px]" title={session.userName}>
-            {session.userName}
-          </span>
-        </div>
-
-        {/* 密钥 */}
-        <div className="flex items-center gap-1 min-w-0">
-          <Key className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span
-            className="truncate text-muted-foreground text-xs font-mono max-w-[80px]"
-            title={session.keyName}
-          >
-            {session.keyName}
-          </span>
-        </div>
-
-        {/* 模型和供应商 */}
-        <div className="flex items-center gap-1 min-w-0">
-          <Cpu className="h-3 w-3 text-muted-foreground flex-shrink-0" />
-          <span
-            className="truncate text-xs font-mono max-w-[120px]"
-            title={`${session.model} @ ${session.providerName}`}
-          >
-            {session.model}
-            {session.providerName && (
-              <span className="text-muted-foreground"> @ {session.providerName}</span>
-            )}
-          </span>
-        </div>
-
-        {/* 时长 */}
-        <div className="flex items-center gap-1 ml-auto flex-shrink-0">
-          <Clock className="h-3 w-3 text-muted-foreground" />
-          <span className="text-xs font-mono text-muted-foreground">
-            {formatDuration(session.durationMs)}
-          </span>
-        </div>
-
-        {/* Token 和成本 */}
-        <div className="flex items-center gap-2 text-xs font-mono flex-shrink-0">
-          {(inputTokensDisplay || outputTokensDisplay) && (
-            <span className="text-muted-foreground">
-              {inputTokensDisplay && `↑${inputTokensDisplay}`}
-              {inputTokensDisplay && outputTokensDisplay && " "}
-              {outputTokensDisplay && `↓${outputTokensDisplay}`}
-            </span>
-          )}
-          {session.costUsd && (
-            <span className="font-medium">{formatCurrency(session.costUsd, currencyCode, 4)}</span>
-          )}
-        </div>
-      </div>
-    </Link>
-  );
-}
-
 interface OverviewPanelProps {
   currencyCode?: CurrencyCode;
   isAdmin?: boolean;
@@ -163,9 +35,8 @@ interface OverviewPanelProps {
 export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: OverviewPanelProps) {
   const router = useRouter();
   const tc = useTranslations("customs");
-  const tu = useTranslations("ui");
 
-  const { data, isLoading } = useQuery<OverviewData, Error>({
+  const { data } = useQuery<OverviewData, Error>({
     queryKey: ["overview-data"],
     queryFn: fetchOverviewData,
     refetchInterval: REFRESH_INTERVAL,
@@ -183,7 +54,6 @@ export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: Overvie
     todayRequests: 0,
     todayCost: 0,
     avgResponseTime: 0,
-    recentSessions: [],
   };
 
   // 对于非 admin 用户,不显示概览面板
@@ -230,46 +100,13 @@ export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: Overvie
 
       {/* 右侧:活跃 Session 列表 */}
       <div className="lg:col-span-9">
-        <div className="border rounded-lg bg-card h-full flex flex-col">
-          <div className="px-4 py-3 border-b flex items-center justify-between flex-shrink-0">
-            <div className="flex items-center gap-2">
-              <Activity className="h-4 w-4 text-primary" />
-              <h3 className="font-semibold text-sm">{tc("activeSessions.title")}</h3>
-              <span className="text-xs text-muted-foreground">
-                {tc("activeSessions.summary", { count: metrics.recentSessions.length, minutes: 5 })}
-              </span>
-            </div>
-            <button
-              onClick={() => router.push("/dashboard/sessions")}
-              className="text-xs text-muted-foreground hover:text-foreground transition-colors"
-            >
-              {tc("activeSessions.viewAll")} →
-            </button>
-          </div>
-
-          <div className="flex-1 overflow-y-auto min-h-0">
-            {isLoading && metrics.recentSessions.length === 0 ? (
-              <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
-                <Loader2 className="h-4 w-4 animate-spin mr-2" />
-                {tu("common.loading")}
-              </div>
-            ) : metrics.recentSessions.length === 0 ? (
-              <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
-                {tc("activeSessions.empty")}
-              </div>
-            ) : (
-              <div className="divide-y">
-                {metrics.recentSessions.map((session) => (
-                  <SessionListItem
-                    key={session.sessionId}
-                    session={session}
-                    currencyCode={currencyCode}
-                  />
-                ))}
-              </div>
-            )}
-          </div>
-        </div>
+        <ActiveSessionsList
+          currencyCode={currencyCode}
+          maxItems={10}
+          showHeader={true}
+          maxHeight="auto"
+          className="h-full"
+        />
       </div>
     </div>
   );

+ 125 - 0
src/components/customs/session-list-item.tsx

@@ -0,0 +1,125 @@
+"use client";
+
+import * as React from "react";
+import { User, Key, Cpu, Clock, CheckCircle, XCircle, Loader2 } from "lucide-react";
+import { cn, formatTokenAmount } from "@/lib/utils";
+import type { ActiveSessionInfo } from "@/types/session";
+import { Link } from "@/i18n/routing";
+import type { CurrencyCode } from "@/lib/utils/currency";
+import { formatCurrency } from "@/lib/utils/currency";
+
+/**
+ * 格式化持续时长
+ */
+function formatDuration(durationMs: number | undefined): string {
+  if (!durationMs) return "-";
+
+  if (durationMs < 1000) {
+    return `${durationMs}ms`;
+  } else if (durationMs < 60000) {
+    return `${(durationMs / 1000).toFixed(1)}s`;
+  } else {
+    const minutes = Math.floor(durationMs / 60000);
+    const seconds = Math.floor((durationMs % 60000) / 1000);
+    return `${minutes}m ${seconds}s`;
+  }
+}
+
+/**
+ * 获取状态图标和颜色
+ */
+function getStatusIcon(status: "in_progress" | "completed" | "error", statusCode?: number) {
+  if (status === "in_progress") {
+    return { icon: Loader2, className: "text-blue-500 animate-spin" };
+  } else if (status === "error" || (statusCode && statusCode >= 400)) {
+    return { icon: XCircle, className: "text-red-500" };
+  } else {
+    return { icon: CheckCircle, className: "text-green-500" };
+  }
+}
+
+/**
+ * 简洁的 Session 列表项
+ * 可复用组件,用于活跃 Session 列表的单项展示
+ */
+export function SessionListItem({
+  session,
+  currencyCode = "USD",
+}: {
+  session: ActiveSessionInfo;
+  currencyCode?: CurrencyCode;
+}) {
+  const statusInfo = getStatusIcon(session.status, session.statusCode);
+  const StatusIcon = statusInfo.icon;
+  const inputTokensDisplay =
+    session.inputTokens !== undefined ? formatTokenAmount(session.inputTokens) : null;
+  const outputTokensDisplay =
+    session.outputTokens !== undefined ? formatTokenAmount(session.outputTokens) : null;
+
+  return (
+    <Link
+      href={`/dashboard/sessions/${session.sessionId}/messages`}
+      className="block hover:bg-muted/50 transition-colors rounded-md px-3 py-2 group"
+    >
+      <div className="flex items-center gap-2 text-sm">
+        {/* 状态图标 */}
+        <StatusIcon className={cn("h-3.5 w-3.5 flex-shrink-0", statusInfo.className)} />
+
+        {/* 用户信息 */}
+        <div className="flex items-center gap-1.5 min-w-0">
+          <User className="h-3 w-3 text-muted-foreground flex-shrink-0" />
+          <span className="truncate font-medium max-w-[100px]" title={session.userName}>
+            {session.userName}
+          </span>
+        </div>
+
+        {/* 密钥 */}
+        <div className="flex items-center gap-1 min-w-0">
+          <Key className="h-3 w-3 text-muted-foreground flex-shrink-0" />
+          <span
+            className="truncate text-muted-foreground text-xs font-mono max-w-[80px]"
+            title={session.keyName}
+          >
+            {session.keyName}
+          </span>
+        </div>
+
+        {/* 模型和供应商 */}
+        <div className="flex items-center gap-1 min-w-0">
+          <Cpu className="h-3 w-3 text-muted-foreground flex-shrink-0" />
+          <span
+            className="truncate text-xs font-mono max-w-[120px]"
+            title={`${session.model} @ ${session.providerName}`}
+          >
+            {session.model}
+            {session.providerName && (
+              <span className="text-muted-foreground"> @ {session.providerName}</span>
+            )}
+          </span>
+        </div>
+
+        {/* 时长 */}
+        <div className="flex items-center gap-1 ml-auto flex-shrink-0">
+          <Clock className="h-3 w-3 text-muted-foreground" />
+          <span className="text-xs font-mono text-muted-foreground">
+            {formatDuration(session.durationMs)}
+          </span>
+        </div>
+
+        {/* Token 和成本 */}
+        <div className="flex items-center gap-2 text-xs font-mono flex-shrink-0">
+          {(inputTokensDisplay || outputTokensDisplay) && (
+            <span className="text-muted-foreground">
+              {inputTokensDisplay && `↑${inputTokensDisplay}`}
+              {inputTokensDisplay && outputTokensDisplay && " "}
+              {outputTokensDisplay && `↓${outputTokensDisplay}`}
+            </span>
+          )}
+          {session.costUsd && (
+            <span className="font-medium">{formatCurrency(session.costUsd, currencyCode, 4)}</span>
+          )}
+        </div>
+      </div>
+    </Link>
+  );
+}