Преглед на файлове

refactor(logs): 精简使用记录页面布局,筛选条件默认折叠 (#921)

* refactor(logs): 精简使用记录页面布局

- 去掉表格区域的 Card 套娃,改用轻量 rounded-lg border 容器
- 筛选触发器与工具按钮合并为一行紧凑工具栏
- 全屏/刷新按钮改为 icon-only ghost 样式
- 自动刷新从 Button 改为 Switch 开关,附带 emerald ping 指示点
- 表头去掉 uppercase,降低背景和字色权重
- 行分隔线和 hover 状态使用更柔和的颜色
- 清理未使用的 imports(Card, ListOrdered, Pause, Play)

* fix(logs): 修复筛选 Badge 计数过多和折叠状态丢失问题

1. activeFilterCount 改为按用户感知维度计数:日期范围算 1 个,
   statusCode + excludeStatusCode200 算 1 个,minRetryCount=0 不计入
2. CollapsibleContent 加 forceMount 保持 UsageLogsFilters 挂载,
   避免折叠时丢失 localFilters 等未提交的本地状态
NieiR преди 3 седмици
родител
ревизия
6ebbb4cfc2

+ 117 - 109
src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx

@@ -1,7 +1,7 @@
 "use client";
 "use client";
 
 
 import { useQuery, useQueryClient } from "@tanstack/react-query";
 import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { Expand, Filter, ListOrdered, Minimize2, Pause, Play, RefreshCw } from "lucide-react";
+import { ChevronDown, Expand, Filter, Minimize2, RefreshCw } from "lucide-react";
 import { useRouter, useSearchParams } from "next/navigation";
 import { useRouter, useSearchParams } from "next/navigation";
 import { useLocale, useTranslations } from "next-intl";
 import { useLocale, useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -10,11 +10,13 @@ import { getKeys } from "@/actions/keys";
 import type { OverviewData } from "@/actions/overview";
 import type { OverviewData } from "@/actions/overview";
 import { getOverviewData } from "@/actions/overview";
 import { getOverviewData } from "@/actions/overview";
 import { getProviders } from "@/actions/providers";
 import { getProviders } from "@/actions/providers";
+import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
 import { Switch } from "@/components/ui/switch";
 import { Switch } from "@/components/ui/switch";
 import { useFullscreen } from "@/hooks/use-fullscreen";
 import { useFullscreen } from "@/hooks/use-fullscreen";
 import { getHiddenColumns, type LogsTableColumn } from "@/lib/column-visibility";
 import { getHiddenColumns, type LogsTableColumn } from "@/lib/column-visibility";
+import { cn } from "@/lib/utils";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
 import type { Key } from "@/types/key";
 import type { Key } from "@/types/key";
@@ -268,129 +270,135 @@ function UsageLogsViewContent({
 
 
   const hasStatsFilters = Object.values(statsFilters).some((v) => v !== undefined && v !== false);
   const hasStatsFilters = Object.values(statsFilters).some((v) => v !== undefined && v !== false);
 
 
+  const activeFilterCount = useMemo(() => {
+    let count = 0;
+    if (statsFilters.startTime || statsFilters.endTime) count++;
+    if (statsFilters.userId !== undefined) count++;
+    if (statsFilters.keyId !== undefined) count++;
+    if (statsFilters.providerId !== undefined) count++;
+    if (statsFilters.sessionId) count++;
+    if (statsFilters.statusCode !== undefined || statsFilters.excludeStatusCode200) count++;
+    if (statsFilters.model) count++;
+    if (statsFilters.endpoint) count++;
+    if (statsFilters.minRetryCount !== undefined && statsFilters.minRetryCount > 0) count++;
+    return count;
+  }, [statsFilters]);
+  const [isFilterOpen, setIsFilterOpen] = useState(activeFilterCount > 0);
+
   return (
   return (
     <>
     <>
-      <div className="space-y-4">
-        {/* Stats Summary - Collapsible */}
+      <div className="space-y-3">
+        {/* Stats Summary */}
         {hasStatsFilters && (
         {hasStatsFilters && (
           <UsageLogsStatsPanel filters={statsFilters} currencyCode={resolvedCurrencyCode} />
           <UsageLogsStatsPanel filters={statsFilters} currencyCode={resolvedCurrencyCode} />
         )}
         )}
 
 
-        {/* Filter Criteria */}
-        <Card className="border-border/50">
-          <CardHeader className="pb-3">
-            <div className="flex items-center gap-2">
-              <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted/50">
-                <Filter className="h-4 w-4 text-muted-foreground" />
-              </div>
-              <div>
-                <CardTitle className="text-base">{t("title.filterCriteria")}</CardTitle>
-                <CardDescription className="text-xs">
-                  {t("title.filterCriteriaDescription")}
-                </CardDescription>
-              </div>
-            </div>
-          </CardHeader>
-          <CardContent className="pt-0">
-            <UsageLogsFilters
-              isAdmin={isAdmin}
-              providers={resolvedProviders}
-              initialKeys={resolvedKeys}
-              filters={filters}
-              onChange={handleFilterChange}
-              onReset={() => router.push("/dashboard/logs")}
-              isProvidersLoading={isProvidersLoading}
-              isKeysLoading={isKeysLoading}
-              serverTimeZone={serverTimeZone}
-            />
-          </CardContent>
-        </Card>
-
-        {/* Usage Records Table */}
-        <Card className="border-border/50">
-          <CardHeader className="pb-3">
-            <div className="flex items-center justify-between">
-              <div className="flex items-center gap-2">
-                <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted/50">
-                  <ListOrdered className="h-4 w-4 text-muted-foreground" />
-                </div>
-                <div>
-                  <CardTitle className="text-base">{t("title.usageLogs")}</CardTitle>
-                  <CardDescription className="text-xs">
-                    {t("title.usageLogsDescription")}
-                  </CardDescription>
-                </div>
-              </div>
-              <div className="flex items-center gap-2">
-                <ColumnVisibilityDropdown
-                  userId={userId}
-                  tableId="usage-logs"
-                  onVisibilityChange={setHiddenColumns}
+        {/* Toolbar + Filter */}
+        <Collapsible open={isFilterOpen} onOpenChange={setIsFilterOpen}>
+          <div className="flex items-center justify-between gap-3">
+            {/* Left: Filter trigger */}
+            <CollapsibleTrigger asChild>
+              <button
+                type="button"
+                className="inline-flex items-center gap-1.5 rounded-lg border border-border/60 bg-card px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-border cursor-pointer select-none transition-colors"
+              >
+                <Filter className="h-3.5 w-3.5" />
+                <span>{t("title.filterCriteria")}</span>
+                {activeFilterCount > 0 && (
+                  <Badge
+                    variant="secondary"
+                    className="bg-primary/10 text-primary text-[10px] h-4.5 min-w-[18px] px-1 rounded-full"
+                  >
+                    {activeFilterCount}
+                  </Badge>
+                )}
+                <ChevronDown
+                  className={cn(
+                    "h-3.5 w-3.5 text-muted-foreground/50 transition-transform duration-200",
+                    isFilterOpen && "rotate-180"
+                  )}
                 />
                 />
+              </button>
+            </CollapsibleTrigger>
 
 
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={() => void handleEnterFullscreen()}
-                  className="gap-1.5 h-8"
-                  aria-label={t("logs.actions.fullscreen")}
-                >
-                  <Expand className="h-3.5 w-3.5" />
-                  <span className="hidden sm:inline">{t("logs.actions.fullscreen")}</span>
-                </Button>
-
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={handleManualRefresh}
-                  className="gap-1.5 h-8"
-                  disabled={isFullscreenOpen}
-                  aria-label={t("logs.actions.refresh")}
-                >
-                  <RefreshCw
-                    className={`h-3.5 w-3.5 ${isManualRefreshing ? "animate-spin" : ""}`}
-                  />
-                  <span className="hidden sm:inline">{t("logs.actions.refresh")}</span>
-                </Button>
-
-                <Button
-                  variant={isAutoRefresh ? "default" : "outline"}
-                  size="sm"
-                  onClick={() => setIsAutoRefresh(!isAutoRefresh)}
-                  className="gap-1.5 h-8"
+            {/* Right: Table controls */}
+            <div className="flex items-center gap-1">
+              <ColumnVisibilityDropdown
+                userId={userId}
+                tableId="usage-logs"
+                onVisibilityChange={setHiddenColumns}
+              />
+
+              <Button
+                variant="ghost"
+                size="icon"
+                onClick={() => void handleEnterFullscreen()}
+                className="h-8 w-8"
+                aria-label={t("logs.actions.fullscreen")}
+              >
+                <Expand className="h-3.5 w-3.5" />
+              </Button>
+
+              <Button
+                variant="ghost"
+                size="icon"
+                onClick={handleManualRefresh}
+                className="h-8 w-8"
+                disabled={isFullscreenOpen}
+                aria-label={t("logs.actions.refresh")}
+              >
+                <RefreshCw className={cn("h-3.5 w-3.5", isManualRefreshing && "animate-spin")} />
+              </Button>
+
+              <div className="flex items-center gap-1.5 ml-1 pl-2 border-l border-border/40">
+                {isAutoRefresh && (
+                  <span className="relative flex h-1.5 w-1.5">
+                    <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
+                    <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
+                  </span>
+                )}
+                <Switch
+                  checked={isAutoRefresh}
+                  onCheckedChange={setIsAutoRefresh}
                   disabled={isFullscreenOpen}
                   disabled={isFullscreenOpen}
                   aria-label={
                   aria-label={
                     isAutoRefresh
                     isAutoRefresh
                       ? t("logs.actions.stopAutoRefresh")
                       ? t("logs.actions.stopAutoRefresh")
                       : t("logs.actions.startAutoRefresh")
                       : t("logs.actions.startAutoRefresh")
                   }
                   }
-                >
-                  {isAutoRefresh ? (
-                    <>
-                      <Pause className="h-3.5 w-3.5" />
-                      <span className="hidden sm:inline">{t("logs.actions.stopAutoRefresh")}</span>
-                    </>
-                  ) : (
-                    <>
-                      <Play className="h-3.5 w-3.5" />
-                      <span className="hidden sm:inline">{t("logs.actions.startAutoRefresh")}</span>
-                    </>
-                  )}
-                </Button>
+                />
               </div>
               </div>
             </div>
             </div>
-          </CardHeader>
-          <CardContent className="px-0 pt-0">
-            <VirtualizedLogsTable
-              filters={filters}
-              currencyCode={resolvedCurrencyCode}
-              billingModelSource={resolvedBillingModelSource}
-              autoRefreshEnabled={!isFullscreenOpen && isAutoRefresh}
-              autoRefreshIntervalMs={logsRefreshIntervalMs ?? 5000}
-              hiddenColumns={hiddenColumns}
-            />
-          </CardContent>
-        </Card>
+          </div>
+
+          <CollapsibleContent forceMount className={cn(!isFilterOpen && "hidden")}>
+            <div className="mt-3 rounded-lg border border-border/60 bg-card p-4">
+              <UsageLogsFilters
+                isAdmin={isAdmin}
+                providers={resolvedProviders}
+                initialKeys={resolvedKeys}
+                filters={filters}
+                onChange={handleFilterChange}
+                onReset={() => router.push("/dashboard/logs")}
+                isProvidersLoading={isProvidersLoading}
+                isKeysLoading={isKeysLoading}
+                serverTimeZone={serverTimeZone}
+              />
+            </div>
+          </CollapsibleContent>
+        </Collapsible>
+
+        {/* Table */}
+        <div className="rounded-lg border border-border/60 overflow-hidden">
+          <VirtualizedLogsTable
+            filters={filters}
+            currencyCode={resolvedCurrencyCode}
+            billingModelSource={resolvedBillingModelSource}
+            autoRefreshEnabled={!isFullscreenOpen && isAutoRefresh}
+            autoRefreshIntervalMs={logsRefreshIntervalMs ?? 5000}
+            hiddenColumns={hiddenColumns}
+          />
+        </div>
       </div>
       </div>
 
 
       {isFullscreenOpen ? (
       {isFullscreenOpen ? (

+ 6 - 6
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx

@@ -208,7 +208,7 @@ export function VirtualizedLogsTable({
     <div className="space-y-4">
     <div className="space-y-4">
       {/* Status bar */}
       {/* Status bar */}
       {hideStatusBar ? null : (
       {hideStatusBar ? null : (
-        <div className="flex items-center justify-between text-sm text-muted-foreground">
+        <div className="flex items-center justify-between text-xs text-muted-foreground/70 px-3 pt-2">
           <span>{t("logs.table.loadedCount", { count: allLogs.length })}</span>
           <span>{t("logs.table.loadedCount", { count: allLogs.length })}</span>
           {isFetchingNextPage && (
           {isFetchingNextPage && (
             <span className="flex items-center gap-2">
             <span className="flex items-center gap-2">
@@ -221,11 +221,11 @@ export function VirtualizedLogsTable({
       )}
       )}
 
 
       {/* Table with virtual scrolling */}
       {/* Table with virtual scrolling */}
-      <div className="border-t overflow-x-auto">
+      <div className="overflow-x-auto">
         <div className="min-w-[800px]">
         <div className="min-w-[800px]">
           {/* Fixed header */}
           {/* Fixed header */}
-          <div className="bg-muted/40 border-b sticky top-0 z-10">
-            <div className="flex items-center h-9 text-xs font-medium text-muted-foreground uppercase tracking-wider">
+          <div className="bg-muted/30 border-b sticky top-0 z-10">
+            <div className="flex items-center h-8 text-[11px] font-medium text-muted-foreground/80 tracking-wide">
               <div className="flex-[0.6] min-w-[56px] pl-3 truncate" title={t("logs.columns.time")}>
               <div className="flex-[0.6] min-w-[56px] pl-3 truncate" title={t("logs.columns.time")}>
                 {t("logs.columns.time")}
                 {t("logs.columns.time")}
               </div>
               </div>
@@ -360,8 +360,8 @@ export function VirtualizedLogsTable({
                       transform: `translateY(${virtualRow.start}px)`,
                       transform: `translateY(${virtualRow.start}px)`,
                     }}
                     }}
                     className={cn(
                     className={cn(
-                      "flex items-center text-sm border-b transition-colors hover:bg-muted/30",
-                      isNonBilling ? "bg-muted/40 text-muted-foreground dark:bg-muted/20" : ""
+                      "flex items-center text-sm border-b border-border/40 transition-colors hover:bg-accent/50",
+                      isNonBilling ? "bg-muted/30 text-muted-foreground dark:bg-muted/15" : ""
                     )}
                     )}
                   >
                   >
                     {/* Time */}
                     {/* Time */}