Procházet zdrojové kódy

refactor: remove unused dashboard components and settings files

- Deleted several components related to the dashboard and settings, including dashboard header, navigation, user menu, statistics charts, and user management forms.
- Cleaned up the codebase by removing obsolete files to improve maintainability and reduce bundle size.
ding113 před 3 měsíci
rodič
revize
21a3cffcbf
53 změnil soubory, kde provedl 0 přidání a 8861 odebrání
  1. 0 44
      src/app/\[locale\]/dashboard/_components/dashboard-header.tsx
  2. 0 64
      src/app/\[locale\]/dashboard/_components/dashboard-nav.tsx
  3. 0 559
      src/app/\[locale\]/dashboard/_components/statistics/chart.tsx
  4. 0 3
      src/app/\[locale\]/dashboard/_components/statistics/index.ts
  5. 0 45
      src/app/\[locale\]/dashboard/_components/statistics/time-range-selector.tsx
  6. 0 67
      src/app/\[locale\]/dashboard/_components/statistics/wrapper.tsx
  7. 0 58
      src/app/\[locale\]/dashboard/_components/user-menu.tsx
  8. 0 37
      src/app/\[locale\]/dashboard/_components/user/add-user-dialog.tsx
  9. 0 156
      src/app/\[locale\]/dashboard/_components/user/forms/add-key-form.tsx
  10. 0 73
      src/app/\[locale\]/dashboard/_components/user/forms/delete-key-confirm.tsx
  11. 0 71
      src/app/\[locale\]/dashboard/_components/user/forms/delete-user-confirm.tsx
  12. 0 163
      src/app/\[locale\]/dashboard/_components/user/forms/edit-key-form.tsx
  13. 0 138
      src/app/\[locale\]/dashboard/_components/user/forms/user-form.tsx
  14. 0 74
      src/app/\[locale\]/dashboard/_components/user/key-actions.tsx
  15. 0 144
      src/app/\[locale\]/dashboard/_components/user/key-limit-usage.tsx
  16. 0 281
      src/app/\[locale\]/dashboard/_components/user/key-list-header.tsx
  17. 0 227
      src/app/\[locale\]/dashboard/_components/user/key-list.tsx
  18. 0 68
      src/app/\[locale\]/dashboard/_components/user/user-actions.tsx
  19. 0 84
      src/app/\[locale\]/dashboard/_components/user/user-key-manager.tsx
  20. 0 61
      src/app/\[locale\]/dashboard/_components/user/user-list.tsx
  21. 0 46
      src/app/\[locale\]/settings/_components/settings-nav.tsx
  22. 0 13
      src/app/\[locale\]/settings/_components/settings-page-header.tsx
  23. 0 125
      src/app/\[locale\]/settings/client-versions/_components/client-version-stats-table.tsx
  24. 0 80
      src/app/\[locale\]/settings/client-versions/_components/client-version-toggle.tsx
  25. 0 179
      src/app/\[locale\]/settings/config/_components/auto-cleanup-form.tsx
  26. 0 138
      src/app/\[locale\]/settings/config/_components/system-settings-form.tsx
  27. 0 68
      src/app/\[locale\]/settings/data/_components/database-export.tsx
  28. 0 253
      src/app/\[locale\]/settings/data/_components/database-import.tsx
  29. 0 128
      src/app/\[locale\]/settings/data/_components/database-status.tsx
  30. 0 219
      src/app/\[locale\]/settings/data/_components/log-cleanup-panel.tsx
  31. 0 120
      src/app/\[locale\]/settings/logs/_components/log-level-form.tsx
  32. 0 376
      src/app/\[locale\]/settings/prices/_components/price-list.tsx
  33. 0 66
      src/app/\[locale\]/settings/prices/_components/sync-litellm-button.tsx
  34. 0 287
      src/app/\[locale\]/settings/prices/_components/upload-price-dialog.tsx
  35. 0 39
      src/app/\[locale\]/settings/providers/_components/add-provider-dialog.tsx
  36. 0 1099
      src/app/\[locale\]/settings/providers/_components/forms/provider-form.tsx
  37. 0 179
      src/app/\[locale\]/settings/providers/_components/forms/proxy-test-button.tsx
  38. 0 275
      src/app/\[locale\]/settings/providers/_components/hooks/use-provider-edit.ts
  39. 0 201
      src/app/\[locale\]/settings/providers/_components/model-multi-select.tsx
  40. 0 180
      src/app/\[locale\]/settings/providers/_components/model-redirect-editor.tsx
  41. 0 738
      src/app/\[locale\]/settings/providers/_components/provider-list-item.legacy.tsx
  42. 0 58
      src/app/\[locale\]/settings/providers/_components/provider-list.tsx
  43. 0 141
      src/app/\[locale\]/settings/providers/_components/provider-manager.tsx
  44. 0 490
      src/app/\[locale\]/settings/providers/_components/provider-rich-list-item.tsx
  45. 0 46
      src/app/\[locale\]/settings/providers/_components/provider-sort-dropdown.tsx
  46. 0 44
      src/app/\[locale\]/settings/providers/_components/provider-type-filter.tsx
  47. 0 280
      src/app/\[locale\]/settings/providers/_components/scheduling-rules-dialog.tsx
  48. 0 144
      src/app/\[locale\]/settings/sensitive-words/_components/add-word-dialog.tsx
  49. 0 142
      src/app/\[locale\]/settings/sensitive-words/_components/edit-word-dialog.tsx
  50. 0 57
      src/app/\[locale\]/settings/sensitive-words/_components/refresh-cache-button.tsx
  51. 0 137
      src/app/\[locale\]/settings/sensitive-words/_components/word-list-table.tsx
  52. 0 36
      src/app/\[locale\]/usage-doc/_components/quick-links.tsx
  53. 0 60
      src/app/\[locale\]/usage-doc/_components/toc-nav.tsx

+ 0 - 44
src/app/\[locale\]/dashboard/_components/dashboard-header.tsx

@@ -1,44 +0,0 @@
-import Link from "next/link";
-
-import type { AuthSession } from "@/lib/auth";
-import { Button } from "@/components/ui/button";
-import { DashboardNav, type DashboardNavItem } from "./dashboard-nav";
-import { UserMenu } from "./user-menu";
-import { VersionUpdateNotifier } from "@/components/customs/version-update-notifier";
-
-interface DashboardHeaderProps {
-  session: AuthSession | null;
-}
-
-const NAV_ITEMS: (DashboardNavItem & { adminOnly?: boolean })[] = [
-  { href: "/dashboard", label: "仪表盘" },
-  { href: "/dashboard/logs", label: "使用记录" },
-  { href: "/dashboard/leaderboard", label: "排行榜" },
-  { href: "/dashboard/quotas", label: "限额管理" },
-  { href: "/usage-doc", label: "文档" },
-  { href: "/settings", label: "系统设置", adminOnly: true },
-  { href: "https://github.com/ding113/claude-code-hub/issues", label: "反馈问题", external: true },
-];
-
-export function DashboardHeader({ session }: DashboardHeaderProps) {
-  const isAdmin = session?.user.role === "admin";
-  const items = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
-
-  return (
-    <header className="sticky top-0 z-40 border-b border-border/80 bg-card/80 backdrop-blur supports-[backdrop-filter]:bg-card/60">
-      <div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
-        <DashboardNav items={items} />
-        <div className="flex items-center gap-3">
-          {session && <VersionUpdateNotifier />}
-          {session ? (
-            <UserMenu user={session.user} />
-          ) : (
-            <Button asChild size="sm" variant="outline">
-              <Link href="/login">登录</Link>
-            </Button>
-          )}
-        </div>
-      </div>
-    </header>
-  );
-}

+ 0 - 64
src/app/\[locale\]/dashboard/_components/dashboard-nav.tsx

@@ -1,64 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-import { cn } from "@/lib/utils";
-
-export interface DashboardNavItem {
-  href: string;
-  label: string;
-  external?: boolean;
-}
-
-interface DashboardNavProps {
-  items: DashboardNavItem[];
-}
-
-export function DashboardNav({ items }: DashboardNavProps) {
-  const pathname = usePathname();
-
-  if (items.length === 0) {
-    return null;
-  }
-
-  const getIsActive = (href: string) => {
-    if (href === "/dashboard") {
-      return pathname === "/dashboard";
-    }
-
-    return pathname.startsWith(href);
-  };
-
-  return (
-    <nav className="flex items-center gap-1 rounded-full border border-border/80 bg-background/80 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/60">
-      {items.map((item) => {
-        const isActive = getIsActive(item.href);
-        const className = cn(
-          "rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground transition-all hover:text-foreground",
-          isActive && "bg-primary/5 text-foreground shadow-[0_1px_0_0_rgba(0,0,0,0.03)]"
-        );
-
-        if (item.external) {
-          return (
-            <a
-              key={item.href}
-              href={item.href}
-              target="_blank"
-              rel="noopener noreferrer"
-              className={className}
-            >
-              {item.label}
-            </a>
-          );
-        }
-
-        return (
-          <Link key={item.href} href={item.href} className={className}>
-            {item.label}
-          </Link>
-        );
-      })}
-    </nav>
-  );
-}

+ 0 - 559
src/app/\[locale\]/dashboard/_components/statistics/chart.tsx

@@ -1,559 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
-import type { CurrencyCode } from "@/lib/utils";
-import { ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/components/ui/chart";
-
-import type { UserStatisticsData, TimeRange } from "@/types/statistics";
-import { TimeRangeSelector } from "./time-range-selector";
-
-// 固定的调色盘,确保新增用户也能获得可辨识的颜色
-const USER_COLOR_PALETTE = [
-  "var(--chart-1)",
-  "var(--chart-2)",
-  "var(--chart-3)",
-  "var(--chart-4)",
-  "var(--chart-5)",
-  "hsl(15, 85%, 60%)",
-  "hsl(195, 85%, 60%)",
-  "hsl(285, 85%, 60%)",
-  "hsl(135, 85%, 50%)",
-  "hsl(45, 85%, 55%)",
-  "hsl(315, 85%, 65%)",
-  "hsl(165, 85%, 55%)",
-  "hsl(35, 85%, 65%)",
-  "hsl(255, 85%, 65%)",
-  "hsl(75, 85%, 50%)",
-  "hsl(345, 85%, 65%)",
-  "hsl(105, 85%, 55%)",
-  "hsl(225, 85%, 65%)",
-  "hsl(55, 85%, 60%)",
-  "hsl(275, 85%, 60%)",
-  "hsl(25, 85%, 65%)",
-  "hsl(185, 85%, 60%)",
-  "hsl(125, 85%, 55%)",
-  "hsl(295, 85%, 70%)",
-] as const;
-
-// 根据索引循环分配颜色,避免重复定义数组
-const getUserColor = (index: number) => USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length];
-
-export interface UserStatisticsChartProps {
-  data: UserStatisticsData;
-  onTimeRangeChange?: (timeRange: TimeRange) => void;
-  currencyCode?: CurrencyCode;
-}
-
-/**
- * 用户统计图表组件
- * 展示用户的消费金额和API调用次数
- */
-export function UserStatisticsChart({
-  data,
-  onTimeRangeChange,
-  currencyCode = "USD",
-}: UserStatisticsChartProps) {
-  const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost");
-
-  // 用户选择状态(仅 Admin 用 users 模式时启用)
-  const [selectedUserIds, setSelectedUserIds] = React.useState<Set<number>>(
-    () => new Set(data.users.map((u) => u.id))
-  );
-
-  // 重置选择状态(当 data.users 变化时)
-  React.useEffect(() => {
-    setSelectedUserIds(new Set(data.users.map((u) => u.id)));
-  }, [data.users]);
-
-  const isAdminMode = data.mode === "users";
-  const enableUserFilter = isAdminMode && data.users.length > 1;
-
-  const toggleUserSelection = (userId: number) => {
-    setSelectedUserIds((prev) => {
-      const next = new Set(prev);
-      if (next.has(userId)) {
-        // 至少保留一个用户
-        if (next.size > 1) {
-          next.delete(userId);
-        }
-      } else {
-        next.add(userId);
-      }
-      return next;
-    });
-  };
-
-  const selectAllUsers = () => {
-    setSelectedUserIds(new Set(data.users.map((u) => u.id)));
-  };
-
-  const deselectAllUsers = () => {
-    // 保留第一个用户
-    if (data.users.length > 0) {
-      setSelectedUserIds(new Set([data.users[0].id]));
-    }
-  };
-
-  // 动态生成图表配置
-  const chartConfig = React.useMemo(() => {
-    const config: ChartConfig = {
-      cost: {
-        label: "消费金额",
-      },
-      calls: {
-        label: "API调用次数",
-      },
-    };
-
-    data.users.forEach((user, index) => {
-      config[user.dataKey] = {
-        label: user.name,
-        color: getUserColor(index),
-      };
-    });
-
-    return config;
-  }, [data.users]);
-
-  const userMap = React.useMemo(() => {
-    return new Map(data.users.map((user) => [user.dataKey, user]));
-  }, [data.users]);
-
-  // 过滤可见用户(如果启用过滤)
-  const visibleUsers = React.useMemo(() => {
-    if (!enableUserFilter) {
-      return data.users;
-    }
-    return data.users.filter((u) => selectedUserIds.has(u.id));
-  }, [data.users, selectedUserIds, enableUserFilter]);
-
-  const numericChartData = React.useMemo(() => {
-    return data.chartData.map((day) => {
-      const normalized: Record<string, string | number> = { ...day };
-
-      // 只处理可见用户的数据
-      visibleUsers.forEach((user) => {
-        const costKey = `${user.dataKey}_cost`;
-        const costDecimal = toDecimal(day[costKey]);
-        normalized[costKey] = costDecimal ? Number(costDecimal.toDecimalPlaces(6).toString()) : 0;
-
-        const callsKey = `${user.dataKey}_calls`;
-        const callsValue = day[callsKey];
-        normalized[callsKey] =
-          typeof callsValue === "number" ? callsValue : Number(callsValue ?? 0);
-      });
-
-      return normalized;
-    });
-  }, [data.chartData, visibleUsers]);
-
-  // 计算每个用户的总数据(包括所有用户,用于 legend 排序)
-  const userTotals = React.useMemo(() => {
-    const totals: Record<string, { cost: Decimal; calls: number }> = {};
-
-    data.users.forEach((user) => {
-      totals[user.dataKey] = { cost: new Decimal(0), calls: 0 };
-    });
-
-    data.chartData.forEach((day) => {
-      data.users.forEach((user) => {
-        const costValue = toDecimal(day[`${user.dataKey}_cost`]);
-        const callsValue = day[`${user.dataKey}_calls`];
-
-        if (costValue) {
-          const current = totals[user.dataKey];
-          current.cost = current.cost.plus(costValue);
-        }
-
-        totals[user.dataKey].calls +=
-          typeof callsValue === "number" ? callsValue : Number(callsValue ?? 0);
-      });
-    });
-
-    return totals;
-  }, [data.chartData, data.users]);
-
-  // 计算可见用户的总计(用于顶部统计卡片)
-  const visibleTotals = React.useMemo(() => {
-    const costTotal = data.chartData.reduce((sum, day) => {
-      const dayTotal = visibleUsers.reduce((daySum, user) => {
-        const costValue = toDecimal(day[`${user.dataKey}_cost`]);
-        return costValue ? daySum.plus(costValue) : daySum;
-      }, new Decimal(0));
-      return sum.plus(dayTotal);
-    }, new Decimal(0));
-
-    const callsTotal = data.chartData.reduce((sum, day) => {
-      const dayTotal = visibleUsers.reduce((daySum, user) => {
-        const callsValue = day[`${user.dataKey}_calls`];
-        return daySum + (typeof callsValue === "number" ? callsValue : 0);
-      }, 0);
-      return sum + dayTotal;
-    }, 0);
-
-    return { cost: costTotal, calls: callsTotal };
-  }, [data.chartData, visibleUsers]);
-
-  const sortedLegendUsers = React.useMemo(() => {
-    return data.users
-      .map((user, index) => ({ user, index }))
-      .sort((a, b) => {
-        const totalsA = userTotals[a.user.dataKey];
-        const totalsB = userTotals[b.user.dataKey];
-        if (!totalsA && !totalsB) {
-          return a.index - b.index;
-        }
-
-        if (!totalsA) return 1;
-        if (!totalsB) return -1;
-
-        if (activeChart === "cost") {
-          const result = totalsB.cost.comparedTo(totalsA.cost);
-          return result !== 0 ? result : a.index - b.index;
-        }
-
-        if (totalsB.calls === totalsA.calls) {
-          return a.index - b.index;
-        }
-
-        return totalsB.calls - totalsA.calls;
-      });
-  }, [data.users, userTotals, activeChart]);
-
-  // 格式化日期显示(根据分辨率)
-  const formatDate = (dateStr: string) => {
-    const date = new Date(dateStr);
-    if (data.resolution === "hour") {
-      return date.toLocaleTimeString("zh-CN", {
-        hour: "2-digit",
-        minute: "2-digit",
-      });
-    } else {
-      return date.toLocaleDateString("zh-CN", {
-        month: "numeric",
-        day: "numeric",
-      });
-    }
-  };
-
-  // 格式化tooltip日期
-  const formatTooltipDate = (dateStr: string) => {
-    const date = new Date(dateStr);
-    if (data.resolution === "hour") {
-      return date.toLocaleString("zh-CN", {
-        month: "long",
-        day: "numeric",
-        hour: "2-digit",
-        minute: "2-digit",
-      });
-    } else {
-      return date.toLocaleDateString("zh-CN", {
-        year: "numeric",
-        month: "long",
-        day: "numeric",
-      });
-    }
-  };
-
-  // 获取时间范围的描述文本
-  const getTimeRangeDescription = () => {
-    switch (data.timeRange) {
-      case "today":
-        return "今天的使用情况";
-      case "7days":
-        return "过去 7 天的使用情况";
-      case "30days":
-        return "过去 30 天的使用情况";
-      default:
-        return "使用情况";
-    }
-  };
-
-  const getAggregationLabel = () => {
-    if (data.mode === "keys") {
-      return "仅显示您名下各密钥的使用统计";
-    } else if (data.mode === "mixed") {
-      return "展示您的密钥明细和其他用户汇总";
-    } else {
-      return "展示所有用户的使用统计";
-    }
-  };
-
-  return (
-    <Card className="gap-0 py-0">
-      <CardHeader
-        className={cn(
-          "flex flex-col items-stretch lg:flex-row",
-          onTimeRangeChange && "border-b !pb-0 !px-0"
-        )}
-      >
-        <div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 lg:!py-0">
-          <CardTitle>使用统计</CardTitle>
-          <CardDescription>
-            {getTimeRangeDescription()} · {getAggregationLabel()}
-          </CardDescription>
-        </div>
-        {/* 时间范围选择器 */}
-        {onTimeRangeChange && (
-          <TimeRangeSelector
-            value={data.timeRange}
-            onChange={onTimeRangeChange}
-            className="border-t lg:border-t-0"
-          />
-        )}
-        {/* 如果没有时间范围选择回调,显示原有的指标切换按钮 */}
-        {!onTimeRangeChange && (
-          <div className="flex">
-            <button
-              data-active={activeChart === "cost"}
-              className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
-              onClick={() => setActiveChart("cost")}
-            >
-              <span className="text-muted-foreground text-xs">总消费金额</span>
-              <span className="text-lg leading-none font-bold sm:text-3xl">
-                {formatCurrency(visibleTotals.cost, currencyCode)}
-              </span>
-            </button>
-            <button
-              data-active={activeChart === "calls"}
-              className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
-              onClick={() => setActiveChart("calls")}
-            >
-              <span className="text-muted-foreground text-xs">总API调用次数</span>
-              <span className="text-lg leading-none font-bold sm:text-3xl">
-                {visibleTotals.calls.toLocaleString()}
-              </span>
-            </button>
-          </div>
-        )}
-      </CardHeader>
-
-      {onTimeRangeChange && (
-        <div className="flex border-b">
-          <button
-            data-active={activeChart === "cost"}
-            className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
-            onClick={() => setActiveChart("cost")}
-          >
-            <span className="text-muted-foreground text-xs">总消费金额</span>
-            <span className="text-lg leading-none font-bold sm:text-xl">
-              {formatCurrency(visibleTotals.cost, currencyCode)}
-            </span>
-          </button>
-          <button
-            data-active={activeChart === "calls"}
-            className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
-            onClick={() => setActiveChart("calls")}
-          >
-            <span className="text-muted-foreground text-xs">总API调用次数</span>
-            <span className="text-lg leading-none font-bold sm:text-xl">
-              {visibleTotals.calls.toLocaleString()}
-            </span>
-          </button>
-        </div>
-      )}
-      <CardContent className="px-1 sm:p-6">
-        <ChartContainer config={chartConfig} className="aspect-auto h-[280px] w-full">
-          <AreaChart
-            data={numericChartData}
-            margin={{
-              left: 12,
-              right: 12,
-            }}
-          >
-            <defs>
-              {data.users.map((user, index) => {
-                const color = getUserColor(index);
-                return (
-                  <linearGradient
-                    key={user.dataKey}
-                    id={`fill-${user.dataKey}`}
-                    x1="0"
-                    y1="0"
-                    x2="0"
-                    y2="1"
-                  >
-                    <stop offset="5%" stopColor={color} stopOpacity={0.8} />
-                    <stop offset="95%" stopColor={color} stopOpacity={0.1} />
-                  </linearGradient>
-                );
-              })}
-            </defs>
-            <CartesianGrid vertical={false} />
-            <XAxis
-              dataKey="date"
-              tickLine={false}
-              axisLine={false}
-              tickMargin={2}
-              tickFormatter={formatDate}
-            />
-            <YAxis
-              tickLine={false}
-              axisLine={false}
-              tickMargin={8}
-              tickFormatter={(value) => {
-                if (activeChart === "cost") {
-                  return formatCurrency(value, currencyCode);
-                }
-                return Number(value).toLocaleString();
-              }}
-            />
-            <ChartTooltip
-              cursor={false}
-              content={({ active, payload, label }) => {
-                if (!active || !payload || !payload.length) return null;
-
-                const filteredPayload = payload.filter((entry) => {
-                  const value =
-                    typeof entry.value === "number" ? entry.value : Number(entry.value ?? 0);
-                  return !Number.isNaN(value) && value !== 0;
-                });
-
-                if (!filteredPayload.length) {
-                  return (
-                    <div className="rounded-lg border bg-background p-3 shadow-sm min-w-[200px]">
-                      <div className="font-medium text-center">{formatTooltipDate(label)}</div>
-                    </div>
-                  );
-                }
-
-                return (
-                  <div className="rounded-lg border bg-background p-3 shadow-sm min-w-[200px]">
-                    <div className="grid gap-2">
-                      <div className="font-medium text-center">{formatTooltipDate(label)}</div>
-                      <div className="grid gap-1.5">
-                        {[...filteredPayload]
-                          .sort((a, b) => (Number(b.value ?? 0) || 0) - (Number(a.value ?? 0) || 0))
-                          .map((entry, index) => {
-                            const baseKey =
-                              entry.dataKey?.toString().replace(`_${activeChart}`, "") || "";
-                            const displayUser = userMap.get(baseKey);
-                            const value =
-                              typeof entry.value === "number"
-                                ? entry.value
-                                : Number(entry.value ?? 0);
-                            const color = entry.color;
-
-                            return (
-                              <div
-                                key={index}
-                                className="flex items-center justify-between gap-3 text-sm"
-                              >
-                                <div className="flex items-center gap-2 min-w-0">
-                                  <div
-                                    className="h-2 w-2 rounded-full flex-shrink-0"
-                                    style={{ backgroundColor: color }}
-                                  />
-                                  <span className="font-medium truncate">
-                                    {displayUser?.name || baseKey}:
-                                  </span>
-                                </div>
-                                <span className="ml-auto font-mono flex-shrink-0">
-                                  {activeChart === "cost"
-                                    ? formatCurrency(value, currencyCode)
-                                    : value.toLocaleString()}
-                                </span>
-                              </div>
-                            );
-                          })}
-                      </div>
-                    </div>
-                  </div>
-                );
-              }}
-            />
-            {visibleUsers.map((user) => {
-              const originalIndex = data.users.findIndex((u) => u.id === user.id);
-              const color = getUserColor(originalIndex);
-              return (
-                <Area
-                  key={user.dataKey}
-                  dataKey={`${user.dataKey}_${activeChart}`}
-                  name={user.name}
-                  type="monotone"
-                  fill={`url(#fill-${user.dataKey})`}
-                  stroke={color}
-                  stackId="a"
-                />
-              );
-            })}
-            <ChartLegend
-              content={() => (
-                <div className="px-1">
-                  {/* 全选/清空按钮 (仅 Admin 且用户数 > 1 时显示) */}
-                  {enableUserFilter && (
-                    <div className="flex items-center justify-center gap-2 mb-2">
-                      <button
-                        onClick={selectAllUsers}
-                        className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted/50 transition-colors"
-                      >
-                        全选 ({data.users.length})
-                      </button>
-                      <span className="text-muted-foreground">·</span>
-                      <button
-                        onClick={deselectAllUsers}
-                        className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted/50 transition-colors"
-                      >
-                        清空
-                      </button>
-                      <span className="text-muted-foreground">·</span>
-                      <span className="text-xs text-muted-foreground">
-                        已选 {selectedUserIds.size}/{data.users.length}
-                      </span>
-                    </div>
-                  )}
-                  <div className="flex flex-wrap justify-center gap-1">
-                    {sortedLegendUsers.map(({ user, index }) => {
-                      const color = getUserColor(index);
-                      const userTotal = userTotals[user.dataKey] ?? {
-                        cost: new Decimal(0),
-                        calls: 0,
-                      };
-                      const isSelected = selectedUserIds.has(user.id);
-
-                      return (
-                        <div
-                          key={user.dataKey}
-                          onClick={() => enableUserFilter && toggleUserSelection(user.id)}
-                          className={cn(
-                            "rounded-md px-3 py-2 text-center transition-all min-w-16",
-                            enableUserFilter && "cursor-pointer",
-                            isSelected
-                              ? "bg-muted/50 hover:bg-muted/70 ring-1 ring-border"
-                              : "bg-muted/10 hover:bg-muted/30 opacity-50"
-                          )}
-                        >
-                          {/* 上方:颜色点 + 用户名 */}
-                          <div className="flex items-center justify-center gap-1 mb-1">
-                            <div
-                              className="h-2 w-2 rounded-full flex-shrink-0"
-                              style={{ backgroundColor: color }}
-                            />
-                            <span className="text-xs font-medium text-foreground truncate max-w-12">
-                              {user.name}
-                            </span>
-                          </div>
-
-                          {/* 下方:数据值 */}
-                          <div className="text-xs font-bold text-foreground">
-                            {activeChart === "cost"
-                              ? formatCurrency(userTotal.cost, currencyCode)
-                              : userTotal.calls.toLocaleString()}
-                          </div>
-                        </div>
-                      );
-                    })}
-                  </div>
-                </div>
-              )}
-            />
-          </AreaChart>
-        </ChartContainer>
-      </CardContent>
-    </Card>
-  );
-}

+ 0 - 3
src/app/\[locale\]/dashboard/_components/statistics/index.ts

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

+ 0 - 45
src/app/\[locale\]/dashboard/_components/statistics/time-range-selector.tsx

@@ -1,45 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { TIME_RANGE_OPTIONS, type TimeRange } from "@/types/statistics";
-import { cn } from "@/lib/utils";
-
-interface TimeRangeSelectorProps {
-  value: TimeRange;
-  onChange: (timeRange: TimeRange) => void;
-  className?: string;
-  disabled?: boolean;
-}
-
-/**
- * 时间范围选择器组件
- * 提供今天、7天、30天的选择
- */
-export function TimeRangeSelector({
-  value,
-  onChange,
-  className,
-  disabled = false,
-}: TimeRangeSelectorProps) {
-  return (
-    <div className={cn("flex flex-wrap ", className)}>
-      {TIME_RANGE_OPTIONS.map((option) => (
-        <button
-          key={option.key}
-          data-active={value === option.key}
-          disabled={disabled}
-          className="data-[active=true]:bg-muted/50 disabled:opacity-50 disabled:cursor-not-allowed relative z-30 flex flex-none flex-col items-start justify-center gap-1 border-t px-6 py-4 text-left lg:border-t-0 lg:px-8 lg:py-6 lg:[&:not(:first-child)]:border-l transition-all duration-200 hover:bg-muted/30 disabled:hover:bg-transparent"
-          onClick={() => !disabled && onChange(option.key)}
-          title={option.description}
-        >
-          <span className="text-muted-foreground text-xs transition-colors whitespace-nowrap">
-            {option.description}
-          </span>
-          <span className="text-lg leading-none font-bold sm:text-xl transition-colors whitespace-nowrap">
-            {option.label}
-          </span>
-        </button>
-      ))}
-    </div>
-  );
-}

+ 0 - 67
src/app/\[locale\]/dashboard/_components/statistics/wrapper.tsx

@@ -1,67 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { useQuery } from "@tanstack/react-query";
-import { UserStatisticsChart } from "./chart";
-import { getUserStatistics } from "@/actions/statistics";
-import type { TimeRange, UserStatisticsData } from "@/types/statistics";
-import type { CurrencyCode } from "@/lib/utils";
-import { DEFAULT_TIME_RANGE } from "@/types/statistics";
-import { toast } from "sonner";
-
-interface StatisticsWrapperProps {
-  initialData?: UserStatisticsData;
-  currencyCode?: CurrencyCode;
-}
-
-const STATISTICS_REFRESH_INTERVAL = 5000; // 5秒刷新一次
-
-async function fetchStatistics(timeRange: TimeRange): Promise<UserStatisticsData> {
-  const result = await getUserStatistics(timeRange);
-  if (!result.ok) {
-    throw new Error(result.error || "获取统计数据失败");
-  }
-  return result.data;
-}
-
-/**
- * 统计组件包装器
- * 处理时间范围状态管理和数据获取
- */
-export function StatisticsWrapper({ initialData, currencyCode = "USD" }: StatisticsWrapperProps) {
-  const [timeRange, setTimeRange] = React.useState<TimeRange>(
-    initialData?.timeRange ?? DEFAULT_TIME_RANGE
-  );
-
-  const { data, error } = useQuery<UserStatisticsData, Error>({
-    queryKey: ["user-statistics", timeRange],
-    queryFn: () => fetchStatistics(timeRange),
-    initialData,
-    refetchInterval: STATISTICS_REFRESH_INTERVAL,
-  });
-
-  // 错误提示
-  React.useEffect(() => {
-    if (error) {
-      toast.error(error.message);
-    }
-  }, [error]);
-
-  // 处理时间范围变化
-  const handleTimeRangeChange = React.useCallback((newTimeRange: TimeRange) => {
-    setTimeRange(newTimeRange);
-  }, []);
-
-  // 如果没有数据,显示空状态
-  if (!data) {
-    return <div className="text-center py-8 text-muted-foreground">暂无统计数据</div>;
-  }
-
-  return (
-    <UserStatisticsChart
-      data={data}
-      onTimeRangeChange={handleTimeRangeChange}
-      currencyCode={currencyCode}
-    />
-  );
-}

+ 0 - 58
src/app/\[locale\]/dashboard/_components/user-menu.tsx

@@ -1,58 +0,0 @@
-"use client";
-
-import { useRouter } from "next/navigation";
-import { Avatar, AvatarFallback } from "@/components/ui/avatar";
-import { Button } from "@/components/ui/button";
-import { LogOut } from "lucide-react";
-
-interface UserMenuProps {
-  user: {
-    id: number;
-    name: string;
-    description?: string | null;
-  };
-}
-
-export function UserMenu({ user }: UserMenuProps) {
-  const router = useRouter();
-
-  const handleLogout = () => {
-    // 立即跳转到登录页面,避免延迟
-    router.push("/login");
-    // 异步调用登出接口,不等待响应
-    fetch("/api/auth/logout", { method: "POST" }).then(() => {
-      router.refresh();
-    });
-  };
-
-  const getInitials = (name: string) => {
-    return name
-      .split(" ")
-      .map((word) => word[0])
-      .join("")
-      .toUpperCase()
-      .slice(0, 2);
-  };
-
-  return (
-    <div className="flex items-center gap-2">
-      <div className="flex items-center gap-2.5 px-3 py-1.5 rounded-full bg-muted/50 border border-border/50">
-        <Avatar className="h-7 w-7">
-          <AvatarFallback className="bg-gradient-to-br from-primary/20 to-primary/10 text-primary font-semibold text-xs">
-            {getInitials(user.name)}
-          </AvatarFallback>
-        </Avatar>
-        <span className="text-sm font-medium text-foreground/90">{user.name}</span>
-      </div>
-      <Button
-        variant="ghost"
-        size="icon"
-        onClick={handleLogout}
-        className="h-9 w-9 rounded-full hover:bg-destructive/10 hover:text-destructive transition-all duration-200"
-        title="退出登录"
-      >
-        <LogOut className="h-4 w-4" />
-      </Button>
-    </div>
-  );
-}

+ 0 - 37
src/app/\[locale\]/dashboard/_components/user/add-user-dialog.tsx

@@ -1,37 +0,0 @@
-"use client";
-import { useState, type ComponentProps } from "react";
-import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { ListPlus } from "lucide-react";
-import { UserForm } from "./forms/user-form";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-
-type ButtonProps = ComponentProps<typeof Button>;
-
-interface AddUserDialogProps {
-  variant?: ButtonProps["variant"];
-  size?: ButtonProps["size"];
-  className?: string;
-}
-
-export function AddUserDialog({
-  variant = "default",
-  size = "default",
-  className,
-}: AddUserDialogProps) {
-  const [open, setOpen] = useState(false);
-  return (
-    <Dialog open={open} onOpenChange={setOpen}>
-      <DialogTrigger asChild>
-        <Button variant={variant} size={size} className={className}>
-          <ListPlus className="h-4 w-4" /> 新增用户
-        </Button>
-      </DialogTrigger>
-      <DialogContent>
-        <FormErrorBoundary>
-          <UserForm onSuccess={() => setOpen(false)} />
-        </FormErrorBoundary>
-      </DialogContent>
-    </Dialog>
-  );
-}

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

@@ -1,156 +0,0 @@
-"use client";
-import { useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { addKey } from "@/actions/keys";
-import { DialogFormLayout } from "@/components/form/form-layout";
-import { TextField, DateField, NumberField } from "@/components/form/form-field";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { useZodForm } from "@/lib/hooks/use-zod-form";
-import { KeyFormSchema } from "@/lib/validation/schemas";
-
-interface AddKeyFormProps {
-  userId?: number;
-  onSuccess?: (result: { generatedKey: string; name: string }) => void;
-}
-
-export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
-  const [isPending, startTransition] = useTransition();
-  const router = useRouter();
-
-  const form = useZodForm({
-    schema: KeyFormSchema,
-    defaultValues: {
-      name: "",
-      expiresAt: "",
-      canLoginWebUi: true,
-      limit5hUsd: null,
-      limitWeeklyUsd: null,
-      limitMonthlyUsd: null,
-      limitConcurrentSessions: 0,
-    },
-    onSubmit: async (data) => {
-      if (!userId) {
-        throw new Error("用户ID不存在");
-      }
-
-      try {
-        const result = await addKey({
-          userId: userId!,
-          name: data.name,
-          expiresAt: data.expiresAt || undefined,
-          canLoginWebUi: data.canLoginWebUi,
-          limit5hUsd: data.limit5hUsd,
-          limitWeeklyUsd: data.limitWeeklyUsd,
-          limitMonthlyUsd: data.limitMonthlyUsd,
-          limitConcurrentSessions: data.limitConcurrentSessions,
-        });
-
-        if (!result.ok) {
-          toast.error(result.error || "创建失败,请稍后重试");
-          return;
-        }
-
-        const payload = result.data;
-        if (!payload) {
-          toast.error("创建成功但未返回密钥");
-          return;
-        }
-
-        startTransition(() => {
-          onSuccess?.({ generatedKey: payload.generatedKey, name: payload.name });
-          router.refresh();
-        });
-      } catch (err) {
-        console.error("添加Key失败:", err);
-        // 使用toast显示具体的错误信息
-        const errorMessage = err instanceof Error ? err.message : "创建失败,请稍后重试";
-        toast.error(errorMessage);
-      }
-    },
-  });
-
-  return (
-    <DialogFormLayout
-      config={{
-        title: "新增 Key",
-        description: "为当前用户创建新的API密钥,Key值将自动生成。",
-        submitText: "确认创建",
-        loadingText: "创建中...",
-      }}
-      onSubmit={form.handleSubmit}
-      isSubmitting={isPending}
-      canSubmit={form.canSubmit && !!userId}
-      error={form.errors._form}
-    >
-      <TextField
-        label="Key名称"
-        required
-        maxLength={64}
-        autoFocus
-        placeholder="请输入Key名称"
-        {...form.getFieldProps("name")}
-      />
-
-      <DateField
-        label="过期时间"
-        placeholder="选择过期时间"
-        description="留空表示永不过期"
-        {...form.getFieldProps("expiresAt")}
-      />
-
-      <div className="flex items-start justify-between gap-4 rounded-lg border border-dashed border-border px-4 py-3">
-        <div>
-          <Label htmlFor="can-login-web-ui" className="text-sm font-medium">
-            允许登录 Web UI
-          </Label>
-          <p className="text-xs text-muted-foreground mt-1">
-            关闭后,此 Key 仅可用于 API 调用,无法登录管理后台
-          </p>
-        </div>
-        <Switch
-          id="can-login-web-ui"
-          checked={form.values.canLoginWebUi}
-          onCheckedChange={(checked) => form.setValue("canLoginWebUi", checked)}
-        />
-      </div>
-
-      <NumberField
-        label="5小时消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="5小时内最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limit5hUsd")}
-      />
-
-      <NumberField
-        label="周消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="每周最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limitWeeklyUsd")}
-      />
-
-      <NumberField
-        label="月消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="每月最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limitMonthlyUsd")}
-      />
-
-      <NumberField
-        label="并发 Session 上限"
-        placeholder="0 表示无限制"
-        description="同时运行的对话数量"
-        min={0}
-        step={1}
-        {...form.getFieldProps("limitConcurrentSessions")}
-      />
-    </DialogFormLayout>
-  );
-}

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

@@ -1,73 +0,0 @@
-"use client";
-import { useTransition } from "react";
-import {
-  DialogHeader,
-  DialogTitle,
-  DialogDescription,
-  DialogFooter,
-  DialogClose,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { useRouter } from "next/navigation";
-import { removeKey } from "@/actions/keys";
-import { toast } from "sonner";
-
-interface DeleteKeyConfirmProps {
-  keyData?: {
-    id: number;
-    name: string;
-    maskedKey: string;
-  };
-}
-
-export function DeleteKeyConfirm({
-  keyData,
-  onSuccess,
-}: DeleteKeyConfirmProps & { onSuccess?: () => void }) {
-  const router = useRouter();
-  const [isPending, startTransition] = useTransition();
-
-  const handleConfirm = () => {
-    if (!keyData) return;
-    startTransition(async () => {
-      try {
-        const res = await removeKey(keyData.id);
-        if (!res.ok) {
-          toast.error(res.error || "删除失败");
-          return;
-        }
-        onSuccess?.();
-        router.refresh();
-      } catch (error) {
-        console.error("删除Key失败:", error);
-        toast.error("删除失败,请稍后重试");
-      }
-    });
-  };
-
-  return (
-    <>
-      <DialogHeader>
-        <DialogTitle>确认删除密钥</DialogTitle>
-        <DialogDescription>
-          您确定要删除密钥 &ldquo;<strong>{keyData?.name}</strong>&rdquo; 吗?
-          <br />
-          <code className="bg-muted px-2 py-1 rounded text-xs">{keyData?.maskedKey}</code>
-          <br />
-          此操作无法撤销,删除后所有使用此密钥的应用将无法访问。
-        </DialogDescription>
-      </DialogHeader>
-
-      <DialogFooter>
-        <DialogClose asChild>
-          <Button type="button" variant="outline" disabled={isPending}>
-            取消
-          </Button>
-        </DialogClose>
-        <Button variant="destructive" onClick={handleConfirm} disabled={isPending}>
-          {isPending ? "删除中..." : "确认删除"}
-        </Button>
-      </DialogFooter>
-    </>
-  );
-}

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

@@ -1,71 +0,0 @@
-"use client";
-import { useTransition } from "react";
-import {
-  DialogHeader,
-  DialogTitle,
-  DialogDescription,
-  DialogFooter,
-  DialogClose,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { useRouter } from "next/navigation";
-import { removeUser } from "@/actions/users";
-import { toast } from "sonner";
-
-interface DeleteUserConfirmProps {
-  user?: {
-    id: number;
-    name: string;
-    keys: { id: number; name: string }[];
-  };
-}
-
-export function DeleteUserConfirm({
-  user,
-  onSuccess,
-}: DeleteUserConfirmProps & { onSuccess?: () => void }) {
-  const router = useRouter();
-  const [isPending, startTransition] = useTransition();
-
-  const handleConfirm = () => {
-    if (!user) return;
-    startTransition(async () => {
-      try {
-        const res = await removeUser(user.id);
-        if (!res.ok) {
-          toast.error(res.error || "删除失败");
-          return;
-        }
-        onSuccess?.();
-        router.refresh();
-      } catch (error) {
-        console.error("删除用户失败:", error);
-        toast.error("删除失败,请稍后重试");
-      }
-    });
-  };
-
-  return (
-    <>
-      <DialogHeader>
-        <DialogTitle>确认删除用户</DialogTitle>
-        <DialogDescription>
-          您确定要删除用户 &ldquo;<strong>{user?.name}</strong>&rdquo; 吗?
-          <br />
-          此操作将同时删除该用户的 {user?.keys.length || 0} 个密钥,且无法撤销。
-        </DialogDescription>
-      </DialogHeader>
-
-      <DialogFooter>
-        <DialogClose asChild>
-          <Button type="button" variant="outline" disabled={isPending}>
-            取消
-          </Button>
-        </DialogClose>
-        <Button variant="destructive" onClick={handleConfirm} disabled={isPending}>
-          {isPending ? "删除中..." : "确认删除"}
-        </Button>
-      </DialogFooter>
-    </>
-  );
-}

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

@@ -1,163 +0,0 @@
-"use client";
-import { useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { editKey } from "@/actions/keys";
-import { DialogFormLayout } from "@/components/form/form-layout";
-import { TextField, DateField, NumberField } from "@/components/form/form-field";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { useZodForm } from "@/lib/hooks/use-zod-form";
-import { KeyFormSchema } from "@/lib/validation/schemas";
-import { toast } from "sonner";
-
-interface EditKeyFormProps {
-  keyData?: {
-    id: number;
-    name: string;
-    expiresAt: string;
-    canLoginWebUi?: boolean;
-    limit5hUsd?: number | null;
-    limitWeeklyUsd?: number | null;
-    limitMonthlyUsd?: number | null;
-    limitConcurrentSessions?: number;
-  };
-  onSuccess?: () => void;
-}
-
-export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
-  const [isPending, startTransition] = useTransition();
-  const router = useRouter();
-
-  const formatExpiresAt = (expiresAt: string) => {
-    if (!expiresAt || expiresAt === "永不过期") return "";
-    try {
-      return new Date(expiresAt).toISOString().split("T")[0];
-    } catch {
-      return "";
-    }
-  };
-
-  const form = useZodForm({
-    schema: KeyFormSchema,
-    defaultValues: {
-      name: keyData?.name || "",
-      expiresAt: formatExpiresAt(keyData?.expiresAt || ""),
-      canLoginWebUi: keyData?.canLoginWebUi ?? true,
-      limit5hUsd: keyData?.limit5hUsd ?? null,
-      limitWeeklyUsd: keyData?.limitWeeklyUsd ?? null,
-      limitMonthlyUsd: keyData?.limitMonthlyUsd ?? null,
-      limitConcurrentSessions: keyData?.limitConcurrentSessions ?? 0,
-    },
-    onSubmit: async (data) => {
-      if (!keyData) {
-        throw new Error("密钥信息不存在");
-      }
-
-      startTransition(async () => {
-        try {
-          const res = await editKey(keyData.id, {
-            name: data.name,
-            expiresAt: data.expiresAt || undefined,
-            canLoginWebUi: data.canLoginWebUi,
-            limit5hUsd: data.limit5hUsd,
-            limitWeeklyUsd: data.limitWeeklyUsd,
-            limitMonthlyUsd: data.limitMonthlyUsd,
-            limitConcurrentSessions: data.limitConcurrentSessions,
-          });
-          if (!res.ok) {
-            toast.error(res.error || "保存失败");
-            return;
-          }
-          onSuccess?.();
-          router.refresh();
-        } catch (err) {
-          console.error("编辑Key失败:", err);
-          toast.error("保存失败,请稍后重试");
-        }
-      });
-    },
-  });
-
-  return (
-    <DialogFormLayout
-      config={{
-        title: "编辑 Key",
-        description: "修改密钥的名称、过期时间和限流配置。",
-        submitText: "保存修改",
-        loadingText: "保存中...",
-      }}
-      onSubmit={form.handleSubmit}
-      isSubmitting={isPending}
-      canSubmit={form.canSubmit}
-      error={form.errors._form}
-    >
-      <TextField
-        label="Key名称"
-        required
-        maxLength={64}
-        autoFocus
-        placeholder="请输入Key名称"
-        {...form.getFieldProps("name")}
-      />
-
-      <DateField
-        label="过期时间"
-        placeholder="选择过期时间"
-        description="留空表示永不过期"
-        {...form.getFieldProps("expiresAt")}
-      />
-
-      <div className="flex items-start justify-between gap-4 rounded-lg border border-dashed border-border px-4 py-3">
-        <div>
-          <Label htmlFor="can-login-web-ui" className="text-sm font-medium">
-            允许登录 Web UI
-          </Label>
-          <p className="text-xs text-muted-foreground mt-1">
-            关闭后,此 Key 仅可用于 API 调用,无法登录管理后台
-          </p>
-        </div>
-        <Switch
-          id="can-login-web-ui"
-          checked={form.values.canLoginWebUi}
-          onCheckedChange={(checked) => form.setValue("canLoginWebUi", checked)}
-        />
-      </div>
-
-      <NumberField
-        label="5小时消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="5小时内最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limit5hUsd")}
-      />
-
-      <NumberField
-        label="周消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="每周最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limitWeeklyUsd")}
-      />
-
-      <NumberField
-        label="月消费上限 (USD)"
-        placeholder="留空表示无限制"
-        description="每月最大消费金额"
-        min={0}
-        step={0.01}
-        {...form.getFieldProps("limitMonthlyUsd")}
-      />
-
-      <NumberField
-        label="并发 Session 上限"
-        placeholder="0 表示无限制"
-        description="同时运行的对话数量"
-        min={0}
-        step={1}
-        {...form.getFieldProps("limitConcurrentSessions")}
-      />
-    </DialogFormLayout>
-  );
-}

+ 0 - 138
src/app/\[locale\]/dashboard/_components/user/forms/user-form.tsx

@@ -1,138 +0,0 @@
-"use client";
-import { useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { addUser, editUser } from "@/actions/users";
-import { DialogFormLayout } from "@/components/form/form-layout";
-import { TextField } from "@/components/form/form-field";
-import { useZodForm } from "@/lib/hooks/use-zod-form";
-import { CreateUserSchema } from "@/lib/validation/schemas";
-import { USER_DEFAULTS } from "@/lib/constants/user.constants";
-import { toast } from "sonner";
-
-interface UserFormProps {
-  user?: {
-    id: number;
-    name: string;
-    note?: string;
-    rpm: number;
-    dailyQuota: number;
-    providerGroup?: string | null;
-  };
-  onSuccess?: () => void;
-}
-
-export function UserForm({ user, onSuccess }: UserFormProps) {
-  const [isPending, startTransition] = useTransition();
-  const router = useRouter();
-  const isEdit = Boolean(user?.id);
-
-  const form = useZodForm({
-    schema: CreateUserSchema, // Use CreateUserSchema for both, it has all fields with defaults
-    defaultValues: {
-      name: user?.name || "",
-      note: user?.note || "",
-      rpm: user?.rpm || USER_DEFAULTS.RPM,
-      dailyQuota: user?.dailyQuota || USER_DEFAULTS.DAILY_QUOTA,
-      providerGroup: user?.providerGroup || "",
-    },
-    onSubmit: async (data) => {
-      startTransition(async () => {
-        try {
-          let res;
-          if (isEdit) {
-            res = await editUser(user!.id, {
-              name: data.name,
-              note: data.note,
-              rpm: data.rpm,
-              dailyQuota: data.dailyQuota,
-              providerGroup: data.providerGroup || null,
-            });
-          } else {
-            res = await addUser({
-              name: data.name,
-              note: data.note,
-              rpm: data.rpm,
-              dailyQuota: data.dailyQuota,
-              providerGroup: data.providerGroup || null,
-            });
-          }
-
-          if (!res.ok) {
-            const msg = res.error || (isEdit ? "保存失败" : "创建失败,请稍后重试");
-            toast.error(msg);
-            return;
-          }
-
-          onSuccess?.();
-          router.refresh();
-        } catch (err) {
-          console.error(`${isEdit ? "编辑" : "添加"}用户失败:`, err);
-          toast.error(isEdit ? "保存失败,请稍后重试" : "创建失败,请稍后重试");
-        }
-      });
-    },
-  });
-
-  return (
-    <DialogFormLayout
-      config={{
-        title: isEdit ? "编辑用户" : "新增用户",
-        description: isEdit ? "修改用户的基本信息。" : "创建新用户,系统将自动为其生成默认密钥。",
-        submitText: isEdit ? "保存修改" : "确认创建",
-        loadingText: isEdit ? "保存中..." : "创建中...",
-      }}
-      onSubmit={form.handleSubmit}
-      isSubmitting={isPending}
-      canSubmit={form.canSubmit}
-      error={form.errors._form}
-    >
-      <TextField
-        label="用户名"
-        required
-        maxLength={64}
-        autoFocus
-        placeholder="请输入用户名"
-        {...form.getFieldProps("name")}
-      />
-
-      <TextField
-        label="备注"
-        maxLength={200}
-        placeholder="请输入备注(可选)"
-        description="用于描述用户的用途或备注信息"
-        {...form.getFieldProps("note")}
-      />
-
-      <TextField
-        label="供应商分组"
-        maxLength={50}
-        placeholder="例如: premium 或 premium,economy(可选)"
-        description="指定用户专属的供应商分组(支持多个,逗号分隔)。系统将只从 groupTag 匹配的供应商中选择。留空=使用所有供应商"
-        {...form.getFieldProps("providerGroup")}
-      />
-
-      <TextField
-        label="RPM限制"
-        type="number"
-        required
-        min={1}
-        max={10000}
-        placeholder="每分钟请求数限制"
-        description={`默认值: ${USER_DEFAULTS.RPM},范围: 1-10000`}
-        {...form.getFieldProps("rpm")}
-      />
-
-      <TextField
-        label="每日额度"
-        type="number"
-        required
-        min={0.01}
-        max={1000}
-        step={0.01}
-        placeholder="每日消费额度限制"
-        description={`默认值: $${USER_DEFAULTS.DAILY_QUOTA},范围: $0.01-$1000`}
-        {...form.getFieldProps("dailyQuota")}
-      />
-    </DialogFormLayout>
-  );
-}

+ 0 - 74
src/app/\[locale\]/dashboard/_components/user/key-actions.tsx

@@ -1,74 +0,0 @@
-"use client";
-import { useState } from "react";
-import { SquarePen, Trash2 } from "lucide-react";
-import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { EditKeyForm } from "./forms/edit-key-form";
-import { DeleteKeyConfirm } from "./forms/delete-key-confirm";
-import type { UserKeyDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-
-interface KeyActionsProps {
-  keyData: UserKeyDisplay;
-  currentUser?: User;
-  keyOwnerUserId: number; // 这个Key所属的用户ID
-  canDelete: boolean;
-}
-
-export function KeyActions({ keyData, currentUser, keyOwnerUserId, canDelete }: KeyActionsProps) {
-  const [openEdit, setOpenEdit] = useState(false);
-  const [openDelete, setOpenDelete] = useState(false);
-
-  // 权限检查:只有管理员或Key的拥有者才能编辑/删除
-  const canManageKey =
-    currentUser && (currentUser.role === "admin" || currentUser.id === keyOwnerUserId);
-
-  // 如果没有权限,不显示任何操作按钮
-  if (!canManageKey) {
-    return null;
-  }
-
-  return (
-    <div className="flex items-center gap-1">
-      {/* 编辑Key */}
-      <Dialog open={openEdit} onOpenChange={setOpenEdit}>
-        <DialogTrigger asChild>
-          <button
-            type="button"
-            aria-label="编辑密钥"
-            className="inline-flex items-center justify-center p-1.5 text-muted-foreground hover:text-foreground transition-colors"
-            title="编辑"
-          >
-            <SquarePen className="h-4 w-4" />
-          </button>
-        </DialogTrigger>
-        <DialogContent>
-          <FormErrorBoundary>
-            <EditKeyForm keyData={keyData} onSuccess={() => setOpenEdit(false)} />
-          </FormErrorBoundary>
-        </DialogContent>
-      </Dialog>
-
-      {/* 删除Key */}
-      {canDelete && (
-        <Dialog open={openDelete} onOpenChange={setOpenDelete}>
-          <DialogTrigger asChild>
-            <button
-              type="button"
-              aria-label="删除密钥"
-              className="inline-flex items-center justify-center p-1.5 text-muted-foreground hover:text-red-600"
-              title="删除"
-            >
-              <Trash2 className="h-4 w-4" />
-            </button>
-          </DialogTrigger>
-          <DialogContent>
-            <FormErrorBoundary>
-              <DeleteKeyConfirm keyData={keyData} onSuccess={() => setOpenDelete(false)} />
-            </FormErrorBoundary>
-          </DialogContent>
-        </Dialog>
-      )}
-    </div>
-  );
-}

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

@@ -1,144 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { getKeyLimitUsage } from "@/actions/keys";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { Loader2, AlertCircle } from "lucide-react";
-import { Progress } from "@/components/ui/progress";
-import { cn } from "@/lib/utils";
-
-interface KeyLimitUsageProps {
-  keyId: number;
-  currencyCode?: CurrencyCode;
-}
-
-interface LimitUsageData {
-  cost5h: { current: number; limit: number | null };
-  costWeekly: { current: number; limit: number | null };
-  costMonthly: { current: number; limit: number | null };
-  concurrentSessions: { current: number; limit: number };
-}
-
-export function KeyLimitUsage({ keyId, currencyCode = "USD" }: KeyLimitUsageProps) {
-  const [data, setData] = useState<LimitUsageData | null>(null);
-  const [loading, setLoading] = useState(true);
-  const [error, setError] = useState<string | null>(null);
-
-  useEffect(() => {
-    async function fetchData() {
-      setLoading(true);
-      setError(null);
-
-      try {
-        const result = await getKeyLimitUsage(keyId);
-        if (result.ok) {
-          setData(result.data);
-        } else {
-          // result.ok === false 时,result 是 { ok: false; error: string }
-          setError(result.error || "获取失败");
-        }
-      } catch {
-        setError("网络错误");
-      } finally {
-        setLoading(false);
-      }
-    }
-
-    void fetchData();
-  }, [keyId]);
-
-  if (loading) {
-    return (
-      <div className="flex items-center gap-2 text-sm text-muted-foreground">
-        <Loader2 className="h-3 w-3 animate-spin" />
-        <span>加载中...</span>
-      </div>
-    );
-  }
-
-  if (error) {
-    return (
-      <div className="flex items-center gap-2 text-sm text-destructive">
-        <AlertCircle className="h-3 w-3" />
-        <span>{error}</span>
-      </div>
-    );
-  }
-
-  if (!data) return null;
-
-  const items = [
-    {
-      label: "5小时消费",
-      current: data.cost5h.current,
-      limit: data.cost5h.limit,
-      isCost: true,
-    },
-    {
-      label: "周消费",
-      current: data.costWeekly.current,
-      limit: data.costWeekly.limit,
-      isCost: true,
-    },
-    {
-      label: "月消费",
-      current: data.costMonthly.current,
-      limit: data.costMonthly.limit,
-      isCost: true,
-    },
-    {
-      label: "并发 Session",
-      current: data.concurrentSessions.current,
-      limit: data.concurrentSessions.limit || null,
-      isCost: false,
-    },
-  ].filter((item) => item.limit !== null && item.limit > 0); // 只显示有限额的项目
-
-  if (items.length === 0) {
-    return <div className="text-xs text-muted-foreground">无限额限制</div>;
-  }
-
-  return (
-    <div className="space-y-2">
-      {items.map((item) => {
-        const percentage = item.limit ? Math.min((item.current / item.limit) * 100, 100) : 0;
-        const isNearLimit = percentage >= 80;
-        const isAtLimit = percentage >= 100;
-
-        return (
-          <div key={item.label} className="space-y-1">
-            <div className="flex items-center justify-between text-xs">
-              <span className="text-muted-foreground">{item.label}</span>
-              <span
-                className={cn(
-                  "font-mono font-medium",
-                  isAtLimit && "text-destructive",
-                  isNearLimit && !isAtLimit && "text-orange-600"
-                )}
-              >
-                {item.isCost ? (
-                  <>
-                    {formatCurrency(item.current, currencyCode)} /{" "}
-                    {formatCurrency(item.limit!, currencyCode)}
-                  </>
-                ) : (
-                  <>
-                    {item.current} / {item.limit}
-                  </>
-                )}
-              </span>
-            </div>
-            <Progress
-              value={percentage}
-              className={cn(
-                "h-1.5",
-                isAtLimit && "[&>div]:bg-destructive",
-                isNearLimit && !isAtLimit && "[&>div]:bg-orange-500"
-              )}
-            />
-          </div>
-        );
-      })}
-    </div>
-  );
-}

+ 0 - 281
src/app/\[locale\]/dashboard/_components/user/key-list-header.tsx

@@ -1,281 +0,0 @@
-"use client";
-import { useMemo, useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogFooter,
-  DialogHeader,
-  DialogTitle,
-  DialogTrigger,
-} from "@/components/ui/dialog";
-import { ListPlus, Copy, CheckCircle } from "lucide-react";
-import { AddKeyForm } from "./forms/add-key-form";
-import { UserActions } from "./user-actions";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import { useQuery } from "@tanstack/react-query";
-import { getProxyStatus } from "@/actions/proxy-status";
-import type { ProxyStatusResponse } from "@/types/proxy-status";
-
-const PROXY_STATUS_REFRESH_INTERVAL = 2000;
-
-async function fetchProxyStatus(): Promise<ProxyStatusResponse> {
-  const result = await getProxyStatus();
-  if (result.ok) {
-    if (result.data) {
-      return result.data;
-    }
-    throw new Error("获取代理状态失败");
-  }
-  throw new Error(result.error || "获取代理状态失败");
-}
-
-function formatRelativeTime(timestamp: number): string {
-  const diff = Date.now() - timestamp;
-  if (diff <= 0) {
-    return "刚刚";
-  }
-
-  const seconds = Math.floor(diff / 1000);
-  if (seconds < 5) {
-    return "刚刚";
-  }
-  if (seconds < 60) {
-    return `${seconds}s前`;
-  }
-
-  const minutes = Math.floor(seconds / 60);
-  if (minutes < 60) {
-    return `${minutes}分钟前`;
-  }
-
-  const hours = Math.floor(minutes / 60);
-  if (hours < 24) {
-    return `${hours}小时前`;
-  }
-
-  const days = Math.floor(hours / 24);
-  if (days < 7) {
-    return `${days}天前`;
-  }
-
-  return new Date(timestamp).toLocaleDateString("zh-CN");
-}
-
-function StatusSpinner() {
-  return (
-    <span
-      aria-hidden="true"
-      className="inline-block h-3 w-3 animate-spin rounded-full border border-muted-foreground/70 border-t-transparent"
-    />
-  );
-}
-
-interface KeyListHeaderProps {
-  activeUser: UserDisplay | null;
-  currentUser?: User;
-  currencyCode?: CurrencyCode;
-}
-
-export function KeyListHeader({
-  activeUser,
-  currentUser,
-  currencyCode = "USD",
-}: KeyListHeaderProps) {
-  const [openAdd, setOpenAdd] = useState(false);
-  const [keyResult, setKeyResult] = useState<{ generatedKey: string; name: string } | null>(null);
-  const [copied, setCopied] = useState(false);
-
-  const totalTodayUsage =
-    activeUser?.keys.reduce((sum, key) => sum + (key.todayUsage ?? 0), 0) ?? 0;
-
-  const proxyStatusEnabled = Boolean(activeUser);
-  const {
-    data: proxyStatus,
-    error: proxyStatusError,
-    isLoading: proxyStatusLoading,
-  } = useQuery<ProxyStatusResponse, Error>({
-    queryKey: ["proxy-status"],
-    queryFn: fetchProxyStatus,
-    refetchInterval: PROXY_STATUS_REFRESH_INTERVAL,
-    enabled: proxyStatusEnabled,
-  });
-
-  const activeUserStatus = useMemo(() => {
-    if (!proxyStatus || !activeUser) {
-      return null;
-    }
-    return proxyStatus.users.find((user) => user.userId === activeUser.id) ?? null;
-  }, [proxyStatus, activeUser]);
-
-  const proxyStatusContent = useMemo(() => {
-    if (!proxyStatusEnabled) {
-      return null;
-    }
-
-    if (proxyStatusLoading) {
-      return (
-        <div className="flex items-center gap-1 text-xs text-muted-foreground">
-          <span>代理状态加载中</span>
-          <StatusSpinner />
-        </div>
-      );
-    }
-
-    if (proxyStatusError) {
-      return <div className="text-xs text-destructive">代理状态获取失败</div>;
-    }
-
-    if (!activeUserStatus) {
-      return <div className="text-xs text-muted-foreground">暂无代理状态</div>;
-    }
-
-    const activeProviders = Array.from(
-      new Set(activeUserStatus.activeRequests.map((request) => request.providerName))
-    );
-
-    return (
-      <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
-        <div className="flex items-center gap-1">
-          <span>活跃请求</span>
-          <span className="font-medium text-foreground">{activeUserStatus.activeCount}</span>
-          {activeProviders.length > 0 && (
-            <span className="text-muted-foreground">({activeProviders.join("、")})</span>
-          )}
-        </div>
-        <div className="flex items-center gap-1">
-          <span>最近请求</span>
-          <span className="text-foreground">
-            {activeUserStatus.lastRequest
-              ? `${activeUserStatus.lastRequest.providerName} / ${activeUserStatus.lastRequest.model}`
-              : "暂无记录"}
-          </span>
-          {activeUserStatus.lastRequest && (
-            <span className="text-muted-foreground">
-              · {formatRelativeTime(activeUserStatus.lastRequest.endTime)}
-            </span>
-          )}
-        </div>
-      </div>
-    );
-  }, [proxyStatusEnabled, proxyStatusLoading, proxyStatusError, activeUserStatus]);
-
-  const handleKeyCreated = (result: { generatedKey: string; name: string }) => {
-    setOpenAdd(false); // 关闭表单dialog
-    setKeyResult(result); // 显示成功dialog
-  };
-
-  const handleCopy = async () => {
-    if (!keyResult) return;
-    try {
-      await navigator.clipboard.writeText(keyResult.generatedKey);
-      setCopied(true);
-      setTimeout(() => setCopied(false), 2000);
-    } catch (err) {
-      console.error("复制失败:", err);
-    }
-  };
-
-  const handleCloseSuccess = () => {
-    setKeyResult(null);
-    setCopied(false);
-  };
-
-  // 权限检查:管理员可以给所有人添加Key,普通用户只能给自己添加Key
-  const canAddKey =
-    currentUser && activeUser && (currentUser.role === "admin" || currentUser.id === activeUser.id);
-
-  return (
-    <>
-      <div className="mb-3 flex items-center justify-between">
-        <div>
-          <div className="flex items-center gap-2 text-base font-semibold tracking-tight">
-            <span>{activeUser ? activeUser.name : "-"}</span>
-            {activeUser && <UserActions user={activeUser} currentUser={currentUser} />}
-          </div>
-          <div className="mt-1">
-            <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
-              <div>
-                今日用量 {activeUser ? formatCurrency(totalTodayUsage, currencyCode) : "-"} /{" "}
-                {activeUser ? formatCurrency(activeUser.dailyQuota, currencyCode) : "-"}
-              </div>
-              {proxyStatusContent}
-            </div>
-          </div>
-        </div>
-        {canAddKey && (
-          <Dialog open={openAdd} onOpenChange={setOpenAdd}>
-            <DialogTrigger asChild>
-              <Button
-                variant="secondary"
-                size="sm"
-                className="hover:bg-primary hover:text-primary-foreground cursor-pointer transition-colors"
-                disabled={!activeUser}
-              >
-                <ListPlus className="h-3.5 w-3.5" /> 新增 Key
-              </Button>
-            </DialogTrigger>
-            <DialogContent>
-              <FormErrorBoundary>
-                <AddKeyForm userId={activeUser?.id} onSuccess={handleKeyCreated} />
-              </FormErrorBoundary>
-            </DialogContent>
-          </Dialog>
-        )}
-      </div>
-
-      {/* Key创建成功弹窗 */}
-      <Dialog open={!!keyResult} onOpenChange={(open) => !open && handleCloseSuccess()}>
-        <DialogContent className="max-w-lg">
-          <DialogHeader>
-            <DialogTitle className="flex items-center gap-2">
-              <CheckCircle className="h-5 w-5 text-green-600" />
-              Key 创建成功
-            </DialogTitle>
-            <DialogDescription>
-              你的 API Key 已成功创建。请务必复制并妥善保存,此密钥仅显示一次。
-            </DialogDescription>
-          </DialogHeader>
-
-          {keyResult && (
-            <div className="space-y-4">
-              <div>
-                <label className="text-sm font-medium mb-2 block">API Key</label>
-                <div className="relative">
-                  <div className="p-3 bg-muted/50 rounded-md font-mono text-sm break-all border-2 border-dashed border-orange-300 pr-12">
-                    {keyResult.generatedKey}
-                  </div>
-                  <Button
-                    variant="ghost"
-                    size="sm"
-                    onClick={handleCopy}
-                    className="absolute top-1/2 right-2 -translate-y-1/2 h-7 w-7 p-0"
-                  >
-                    {copied ? (
-                      <CheckCircle className="h-3 w-3 text-orange-600" />
-                    ) : (
-                      <Copy className="h-3 w-3" />
-                    )}
-                  </Button>
-                </div>
-                <p className="text-xs text-muted-foreground mt-2">
-                  请在关闭前复制并保存,关闭后将无法再次查看此密钥
-                </p>
-              </div>
-            </div>
-          )}
-
-          <DialogFooter>
-            <Button onClick={handleCloseSuccess} variant="secondary">
-              关闭
-            </Button>
-          </DialogFooter>
-        </DialogContent>
-      </Dialog>
-    </>
-  );
-}

+ 0 - 227
src/app/\[locale\]/dashboard/_components/user/key-list.tsx

@@ -1,227 +0,0 @@
-"use client";
-import { useState } from "react";
-import { DataTable, TableColumnTypes } from "@/components/ui/data-table";
-import { Button } from "@/components/ui/button";
-import { Copy, Check, ExternalLink, ChevronDown, ChevronRight } from "lucide-react";
-import { KeyActions } from "./key-actions";
-import { KeyLimitUsage } from "./key-limit-usage";
-import type { UserKeyDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { RelativeTime } from "@/components/ui/relative-time";
-import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
-import Link from "next/link";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
-import {
-  Table,
-  TableBody,
-  TableCell,
-  TableHead,
-  TableHeader,
-  TableRow,
-} from "@/components/ui/table";
-
-interface KeyListProps {
-  keys: UserKeyDisplay[];
-  currentUser?: User;
-  keyOwnerUserId: number; // 这些Key所属的用户ID
-  currencyCode?: CurrencyCode;
-}
-
-export function KeyList({ keys, currentUser, keyOwnerUserId, currencyCode = "USD" }: KeyListProps) {
-  const [copiedKeyId, setCopiedKeyId] = useState<number | null>(null);
-  const [expandedKeys, setExpandedKeys] = useState<Set<number>>(new Set());
-  const canDeleteKeys = keys.length > 1;
-
-  const toggleExpanded = (keyId: number) => {
-    setExpandedKeys((prev) => {
-      const newSet = new Set(prev);
-      if (newSet.has(keyId)) {
-        newSet.delete(keyId);
-      } else {
-        newSet.add(keyId);
-      }
-      return newSet;
-    });
-  };
-
-  const handleCopyKey = async (key: UserKeyDisplay) => {
-    if (!key.fullKey || !key.canCopy) return;
-
-    try {
-      await navigator.clipboard.writeText(key.fullKey);
-      setCopiedKeyId(key.id);
-      setTimeout(() => setCopiedKeyId(null), 2000);
-    } catch (err) {
-      console.error("复制失败:", err);
-    }
-  };
-
-  const columns = [
-    TableColumnTypes.text<UserKeyDisplay>("name", "名称", {
-      render: (value, record) => {
-        // 检查是否有限额配置
-        const hasLimitConfig =
-          (record.limit5hUsd && record.limit5hUsd > 0) ||
-          (record.limitWeeklyUsd && record.limitWeeklyUsd > 0) ||
-          (record.limitMonthlyUsd && record.limitMonthlyUsd > 0) ||
-          (record.limitConcurrentSessions && record.limitConcurrentSessions > 0);
-
-        const hasModelStats = record.modelStats.length > 0;
-        const showDetails = hasModelStats || hasLimitConfig;
-
-        return (
-          <div className="space-y-1">
-            <div className="truncate font-medium">{value}</div>
-            {showDetails && (
-              <Collapsible open={expandedKeys.has(record.id)}>
-                <CollapsibleTrigger asChild>
-                  <button
-                    onClick={() => toggleExpanded(record.id)}
-                    className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
-                  >
-                    {expandedKeys.has(record.id) ? (
-                      <ChevronDown className="h-3 w-3" />
-                    ) : (
-                      <ChevronRight className="h-3 w-3" />
-                    )}
-                    详细信息
-                  </button>
-                </CollapsibleTrigger>
-                <CollapsibleContent className="mt-2 space-y-3">
-                  {/* 模型统计 */}
-                  {hasModelStats && (
-                    <div>
-                      <div className="text-xs font-medium text-muted-foreground mb-1.5">
-                        模型统计 ({record.modelStats.length})
-                      </div>
-                      <div className="border rounded-md">
-                        <Table>
-                          <TableHeader>
-                            <TableRow>
-                              <TableHead className="text-xs py-1.5">模型</TableHead>
-                              <TableHead className="text-xs py-1.5 text-right">调用次数</TableHead>
-                              <TableHead className="text-xs py-1.5 text-right">消耗</TableHead>
-                            </TableRow>
-                          </TableHeader>
-                          <TableBody>
-                            {record.modelStats.map((stat) => (
-                              <TableRow key={stat.model}>
-                                <TableCell className="text-xs py-1.5 font-mono">
-                                  {stat.model}
-                                </TableCell>
-                                <TableCell className="text-xs py-1.5 text-right">
-                                  {stat.callCount}
-                                </TableCell>
-                                <TableCell className="text-xs py-1.5 text-right font-mono">
-                                  {formatCurrency(stat.totalCost, currencyCode)}
-                                </TableCell>
-                              </TableRow>
-                            ))}
-                          </TableBody>
-                        </Table>
-                      </div>
-                    </div>
-                  )}
-
-                  {/* 限额使用情况 */}
-                  {hasLimitConfig && (
-                    <div>
-                      <div className="text-xs font-medium text-muted-foreground mb-1.5">
-                        限额使用情况
-                      </div>
-                      <div className="border rounded-md p-3 bg-muted/30">
-                        <KeyLimitUsage keyId={record.id} currencyCode={currencyCode} />
-                      </div>
-                    </div>
-                  )}
-                </CollapsibleContent>
-              </Collapsible>
-            )}
-          </div>
-        );
-      },
-    }),
-    TableColumnTypes.text<UserKeyDisplay>("maskedKey", "Key", {
-      render: (_, record: UserKeyDisplay) => (
-        <div className="group inline-flex items-center gap-1">
-          <div className="font-mono truncate">{record.maskedKey || "-"}</div>
-          {record.canCopy && record.fullKey && (
-            <Button
-              variant="ghost"
-              size="sm"
-              onClick={() => handleCopyKey(record)}
-              className="h-5 w-5 p-0 hover:bg-muted opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
-              title="复制完整密钥"
-            >
-              {copiedKeyId === record.id ? (
-                <Check className="h-3 w-3 text-green-600" />
-              ) : (
-                <Copy className="h-3 w-3" />
-              )}
-            </Button>
-          )}
-        </div>
-      ),
-    }),
-    TableColumnTypes.text<UserKeyDisplay>("todayCallCount", "今日调用", {
-      render: (value) => (
-        <div className="text-sm">{typeof value === "number" ? value.toLocaleString() : 0} 次</div>
-      ),
-    }),
-    TableColumnTypes.number<UserKeyDisplay>("todayUsage", "今日消耗", {
-      render: (value) => {
-        const amount = typeof value === "number" ? value : 0;
-        return formatCurrency(amount, currencyCode);
-      },
-    }),
-    TableColumnTypes.text<UserKeyDisplay>("lastUsedAt", "最后使用", {
-      render: (_, record: UserKeyDisplay) => (
-        <div className="space-y-0.5">
-          {record.lastUsedAt ? (
-            <>
-              <div className="text-sm">
-                <RelativeTime date={record.lastUsedAt} />
-              </div>
-              {record.lastProviderName && (
-                <div className="text-xs text-muted-foreground">
-                  供应商: {record.lastProviderName}
-                </div>
-              )}
-            </>
-          ) : (
-            <div className="text-sm text-muted-foreground">未使用</div>
-          )}
-        </div>
-      ),
-    }),
-    TableColumnTypes.actions<UserKeyDisplay>("操作", (value, record) => (
-      <div className="flex items-center gap-1">
-        <Link href={`/dashboard/logs?keyId=${record.id}`}>
-          <Button variant="ghost" size="sm" className="h-7 text-xs" title="查看详细日志">
-            <ExternalLink className="h-3.5 w-3.5 mr-1" />
-            日志
-          </Button>
-        </Link>
-        <KeyActions
-          keyData={record}
-          currentUser={currentUser}
-          keyOwnerUserId={keyOwnerUserId}
-          canDelete={canDeleteKeys}
-        />
-      </div>
-    )),
-  ];
-
-  return (
-    <DataTable
-      columns={columns}
-      data={keys}
-      emptyState={{
-        title: "暂无 Key",
-        description: '可点击右上角 "新增 Key" 按钮添加密钥',
-      }}
-      maxHeight="600px"
-      stickyHeader
-    />
-  );
-}

+ 0 - 68
src/app/\[locale\]/dashboard/_components/user/user-actions.tsx

@@ -1,68 +0,0 @@
-"use client";
-import { useState } from "react";
-import { SquarePen, Trash } from "lucide-react";
-import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { UserForm } from "./forms/user-form";
-import { DeleteUserConfirm } from "./forms/delete-user-confirm";
-import type { UserDisplay, User } from "@/types/user";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-
-interface UserActionsProps {
-  user: UserDisplay;
-  currentUser?: User;
-}
-
-export function UserActions({ user, currentUser }: UserActionsProps) {
-  const [openEdit, setOpenEdit] = useState(false);
-  const [openDelete, setOpenDelete] = useState(false);
-
-  // 权限检查:只有管理员才能编辑用户信息
-  const canEditUser = currentUser?.role === "admin";
-
-  // 如果没有权限,不显示任何按钮
-  if (!canEditUser) {
-    return null;
-  }
-
-  return (
-    <div className="flex items-center gap-1">
-      {/* 编辑用户 */}
-      <Dialog open={openEdit} onOpenChange={setOpenEdit}>
-        <DialogTrigger asChild>
-          <button
-            type="button"
-            aria-label="编辑用户"
-            className="inline-flex items-center justify-center p-1 text-muted-foreground hover:text-foreground transition-colors"
-            title="编辑用户"
-          >
-            <SquarePen className="h-3.5 w-3.5" />
-          </button>
-        </DialogTrigger>
-        <DialogContent>
-          <FormErrorBoundary>
-            <UserForm user={user} onSuccess={() => setOpenEdit(false)} />
-          </FormErrorBoundary>
-        </DialogContent>
-      </Dialog>
-
-      {/* 删除用户 */}
-      <Dialog open={openDelete} onOpenChange={setOpenDelete}>
-        <DialogTrigger asChild>
-          <button
-            type="button"
-            aria-label="删除用户"
-            className="inline-flex items-center justify-center p-1 text-muted-foreground hover:text-red-600 transition-colors"
-            title="删除用户"
-          >
-            <Trash className="h-3.5 w-3.5" />
-          </button>
-        </DialogTrigger>
-        <DialogContent>
-          <FormErrorBoundary>
-            <DeleteUserConfirm user={user} onSuccess={() => setOpenDelete(false)} />
-          </FormErrorBoundary>
-        </DialogContent>
-      </Dialog>
-    </div>
-  );
-}

+ 0 - 84
src/app/\[locale\]/dashboard/_components/user/user-key-manager.tsx

@@ -1,84 +0,0 @@
-"use client";
-import { useState } from "react";
-import { UserList } from "./user-list";
-import { KeyListHeader } from "./key-list-header";
-import { KeyList } from "./key-list";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import type { CurrencyCode } from "@/lib/utils/currency";
-
-interface UserKeyManagerProps {
-  users: UserDisplay[];
-  currentUser?: User;
-  currencyCode?: CurrencyCode;
-}
-
-export function UserKeyManager({ users, currentUser, currencyCode = "USD" }: UserKeyManagerProps) {
-  // 普通用户默认选择自己,管理员选择第一个用户
-  const getInitialUser = () => {
-    if (currentUser?.role === "user") {
-      // 普通用户只能看到自己
-      return users.find((u) => u.id === currentUser.id) || users[0];
-    }
-    // 管理员看到第一个用户
-    return users[0];
-  };
-
-  const [activeUserId, setActiveUserId] = useState<number | null>(getInitialUser()?.id ?? null);
-  const activeUser = users.find((u) => u.id === activeUserId) ?? getInitialUser();
-
-  // 普通用户只显示Key列表,不显示用户列表
-  if (currentUser?.role === "user") {
-    return (
-      <div className="space-y-3">
-        <div className="bg-card text-card-foreground border border-border rounded-xl p-4">
-          <KeyListHeader
-            activeUser={activeUser}
-            currentUser={currentUser}
-            currencyCode={currencyCode}
-          />
-          <KeyList
-            keys={activeUser?.keys || []}
-            currentUser={currentUser}
-            keyOwnerUserId={activeUser?.id || 0}
-            currencyCode={currencyCode}
-          />
-        </div>
-      </div>
-    );
-  }
-
-  // 管理员显示完整布局(用户列表 + Key列表)
-  return (
-    <div className="space-y-3">
-      {/* 主从布局 */}
-      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
-        {/* 左侧用户列表 */}
-        <UserList
-          users={users}
-          activeUserId={activeUser?.id}
-          onUserSelect={setActiveUserId}
-          currentUser={currentUser}
-        />
-
-        {/* 右侧:当前用户的 Key 列表 */}
-        <div className="md:col-span-2 bg-card text-card-foreground border border-border rounded-xl p-4">
-          <KeyListHeader
-            activeUser={activeUser}
-            currentUser={currentUser}
-            currencyCode={currencyCode}
-          />
-          <KeyList
-            keys={activeUser?.keys || []}
-            currentUser={currentUser}
-            keyOwnerUserId={activeUser?.id || 0}
-            currencyCode={currencyCode}
-          />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-// 导出新的统一类型
-export type { UserDisplay, UserKeyDisplay } from "@/types/user";

+ 0 - 61
src/app/\[locale\]/dashboard/_components/user/user-list.tsx

@@ -1,61 +0,0 @@
-"use client";
-import type { UserDisplay } from "@/types/user";
-import type { User } from "@/types/user";
-import { ListContainer, ListItem, ListItemData } from "@/components/ui/list";
-import { AddUserDialog } from "./add-user-dialog";
-
-interface UserListProps {
-  users: UserDisplay[];
-  activeUserId: number | null;
-  onUserSelect: (userId: number) => void;
-  currentUser?: User;
-}
-
-export function UserList({ users, activeUserId, onUserSelect, currentUser }: UserListProps) {
-  // 转换数据格式
-  const listItems: ListItemData[] = users.map((user) => ({
-    id: user.id,
-    title: user.name,
-    subtitle: user.note,
-    badge: {
-      text: `${user.keys.length} 个 Key`,
-      variant: "outline" as const,
-    },
-    metadata: [
-      {
-        label: "活跃密钥",
-        value: user.keys.filter((k) => k.status === "enabled").length.toString(),
-      },
-      {
-        label: "总密钥",
-        value: user.keys.length.toString(),
-      },
-    ],
-  }));
-
-  return (
-    <div className="space-y-3">
-      <ListContainer
-        emptyState={{
-          title: "暂无用户",
-          description: "点击下方按钮创建第一个用户",
-        }}
-      >
-        <div className="space-y-2">
-          {listItems.map((item) => (
-            <ListItem
-              key={item.id}
-              data={item}
-              isActive={item.id === activeUserId}
-              onClick={() => onUserSelect(item.id as number)}
-              compact
-            />
-          ))}
-        </div>
-      </ListContainer>
-
-      {/* 新增用户按钮:列表下方、与列表同宽,中性配色 - 仅管理员可见 */}
-      {currentUser?.role === "admin" && <AddUserDialog variant="secondary" className="w-full" />}
-    </div>
-  );
-}

+ 0 - 46
src/app/\[locale\]/settings/_components/settings-nav.tsx

@@ -1,46 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-import { cn } from "@/lib/utils";
-import type { SettingsNavItem } from "../_lib/nav-items";
-
-interface SettingsNavProps {
-  items: SettingsNavItem[];
-}
-
-export function SettingsNav({ items }: SettingsNavProps) {
-  const pathname = usePathname();
-
-  if (items.length === 0) {
-    return null;
-  }
-
-  const getIsActive = (href: string) => {
-    return pathname === href || pathname.startsWith(`${href}/`);
-  };
-
-  return (
-    <nav className="rounded-xl border border-border/80 bg-card/70 p-1 backdrop-blur supports-[backdrop-filter]:bg-card/50">
-      <ul className="flex flex-col gap-1">
-        {items.map((item) => {
-          const isActive = getIsActive(item.href);
-          return (
-            <li key={item.href}>
-              <Link
-                href={item.href}
-                className={cn(
-                  "flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-all hover:text-foreground",
-                  isActive && "bg-primary/5 text-foreground shadow-[0_1px_0_rgba(0,0,0,0.03)]"
-                )}
-              >
-                {item.label}
-              </Link>
-            </li>
-          );
-        })}
-      </ul>
-    </nav>
-  );
-}

+ 0 - 13
src/app/\[locale\]/settings/_components/settings-page-header.tsx

@@ -1,13 +0,0 @@
-interface SettingsPageHeaderProps {
-  title: string;
-  description?: string;
-}
-
-export function SettingsPageHeader({ title, description }: SettingsPageHeaderProps) {
-  return (
-    <div className="space-y-1">
-      <h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
-      {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
-    </div>
-  );
-}

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

@@ -1,125 +0,0 @@
-"use client";
-
-import type { ClientVersionStats } from "@/lib/client-version-checker";
-import { getClientTypeDisplayName } from "@/lib/ua-parser";
-import { Badge } from "@/components/ui/badge";
-import { Code2, Terminal, HelpCircle, Package, Check, AlertTriangle } from "lucide-react";
-import {
-  Table,
-  TableBody,
-  TableCell,
-  TableHead,
-  TableHeader,
-  TableRow,
-} from "@/components/ui/table";
-import { formatDistanceToNow } from "date-fns";
-import { zhCN } from "date-fns/locale";
-
-interface ClientVersionStatsTableProps {
-  data: ClientVersionStats[];
-}
-
-/**
- * 获取客户端类型对应的图标组件
- */
-function getClientTypeIcon(clientType: string): React.ComponentType<{ className?: string }> {
-  const icons: Record<string, React.ComponentType<{ className?: string }>> = {
-    "claude-vscode": Code2,
-    "claude-cli": Terminal,
-    "claude-cli-unknown": HelpCircle,
-    "anthropic-sdk-typescript": Package,
-  };
-  return icons[clientType] || HelpCircle;
-}
-
-export function ClientVersionStatsTable({ data }: ClientVersionStatsTableProps) {
-  return (
-    <div className="space-y-8">
-      {data.map((clientStats) => {
-        const displayName = getClientTypeDisplayName(clientStats.clientType);
-        const IconComponent = getClientTypeIcon(clientStats.clientType);
-
-        return (
-          <div key={clientStats.clientType} className="space-y-3">
-            {/* 客户端类型标题 */}
-            <div className="flex items-center justify-between">
-              <div>
-                <h3 className="text-lg font-semibold flex items-center gap-2">
-                  <IconComponent className="h-5 w-5 text-blue-600" />
-                  {displayName}
-                </h3>
-                <p className="text-sm text-muted-foreground">
-                  内部类型:<code className="text-xs">{clientStats.clientType}</code>
-                  {" · "}当前 GA 版本:
-                  <Badge variant="outline" className="ml-2">
-                    {clientStats.gaVersion || "无(暂无用户使用该版本)"}
-                  </Badge>
-                </p>
-              </div>
-              <Badge variant="secondary">{clientStats.totalUsers} 位用户</Badge>
-            </div>
-
-            {/* 用户版本列表 */}
-            <div className="rounded-md border">
-              <Table>
-                <TableHeader>
-                  <TableRow>
-                    <TableHead>用户</TableHead>
-                    <TableHead>当前版本</TableHead>
-                    <TableHead>最后活跃时间</TableHead>
-                    <TableHead>状态</TableHead>
-                  </TableRow>
-                </TableHeader>
-                <TableBody>
-                  {clientStats.users.length === 0 ? (
-                    <TableRow>
-                      <TableCell colSpan={4} className="text-center text-muted-foreground">
-                        暂无用户数据
-                      </TableCell>
-                    </TableRow>
-                  ) : (
-                    clientStats.users.map((user) => (
-                      <TableRow key={`${user.userId}-${user.version}`}>
-                        <TableCell className="font-medium">{user.username}</TableCell>
-                        <TableCell>
-                          <code className="rounded bg-muted px-2 py-1 text-sm">{user.version}</code>
-                        </TableCell>
-                        <TableCell className="text-sm text-muted-foreground">
-                          {formatDistanceToNow(new Date(user.lastSeen), {
-                            addSuffix: true,
-                            locale: zhCN,
-                          })}
-                        </TableCell>
-                        <TableCell>
-                          {user.isLatest ? (
-                            <Badge
-                              variant="default"
-                              className="bg-green-500 hover:bg-green-600 gap-1"
-                            >
-                              <Check className="h-3 w-3" />
-                              最新
-                            </Badge>
-                          ) : user.needsUpgrade ? (
-                            <Badge variant="destructive" className="gap-1">
-                              <AlertTriangle className="h-3 w-3" />
-                              需升级
-                            </Badge>
-                          ) : (
-                            <Badge variant="outline" className="gap-1">
-                              <HelpCircle className="h-3 w-3" />
-                              未知
-                            </Badge>
-                          )}
-                        </TableCell>
-                      </TableRow>
-                    ))
-                  )}
-                </TableBody>
-              </Table>
-            </div>
-          </div>
-        );
-      })}
-    </div>
-  );
-}

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

@@ -1,80 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { AlertCircle } from "lucide-react";
-import { saveSystemSettings } from "@/actions/system-config";
-import { toast } from "sonner";
-
-interface ClientVersionToggleProps {
-  enabled: boolean;
-}
-
-export function ClientVersionToggle({ enabled }: ClientVersionToggleProps) {
-  const [isEnabled, setIsEnabled] = useState(enabled);
-  const [isPending, startTransition] = useTransition();
-
-  async function handleToggle(checked: boolean) {
-    startTransition(async () => {
-      const result = await saveSystemSettings({
-        enableClientVersionCheck: checked,
-      });
-
-      if (result.ok) {
-        setIsEnabled(checked);
-        toast.success(checked ? "已启用客户端版本检查" : "已关闭客户端版本检查");
-      } else {
-        toast.error(result.error || "更新失败");
-      }
-    });
-  }
-
-  return (
-    <div className="space-y-4">
-      {/* 开关 */}
-      <div className="flex items-center justify-between">
-        <div className="space-y-1">
-          <Label htmlFor="enable-version-check">启用升级提醒</Label>
-          <p className="text-sm text-muted-foreground">启用后,系统将拦截使用旧版本客户端的请求</p>
-        </div>
-        <Switch
-          id="enable-version-check"
-          checked={isEnabled}
-          onCheckedChange={handleToggle}
-          disabled={isPending}
-        />
-      </div>
-
-      {/* 详细说明 */}
-      <Alert variant={isEnabled ? "destructive" : "default"}>
-        <AlertCircle className="h-4 w-4" />
-        <AlertTitle>功能说明</AlertTitle>
-        <AlertDescription className="space-y-3">
-          <div>
-            <strong>启用后会发生什么:</strong>
-          </div>
-          <ul className="list-inside list-disc space-y-1">
-            <li>系统会自动检测每种客户端的最新稳定版本(GA 版本)</li>
-            <li>
-              <strong>判定规则:</strong>当某个版本被 1 个以上用户使用时,视为 GA 版本
-            </li>
-            <li>
-              <strong>活跃窗口:</strong>仅统计过去 7 天内有请求的用户
-            </li>
-            <li className={isEnabled ? "text-destructive font-semibold" : ""}>
-              使用旧版本的用户将收到 HTTP 400 错误,无法继续使用服务
-            </li>
-            <li>错误提示中包含当前版本和需要升级的版本号</li>
-          </ul>
-
-          <div className="mt-3 pt-3 border-t">
-            <strong>推荐做法:</strong>
-            <span className="ml-2">先观察下方的版本分布,确认新版本稳定后再启用。</span>
-          </div>
-        </AlertDescription>
-      </Alert>
-    </div>
-  );
-}

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

@@ -1,179 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Loader2 } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { toast } from "sonner";
-import type { SystemSettings } from "@/types/system-config";
-
-/**
- * 自动清理配置表单 Schema
- */
-const autoCleanupSchema = z.object({
-  enableAutoCleanup: z.boolean(),
-  cleanupRetentionDays: z.number().int().min(1).max(365),
-  cleanupSchedule: z.string().min(1),
-  cleanupBatchSize: z.number().int().min(1000).max(100000),
-});
-
-type AutoCleanupFormData = z.infer<typeof autoCleanupSchema>;
-
-interface AutoCleanupFormProps {
-  settings: SystemSettings;
-  onSuccess?: () => void;
-}
-
-export function AutoCleanupForm({ settings, onSuccess }: AutoCleanupFormProps) {
-  const [isSubmitting, setIsSubmitting] = useState(false);
-
-  const {
-    register,
-    handleSubmit,
-    watch,
-    setValue,
-    formState: { errors },
-  } = useForm<AutoCleanupFormData>({
-    resolver: zodResolver(autoCleanupSchema),
-    defaultValues: {
-      enableAutoCleanup: settings.enableAutoCleanup ?? false,
-      cleanupRetentionDays: settings.cleanupRetentionDays ?? 30,
-      cleanupSchedule: settings.cleanupSchedule ?? "0 2 * * *",
-      cleanupBatchSize: settings.cleanupBatchSize ?? 10000,
-    },
-  });
-
-  const enableAutoCleanup = watch("enableAutoCleanup");
-
-  const onSubmit = async (data: AutoCleanupFormData) => {
-    setIsSubmitting(true);
-
-    try {
-      const response = await fetch("/api/admin/system-config", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        credentials: "include",
-        body: JSON.stringify({
-          siteTitle: settings.siteTitle,
-          allowGlobalUsageView: settings.allowGlobalUsageView,
-          ...data,
-        }),
-      });
-
-      if (!response.ok) {
-        const error = await response.json();
-        throw new Error(error.error || "保存失败");
-      }
-
-      toast.success("自动清理配置已保存");
-      onSuccess?.();
-    } catch (error) {
-      console.error("Save error:", error);
-      toast.error(error instanceof Error ? error.message : "保存配置失败");
-    } finally {
-      setIsSubmitting(false);
-    }
-  };
-
-  return (
-    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-      {/* 启用开关 */}
-      <div className="flex items-center justify-between">
-        <div className="space-y-0.5">
-          <Label htmlFor="enableAutoCleanup">启用自动清理</Label>
-          <p className="text-sm text-muted-foreground">定时自动清理历史日志数据</p>
-        </div>
-        <Switch
-          id="enableAutoCleanup"
-          checked={enableAutoCleanup}
-          onCheckedChange={(checked) => setValue("enableAutoCleanup", checked)}
-        />
-      </div>
-
-      {/* 仅在启用时显示配置项 */}
-      {enableAutoCleanup && (
-        <>
-          {/* 保留天数 */}
-          <div className="space-y-2">
-            <Label htmlFor="cleanupRetentionDays">
-              保留天数 <span className="text-destructive">*</span>
-            </Label>
-            <Input
-              id="cleanupRetentionDays"
-              type="number"
-              min={1}
-              max={365}
-              {...register("cleanupRetentionDays", { valueAsNumber: true })}
-              placeholder="30"
-            />
-            {errors.cleanupRetentionDays && (
-              <p className="text-sm text-destructive">{errors.cleanupRetentionDays.message}</p>
-            )}
-            <p className="text-xs text-muted-foreground">
-              超过此天数的日志将被自动清理(范围:1-365 天)
-            </p>
-          </div>
-
-          {/* Cron 表达式 */}
-          <div className="space-y-2">
-            <Label htmlFor="cleanupSchedule">
-              执行时间 (Cron) <span className="text-destructive">*</span>
-            </Label>
-            <Input
-              id="cleanupSchedule"
-              type="text"
-              {...register("cleanupSchedule")}
-              placeholder="0 2 * * *"
-            />
-            {errors.cleanupSchedule && (
-              <p className="text-sm text-destructive">{errors.cleanupSchedule.message}</p>
-            )}
-            <p className="text-xs text-muted-foreground">
-              Cron 表达式,默认:0 2 * * *(每天凌晨 2 点)
-              <br />
-              示例:0 3 * * 0(每周日凌晨 3 点)
-            </p>
-          </div>
-
-          {/* 批量大小 */}
-          <div className="space-y-2">
-            <Label htmlFor="cleanupBatchSize">
-              批量大小 <span className="text-destructive">*</span>
-            </Label>
-            <Input
-              id="cleanupBatchSize"
-              type="number"
-              min={1000}
-              max={100000}
-              {...register("cleanupBatchSize", { valueAsNumber: true })}
-              placeholder="10000"
-            />
-            {errors.cleanupBatchSize && (
-              <p className="text-sm text-destructive">{errors.cleanupBatchSize.message}</p>
-            )}
-            <p className="text-xs text-muted-foreground">
-              每批删除的记录数(范围:1000-100000,推荐 10000)
-            </p>
-          </div>
-        </>
-      )}
-
-      {/* 提交按钮 */}
-      <Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
-        {isSubmitting ? (
-          <>
-            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-            保存中...
-          </>
-        ) : (
-          "保存配置"
-        )}
-      </Button>
-    </form>
-  );
-}

+ 0 - 138
src/app/\[locale\]/settings/config/_components/system-settings-form.tsx

@@ -1,138 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { Button } from "@/components/ui/button";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { saveSystemSettings } from "@/actions/system-config";
-import { toast } from "sonner";
-import { CURRENCY_CONFIG } from "@/lib/utils";
-import type { SystemSettings } from "@/types/system-config";
-import type { CurrencyCode } from "@/lib/utils";
-
-interface SystemSettingsFormProps {
-  initialSettings: Pick<SystemSettings, "siteTitle" | "allowGlobalUsageView" | "currencyDisplay">;
-}
-
-export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) {
-  const [siteTitle, setSiteTitle] = useState(initialSettings.siteTitle);
-  const [allowGlobalUsageView, setAllowGlobalUsageView] = useState(
-    initialSettings.allowGlobalUsageView
-  );
-  const [currencyDisplay, setCurrencyDisplay] = useState<CurrencyCode>(
-    initialSettings.currencyDisplay
-  );
-  const [isPending, startTransition] = useTransition();
-
-  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    if (!siteTitle.trim()) {
-      toast.error("站点标题不能为空");
-      return;
-    }
-
-    startTransition(async () => {
-      const result = await saveSystemSettings({
-        siteTitle,
-        allowGlobalUsageView,
-        currencyDisplay,
-      });
-
-      if (!result.ok) {
-        toast.error(result.error || "保存失败");
-        return;
-      }
-
-      if (result.data) {
-        setSiteTitle(result.data.siteTitle);
-        setAllowGlobalUsageView(result.data.allowGlobalUsageView);
-        setCurrencyDisplay(result.data.currencyDisplay);
-      }
-
-      toast.success("系统设置已更新,页面将刷新以应用货币显示变更");
-      // 刷新页面以应用货币显示变更
-      setTimeout(() => {
-        window.location.reload();
-      }, 1000);
-    });
-  };
-
-  return (
-    <form onSubmit={handleSubmit} className="space-y-6">
-      <div className="space-y-2">
-        <Label htmlFor="site-title">站点标题</Label>
-        <Input
-          id="site-title"
-          value={siteTitle}
-          onChange={(event) => setSiteTitle(event.target.value)}
-          placeholder="例如:Claude Code Hub"
-          disabled={isPending}
-          maxLength={128}
-          required
-        />
-        <p className="text-xs text-muted-foreground">
-          用于设置浏览器标签页标题以及系统默认显示名称。
-        </p>
-      </div>
-
-      <div className="space-y-2">
-        <Label htmlFor="currency-display">货币显示单位</Label>
-        <Select
-          value={currencyDisplay}
-          onValueChange={(value) => setCurrencyDisplay(value as CurrencyCode)}
-          disabled={isPending}
-        >
-          <SelectTrigger id="currency-display">
-            <SelectValue placeholder="选择货币单位" />
-          </SelectTrigger>
-          <SelectContent>
-            {(Object.keys(CURRENCY_CONFIG) as CurrencyCode[]).map((code) => {
-              const config = CURRENCY_CONFIG[code];
-              return (
-                <SelectItem key={code} value={code}>
-                  {config.symbol} {config.name} ({code})
-                </SelectItem>
-              );
-            })}
-          </SelectContent>
-        </Select>
-        <p className="text-xs text-muted-foreground">
-          修改后,系统所有页面和 API
-          接口的金额显示将使用对应的货币符号(仅修改符号,不进行汇率转换)。
-        </p>
-      </div>
-
-      <div className="flex items-start justify-between gap-4 rounded-lg border border-dashed border-border px-4 py-3">
-        <div>
-          <Label htmlFor="allow-global-usage" className="text-sm font-medium">
-            允许查看全站使用量
-          </Label>
-          <p className="text-xs text-muted-foreground mt-1">
-            关闭后,普通用户在仪表盘仅能查看自己密钥的使用统计。
-          </p>
-        </div>
-        <Switch
-          id="allow-global-usage"
-          checked={allowGlobalUsageView}
-          onCheckedChange={(checked) => setAllowGlobalUsageView(checked)}
-          disabled={isPending}
-        />
-      </div>
-
-      <div className="flex justify-end">
-        <Button type="submit" disabled={isPending}>
-          {isPending ? "保存中..." : "保存设置"}
-        </Button>
-      </div>
-    </form>
-  );
-}

+ 0 - 68
src/app/\[locale\]/settings/data/_components/database-export.tsx

@@ -1,68 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Download } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { toast } from "sonner";
-
-export function DatabaseExport() {
-  const [isExporting, setIsExporting] = useState(false);
-
-  const handleExport = async () => {
-    setIsExporting(true);
-
-    try {
-      // 调用导出 API(自动携带 cookie)
-      const response = await fetch('/api/admin/database/export', {
-        method: 'GET',
-        credentials: 'include',
-      });
-
-      if (!response.ok) {
-        const error = await response.json();
-        throw new Error(error.error || '导出失败');
-      }
-
-      // 获取文件名(从 Content-Disposition header)
-      const contentDisposition = response.headers.get('Content-Disposition');
-      const filenameMatch = contentDisposition?.match(/filename="(.+)"/);
-      const filename = filenameMatch?.[1] || `backup_${new Date().toISOString()}.dump`;
-
-      // 下载文件
-      const blob = await response.blob();
-      const url = window.URL.createObjectURL(blob);
-      const a = document.createElement('a');
-      a.href = url;
-      a.download = filename;
-      document.body.appendChild(a);
-      a.click();
-      document.body.removeChild(a);
-      window.URL.revokeObjectURL(url);
-
-      toast.success('数据库导出成功!');
-    } catch (error) {
-      console.error('Export error:', error);
-      toast.error(error instanceof Error ? error.message : '导出数据库失败');
-    } finally {
-      setIsExporting(false);
-    }
-  };
-
-  return (
-    <div className="flex flex-col gap-4">
-      <p className="text-sm text-muted-foreground">
-        导出完整的数据库备份文件(.dump 格式),可用于数据迁移或恢复。
-        备份文件使用 PostgreSQL custom format,自动压缩且兼容不同版本的数据库结构。
-      </p>
-
-      <Button
-        onClick={handleExport}
-        disabled={isExporting}
-        className="w-full sm:w-auto"
-      >
-        <Download className="mr-2 h-4 w-4" />
-        {isExporting ? '正在导出...' : '导出数据库'}
-      </Button>
-    </div>
-  );
-}

+ 0 - 253
src/app/\[locale\]/settings/data/_components/database-import.tsx

@@ -1,253 +0,0 @@
-"use client";
-
-import { useState, useRef, useEffect } from "react";
-import { Upload, AlertCircle } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { Checkbox } from "@/components/ui/checkbox";
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogFooter,
-  AlertDialogHeader,
-  AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-import { toast } from "sonner";
-import type { ImportProgressEvent } from "@/types/database-backup";
-
-export function DatabaseImport() {
-  const [selectedFile, setSelectedFile] = useState<File | null>(null);
-  const [cleanFirst, setCleanFirst] = useState(true);
-  const [isImporting, setIsImporting] = useState(false);
-  const [showConfirmDialog, setShowConfirmDialog] = useState(false);
-  const [progressMessages, setProgressMessages] = useState<string[]>([]);
-  const fileInputRef = useRef<HTMLInputElement>(null);
-  const progressContainerRef = useRef<HTMLDivElement>(null);
-
-  // 自动滚动到最新进度
-  useEffect(() => {
-    if (progressContainerRef.current) {
-      progressContainerRef.current.scrollTop = progressContainerRef.current.scrollHeight;
-    }
-  }, [progressMessages]);
-
-  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    const file = event.target.files?.[0];
-    if (file) {
-      if (!file.name.endsWith('.dump')) {
-        toast.error('请选择 .dump 格式的备份文件');
-        return;
-      }
-      setSelectedFile(file);
-    }
-  };
-
-  const handleImportClick = () => {
-    if (!selectedFile) {
-      toast.error('请先选择备份文件');
-      return;
-    }
-    setShowConfirmDialog(true);
-  };
-
-  const handleConfirmImport = async () => {
-    if (!selectedFile) return;
-
-    setShowConfirmDialog(false);
-    setIsImporting(true);
-    setProgressMessages([]);
-
-    try {
-      // 构造表单数据
-      const formData = new FormData();
-      formData.append('file', selectedFile);
-      formData.append('cleanFirst', cleanFirst.toString());
-
-      // 调用导入 API(SSE 流式响应,自动携带 cookie)
-      const response = await fetch('/api/admin/database/import', {
-        method: 'POST',
-        credentials: 'include',
-        body: formData,
-      });
-
-      if (!response.ok) {
-        const error = await response.json();
-        throw new Error(error.error || '导入失败');
-      }
-
-      // 处理 SSE 流
-      const reader = response.body?.getReader();
-      const decoder = new TextDecoder();
-
-      if (!reader) {
-        throw new Error('无法读取响应流');
-      }
-
-      while (true) {
-        const { done, value } = await reader.read();
-
-        if (done) {
-          break;
-        }
-
-        const chunk = decoder.decode(value);
-        const lines = chunk.split('\n');
-
-        for (const line of lines) {
-          if (line.startsWith('data: ')) {
-            try {
-              const data: ImportProgressEvent = JSON.parse(line.slice(6));
-
-              if (data.type === 'progress') {
-                setProgressMessages(prev => [...prev, data.message]);
-              } else if (data.type === 'complete') {
-                setProgressMessages(prev => [...prev, `✅ ${data.message}`]);
-                toast.success('数据导入完成!');
-              } else if (data.type === 'error') {
-                setProgressMessages(prev => [...prev, `❌ ${data.message}`]);
-                toast.error('数据导入失败,请查看详细日志');
-              }
-            } catch (parseError) {
-              console.error('Parse SSE error:', parseError);
-            }
-          }
-        }
-      }
-
-      // 清空文件选择
-      setSelectedFile(null);
-      if (fileInputRef.current) {
-        fileInputRef.current.value = '';
-      }
-    } catch (error) {
-      console.error('Import error:', error);
-      toast.error(error instanceof Error ? error.message : '导入数据库失败');
-      setProgressMessages(prev => [
-        ...prev,
-        `❌ 错误: ${error instanceof Error ? error.message : '未知错误'}`
-      ]);
-    } finally {
-      setIsImporting(false);
-    }
-  };
-
-  return (
-    <div className="flex flex-col gap-4">
-      <p className="text-sm text-muted-foreground">
-        从备份文件恢复数据库。支持 PostgreSQL custom format (.dump) 格式的备份文件。
-      </p>
-
-      {/* 文件选择 */}
-      <div className="flex flex-col gap-2">
-        <Label htmlFor="backup-file">选择备份文件</Label>
-        <div className="flex gap-2">
-          <input
-            ref={fileInputRef}
-            id="backup-file"
-            type="file"
-            accept=".dump"
-            onChange={handleFileChange}
-            disabled={isImporting}
-            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
-          />
-        </div>
-        {selectedFile && (
-          <p className="text-xs text-muted-foreground">
-            已选择: {selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)
-          </p>
-        )}
-      </div>
-
-      {/* 导入选项 */}
-      <div className="flex items-start gap-2">
-        <Checkbox
-          id="clean-first"
-          checked={cleanFirst}
-          onCheckedChange={(checked: boolean) => setCleanFirst(checked === true)}
-          disabled={isImporting}
-        />
-        <div className="grid gap-1.5 leading-none">
-          <Label
-            htmlFor="clean-first"
-            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-          >
-            清除现有数据(覆盖模式)
-          </Label>
-          <p className="text-xs text-muted-foreground">
-            导入前删除所有现有数据,确保数据库与备份文件完全一致。
-            如果不勾选,将尝试合并数据,但可能因主键冲突而失败。
-          </p>
-        </div>
-      </div>
-
-      {/* 导入按钮 */}
-      <Button
-        onClick={handleImportClick}
-        disabled={!selectedFile || isImporting}
-        className="w-full sm:w-auto"
-      >
-        <Upload className="mr-2 h-4 w-4" />
-        {isImporting ? '正在导入...' : '导入数据库'}
-      </Button>
-
-      {/* 进度显示 */}
-      {progressMessages.length > 0 && (
-        <div className="mt-2 rounded-md border border-border bg-muted/30 p-3">
-          <h3 className="text-sm font-medium mb-2">导入进度</h3>
-          <div
-            ref={progressContainerRef}
-            className="max-h-60 overflow-y-auto rounded bg-background p-2 font-mono text-xs space-y-1"
-          >
-            {progressMessages.map((message, index) => (
-              <div key={index} className="text-muted-foreground">
-                {message}
-              </div>
-            ))}
-          </div>
-        </div>
-      )}
-
-      {/* 确认对话框 */}
-      <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
-        <AlertDialogContent>
-          <AlertDialogHeader>
-            <AlertDialogTitle className="flex items-center gap-2">
-              <AlertCircle className="h-5 w-5 text-orange-500" />
-              确认导入数据库
-            </AlertDialogTitle>
-            <AlertDialogDescription className="space-y-2">
-              <p>
-                {cleanFirst
-                  ? '您选择了「覆盖模式」,这将会删除所有现有数据后导入备份。'
-                  : '您选择了「合并模式」,这将尝试在保留现有数据的基础上导入备份。'}
-              </p>
-              <p className="font-semibold text-foreground">
-                {cleanFirst
-                  ? '⚠️ 警告:此操作不可逆,所有当前数据将被永久删除!'
-                  : '⚠️ 注意:如果存在主键冲突,导入可能会失败。'}
-              </p>
-              <p>
-                备份文件: <span className="font-mono text-xs">{selectedFile?.name}</span>
-              </p>
-              <p className="text-xs text-muted-foreground">
-                建议在执行此操作前,先导出当前数据库作为备份。
-              </p>
-            </AlertDialogDescription>
-          </AlertDialogHeader>
-          <AlertDialogFooter>
-            <AlertDialogCancel>取消</AlertDialogCancel>
-            <AlertDialogAction
-              onClick={handleConfirmImport}
-              className="bg-orange-500 hover:bg-orange-600 text-white"
-            >
-              确认导入
-            </AlertDialogAction>
-          </AlertDialogFooter>
-        </AlertDialogContent>
-      </AlertDialog>
-    </div>
-  );
-}

+ 0 - 128
src/app/\[locale\]/settings/data/_components/database-status.tsx

@@ -1,128 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { Database, Table, AlertCircle, RefreshCw } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import type { DatabaseStatus } from "@/types/database-backup";
-
-export function DatabaseStatusDisplay() {
-  const [status, setStatus] = useState<DatabaseStatus | null>(null);
-  const [isLoading, setIsLoading] = useState(true);
-  const [error, setError] = useState<string | null>(null);
-
-  const fetchStatus = async () => {
-    setIsLoading(true);
-    setError(null);
-
-    try {
-      const response = await fetch('/api/admin/database/status', {
-        method: 'GET',
-        credentials: 'include',
-      });
-
-      if (!response.ok) {
-        const errorData = await response.json();
-        throw new Error(errorData.error || '获取状态失败');
-      }
-
-      const data: DatabaseStatus = await response.json();
-      setStatus(data);
-    } catch (err) {
-      console.error('Fetch status error:', err);
-      setError(err instanceof Error ? err.message : '获取数据库状态失败');
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    fetchStatus();
-  }, []);
-
-  if (isLoading) {
-    return (
-      <div className="flex items-center justify-center py-8">
-        <div className="text-sm text-muted-foreground">加载中...</div>
-      </div>
-    );
-  }
-
-  if (error) {
-    return (
-      <div className="flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-4">
-        <AlertCircle className="h-5 w-5 text-destructive" />
-        <div className="flex-1">
-          <p className="text-sm font-medium text-destructive">{error}</p>
-        </div>
-        <Button variant="outline" size="sm" onClick={fetchStatus}>
-          重试
-        </Button>
-      </div>
-    );
-  }
-
-  if (!status) {
-    return null;
-  }
-
-  return (
-    <div className="space-y-3">
-      {/* 紧凑的横向状态栏 */}
-      <div className="flex items-center gap-6 rounded-lg border border-border bg-muted/30 px-4 py-3">
-        {/* 连接状态 */}
-        <div className="flex items-center gap-2">
-          {status.isAvailable ? (
-            <>
-              <div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
-              <span className="text-sm font-medium">数据库连接正常</span>
-            </>
-          ) : (
-            <>
-              <div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" />
-              <span className="text-sm font-medium text-orange-500">数据库不可用</span>
-            </>
-          )}
-        </div>
-
-        {/* 分隔符 */}
-        {status.isAvailable && (
-          <>
-            <div className="h-4 w-px bg-border" />
-
-            {/* 数据库大小 */}
-            <div className="flex items-center gap-2">
-              <Database className="h-4 w-4 text-muted-foreground" />
-              <span className="text-sm font-semibold">{status.databaseSize}</span>
-            </div>
-
-            {/* 分隔符 */}
-            <div className="h-4 w-px bg-border" />
-
-            {/* 表数量 */}
-            <div className="flex items-center gap-2">
-              <Table className="h-4 w-4 text-muted-foreground" />
-              <span className="text-sm font-semibold">{status.tableCount} 个表</span>
-            </div>
-          </>
-        )}
-
-        {/* 刷新按钮 */}
-        <Button
-          variant="ghost"
-          size="sm"
-          onClick={fetchStatus}
-          className="ml-auto h-8"
-        >
-          <RefreshCw className="h-3.5 w-3.5" />
-        </Button>
-      </div>
-
-      {/* 错误信息 */}
-      {status.error && (
-        <div className="rounded-md border border-orange-200 bg-orange-50 p-3 text-sm text-orange-800 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-200">
-          {status.error}
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 219
src/app/\[locale\]/settings/data/_components/log-cleanup-panel.tsx

@@ -1,219 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { Trash2, AlertTriangle, Loader2 } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogFooter,
-  AlertDialogHeader,
-  AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-import { toast } from "sonner";
-
-export function LogCleanupPanel() {
-  const [isOpen, setIsOpen] = useState(false);
-  const [isLoading, setIsLoading] = useState(false);
-  const [isPreviewLoading, setIsPreviewLoading] = useState(false);
-  const [timeRange, setTimeRange] = useState<string>("30");
-  const [previewCount, setPreviewCount] = useState<number | null>(null);
-
-  const fetchPreview = useCallback(async () => {
-    setIsPreviewLoading(true);
-
-    try {
-      const beforeDate = new Date();
-      beforeDate.setDate(beforeDate.getDate() - parseInt(timeRange));
-
-      const response = await fetch('/api/admin/log-cleanup/manual', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        credentials: 'include',
-        body: JSON.stringify({
-          beforeDate: beforeDate.toISOString(),
-          dryRun: true,
-        }),
-      });
-
-      const result = await response.json();
-
-      if (response.ok && result.success) {
-        setPreviewCount(result.totalDeleted);
-      } else {
-        console.error('Preview error:', result.error);
-        setPreviewCount(null);
-      }
-    } catch (error) {
-      console.error('Preview error:', error);
-      setPreviewCount(null);
-    } finally {
-      setIsPreviewLoading(false);
-    }
-  }, [timeRange]);
-
-  // 当对话框打开时,自动预览
-  useEffect(() => {
-    if (isOpen) {
-      fetchPreview();
-    } else {
-      setPreviewCount(null);
-    }
-  }, [isOpen, fetchPreview]);
-
-  const handleCleanup = async () => {
-    setIsLoading(true);
-
-    try {
-      const beforeDate = new Date();
-      beforeDate.setDate(beforeDate.getDate() - parseInt(timeRange));
-
-      const response = await fetch('/api/admin/log-cleanup/manual', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        credentials: 'include',
-        body: JSON.stringify({
-          beforeDate: beforeDate.toISOString(),
-        }),
-      });
-
-      const result = await response.json();
-
-      if (!response.ok) {
-        throw new Error(result.error || '清理失败');
-      }
-
-      if (result.success) {
-        toast.success(`成功清理 ${result.totalDeleted.toLocaleString()} 条日志记录(${result.batchCount} 批次,耗时 ${(result.durationMs / 1000).toFixed(2)}s)`);
-        setIsOpen(false);
-      } else {
-        toast.error(result.error || '清理失败');
-      }
-    } catch (error) {
-      console.error('Cleanup error:', error);
-      toast.error(error instanceof Error ? error.message : '清理日志失败');
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  const getTimeRangeDescription = () => {
-    const days = parseInt(timeRange);
-    if (days === 7) return '一周前';
-    if (days === 30) return '一个月前';
-    if (days === 90) return '三个月前';
-    if (days === 180) return '六个月前';
-    return `${days} 天前`;
-  };
-
-  return (
-    <div className="flex flex-col gap-4">
-      <p className="text-sm text-muted-foreground">
-        清理历史日志数据以释放数据库存储空间。
-        <strong>注意:统计数据将被保留,但日志详情将被永久删除。</strong>
-      </p>
-
-      <div className="flex flex-col gap-3">
-        <Label htmlFor="time-range">清理范围</Label>
-        <Select value={timeRange} onValueChange={setTimeRange}>
-          <SelectTrigger id="time-range" className="w-full sm:w-[300px]">
-            <SelectValue />
-          </SelectTrigger>
-          <SelectContent>
-            <SelectItem value="7">一周前的日志 (7 天)</SelectItem>
-            <SelectItem value="30">一个月前的日志 (30 天)</SelectItem>
-            <SelectItem value="90">三个月前的日志 (90 天)</SelectItem>
-            <SelectItem value="180">六个月前的日志 (180 天)</SelectItem>
-          </SelectContent>
-        </Select>
-        <p className="text-xs text-muted-foreground">
-          将清理 {getTimeRangeDescription()} 的所有日志记录
-        </p>
-      </div>
-
-      <Button
-        onClick={() => setIsOpen(true)}
-        variant="destructive"
-        className="w-full sm:w-auto"
-      >
-        <Trash2 className="mr-2 h-4 w-4" />
-        清理日志
-      </Button>
-
-      <AlertDialog open={isOpen} onOpenChange={setIsOpen}>
-        <AlertDialogContent>
-          <AlertDialogHeader>
-            <AlertDialogTitle className="flex items-center gap-2">
-              <AlertTriangle className="h-5 w-5 text-destructive" />
-              确认清理日志
-            </AlertDialogTitle>
-            <AlertDialogDescription className="space-y-3">
-              <p>
-                此操作将<strong className="text-destructive">永久删除</strong>{" "}
-                {getTimeRangeDescription()}的所有日志记录,
-                且<strong className="text-destructive">无法恢复</strong>。
-              </p>
-
-              {/* 预览信息 */}
-              <div className="bg-muted p-3 rounded-md">
-                {isPreviewLoading ? (
-                  <div className="flex items-center gap-2 text-sm">
-                    <Loader2 className="h-4 w-4 animate-spin" />
-                    <span>正在统计...</span>
-                  </div>
-                ) : previewCount !== null ? (
-                  <p className="text-sm font-medium">
-                    将删除 <span className="text-destructive text-lg">{previewCount.toLocaleString()}</span> 条日志记录
-                  </p>
-                ) : (
-                  <p className="text-sm text-muted-foreground">
-                    无法获取预览信息
-                  </p>
-                )}
-              </div>
-
-              <p className="text-sm">
-                ✓ 统计数据将被保留(用于趋势分析)<br />
-                ✗ 日志详情将被删除(请求/响应内容、错误信息等)
-              </p>
-              <p className="text-sm text-muted-foreground">
-                建议:在清理前先<strong>导出数据库备份</strong>,以防需要恢复数据。
-              </p>
-            </AlertDialogDescription>
-          </AlertDialogHeader>
-          <AlertDialogFooter>
-            <AlertDialogCancel disabled={isLoading}>取消</AlertDialogCancel>
-            <AlertDialogAction
-              onClick={(e) => {
-                e.preventDefault();
-                handleCleanup();
-              }}
-              disabled={isLoading || isPreviewLoading}
-              className="bg-destructive hover:bg-destructive/90"
-            >
-              {isLoading ? (
-                <>
-                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-                  正在清理...
-                </>
-              ) : (
-                '确认清理'
-              )}
-            </AlertDialogAction>
-          </AlertDialogFooter>
-        </AlertDialogContent>
-      </AlertDialog>
-    </div>
-  );
-}

+ 0 - 120
src/app/\[locale\]/settings/logs/_components/log-level-form.tsx

@@ -1,120 +0,0 @@
-"use client";
-
-import { useState, useTransition, useEffect } from "react";
-import { Label } from "@/components/ui/label";
-import { Button } from "@/components/ui/button";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { toast } from "sonner";
-
-type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
-
-const LOG_LEVELS: { value: LogLevel; label: string; description: string }[] = [
-  { value: 'fatal', label: 'Fatal', description: '仅致命错误' },
-  { value: 'error', label: 'Error', description: '错误信息' },
-  { value: 'warn', label: 'Warn', description: '警告 + 错误' },
-  { value: 'info', label: 'Info', description: '关键业务事件 + 警告 + 错误(推荐生产)' },
-  { value: 'debug', label: 'Debug', description: '调试信息 + 所有级别(推荐开发)' },
-  { value: 'trace', label: 'Trace', description: '极详细追踪 + 所有级别' },
-];
-
-export function LogLevelForm() {
-  const [currentLevel, setCurrentLevel] = useState<LogLevel>('info');
-  const [selectedLevel, setSelectedLevel] = useState<LogLevel>('info');
-  const [isPending, startTransition] = useTransition();
-
-  // 获取当前日志级别
-  useEffect(() => {
-    fetch('/api/admin/log-level')
-      .then((res) => res.json())
-      .then((data) => {
-        setCurrentLevel(data.level);
-        setSelectedLevel(data.level);
-      })
-      .catch(() => {
-        toast.error('获取日志级别失败');
-      });
-  }, []);
-
-  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    startTransition(async () => {
-      try {
-        const response = await fetch('/api/admin/log-level', {
-          method: 'POST',
-          headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify({ level: selectedLevel }),
-        });
-
-        const result = await response.json();
-
-        if (!response.ok) {
-          toast.error(result.error || '设置失败');
-          return;
-        }
-
-        setCurrentLevel(selectedLevel);
-        toast.success(`日志级别已设置为: ${selectedLevel.toUpperCase()}`);
-      } catch {
-        toast.error('设置日志级别失败');
-      }
-    });
-  };
-
-  return (
-    <form onSubmit={handleSubmit} className="space-y-6">
-      <div className="space-y-2">
-        <Label htmlFor="log-level">当前日志级别</Label>
-        <Select value={selectedLevel} onValueChange={(value) => setSelectedLevel(value as LogLevel)}>
-          <SelectTrigger id="log-level" disabled={isPending}>
-            <SelectValue />
-          </SelectTrigger>
-          <SelectContent>
-            {LOG_LEVELS.map((level) => (
-              <SelectItem key={level.value} value={level.value}>
-                <div className="flex flex-col">
-                  <span className="font-medium">{level.label}</span>
-                  <span className="text-xs text-muted-foreground">{level.description}</span>
-                </div>
-              </SelectItem>
-            ))}
-          </SelectContent>
-        </Select>
-        <p className="text-xs text-muted-foreground">
-          调整日志级别后立即生效,无需重启服务。
-        </p>
-      </div>
-
-      <div className="rounded-lg border border-dashed border-border px-4 py-3 space-y-2">
-        <h4 className="text-sm font-medium">日志级别说明</h4>
-        <ul className="text-xs text-muted-foreground space-y-1">
-          <li><strong>Fatal/Error</strong>: 仅显示错误,日志最少,适合高负载生产环境</li>
-          <li><strong>Warn</strong>: 包含警告(限流触发、熔断器打开等)+ 错误</li>
-          <li><strong>Info(推荐生产)</strong>: 显示关键业务事件(供应商选择、Session 复用、价格同步)+ 警告 + 错误</li>
-          <li><strong>Debug(推荐开发)</strong>: 包含详细调试信息,适合排查问题时使用</li>
-          <li><strong>Trace</strong>: 极详细的追踪信息,包含所有细节</li>
-        </ul>
-      </div>
-
-      {selectedLevel !== currentLevel && (
-        <div className="rounded-lg bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800 px-4 py-3">
-          <p className="text-sm text-orange-800 dark:text-orange-200">
-            当前级别为 <strong>{currentLevel.toUpperCase()}</strong>,点击保存后将切换到 <strong>{selectedLevel.toUpperCase()}</strong>
-          </p>
-        </div>
-      )}
-
-      <div className="flex justify-end">
-        <Button type="submit" disabled={isPending || selectedLevel === currentLevel}>
-          {isPending ? "保存中..." : "保存设置"}
-        </Button>
-      </div>
-    </form>
-  );
-}

+ 0 - 376
src/app/\[locale\]/settings/prices/_components/price-list.tsx

@@ -1,376 +0,0 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import { Search, Package, DollarSign, ChevronLeft, ChevronRight } from "lucide-react";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import {
-  Table,
-  TableBody,
-  TableCell,
-  TableHead,
-  TableHeader,
-  TableRow,
-} from "@/components/ui/table";
-import { Badge } from "@/components/ui/badge";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import type { ModelPrice } from "@/types/model-price";
-import { useDebounce } from "@/lib/hooks/use-debounce";
-
-interface PriceListProps {
-  initialPrices: ModelPrice[];
-  initialTotal: number;
-  initialPage: number;
-  initialPageSize: number;
-}
-
-/**
- * 价格列表组件(支持分页)
- */
-export function PriceList({
-  initialPrices,
-  initialTotal,
-  initialPage,
-  initialPageSize,
-}: PriceListProps) {
-  const [searchTerm, setSearchTerm] = useState("");
-  const [prices, setPrices] = useState<ModelPrice[]>(initialPrices);
-  const [total, setTotal] = useState(initialTotal);
-  const [page, setPage] = useState(initialPage);
-  const [pageSize, setPageSize] = useState(initialPageSize);
-  const [isLoading, setIsLoading] = useState(false);
-
-  // 使用防抖,避免频繁请求
-  const debouncedSearchTerm = useDebounce(searchTerm, 500);
-
-  // 计算总页数
-  const totalPages = Math.ceil(total / pageSize);
-
-  // 从 URL 搜索参数中读取初始状态(仅在挂载时执行一次)
-  useEffect(() => {
-    const urlParams = new URLSearchParams(window.location.search);
-    const searchParam = urlParams.get("search");
-    const pageParam = urlParams.get("page");
-    const sizeParam = urlParams.get("size");
-
-    if (searchParam) setSearchTerm(searchParam);
-    if (pageParam) setPage(parseInt(pageParam, 10));
-    if (sizeParam) setPageSize(parseInt(sizeParam, 10));
-  }, []); // 空依赖数组,仅在挂载时执行一次
-
-  // 更新 URL 搜索参数
-  const updateURL = (newSearchTerm: string, newPage: number, newPageSize: number) => {
-    const url = new URL(window.location.href);
-    if (newSearchTerm) {
-      url.searchParams.set("search", newSearchTerm);
-    } else {
-      url.searchParams.delete("search");
-    }
-    if (newPage > 1) {
-      url.searchParams.set("page", newPage.toString());
-    } else {
-      url.searchParams.delete("page");
-    }
-    if (newPageSize !== 50) {
-      url.searchParams.set("size", newPageSize.toString());
-    } else {
-      url.searchParams.delete("size");
-    }
-    window.history.replaceState({}, "", url.toString());
-  };
-
-  // 获取价格数据
-  const fetchPrices = async (newPage: number, newPageSize: number, newSearchTerm: string) => {
-    setIsLoading(true);
-    try {
-      const response = await fetch(
-        `/api/prices?page=${newPage}&pageSize=${newPageSize}&search=${encodeURIComponent(newSearchTerm)}`
-      );
-      const result = await response.json();
-
-      if (result.ok) {
-        setPrices(result.data.data);
-        setTotal(result.data.total);
-        setPage(result.data.page);
-        setPageSize(result.data.pageSize);
-      }
-    } catch (error) {
-      console.error("获取价格数据失败:", error);
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  // 当防抖后的搜索词变化时,触发搜索(重置到第一页)
-  useEffect(() => {
-    // 跳过初始渲染(当 debouncedSearchTerm 等于初始 searchTerm 时)
-    if (debouncedSearchTerm !== searchTerm) return;
-
-    const newPage = 1; // 搜索时重置到第一页
-    setPage(newPage);
-    updateURL(debouncedSearchTerm, newPage, pageSize);
-    fetchPrices(newPage, pageSize, debouncedSearchTerm);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [debouncedSearchTerm]); // 仅依赖 debouncedSearchTerm
-
-  // 搜索输入处理(只更新状态,不触发请求)
-  const handleSearchChange = (value: string) => {
-    setSearchTerm(value);
-  };
-
-  // 页面大小变化处理
-  const handlePageSizeChange = (newPageSize: number) => {
-    const newPage = Math.max(1, Math.min(page, Math.ceil(total / newPageSize)));
-    setPageSize(newPageSize);
-    setPage(newPage);
-    updateURL(debouncedSearchTerm, newPage, newPageSize);
-    fetchPrices(newPage, newPageSize, debouncedSearchTerm);
-  };
-
-  // 页面跳转处理
-  const handlePageChange = (newPage: number) => {
-    if (newPage < 1 || newPage > totalPages) return;
-    setPage(newPage);
-    updateURL(debouncedSearchTerm, newPage, pageSize);
-    fetchPrices(newPage, pageSize, debouncedSearchTerm);
-  };
-
-  // 移除客户端过滤逻辑(现在由后端处理)
-  const filteredPrices = prices;
-
-  /**
-   * 格式化价格显示为每百万token的价格
-   */
-  const formatPrice = (value?: number): string => {
-    if (!value) return "-";
-    // 将每token的价格转换为每百万token的价格
-    const pricePerMillion = value * 1000000;
-    // 格式化为合适的小数位数
-    if (pricePerMillion < 0.01) {
-      return pricePerMillion.toFixed(4);
-    } else if (pricePerMillion < 1) {
-      return pricePerMillion.toFixed(3);
-    } else if (pricePerMillion < 100) {
-      return pricePerMillion.toFixed(2);
-    } else {
-      return pricePerMillion.toFixed(0);
-    }
-  };
-
-  /**
-   * 获取模型类型标签
-   */
-  const getModeLabel = (mode?: string) => {
-    switch (mode) {
-      case "chat":
-        return <Badge variant="default">对话</Badge>;
-      case "image_generation":
-        return <Badge variant="secondary">图像生成</Badge>;
-      case "completion":
-        return <Badge variant="outline">补全</Badge>;
-      default:
-        return <Badge variant="outline">未知</Badge>;
-    }
-  };
-
-  return (
-    <div className="space-y-4">
-      {/* 搜索和页面大小控制 */}
-      <div className="flex items-center gap-4">
-        <div className="relative flex-1">
-          <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-          <Input
-            placeholder="搜索模型名称..."
-            value={searchTerm}
-            onChange={(e) => handleSearchChange(e.target.value)}
-            className="pl-9"
-          />
-        </div>
-        <div className="flex items-center gap-2">
-          <span className="text-sm text-muted-foreground">每页显示:</span>
-          <Select
-            value={pageSize.toString()}
-            onValueChange={(value) => handlePageSizeChange(parseInt(value, 10))}
-          >
-            <SelectTrigger className="w-20">
-              <SelectValue />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value="20">20</SelectItem>
-              <SelectItem value="50">50</SelectItem>
-              <SelectItem value="100">100</SelectItem>
-              <SelectItem value="200">200</SelectItem>
-            </SelectContent>
-          </Select>
-        </div>
-      </div>
-
-      {/* 价格表格 */}
-      <div className="border rounded-lg">
-        <Table className="table-fixed">
-          <TableHeader>
-            <TableRow>
-              <TableHead className="w-48 whitespace-normal">模型名称</TableHead>
-              <TableHead className="w-24">类型</TableHead>
-              <TableHead className="w-32 whitespace-normal">提供商</TableHead>
-              <TableHead className="w-32 text-right">输入价格 ($/M)</TableHead>
-              <TableHead className="w-32 text-right">输出价格 ($/M)</TableHead>
-              <TableHead className="w-32">更新时间</TableHead>
-            </TableRow>
-          </TableHeader>
-          <TableBody>
-            {isLoading ? (
-              <TableRow>
-                <TableCell colSpan={6} className="text-center py-8">
-                  <div className="flex items-center justify-center gap-2 text-muted-foreground">
-                    <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-current"></div>
-                    <span>加载中...</span>
-                  </div>
-                </TableCell>
-              </TableRow>
-            ) : filteredPrices.length > 0 ? (
-              filteredPrices.map((price) => (
-                <TableRow key={price.id}>
-                  <TableCell className="font-mono text-sm whitespace-normal break-words">
-                    {price.modelName}
-                  </TableCell>
-                  <TableCell>{getModeLabel(price.priceData.mode)}</TableCell>
-                  <TableCell className="whitespace-normal break-words">
-                    {price.priceData.litellm_provider || "-"}
-                  </TableCell>
-                  <TableCell className="font-mono text-sm text-right">
-                    {price.priceData.mode === "image_generation" ? (
-                      "-"
-                    ) : (
-                      <span className="text-muted-foreground">
-                        ${formatPrice(price.priceData.input_cost_per_token)}/M
-                      </span>
-                    )}
-                  </TableCell>
-                  <TableCell className="font-mono text-sm text-right">
-                    {price.priceData.mode === "image_generation" ? (
-                      <span className="text-muted-foreground">
-                        ${formatPrice(price.priceData.output_cost_per_image)}/img
-                      </span>
-                    ) : (
-                      <span className="text-muted-foreground">
-                        ${formatPrice(price.priceData.output_cost_per_token)}/M
-                      </span>
-                    )}
-                  </TableCell>
-                  <TableCell className="text-sm text-muted-foreground">
-                    {new Date(price.createdAt).toLocaleDateString("zh-CN")}
-                  </TableCell>
-                </TableRow>
-              ))
-            ) : (
-              <TableRow>
-                <TableCell colSpan={6} className="text-center py-8">
-                  <div className="flex flex-col items-center gap-2 text-muted-foreground">
-                    {searchTerm ? (
-                      <>
-                        <Search className="h-8 w-8 opacity-50" />
-                        <p>未找到匹配的模型</p>
-                      </>
-                    ) : (
-                      <>
-                        <Package className="h-8 w-8 opacity-50" />
-                        <p>暂无价格数据</p>
-                        <p className="text-sm">系统已内置价格表,请通过上方按钮同步或更新</p>
-                      </>
-                    )}
-                  </div>
-                </TableCell>
-              </TableRow>
-            )}
-          </TableBody>
-        </Table>
-      </div>
-
-      {/* 分页控件 */}
-      {totalPages > 1 && (
-        <div className="flex items-center justify-between">
-          <div className="text-sm text-muted-foreground">
-            显示第 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} 条,共 {total}{" "}
-            条记录
-          </div>
-          <div className="flex items-center gap-2">
-            <Button
-              variant="outline"
-              size="sm"
-              onClick={() => handlePageChange(page - 1)}
-              disabled={page <= 1 || isLoading}
-            >
-              <ChevronLeft className="h-4 w-4" />
-              上一页
-            </Button>
-
-            <div className="flex items-center gap-1">
-              {/* 页码显示逻辑 */}
-              {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
-                let pageNum;
-                if (totalPages <= 5) {
-                  pageNum = i + 1;
-                } else if (page <= 3) {
-                  pageNum = i + 1;
-                } else if (page >= totalPages - 2) {
-                  pageNum = totalPages - 4 + i;
-                } else {
-                  pageNum = page - 2 + i;
-                }
-
-                return (
-                  <Button
-                    key={pageNum}
-                    variant={page === pageNum ? "default" : "outline"}
-                    size="sm"
-                    onClick={() => handlePageChange(pageNum)}
-                    disabled={isLoading}
-                    className="w-8 h-8"
-                  >
-                    {pageNum}
-                  </Button>
-                );
-              })}
-            </div>
-
-            <Button
-              variant="outline"
-              size="sm"
-              onClick={() => handlePageChange(page + 1)}
-              disabled={page >= totalPages || isLoading}
-            >
-              下一页
-              <ChevronRight className="h-4 w-4" />
-            </Button>
-          </div>
-        </div>
-      )}
-
-      {/* 统计信息 */}
-      <div className="flex items-center justify-between text-sm text-muted-foreground">
-        <div className="flex items-center gap-1">
-          <DollarSign className="h-4 w-4" />
-          <span>共 {total} 个模型价格</span>
-          {searchTerm && (
-            <span className="text-muted-foreground">(搜索结果:{filteredPrices.length} 个)</span>
-          )}
-        </div>
-        <div>
-          最后更新:
-          {prices.length > 0
-            ? new Date(
-                Math.max(...prices.map((p) => new Date(p.createdAt).getTime()))
-              ).toLocaleDateString("zh-CN")
-            : "-"}
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 66
src/app/\[locale\]/settings/prices/_components/sync-litellm-button.tsx

@@ -1,66 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { RefreshCw } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { syncLiteLLMPrices } from "@/actions/model-prices";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-
-/**
- * LiteLLM 价格同步按钮组件
- */
-export function SyncLiteLLMButton() {
-  const router = useRouter();
-  const [syncing, setSyncing] = useState(false);
-
-  const handleSync = async () => {
-    setSyncing(true);
-
-    try {
-      const response = await syncLiteLLMPrices();
-
-      if (!response.ok) {
-        toast.error(response.error || "同步失败");
-        return;
-      }
-
-      if (!response.data) {
-        toast.error("同步成功但未返回处理结果");
-        return;
-      }
-
-      const { added, updated, unchanged, failed } = response.data;
-
-      // 显示详细结果
-      if (added.length > 0 || updated.length > 0) {
-        toast.success(
-          `同步成功:新增 ${added.length} 个,更新 ${updated.length} 个,未变化 ${unchanged.length} 个`
-        );
-      } else if (unchanged.length > 0) {
-        toast.info(`所有 ${unchanged.length} 个模型价格均为最新`);
-      } else {
-        toast.warning("未找到支持的模型价格");
-      }
-
-      if (failed.length > 0) {
-        toast.error(`${failed.length} 个模型处理失败`);
-      }
-
-      // 刷新页面数据
-      router.refresh();
-    } catch (error) {
-      console.error("同步失败:", error);
-      toast.error("同步失败,请重试");
-    } finally {
-      setSyncing(false);
-    }
-  };
-
-  return (
-    <Button variant="outline" size="sm" onClick={handleSync} disabled={syncing}>
-      <RefreshCw className={`h-4 w-4 mr-2 ${syncing ? "animate-spin" : ""}`} />
-      {syncing ? "同步中..." : "同步 LiteLLM 价格"}
-    </Button>
-  );
-}

+ 0 - 287
src/app/\[locale\]/settings/prices/_components/upload-price-dialog.tsx

@@ -1,287 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { useRouter } from "next/navigation";
-import { createPortal } from "react-dom";
-import { Upload, FileJson, CheckCircle, XCircle, AlertCircle, Loader2 } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogHeader,
-  DialogTitle,
-  DialogTrigger,
-} from "@/components/ui/dialog";
-import { uploadPriceTable } from "@/actions/model-prices";
-import { toast } from "sonner";
-import type { PriceUpdateResult } from "@/types/model-price";
-
-interface PageLoadingOverlayProps {
-  active: boolean;
-}
-
-interface UploadPriceDialogProps {
-  defaultOpen?: boolean;
-  isRequired?: boolean;
-}
-
-function PageLoadingOverlay({ active }: PageLoadingOverlayProps) {
-  const [mounted, setMounted] = useState(false);
-
-  useEffect(() => {
-    setMounted(true);
-  }, []);
-
-  if (!mounted || !active) {
-    return null;
-  }
-
-  return createPortal(
-    <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-background/80 backdrop-blur-sm">
-      <div className="flex items-center gap-3 rounded-lg bg-card/90 px-5 py-4 shadow-lg ring-1 ring-border/40">
-        <Loader2 className="h-5 w-5 animate-spin text-primary" />
-        <span className="text-sm text-muted-foreground">正在更新模型价格...</span>
-      </div>
-    </div>,
-    document.body
-  );
-}
-
-/**
- * 价格表上传对话框组件
- */
-export function UploadPriceDialog({
-  defaultOpen = false,
-  isRequired = false,
-}: UploadPriceDialogProps) {
-  const router = useRouter();
-  const [open, setOpen] = useState(defaultOpen);
-  const [uploading, setUploading] = useState(false);
-  const [result, setResult] = useState<PriceUpdateResult | null>(null);
-
-  const handleOpenChange = (nextOpen: boolean) => {
-    if (!nextOpen && uploading) {
-      return;
-    }
-
-    if (!nextOpen) {
-      setResult(null);
-    }
-
-    setOpen(nextOpen);
-  };
-
-  /**
-   * 处理文件选择
-   */
-  const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
-    const file = event.target.files?.[0];
-    if (!file) return;
-
-    // 验证文件类型
-    if (!file.name.endsWith(".json")) {
-      toast.error("请选择JSON文件");
-      return;
-    }
-
-    // 验证文件大小(限制10MB)
-    if (file.size > 10 * 1024 * 1024) {
-      toast.error("文件大小不能超过10MB");
-      return;
-    }
-
-    setUploading(true);
-    setResult(null);
-
-    try {
-      // 读取文件内容
-      const text = await file.text();
-
-      // 上传并处理
-      const response = await uploadPriceTable(text);
-
-      if (!response.ok) {
-        toast.error(response.error);
-        return;
-      }
-
-      if (!response.data) {
-        toast.error("价格表更新成功但未返回处理结果");
-        return;
-      }
-
-      setResult(response.data);
-      toast.success("价格表更新成功");
-    } catch (error) {
-      console.error("更新失败:", error);
-      toast.error("更新失败,请重试");
-    } finally {
-      setUploading(false);
-      // 清除文件输入
-      event.target.value = "";
-    }
-  };
-
-  /**
-   * 关闭对话框
-   */
-  const handleClose = () => {
-    if (uploading) {
-      return;
-    }
-
-    // 如果是必需上传且已成功上传,跳转到dashboard
-    if (isRequired && result && (result.added.length > 0 || result.updated.length > 0)) {
-      router.push("/dashboard");
-      return;
-    }
-
-    setOpen(false);
-    setResult(null);
-  };
-
-  return (
-    <>
-      <PageLoadingOverlay active={uploading} />
-      <Dialog open={open} onOpenChange={handleOpenChange}>
-        <DialogTrigger asChild>
-          <Button variant="outline" size="sm" disabled={uploading}>
-            <Upload className="h-4 w-4 mr-2" />
-            更新价格表
-          </Button>
-        </DialogTrigger>
-        <DialogContent
-          className="max-w-lg"
-          onEscapeKeyDown={(event) => {
-            if (uploading) {
-              event.preventDefault();
-            }
-          }}
-          onPointerDownOutside={(event) => {
-            if (uploading) {
-              event.preventDefault();
-            }
-          }}
-        >
-          <DialogHeader>
-            <DialogTitle>{isRequired ? "更新模型价格表" : "更新模型价格表"}</DialogTitle>
-            <DialogDescription>
-              {isRequired
-                ? "选择包含模型价格数据的 JSON 文件以更新价格表"
-                : "选择包含模型价格数据的 JSON 文件以更新价格表"}
-            </DialogDescription>
-          </DialogHeader>
-
-          {!result ? (
-            <div className="space-y-4">
-              <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-6">
-                <div className="flex flex-col items-center space-y-3">
-                  <FileJson className="h-10 w-10 text-muted-foreground/50" />
-                  <div className="text-center">
-                    <p className="text-sm text-muted-foreground">点击选择JSON文件或拖拽到此处</p>
-                    <p className="text-xs text-muted-foreground mt-1">文件大小不超过10MB</p>
-                  </div>
-                  <label htmlFor="price-file-input">
-                    <Button variant="secondary" size="sm" disabled={uploading} asChild>
-                      <span>{uploading ? "更新中..." : "选择文件"}</span>
-                    </Button>
-                  </label>
-                  <input
-                    id="price-file-input"
-                    type="file"
-                    accept=".json"
-                    className="hidden"
-                    onChange={handleFileSelect}
-                    disabled={uploading}
-                  />
-                </div>
-              </div>
-
-              <div className="text-xs text-muted-foreground space-y-1">
-                <p>
-                  • 系统已内置 LiteLLM 价格表,如需更新可使用左侧&quot;同步 LiteLLM 价格&quot;按钮
-                </p>
-                <p>
-                  • 也可以手动下载{" "}
-                  <a
-                    className="text-blue-500 underline"
-                    href="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
-                    target="_blank"
-                    rel="noopener noreferrer"
-                  >
-                    最新价格表
-                  </a>{" "}
-                  并通过此按钮更新
-                </p>
-                <p>• 支持 Claude 和 OpenAI 模型(claude-, gpt-, o1-, o3- 前缀)</p>
-              </div>
-            </div>
-          ) : (
-            <div className="space-y-4">
-              <div className="text-sm space-y-2">
-                <div className="flex items-center justify-between p-2 bg-muted/50 rounded">
-                  <span>处理总数</span>
-                  <span className="font-mono">{result.total}</span>
-                </div>
-
-                {result.added.length > 0 && (
-                  <div className="p-2 bg-green-50 dark:bg-green-950/20 rounded">
-                    <div className="flex items-center gap-2 mb-1">
-                      <CheckCircle className="h-4 w-4 text-green-600" />
-                      <span className="font-medium">新增模型 ({result.added.length})</span>
-                    </div>
-                    <div className="text-xs text-muted-foreground ml-6">
-                      {result.added.slice(0, 3).join(", ")}
-                      {result.added.length > 3 && ` 等${result.added.length}个`}
-                    </div>
-                  </div>
-                )}
-
-                {result.updated.length > 0 && (
-                  <div className="p-2 bg-blue-50 dark:bg-blue-950/20 rounded">
-                    <div className="flex items-center gap-2 mb-1">
-                      <AlertCircle className="h-4 w-4 text-blue-600" />
-                      <span className="font-medium">更新模型 ({result.updated.length})</span>
-                    </div>
-                    <div className="text-xs text-muted-foreground ml-6">
-                      {result.updated.slice(0, 3).join(", ")}
-                      {result.updated.length > 3 && ` 等${result.updated.length}个`}
-                    </div>
-                  </div>
-                )}
-
-                {result.unchanged.length > 0 && (
-                  <div className="p-2 bg-gray-50 dark:bg-gray-950/20 rounded">
-                    <div className="flex items-center gap-2">
-                      <span className="font-medium">未变化 ({result.unchanged.length})</span>
-                    </div>
-                  </div>
-                )}
-
-                {result.failed.length > 0 && (
-                  <div className="p-2 bg-red-50 dark:bg-red-950/20 rounded">
-                    <div className="flex items-center gap-2 mb-1">
-                      <XCircle className="h-4 w-4 text-red-600" />
-                      <span className="font-medium">处理失败 ({result.failed.length})</span>
-                    </div>
-                    <div className="text-xs text-muted-foreground ml-6">
-                      {result.failed.slice(0, 3).join(", ")}
-                      {result.failed.length > 3 && ` 等${result.failed.length}个`}
-                    </div>
-                  </div>
-                )}
-              </div>
-
-              <Button onClick={handleClose} className="w-full">
-                {isRequired && result && (result.added.length > 0 || result.updated.length > 0)
-                  ? "进入控制面板"
-                  : "完成"}
-              </Button>
-            </div>
-          )}
-        </DialogContent>
-      </Dialog>
-    </>
-  );
-}

+ 0 - 39
src/app/\[locale\]/settings/providers/_components/add-provider-dialog.tsx

@@ -1,39 +0,0 @@
-"use client";
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { ServerCog } from "lucide-react";
-import { ProviderForm } from "./forms/provider-form";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-
-interface AddProviderDialogProps {
-  enableMultiProviderTypes: boolean;
-}
-
-export function AddProviderDialog({ enableMultiProviderTypes }: AddProviderDialogProps) {
-  const router = useRouter();
-  const [open, setOpen] = useState(false);
-  return (
-    <Dialog open={open} onOpenChange={setOpen}>
-      <DialogTrigger asChild>
-        <Button>
-          <ServerCog className="h-4 w-4" /> 新增服务商
-        </Button>
-      </DialogTrigger>
-      <DialogContent className="sm:max-w-3xl max-h-[85vh] overflow-y-auto">
-        <FormErrorBoundary>
-          <ProviderForm
-            mode="create"
-            enableMultiProviderTypes={enableMultiProviderTypes}
-            onSuccess={() => {
-              setOpen(false);
-              // 刷新页面数据以显示新添加的服务商
-              router.refresh();
-            }}
-          />
-        </FormErrorBoundary>
-      </DialogContent>
-    </Dialog>
-  );
-}

+ 0 - 1099
src/app/\[locale\]/settings/providers/_components/forms/provider-form.tsx

@@ -1,1099 +0,0 @@
-"use client";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Badge } from "@/components/ui/badge";
-import { Switch } from "@/components/ui/switch";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
-import { useState, useTransition, useEffect, useRef } from "react";
-import { addProvider, editProvider, removeProvider } from "@/actions/providers";
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogHeader as AlertHeader,
-  AlertDialogTitle as AlertTitle,
-  AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import type { ProviderDisplay, ProviderType, CodexInstructionsStrategy } from "@/types/provider";
-import { validateNumericField, isValidUrl } from "@/lib/utils/validation";
-import { PROVIDER_DEFAULTS } from "@/lib/constants/provider.constants";
-import { toast } from "sonner";
-import { ModelMultiSelect } from "../model-multi-select";
-import { ModelRedirectEditor } from "../model-redirect-editor";
-import { ProxyTestButton } from "./proxy-test-button";
-import { ChevronDown } from "lucide-react";
-
-type Mode = "create" | "edit";
-
-interface ProviderFormProps {
-  mode: Mode;
-  onSuccess?: () => void;
-  provider?: ProviderDisplay; // edit 模式需要,create 可空
-  cloneProvider?: ProviderDisplay; // create 模式用于克隆数据
-  enableMultiProviderTypes: boolean;
-}
-
-export function ProviderForm({
-  mode,
-  onSuccess,
-  provider,
-  cloneProvider,
-  enableMultiProviderTypes,
-}: ProviderFormProps) {
-  const isEdit = mode === "edit";
-  const [isPending, startTransition] = useTransition();
-
-  // 名称输入框引用,用于自动聚焦
-  const nameInputRef = useRef<HTMLInputElement>(null);
-
-  // 获取初始数据源:编辑模式用 provider,创建模式用 cloneProvider(如果有)
-  const sourceProvider = isEdit ? provider : cloneProvider;
-
-  const [name, setName] = useState(
-    isEdit ? (provider?.name ?? "") : cloneProvider ? `${cloneProvider.name}_Copy` : ""
-  );
-  const [url, setUrl] = useState(sourceProvider?.url ?? "");
-  const [key, setKey] = useState(""); // 编辑时留空代表不更新
-  const [providerType, setProviderType] = useState<ProviderType>(
-    sourceProvider?.providerType ?? "claude"
-  );
-  const [modelRedirects, setModelRedirects] = useState<Record<string, string>>(
-    sourceProvider?.modelRedirects ?? {}
-  );
-  const [priority, setPriority] = useState<number>(sourceProvider?.priority ?? 0);
-  const [weight, setWeight] = useState<number>(sourceProvider?.weight ?? 1);
-  const [costMultiplier, setCostMultiplier] = useState<number>(
-    sourceProvider?.costMultiplier ?? 1.0
-  );
-  const [groupTag, setGroupTag] = useState<string>(sourceProvider?.groupTag ?? "");
-  const [limit5hUsd, setLimit5hUsd] = useState<number | null>(sourceProvider?.limit5hUsd ?? null);
-  const [limitWeeklyUsd, setLimitWeeklyUsd] = useState<number | null>(
-    sourceProvider?.limitWeeklyUsd ?? null
-  );
-  const [limitMonthlyUsd, setLimitMonthlyUsd] = useState<number | null>(
-    sourceProvider?.limitMonthlyUsd ?? null
-  );
-  const [limitConcurrentSessions, setLimitConcurrentSessions] = useState<number | null>(
-    sourceProvider?.limitConcurrentSessions ?? null
-  );
-  const [allowedModels, setAllowedModels] = useState<string[]>(sourceProvider?.allowedModels ?? []);
-  const [joinClaudePool, setJoinClaudePool] = useState<boolean>(
-    sourceProvider?.joinClaudePool ?? false
-  );
-
-  // 熔断器配置(以分钟为单位显示,提交时转换为毫秒)
-  // 允许 undefined,用户可以清空输入框,提交时使用默认值
-  const [failureThreshold, setFailureThreshold] = useState<number | undefined>(
-    sourceProvider?.circuitBreakerFailureThreshold
-  );
-  const [openDurationMinutes, setOpenDurationMinutes] = useState<number | undefined>(
-    sourceProvider?.circuitBreakerOpenDuration
-      ? sourceProvider.circuitBreakerOpenDuration / 60000
-      : undefined
-  );
-  const [halfOpenSuccessThreshold, setHalfOpenSuccessThreshold] = useState<number | undefined>(
-    sourceProvider?.circuitBreakerHalfOpenSuccessThreshold
-  );
-
-  // 代理配置
-  const [proxyUrl, setProxyUrl] = useState<string>(sourceProvider?.proxyUrl ?? "");
-  const [proxyFallbackToDirect, setProxyFallbackToDirect] = useState<boolean>(
-    sourceProvider?.proxyFallbackToDirect ?? false
-  );
-
-  // 供应商官网地址
-  const [websiteUrl, setWebsiteUrl] = useState<string>(sourceProvider?.websiteUrl ?? "");
-
-  // Codex Instructions 策略配置
-  const [codexInstructionsStrategy, setCodexInstructionsStrategy] =
-    useState<CodexInstructionsStrategy>(sourceProvider?.codexInstructionsStrategy ?? "auto");
-
-  // 折叠区域状态管理
-  type SectionKey = "routing" | "rateLimit" | "circuitBreaker" | "proxy" | "codexStrategy";
-  const [openSections, setOpenSections] = useState<Record<SectionKey, boolean>>({
-    routing: false,
-    rateLimit: false,
-    circuitBreaker: false,
-    proxy: false,
-    codexStrategy: false,
-  });
-
-  // 从 localStorage 加载折叠偏好
-  useEffect(() => {
-    const saved = localStorage.getItem("provider-form-sections");
-    if (saved) {
-      try {
-        const parsed = JSON.parse(saved);
-        setOpenSections(parsed);
-      } catch (e) {
-        console.error("Failed to parse saved sections state:", e);
-      }
-    }
-  }, []);
-
-  // 保存折叠状态到 localStorage
-  useEffect(() => {
-    localStorage.setItem("provider-form-sections", JSON.stringify(openSections));
-  }, [openSections]);
-
-  // 自动聚焦名称输入框
-  useEffect(() => {
-    // 延迟聚焦,确保 Dialog 动画完成
-    const timer = setTimeout(() => {
-      nameInputRef.current?.focus();
-    }, 100);
-    return () => clearTimeout(timer);
-  }, []);
-
-  // 折叠区域切换函数
-  const toggleSection = (key: SectionKey) => {
-    setOpenSections((prev) => ({ ...prev, [key]: !prev[key] }));
-  };
-
-  // 展开全部高级配置
-  const expandAll = () => {
-    setOpenSections({
-      routing: true,
-      rateLimit: true,
-      circuitBreaker: true,
-      proxy: true,
-      codexStrategy: true,
-    });
-  };
-
-  // 折叠全部高级配置
-  const collapseAll = () => {
-    setOpenSections({
-      routing: false,
-      rateLimit: false,
-      circuitBreaker: false,
-      proxy: false,
-      codexStrategy: false,
-    });
-  };
-
-  const handleSubmit = (e: React.FormEvent) => {
-    e.preventDefault();
-
-    if (!name.trim() || !url.trim() || (!isEdit && !key.trim())) {
-      return;
-    }
-
-    if (!isValidUrl(url.trim())) {
-      toast.error("请输入有效的 API 地址");
-      return;
-    }
-
-    // 验证 websiteUrl(可选,但如果填写了必须是有效 URL)
-    if (websiteUrl.trim() && !isValidUrl(websiteUrl.trim())) {
-      toast.error("请输入有效的供应商官网地址");
-      return;
-    }
-
-    // 处理模型重定向(空对象转为 null)
-    const parsedModelRedirects = Object.keys(modelRedirects).length > 0 ? modelRedirects : null;
-
-    startTransition(async () => {
-      try {
-        if (isEdit && provider) {
-          const updateData: {
-            name?: string;
-            url?: string;
-            key?: string;
-            provider_type?: ProviderType;
-            model_redirects?: Record<string, string> | null;
-            allowed_models?: string[] | null;
-            join_claude_pool?: boolean;
-            priority?: number;
-            weight?: number;
-            cost_multiplier?: number;
-            group_tag?: string | null;
-            limit_5h_usd?: number | null;
-            limit_weekly_usd?: number | null;
-            limit_monthly_usd?: number | null;
-            limit_concurrent_sessions?: number | null;
-            circuit_breaker_failure_threshold?: number;
-            circuit_breaker_open_duration?: number;
-            circuit_breaker_half_open_success_threshold?: number;
-            proxy_url?: string | null;
-            proxy_fallback_to_direct?: boolean;
-            website_url?: string | null;
-            codex_instructions_strategy?: CodexInstructionsStrategy;
-            tpm?: number | null;
-            rpm?: number | null;
-            rpd?: number | null;
-            cc?: number | null;
-          } = {
-            name: name.trim(),
-            url: url.trim(),
-            provider_type: providerType,
-            model_redirects: parsedModelRedirects,
-            allowed_models: allowedModels.length > 0 ? allowedModels : null,
-            join_claude_pool: joinClaudePool,
-            priority: priority,
-            weight: weight,
-            cost_multiplier: costMultiplier,
-            group_tag: groupTag.trim() || null,
-            limit_5h_usd: limit5hUsd,
-            limit_weekly_usd: limitWeeklyUsd,
-            limit_monthly_usd: limitMonthlyUsd,
-            limit_concurrent_sessions: limitConcurrentSessions,
-            circuit_breaker_failure_threshold: failureThreshold ?? 5,
-            circuit_breaker_open_duration: openDurationMinutes
-              ? openDurationMinutes * 60000
-              : 1800000,
-            circuit_breaker_half_open_success_threshold: halfOpenSuccessThreshold ?? 2,
-            proxy_url: proxyUrl.trim() || null,
-            proxy_fallback_to_direct: proxyFallbackToDirect,
-            website_url: websiteUrl.trim() || null,
-            codex_instructions_strategy: codexInstructionsStrategy,
-            tpm: null,
-            rpm: null,
-            rpd: null,
-            cc: null,
-          };
-          if (key.trim()) {
-            updateData.key = key.trim();
-          }
-          const res = await editProvider(provider.id, updateData);
-          if (!res.ok) {
-            toast.error(res.error || "更新服务商失败");
-            return;
-          }
-        } else {
-          const res = await addProvider({
-            name: name.trim(),
-            url: url.trim(),
-            key: key.trim(),
-            provider_type: providerType,
-            model_redirects: parsedModelRedirects,
-            allowed_models: allowedModels.length > 0 ? allowedModels : null,
-            join_claude_pool: joinClaudePool,
-            // 使用配置的默认值:默认不启用、权重=1
-            is_enabled: PROVIDER_DEFAULTS.IS_ENABLED,
-            weight: weight,
-            priority: priority,
-            cost_multiplier: costMultiplier,
-            group_tag: groupTag.trim() || null,
-            limit_5h_usd: limit5hUsd,
-            limit_weekly_usd: limitWeeklyUsd,
-            limit_monthly_usd: limitMonthlyUsd,
-            limit_concurrent_sessions: limitConcurrentSessions ?? 0,
-            circuit_breaker_failure_threshold: failureThreshold ?? 5,
-            circuit_breaker_open_duration: openDurationMinutes
-              ? openDurationMinutes * 60000
-              : 1800000,
-            circuit_breaker_half_open_success_threshold: halfOpenSuccessThreshold ?? 2,
-            proxy_url: proxyUrl.trim() || null,
-            proxy_fallback_to_direct: proxyFallbackToDirect,
-            website_url: websiteUrl.trim() || null,
-            codex_instructions_strategy: codexInstructionsStrategy,
-            tpm: null,
-            rpm: null,
-            rpd: null,
-            cc: null,
-          });
-          if (!res.ok) {
-            toast.error(res.error || "添加服务商失败");
-            return;
-          }
-          // 添加成功提示
-          toast.success("添加服务商成功", {
-            description: `服务商 "${name.trim()}" 已添加`,
-          });
-          // 重置表单(仅新增)
-          setName("");
-          setUrl("");
-          setKey("");
-          setProviderType("claude");
-          setModelRedirects({});
-          setAllowedModels([]);
-          setJoinClaudePool(false);
-          setPriority(0);
-          setWeight(1);
-          setCostMultiplier(1.0);
-          setGroupTag("");
-          setLimit5hUsd(null);
-          setLimitWeeklyUsd(null);
-          setLimitMonthlyUsd(null);
-          setLimitConcurrentSessions(null);
-          setFailureThreshold(5);
-          setOpenDurationMinutes(30);
-          setHalfOpenSuccessThreshold(2);
-          setProxyUrl("");
-          setProxyFallbackToDirect(false);
-          setWebsiteUrl("");
-          setCodexInstructionsStrategy("auto");
-        }
-        onSuccess?.();
-      } catch (error) {
-        console.error(isEdit ? "更新服务商失败:" : "添加服务商失败:", error);
-        toast.error(isEdit ? "更新服务商失败" : "添加服务商失败");
-      }
-    });
-  };
-
-  return (
-    <div className="space-y-4">
-      <DialogHeader>
-        <DialogTitle>{isEdit ? "编辑服务商" : "新增服务商"}</DialogTitle>
-      </DialogHeader>
-
-      <form onSubmit={handleSubmit} className="space-y-4">
-        <div className="space-y-2">
-          <Label htmlFor={isEdit ? "edit-name" : "name"}>服务商名称 *</Label>
-          <Input
-            ref={nameInputRef}
-            id={isEdit ? "edit-name" : "name"}
-            value={name}
-            onChange={(e) => setName(e.target.value)}
-            placeholder="例如: 智谱"
-            disabled={isPending}
-            required
-          />
-        </div>
-
-        {/* 移除描述字段 */}
-
-        <div className="space-y-2">
-          <Label htmlFor={isEdit ? "edit-url" : "url"}>API 地址 *</Label>
-          <Input
-            id={isEdit ? "edit-url" : "url"}
-            value={url}
-            onChange={(e) => setUrl(e.target.value)}
-            placeholder="例如: https://open.bigmodel.cn/api/anthropic"
-            disabled={isPending}
-            required
-          />
-        </div>
-
-        <div className="space-y-2">
-          <Label htmlFor={isEdit ? "edit-key" : "key"}>
-            API 密钥{isEdit ? "(留空不更改)" : " *"}
-          </Label>
-          <Input
-            id={isEdit ? "edit-key" : "key"}
-            type="password"
-            value={key}
-            onChange={(e) => setKey(e.target.value)}
-            placeholder={isEdit ? "留空则不更改密钥" : "输入 API 密钥"}
-            disabled={isPending}
-            required={!isEdit}
-          />
-          {isEdit && provider ? (
-            <div className="text-xs text-muted-foreground">当前密钥: {provider.maskedKey}</div>
-          ) : null}
-        </div>
-
-        <div className="space-y-2">
-          <Label htmlFor={isEdit ? "edit-website-url" : "website-url"}>供应商官网地址</Label>
-          <Input
-            id={isEdit ? "edit-website-url" : "website-url"}
-            type="url"
-            value={websiteUrl}
-            onChange={(e) => setWebsiteUrl(e.target.value)}
-            placeholder="https://example.com"
-            disabled={isPending}
-          />
-          <div className="text-xs text-muted-foreground">供应商官网地址,用于快速跳转管理</div>
-        </div>
-
-        {/* 展开/折叠全部按钮 */}
-        <div className="flex gap-2 py-2 border-t">
-          <Button
-            type="button"
-            variant="outline"
-            size="sm"
-            onClick={expandAll}
-            disabled={isPending}
-          >
-            展开全部高级配置
-          </Button>
-          <Button
-            type="button"
-            variant="outline"
-            size="sm"
-            onClick={collapseAll}
-            disabled={isPending}
-          >
-            折叠全部高级配置
-          </Button>
-        </div>
-
-        {/* Codex 支持:供应商类型和模型重定向 */}
-        <Collapsible open={openSections.routing} onOpenChange={(open) => toggleSection("routing")}>
-          <CollapsibleTrigger asChild>
-            <button
-              type="button"
-              className="flex items-center justify-between w-full py-4 border-t hover:bg-muted/50 transition-colors"
-              disabled={isPending}
-            >
-              <div className="flex items-center gap-2">
-                <ChevronDown
-                  className={`h-4 w-4 transition-transform ${
-                    openSections.routing ? "rotate-180" : ""
-                  }`}
-                />
-                <span className="text-sm font-medium">路由配置</span>
-              </div>
-              <span className="text-xs text-muted-foreground">
-                {(() => {
-                  const parts = [];
-                  if (allowedModels.length > 0) parts.push(`${allowedModels.length} 个模型白名单`);
-                  if (Object.keys(modelRedirects).length > 0)
-                    parts.push(`${Object.keys(modelRedirects).length} 个重定向`);
-                  return parts.length > 0 ? parts.join(", ") : "未配置";
-                })()}
-              </span>
-            </button>
-          </CollapsibleTrigger>
-          <CollapsibleContent className="space-y-4 pb-4">
-            <div className="space-y-4">
-              <div className="space-y-2">
-                <Label htmlFor={isEdit ? "edit-provider-type" : "provider-type"}>
-                  供应商类型
-                  <span className="text-xs text-muted-foreground ml-1">(决定调度策略)</span>
-                </Label>
-                <Select
-                  value={providerType}
-                  onValueChange={(value) => setProviderType(value as ProviderType)}
-                  disabled={isPending}
-                >
-                  <SelectTrigger id={isEdit ? "edit-provider-type" : "provider-type"}>
-                    <SelectValue placeholder="选择供应商类型" />
-                  </SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="claude">Claude (Anthropic Messages API)</SelectItem>
-                    <SelectItem value="claude-auth">Claude (Anthropic Auth Token)</SelectItem>
-                    <SelectItem value="codex">Codex (Response API)</SelectItem>
-                    <SelectItem value="gemini-cli" disabled={!enableMultiProviderTypes}>
-                      Gemini CLI{!enableMultiProviderTypes && " - 功能开发中"}
-                    </SelectItem>
-                    <SelectItem value="openai-compatible" disabled={!enableMultiProviderTypes}>
-                      OpenAI Compatible{!enableMultiProviderTypes && " - 功能开发中"}
-                    </SelectItem>
-                  </SelectContent>
-                </Select>
-                <p className="text-xs text-muted-foreground">
-                  选择供应商的 API 格式类型。
-                  {!enableMultiProviderTypes && (
-                    <span className="text-amber-600 ml-1">
-                      注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用
-                    </span>
-                  )}
-                </p>
-              </div>
-
-              <div className="space-y-2">
-                <Label>
-                  模型重定向配置
-                  <span className="text-xs text-muted-foreground ml-1">(可选)</span>
-                </Label>
-                <ModelRedirectEditor
-                  value={modelRedirects}
-                  onChange={setModelRedirects}
-                  disabled={isPending}
-                />
-              </div>
-
-              {/* joinClaudePool 开关 - 仅非 Claude 供应商显示 */}
-              {providerType !== "claude" &&
-                (() => {
-                  // 检查是否有重定向到 Claude 模型的映射
-                  const hasClaudeRedirects = Object.values(modelRedirects).some((target) =>
-                    target.startsWith("claude-")
-                  );
-
-                  if (!hasClaudeRedirects) return null;
-
-                  return (
-                    <div className="space-y-2">
-                      <div className="flex items-center justify-between">
-                        <div className="space-y-0.5">
-                          <Label htmlFor={isEdit ? "edit-join-claude-pool" : "join-claude-pool"}>
-                            加入 Claude 调度池
-                          </Label>
-                          <p className="text-xs text-muted-foreground">
-                            启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度
-                          </p>
-                        </div>
-                        <Switch
-                          id={isEdit ? "edit-join-claude-pool" : "join-claude-pool"}
-                          checked={joinClaudePool}
-                          onCheckedChange={setJoinClaudePool}
-                          disabled={isPending}
-                        />
-                      </div>
-                      <p className="text-xs text-muted-foreground">
-                        仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求
-                        claude-* 模型时,此供应商也会参与调度选择。
-                      </p>
-                    </div>
-                  );
-                })()}
-
-              {/* 模型白名单配置 */}
-              <div className="space-y-1">
-                <div className="text-sm font-medium">模型白名单</div>
-                <p className="text-xs text-muted-foreground">
-                  限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。
-                </p>
-              </div>
-
-              <div className="space-y-2">
-                <Label htmlFor="allowed-models">
-                  允许的模型
-                  <span className="text-xs text-muted-foreground ml-1">(可选)</span>
-                </Label>
-
-                <ModelMultiSelect
-                  providerType={
-                    providerType as "claude" | "codex" | "gemini-cli" | "openai-compatible"
-                  }
-                  selectedModels={allowedModels}
-                  onChange={setAllowedModels}
-                  disabled={isPending}
-                />
-
-                {allowedModels.length > 0 && (
-                  <div className="flex flex-wrap gap-1 mt-2 p-2 bg-muted/50 rounded-md">
-                    {allowedModels.slice(0, 5).map((model) => (
-                      <Badge key={model} variant="outline" className="font-mono text-xs">
-                        {model}
-                      </Badge>
-                    ))}
-                    {allowedModels.length > 5 && (
-                      <Badge variant="secondary" className="text-xs">
-                        +{allowedModels.length - 5} 更多
-                      </Badge>
-                    )}
-                  </div>
-                )}
-
-                <p className="text-xs text-muted-foreground">
-                  {allowedModels.length === 0 ? (
-                    <span className="text-green-600">✓ 允许所有模型(推荐)</span>
-                  ) : (
-                    <span>
-                      仅允许选中的 {allowedModels.length} 个模型。其他模型的请求不会调度到此供应商。
-                    </span>
-                  )}
-                </p>
-              </div>
-
-              {/* 路由配置 - 优先级、权重、成本 */}
-              <div className="space-y-4">
-                <div className="text-sm font-medium">调度参数</div>
-                <div className="grid grid-cols-3 gap-4">
-                  <div className="space-y-2">
-                    <Label htmlFor={isEdit ? "edit-priority" : "priority"}>优先级</Label>
-                    <Input
-                      id={isEdit ? "edit-priority" : "priority"}
-                      type="number"
-                      value={priority}
-                      onChange={(e) => setPriority(parseInt(e.target.value) || 0)}
-                      placeholder="0"
-                      disabled={isPending}
-                      min="0"
-                      step="1"
-                    />
-                    <p className="text-xs text-muted-foreground">
-                      数值越小优先级越高(0
-                      最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2
-                    </p>
-                  </div>
-                  <div className="space-y-2">
-                    <Label htmlFor={isEdit ? "edit-weight" : "weight"}>权重</Label>
-                    <Input
-                      id={isEdit ? "edit-weight" : "weight"}
-                      type="number"
-                      value={weight}
-                      onChange={(e) => setWeight(parseInt(e.target.value) || 1)}
-                      placeholder="1"
-                      disabled={isPending}
-                      min="1"
-                      step="1"
-                    />
-                    <p className="text-xs text-muted-foreground">
-                      加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为
-                      16%:33%:50%
-                    </p>
-                  </div>
-                  <div className="space-y-2">
-                    <Label htmlFor={isEdit ? "edit-cost" : "cost"}>成本倍率</Label>
-                    <Input
-                      id={isEdit ? "edit-cost" : "cost"}
-                      type="number"
-                      value={costMultiplier}
-                      onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1.0)}
-                      placeholder="1.0"
-                      disabled={isPending}
-                      min="0"
-                      step="0.0001"
-                    />
-                    <p className="text-xs text-muted-foreground">
-                      成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)
-                    </p>
-                  </div>
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-group" : "group"}>供应商分组</Label>
-                  <Input
-                    id={isEdit ? "edit-group" : "group"}
-                    value={groupTag}
-                    onChange={(e) => setGroupTag(e.target.value)}
-                    placeholder="例如: premium, economy"
-                    disabled={isPending}
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    供应商分组标签。只有用户的 providerGroup
-                    与此值匹配时,该用户才能使用此供应商。示例:设置为 &quot;premium&quot; 表示只供
-                    providerGroup=&quot;premium&quot; 的用户使用
-                  </p>
-                </div>
-              </div>
-            </div>
-          </CollapsibleContent>
-        </Collapsible>
-
-        {/* 限流配置 */}
-        <Collapsible
-          open={openSections.rateLimit}
-          onOpenChange={(open) => toggleSection("rateLimit")}
-        >
-          <CollapsibleTrigger asChild>
-            <button
-              type="button"
-              className="flex items-center justify-between w-full py-4 border-t hover:bg-muted/50 transition-colors"
-              disabled={isPending}
-            >
-              <div className="flex items-center gap-2">
-                <ChevronDown
-                  className={`h-4 w-4 transition-transform ${
-                    openSections.rateLimit ? "rotate-180" : ""
-                  }`}
-                />
-                <span className="text-sm font-medium">限流配置</span>
-              </div>
-              <span className="text-xs text-muted-foreground">
-                {(() => {
-                  const limits = [];
-                  if (limit5hUsd) limits.push(`5h: $${limit5hUsd}`);
-                  if (limitWeeklyUsd) limits.push(`周: $${limitWeeklyUsd}`);
-                  if (limitMonthlyUsd) limits.push(`月: $${limitMonthlyUsd}`);
-                  if (limitConcurrentSessions) limits.push(`并发: ${limitConcurrentSessions}`);
-                  return limits.length > 0 ? limits.join(", ") : "无限制";
-                })()}
-              </span>
-            </button>
-          </CollapsibleTrigger>
-          <CollapsibleContent className="space-y-4 pb-4">
-            <div className="space-y-4">
-              <div className="grid grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-limit-5h" : "limit-5h"}>5小时消费上限 (USD)</Label>
-                  <Input
-                    id={isEdit ? "edit-limit-5h" : "limit-5h"}
-                    type="number"
-                    value={limit5hUsd?.toString() ?? ""}
-                    onChange={(e) => setLimit5hUsd(validateNumericField(e.target.value))}
-                    placeholder="留空表示无限制"
-                    disabled={isPending}
-                    min="0"
-                    step="0.01"
-                  />
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-limit-weekly" : "limit-weekly"}>
-                    周消费上限 (USD)
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-limit-weekly" : "limit-weekly"}
-                    type="number"
-                    value={limitWeeklyUsd?.toString() ?? ""}
-                    onChange={(e) => setLimitWeeklyUsd(validateNumericField(e.target.value))}
-                    placeholder="留空表示无限制"
-                    disabled={isPending}
-                    min="0"
-                    step="0.01"
-                  />
-                </div>
-              </div>
-
-              <div className="grid grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-limit-monthly" : "limit-monthly"}>
-                    月消费上限 (USD)
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-limit-monthly" : "limit-monthly"}
-                    type="number"
-                    value={limitMonthlyUsd?.toString() ?? ""}
-                    onChange={(e) => setLimitMonthlyUsd(validateNumericField(e.target.value))}
-                    placeholder="留空表示无限制"
-                    disabled={isPending}
-                    min="0"
-                    step="0.01"
-                  />
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-limit-concurrent" : "limit-concurrent"}>
-                    并发 Session 上限
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-limit-concurrent" : "limit-concurrent"}
-                    type="number"
-                    value={limitConcurrentSessions?.toString() ?? ""}
-                    onChange={(e) =>
-                      setLimitConcurrentSessions(validateNumericField(e.target.value))
-                    }
-                    placeholder="0 表示无限制"
-                    disabled={isPending}
-                    min="0"
-                    step="1"
-                  />
-                </div>
-              </div>
-            </div>
-          </CollapsibleContent>
-        </Collapsible>
-
-        {/* 熔断器配置 */}
-        <Collapsible
-          open={openSections.circuitBreaker}
-          onOpenChange={(open) => toggleSection("circuitBreaker")}
-        >
-          <CollapsibleTrigger asChild>
-            <button
-              type="button"
-              className="flex items-center justify-between w-full py-4 border-t hover:bg-muted/50 transition-colors"
-              disabled={isPending}
-            >
-              <div className="flex items-center gap-2">
-                <ChevronDown
-                  className={`h-4 w-4 transition-transform ${
-                    openSections.circuitBreaker ? "rotate-180" : ""
-                  }`}
-                />
-                <span className="text-sm font-medium">熔断器配置</span>
-              </div>
-              <span className="text-xs text-muted-foreground">
-                {failureThreshold ?? 5} 次失败 / {openDurationMinutes ?? 30} 分钟熔断 /{" "}
-                {halfOpenSuccessThreshold ?? 2} 次成功恢复
-              </span>
-            </button>
-          </CollapsibleTrigger>
-          <CollapsibleContent className="space-y-4 pb-4">
-            <div className="space-y-4">
-              <div className="space-y-1">
-                <p className="text-xs text-muted-foreground">
-                  供应商连续失败时自动熔断,避免影响整体服务质量
-                </p>
-              </div>
-              <div className="grid grid-cols-3 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-failure-threshold" : "failure-threshold"}>
-                    失败阈值(次)
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-failure-threshold" : "failure-threshold"}
-                    type="number"
-                    value={failureThreshold ?? ""}
-                    onChange={(e) => {
-                      const val = e.target.value;
-                      setFailureThreshold(val === "" ? undefined : parseInt(val));
-                    }}
-                    placeholder="5"
-                    disabled={isPending}
-                    min="1"
-                    max="100"
-                    step="1"
-                  />
-                  <p className="text-xs text-muted-foreground">连续失败多少次后触发熔断</p>
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-open-duration" : "open-duration"}>
-                    熔断时长(分钟)
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-open-duration" : "open-duration"}
-                    type="number"
-                    value={openDurationMinutes ?? ""}
-                    onChange={(e) => {
-                      const val = e.target.value;
-                      setOpenDurationMinutes(val === "" ? undefined : parseInt(val));
-                    }}
-                    placeholder="30"
-                    disabled={isPending}
-                    min="1"
-                    max="1440"
-                    step="1"
-                  />
-                  <p className="text-xs text-muted-foreground">熔断后多久自动进入半开状态</p>
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-success-threshold" : "success-threshold"}>
-                    恢复阈值(次)
-                  </Label>
-                  <Input
-                    id={isEdit ? "edit-success-threshold" : "success-threshold"}
-                    type="number"
-                    value={halfOpenSuccessThreshold ?? ""}
-                    onChange={(e) => {
-                      const val = e.target.value;
-                      setHalfOpenSuccessThreshold(val === "" ? undefined : parseInt(val));
-                    }}
-                    placeholder="2"
-                    disabled={isPending}
-                    min="1"
-                    max="10"
-                    step="1"
-                  />
-                  <p className="text-xs text-muted-foreground">半开状态下成功多少次后完全恢复</p>
-                </div>
-              </div>
-            </div>
-          </CollapsibleContent>
-        </Collapsible>
-
-        {/* 代理配置 */}
-        <Collapsible open={openSections.proxy} onOpenChange={(open) => toggleSection("proxy")}>
-          <CollapsibleTrigger asChild>
-            <button
-              type="button"
-              className="flex items-center justify-between w-full py-4 border-t hover:bg-muted/50 transition-colors"
-              disabled={isPending}
-            >
-              <div className="flex items-center gap-2">
-                <ChevronDown
-                  className={`h-4 w-4 transition-transform ${
-                    openSections.proxy ? "rotate-180" : ""
-                  }`}
-                />
-                <span className="text-sm font-medium">代理配置</span>
-              </div>
-              <span className="text-xs text-muted-foreground">
-                {proxyUrl.trim() ? "已配置代理" : "未配置"}
-                {proxyUrl.trim() && proxyFallbackToDirect ? " (启用降级)" : ""}
-              </span>
-            </button>
-          </CollapsibleTrigger>
-          <CollapsibleContent className="space-y-4 pb-4">
-            <div className="space-y-4">
-              <div className="space-y-1">
-                <p className="text-xs text-muted-foreground">
-                  配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)
-                </p>
-              </div>
-
-              {/* 代理地址输入 */}
-              <div className="space-y-2">
-                <Label htmlFor={isEdit ? "edit-proxy-url" : "proxy-url"}>
-                  代理地址
-                  <span className="text-xs text-muted-foreground ml-1">(可选)</span>
-                </Label>
-                <Input
-                  id={isEdit ? "edit-proxy-url" : "proxy-url"}
-                  value={proxyUrl}
-                  onChange={(e) => setProxyUrl(e.target.value)}
-                  placeholder="例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080"
-                  disabled={isPending}
-                />
-                <p className="text-xs text-muted-foreground">
-                  支持格式: <code className="bg-muted px-1 rounded">http://</code>、
-                  <code className="bg-muted px-1 rounded">https://</code>、
-                  <code className="bg-muted px-1 rounded">socks4://</code>、
-                  <code className="bg-muted px-1 rounded">socks5://</code>
-                </p>
-              </div>
-
-              {/* 降级策略开关 */}
-              <div className="space-y-2">
-                <div className="flex items-center justify-between">
-                  <div className="space-y-0.5">
-                    <Label htmlFor={isEdit ? "edit-proxy-fallback" : "proxy-fallback"}>
-                      代理失败时降级到直连
-                    </Label>
-                    <p className="text-xs text-muted-foreground">
-                      启用后,代理连接失败时自动尝试直接连接供应商
-                    </p>
-                  </div>
-                  <Switch
-                    id={isEdit ? "edit-proxy-fallback" : "proxy-fallback"}
-                    checked={proxyFallbackToDirect}
-                    onCheckedChange={setProxyFallbackToDirect}
-                    disabled={isPending}
-                  />
-                </div>
-              </div>
-
-              {/* 测试连接按钮 */}
-              <div className="space-y-2">
-                <Label>连接测试</Label>
-                <ProxyTestButton
-                  providerUrl={url}
-                  proxyUrl={proxyUrl}
-                  proxyFallbackToDirect={proxyFallbackToDirect}
-                  disabled={isPending || !url.trim()}
-                />
-                <p className="text-xs text-muted-foreground">
-                  测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)
-                </p>
-              </div>
-            </div>
-          </CollapsibleContent>
-        </Collapsible>
-
-        {/* Codex Instructions 策略配置 - 仅 Codex 供应商显示 */}
-        {providerType === "codex" && (
-          <Collapsible
-            open={openSections.codexStrategy}
-            onOpenChange={(open) => toggleSection("codexStrategy")}
-          >
-            <CollapsibleTrigger asChild>
-              <button
-                type="button"
-                className="flex items-center justify-between w-full py-4 border-t hover:bg-muted/50 transition-colors"
-                disabled={isPending}
-              >
-                <div className="flex items-center gap-2">
-                  <ChevronDown
-                    className={`h-4 w-4 transition-transform ${
-                      openSections.codexStrategy ? "rotate-180" : ""
-                    }`}
-                  />
-                  <span className="text-sm font-medium">Codex Instructions 策略</span>
-                </div>
-                <span className="text-xs text-muted-foreground">
-                  {codexInstructionsStrategy === "auto" && "自动 (推荐)"}
-                  {codexInstructionsStrategy === "force_official" && "强制官方"}
-                  {codexInstructionsStrategy === "keep_original" && "透传原样"}
-                </span>
-              </button>
-            </CollapsibleTrigger>
-            <CollapsibleContent className="space-y-4 pb-4">
-              <div className="space-y-4">
-                <div className="space-y-1">
-                  <p className="text-xs text-muted-foreground">
-                    控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性
-                  </p>
-                </div>
-
-                <div className="space-y-2">
-                  <Label htmlFor={isEdit ? "edit-codex-strategy" : "codex-strategy"}>
-                    策略选择
-                  </Label>
-                  <Select
-                    value={codexInstructionsStrategy}
-                    onValueChange={(value) =>
-                      setCodexInstructionsStrategy(value as CodexInstructionsStrategy)
-                    }
-                    disabled={isPending}
-                  >
-                    <SelectTrigger id={isEdit ? "edit-codex-strategy" : "codex-strategy"}>
-                      <SelectValue placeholder="选择策略" />
-                    </SelectTrigger>
-                    <SelectContent>
-                      <SelectItem value="auto">
-                        <div className="space-y-1">
-                          <div className="font-medium">自动 (推荐)</div>
-                          <div className="text-xs text-muted-foreground max-w-xs">
-                            透传客户端 instructions,400 错误时自动重试官方 prompt
-                          </div>
-                        </div>
-                      </SelectItem>
-                      <SelectItem value="force_official">
-                        <div className="space-y-1">
-                          <div className="font-medium">强制官方</div>
-                          <div className="text-xs text-muted-foreground max-w-xs">
-                            始终使用官方 Codex CLI instructions(约 4000+ 字)
-                          </div>
-                        </div>
-                      </SelectItem>
-                      <SelectItem value="keep_original">
-                        <div className="space-y-1">
-                          <div className="font-medium">透传原样</div>
-                          <div className="text-xs text-muted-foreground max-w-xs">
-                            始终透传客户端 instructions,不自动重试(适用于宽松中转站)
-                          </div>
-                        </div>
-                      </SelectItem>
-                    </SelectContent>
-                  </Select>
-                  <p className="text-xs text-muted-foreground">
-                    <strong>提示</strong>: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方
-                    instructions,选择&quot;自动&quot;或&quot;强制官方&quot;策略
-                  </p>
-                </div>
-              </div>
-            </CollapsibleContent>
-          </Collapsible>
-        )}
-
-        {isEdit ? (
-          <div className="flex items-center justify-between pt-4">
-            <AlertDialog>
-              <AlertDialogTrigger asChild>
-                <Button type="button" variant="destructive" disabled={isPending}>
-                  删除
-                </Button>
-              </AlertDialogTrigger>
-              <AlertDialogContent>
-                <AlertHeader>
-                  <AlertTitle>删除服务商</AlertTitle>
-                  <AlertDialogDescription>
-                    确定要删除服务商&ldquo;{provider?.name}&rdquo;吗?此操作不可恢复。
-                  </AlertDialogDescription>
-                </AlertHeader>
-                <div className="flex gap-2 justify-end">
-                  <AlertDialogCancel>取消</AlertDialogCancel>
-                  <AlertDialogAction
-                    onClick={() => {
-                      if (!provider) return;
-                      startTransition(async () => {
-                        try {
-                          const res = await removeProvider(provider.id);
-                          if (!res.ok) {
-                            toast.error(res.error || "删除服务商失败");
-                            return;
-                          }
-                          onSuccess?.();
-                        } catch (e) {
-                          console.error("删除服务商失败", e);
-                          toast.error("删除服务商失败");
-                        }
-                      });
-                    }}
-                  >
-                    确认删除
-                  </AlertDialogAction>
-                </div>
-              </AlertDialogContent>
-            </AlertDialog>
-
-            <Button type="submit" disabled={isPending}>
-              {isPending ? "更新中..." : "确认更新"}
-            </Button>
-          </div>
-        ) : (
-          <div className="flex justify-end gap-2 pt-4">
-            <Button type="submit" disabled={isPending}>
-              {isPending ? "添加中..." : "确认添加"}
-            </Button>
-          </div>
-        )}
-      </form>
-    </div>
-  );
-}

+ 0 - 179
src/app/\[locale\]/settings/providers/_components/forms/proxy-test-button.tsx

@@ -1,179 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import { Loader2, CheckCircle2, XCircle, Activity } from "lucide-react";
-import { testProviderProxy } from "@/actions/providers";
-import { toast } from "sonner";
-
-interface ProxyTestButtonProps {
-  providerUrl: string;
-  proxyUrl?: string | null;
-  proxyFallbackToDirect?: boolean;
-  disabled?: boolean;
-}
-
-/**
- * 代理连接测试按钮组件
- *
- * 通过配置的代理访问供应商 URL,验证代理配置是否正确
- */
-export function ProxyTestButton({
-  providerUrl,
-  proxyUrl,
-  proxyFallbackToDirect = false,
-  disabled = false,
-}: ProxyTestButtonProps) {
-  const [isTesting, setIsTesting] = useState(false);
-  const [testResult, setTestResult] = useState<{
-    success: boolean;
-    message: string;
-    details?: {
-      statusCode?: number;
-      responseTime?: number;
-      usedProxy?: boolean;
-      proxyUrl?: string;
-      error?: string;
-      errorType?: string;
-    };
-  } | null>(null);
-
-  const handleTest = async () => {
-    // 验证必填字段
-    if (!providerUrl.trim()) {
-      toast.error("请先填写供应商 URL");
-      return;
-    }
-
-    setIsTesting(true);
-    setTestResult(null);
-
-    try {
-      const response = await testProviderProxy({
-        providerUrl: providerUrl.trim(),
-        proxyUrl: proxyUrl?.trim() || null,
-        proxyFallbackToDirect,
-      });
-
-      if (!response.ok) {
-        toast.error(response.error || "测试失败");
-        return;
-      }
-
-      if (!response.data) {
-        toast.error("测试成功但未返回结果");
-        return;
-      }
-
-      setTestResult(response.data);
-
-      // 显示测试结果
-      if (response.data.success) {
-        const details = response.data.details;
-        const proxyUsed = details?.usedProxy ? "(通过代理)" : "(直连)";
-        const responseTime = details?.responseTime ? `${details.responseTime}ms` : "N/A";
-
-        toast.success(`连接成功 ${proxyUsed}`, {
-          description: `响应时间: ${responseTime}${details?.statusCode ? ` | 状态码: ${details.statusCode}` : ""}`,
-        });
-      } else {
-        const errorType = response.data.details?.errorType;
-        const errorMessage = response.data.details?.error || response.data.message;
-
-        toast.error("连接失败", {
-          description:
-            errorType === "Timeout"
-              ? "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确"
-              : errorType === "ProxyError"
-                ? `代理错误: ${errorMessage}`
-                : `网络错误: ${errorMessage}`,
-          duration: 5000, // 延长显示时间,让用户看清楚诊断提示
-        });
-      }
-    } catch (error) {
-      console.error("测试代理连接失败:", error);
-      toast.error("测试失败,请重试");
-    } finally {
-      setIsTesting(false);
-    }
-  };
-
-  // 确定按钮图标和样式
-  const getButtonContent = () => {
-    if (isTesting) {
-      return (
-        <>
-          <Loader2 className="h-4 w-4 mr-2 animate-spin" />
-          测试中...
-        </>
-      );
-    }
-
-    if (testResult) {
-      if (testResult.success) {
-        return (
-          <>
-            <CheckCircle2 className="h-4 w-4 mr-2 text-green-600" />
-            连接成功
-          </>
-        );
-      } else {
-        return (
-          <>
-            <XCircle className="h-4 w-4 mr-2 text-red-600" />
-            连接失败
-          </>
-        );
-      }
-    }
-
-    return (
-      <>
-        <Activity className="h-4 w-4 mr-2" />
-        测试连接
-      </>
-    );
-  };
-
-  return (
-    <div className="space-y-2">
-      <Button
-        type="button"
-        variant="outline"
-        size="sm"
-        onClick={handleTest}
-        disabled={disabled || isTesting || !providerUrl.trim()}
-      >
-        {getButtonContent()}
-      </Button>
-
-      {/* 显示详细测试结果 */}
-      {testResult && !isTesting && (
-        <div
-          className={`text-xs p-2 rounded-md ${
-            testResult.success
-              ? "bg-green-50 text-green-700 border border-green-200"
-              : "bg-red-50 text-red-700 border border-red-200"
-          }`}
-        >
-          <div className="font-medium mb-1">{testResult.message}</div>
-          {testResult.details && (
-            <div className="space-y-0.5 text-xs opacity-80">
-              {testResult.details.statusCode && <div>状态码: {testResult.details.statusCode}</div>}
-              {testResult.details.responseTime !== undefined && (
-                <div>响应时间: {testResult.details.responseTime}ms</div>
-              )}
-              {testResult.details.usedProxy !== undefined && (
-                <div>
-                  连接方式: {testResult.details.usedProxy ? "代理" : "直连"}
-                  {testResult.details.proxyUrl && ` (${testResult.details.proxyUrl})`}
-                </div>
-              )}
-              {testResult.details.errorType && <div>错误类型: {testResult.details.errorType}</div>}
-            </div>
-          )}
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 275
src/app/\[locale\]/settings/providers/_components/hooks/use-provider-edit.ts

@@ -1,275 +0,0 @@
-import { useRef, useState } from "react";
-import { useRouter } from "next/navigation";
-import { logger } from "@/lib/logger";
-import { toast } from "sonner";
-import { editProvider } from "@/actions/providers";
-import type { ProviderDisplay } from "@/types/provider";
-import { clampWeight } from "@/lib/utils/validation";
-import { PROVIDER_LIMITS } from "@/lib/constants/provider.constants";
-
-export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
-  const router = useRouter();
-  // 基本状态
-  const [enabled, setEnabled] = useState<boolean>(item.isEnabled);
-  const [togglePending, setTogglePending] = useState(false);
-
-  // 权重编辑
-  const [showWeight, setShowWeight] = useState(false);
-  const [weight, setWeight] = useState<number>(clampWeight(item.weight));
-  const initialWeightRef = useRef<number>(item.weight);
-
-  // 5小时消费上限
-  const [show5hLimit, setShow5hLimit] = useState(false);
-  const [limit5hInfinite, setLimit5hInfinite] = useState<boolean>(item.limit5hUsd === null);
-  const [limit5hValue, setLimit5hValue] = useState<number>(() => {
-    return item.limit5hUsd ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN;
-  });
-  const initial5hRef = useRef<number | null>(item.limit5hUsd);
-
-  // 周消费上限
-  const [showWeeklyLimit, setShowWeeklyLimit] = useState(false);
-  const [limitWeeklyInfinite, setLimitWeeklyInfinite] = useState<boolean>(
-    item.limitWeeklyUsd === null
-  );
-  const [limitWeeklyValue, setLimitWeeklyValue] = useState<number>(() => {
-    return item.limitWeeklyUsd ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN;
-  });
-  const initialWeeklyRef = useRef<number | null>(item.limitWeeklyUsd);
-
-  // 月消费上限
-  const [showMonthlyLimit, setShowMonthlyLimit] = useState(false);
-  const [limitMonthlyInfinite, setLimitMonthlyInfinite] = useState<boolean>(
-    item.limitMonthlyUsd === null
-  );
-  const [limitMonthlyValue, setLimitMonthlyValue] = useState<number>(() => {
-    return item.limitMonthlyUsd ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN;
-  });
-  const initialMonthlyRef = useRef<number | null>(item.limitMonthlyUsd);
-
-  // 并发Session上限
-  const [showConcurrent, setShowConcurrent] = useState(false);
-  const [concurrentInfinite, setConcurrentInfinite] = useState<boolean>(
-    item.limitConcurrentSessions === 0
-  );
-  const [concurrentValue, setConcurrentValue] = useState<number>(() => {
-    return item.limitConcurrentSessions === 0
-      ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN
-      : item.limitConcurrentSessions;
-  });
-  const initialConcurrentRef = useRef<number>(item.limitConcurrentSessions);
-
-  // 切换启用状态
-  const handleToggle = async (next: boolean) => {
-    if (!canEdit || togglePending) return;
-    setTogglePending(true);
-    const prev = enabled;
-    setEnabled(next);
-
-    try {
-      const res = await editProvider(item.id, { is_enabled: next });
-      if (!res.ok) {
-        throw new Error(res.error);
-      }
-      // 刷新页面数据以同步所有字段
-      router.refresh();
-    } catch (e) {
-      logger.error("切换服务商启用状态失败", { context: e });
-      setEnabled(prev);
-      const msg = e instanceof Error ? e.message : "切换失败";
-      toast.error(msg);
-    } finally {
-      setTogglePending(false);
-    }
-  };
-
-  // 权重编辑处理
-  const handleWeightPopover = (open: boolean) => {
-    if (!canEdit) return;
-    setShowWeight(open);
-    if (open) {
-      initialWeightRef.current = clampWeight(weight);
-      return;
-    }
-
-    const next = clampWeight(weight);
-    if (next !== clampWeight(initialWeightRef.current)) {
-      editProvider(item.id, { weight: next })
-        .then((res) => {
-          if (!res.ok) throw new Error(res.error);
-          // 刷新页面数据以同步所有字段
-          router.refresh();
-        })
-        .catch((e) => {
-          logger.error("更新权重失败", { context: e });
-          const msg = e instanceof Error ? e.message : "更新权重失败";
-          toast.error(msg);
-          setWeight(clampWeight(initialWeightRef.current));
-        });
-    }
-  };
-
-  // 5小时消费上限编辑处理
-  const handle5hLimitPopover = (open: boolean) => {
-    if (!canEdit) return;
-    setShow5hLimit(open);
-    if (open) {
-      initial5hRef.current = item.limit5hUsd;
-      return;
-    }
-
-    const nextValue = limit5hInfinite
-      ? null
-      : Math.max(PROVIDER_LIMITS.LIMIT_5H_USD.MIN, limit5hValue);
-    if (nextValue !== initial5hRef.current) {
-      editProvider(item.id, { limit_5h_usd: nextValue })
-        .then((res) => {
-          if (!res.ok) throw new Error(res.error);
-          // 刷新页面数据以同步所有字段
-          router.refresh();
-        })
-        .catch((e) => {
-          logger.error("更新5小时消费上限失败", { context: e });
-          const msg = e instanceof Error ? e.message : "更新5小时消费上限失败";
-          toast.error(msg);
-          setLimit5hInfinite(initial5hRef.current === null);
-          setLimit5hValue(initial5hRef.current ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN);
-        });
-    }
-  };
-
-  // 周消费上限编辑处理
-  const handleWeeklyLimitPopover = (open: boolean) => {
-    if (!canEdit) return;
-    setShowWeeklyLimit(open);
-    if (open) {
-      initialWeeklyRef.current = item.limitWeeklyUsd;
-      return;
-    }
-
-    const nextValue = limitWeeklyInfinite
-      ? null
-      : Math.max(PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN, limitWeeklyValue);
-    if (nextValue !== initialWeeklyRef.current) {
-      editProvider(item.id, { limit_weekly_usd: nextValue })
-        .then((res) => {
-          if (!res.ok) throw new Error(res.error);
-          // 刷新页面数据以同步所有字段
-          router.refresh();
-        })
-        .catch((e) => {
-          logger.error("更新周消费上限失败", { context: e });
-          const msg = e instanceof Error ? e.message : "更新周消费上限失败";
-          toast.error(msg);
-          setLimitWeeklyInfinite(initialWeeklyRef.current === null);
-          setLimitWeeklyValue(initialWeeklyRef.current ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN);
-        });
-    }
-  };
-
-  // 月消费上限编辑处理
-  const handleMonthlyLimitPopover = (open: boolean) => {
-    if (!canEdit) return;
-    setShowMonthlyLimit(open);
-    if (open) {
-      initialMonthlyRef.current = item.limitMonthlyUsd;
-      return;
-    }
-
-    const nextValue = limitMonthlyInfinite
-      ? null
-      : Math.max(PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN, limitMonthlyValue);
-    if (nextValue !== initialMonthlyRef.current) {
-      editProvider(item.id, { limit_monthly_usd: nextValue })
-        .then((res) => {
-          if (!res.ok) throw new Error(res.error);
-          // 刷新页面数据以同步所有字段
-          router.refresh();
-        })
-        .catch((e) => {
-          logger.error("更新月消费上限失败", { context: e });
-          const msg = e instanceof Error ? e.message : "更新月消费上限失败";
-          toast.error(msg);
-          setLimitMonthlyInfinite(initialMonthlyRef.current === null);
-          setLimitMonthlyValue(initialMonthlyRef.current ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN);
-        });
-    }
-  };
-
-  // 并发Session上限编辑处理
-  const handleConcurrentPopover = (open: boolean) => {
-    if (!canEdit) return;
-    setShowConcurrent(open);
-    if (open) {
-      initialConcurrentRef.current = item.limitConcurrentSessions;
-      return;
-    }
-
-    const nextValue = concurrentInfinite
-      ? 0
-      : Math.max(PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN, concurrentValue);
-    if (nextValue !== initialConcurrentRef.current) {
-      editProvider(item.id, { limit_concurrent_sessions: nextValue })
-        .then((res) => {
-          if (!res.ok) throw new Error(res.error);
-          // 刷新页面数据以同步所有字段
-          router.refresh();
-        })
-        .catch((e) => {
-          logger.error("更新并发Session上限失败", { context: e });
-          const msg = e instanceof Error ? e.message : "更新并发Session上限失败";
-          toast.error(msg);
-          setConcurrentInfinite(initialConcurrentRef.current === 0);
-          setConcurrentValue(
-            initialConcurrentRef.current === 0
-              ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN
-              : initialConcurrentRef.current
-          );
-        });
-    }
-  };
-
-  return {
-    // 状态
-    enabled,
-    togglePending,
-    weight,
-    setWeight,
-    showWeight,
-
-    // 5小时消费上限
-    limit5hInfinite,
-    setLimit5hInfinite,
-    limit5hValue,
-    setLimit5hValue,
-    show5hLimit,
-
-    // 周消费上限
-    limitWeeklyInfinite,
-    setLimitWeeklyInfinite,
-    limitWeeklyValue,
-    setLimitWeeklyValue,
-    showWeeklyLimit,
-
-    // 月消费上限
-    limitMonthlyInfinite,
-    setLimitMonthlyInfinite,
-    limitMonthlyValue,
-    setLimitMonthlyValue,
-    showMonthlyLimit,
-
-    // 并发Session上限
-    concurrentInfinite,
-    setConcurrentInfinite,
-    concurrentValue,
-    setConcurrentValue,
-    showConcurrent,
-
-    // 处理函数
-    handleToggle,
-    handleWeightPopover,
-    handle5hLimitPopover,
-    handleWeeklyLimitPopover,
-    handleMonthlyLimitPopover,
-    handleConcurrentPopover,
-  };
-}

+ 0 - 201
src/app/\[locale\]/settings/providers/_components/model-multi-select.tsx

@@ -1,201 +0,0 @@
-"use client";
-import { useState, useEffect } from "react";
-import { Check, ChevronsUpDown, Loader2, Plus } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from "@/components/ui/command";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { getAvailableModelsByProviderType } from "@/actions/model-prices";
-
-interface ModelMultiSelectProps {
-  providerType: "claude" | "codex" | "gemini-cli" | "openai-compatible";
-  selectedModels: string[];
-  onChange: (models: string[]) => void;
-  disabled?: boolean;
-}
-
-export function ModelMultiSelect({
-  providerType,
-  selectedModels,
-  onChange,
-  disabled = false,
-}: ModelMultiSelectProps) {
-  const [open, setOpen] = useState(false);
-  const [availableModels, setAvailableModels] = useState<string[]>([]);
-  const [loading, setLoading] = useState(true);
-  // 新增:手动输入自定义模型的状态
-  const [customModel, setCustomModel] = useState("");
-
-  // 当供应商类型变化时,重新加载模型列表
-  useEffect(() => {
-    async function loadModels() {
-      setLoading(true);
-      const models = await getAvailableModelsByProviderType();
-      setAvailableModels(models);
-      setLoading(false);
-    }
-    loadModels();
-  }, [providerType]);
-
-  const toggleModel = (model: string) => {
-    if (selectedModels.includes(model)) {
-      onChange(selectedModels.filter((m) => m !== model));
-    } else {
-      onChange([...selectedModels, model]);
-    }
-  };
-
-  const selectAll = () => onChange(availableModels);
-  const clearAll = () => onChange([]);
-
-  // 新增:手动添加自定义模型
-  const handleAddCustomModel = () => {
-    const trimmed = customModel.trim();
-    if (!trimmed) return;
-
-    if (selectedModels.includes(trimmed)) {
-      // 已存在,清空输入框
-      setCustomModel("");
-      return;
-    }
-
-    // 添加到选中列表
-    onChange([...selectedModels, trimmed]);
-    setCustomModel("");
-  };
-
-  return (
-    <Popover open={open} onOpenChange={setOpen}>
-      <PopoverTrigger asChild>
-        <Button
-          variant="outline"
-          role="combobox"
-          aria-expanded={open}
-          disabled={disabled}
-          className="w-full justify-between"
-        >
-          {selectedModels.length === 0 ? (
-            <span className="text-muted-foreground">
-              允许所有 {providerType === "claude" ? "Claude" : "OpenAI"} 模型
-            </span>
-          ) : (
-            <div className="flex gap-2 items-center">
-              <span className="truncate">已选择 {selectedModels.length} 个模型</span>
-              <Badge variant="secondary" className="ml-auto">
-                {selectedModels.length}
-              </Badge>
-            </div>
-          )}
-          {loading ? (
-            <Loader2 className="ml-2 h-4 w-4 shrink-0 animate-spin opacity-50" />
-          ) : (
-            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-          )}
-        </Button>
-      </PopoverTrigger>
-      <PopoverContent
-        className="w-[400px] p-0 flex flex-col"
-        align="start"
-        onWheel={(e) => e.stopPropagation()}
-        onTouchMove={(e) => e.stopPropagation()}
-      >
-        <Command shouldFilter={true}>
-          <CommandInput placeholder="搜索模型名称..." />
-          <CommandList className="max-h-[250px] overflow-y-auto">
-            <CommandEmpty>{loading ? "加载中..." : "未找到模型"}</CommandEmpty>
-
-            {!loading && (
-              <>
-                {/* 快捷操作 */}
-                <CommandGroup>
-                  <div className="flex gap-2 p-2">
-                    <Button
-                      size="sm"
-                      variant="outline"
-                      onClick={selectAll}
-                      className="flex-1"
-                      type="button"
-                    >
-                      全选 ({availableModels.length})
-                    </Button>
-                    <Button
-                      size="sm"
-                      variant="outline"
-                      onClick={clearAll}
-                      disabled={selectedModels.length === 0}
-                      className="flex-1"
-                      type="button"
-                    >
-                      清空
-                    </Button>
-                  </div>
-                </CommandGroup>
-
-                {/* 模型列表(不分组,字母排序) */}
-                <CommandGroup>
-                  {availableModels.map((model) => (
-                    <CommandItem
-                      key={model}
-                      value={model}
-                      onSelect={() => toggleModel(model)}
-                      className="cursor-pointer"
-                    >
-                      <Checkbox
-                        checked={selectedModels.includes(model)}
-                        className="mr-2"
-                        onCheckedChange={() => toggleModel(model)}
-                      />
-                      <span className="font-mono text-sm flex-1">{model}</span>
-                      {selectedModels.includes(model) && <Check className="h-4 w-4 text-primary" />}
-                    </CommandItem>
-                  ))}
-                </CommandGroup>
-              </>
-            )}
-          </CommandList>
-        </Command>
-
-        {/* 新增:手动输入区域 */}
-        <div className="border-t p-3 space-y-2">
-          <Label className="text-xs font-medium">手动添加模型</Label>
-          <div className="flex gap-2">
-            <Input
-              placeholder="输入模型名称(如 gpt-5-turbo)"
-              value={customModel}
-              onChange={(e) => setCustomModel(e.target.value)}
-              onKeyDown={(e) => {
-                if (e.key === "Enter") {
-                  e.preventDefault();
-                  handleAddCustomModel();
-                }
-              }}
-              disabled={disabled}
-              className="font-mono text-sm flex-1"
-            />
-            <Button
-              size="sm"
-              onClick={handleAddCustomModel}
-              disabled={disabled || !customModel.trim()}
-              type="button"
-            >
-              <Plus className="h-4 w-4" />
-            </Button>
-          </div>
-          <p className="text-xs text-muted-foreground">
-            支持添加任意模型名称(不限于价格表中的模型)
-          </p>
-        </div>
-      </PopoverContent>
-    </Popover>
-  );
-}

+ 0 - 180
src/app/\[locale\]/settings/providers/_components/model-redirect-editor.tsx

@@ -1,180 +0,0 @@
-"use client";
-import { useState } from "react";
-import { Plus, X, ArrowRight, AlertCircle } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Badge } from "@/components/ui/badge";
-
-interface ModelRedirectEditorProps {
-  value: Record<string, string>;
-  onChange: (value: Record<string, string>) => void;
-  disabled?: boolean;
-}
-
-export function ModelRedirectEditor({
-  value,
-  onChange,
-  disabled = false,
-}: ModelRedirectEditorProps) {
-  const [newSource, setNewSource] = useState("");
-  const [newTarget, setNewTarget] = useState("");
-  const [error, setError] = useState<string | null>(null);
-
-  // 将 Record 转换为数组用于渲染
-  const redirects = Object.entries(value);
-
-  const handleAdd = () => {
-    setError(null);
-
-    // 验证输入
-    if (!newSource.trim()) {
-      setError("源模型名称不能为空");
-      return;
-    }
-    if (!newTarget.trim()) {
-      setError("目标模型名称不能为空");
-      return;
-    }
-
-    // 检查是否已存在
-    if (value[newSource.trim()]) {
-      setError(`模型 "${newSource.trim()}" 已存在重定向规则`);
-      return;
-    }
-
-    // 添加新的映射
-    onChange({
-      ...value,
-      [newSource.trim()]: newTarget.trim(),
-    });
-
-    // 清空输入
-    setNewSource("");
-    setNewTarget("");
-  };
-
-  const handleRemove = (sourceModel: string) => {
-    const newValue = { ...value };
-    delete newValue[sourceModel];
-    onChange(newValue);
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === "Enter") {
-      e.preventDefault();
-      handleAdd();
-    }
-  };
-
-  return (
-    <div className="space-y-3">
-      {/* 现有的重定向规则列表 */}
-      {redirects.length > 0 && (
-        <div className="space-y-2">
-          <div className="text-xs font-medium text-muted-foreground">
-            当前规则 ({redirects.length})
-          </div>
-          <div className="space-y-1">
-            {redirects.map(([source, target]) => (
-              <div
-                key={source}
-                className="group flex items-center gap-2 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors"
-              >
-                <Badge variant="outline" className="font-mono text-xs shrink-0">
-                  {source}
-                </Badge>
-                <ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
-                <Badge variant="secondary" className="font-mono text-xs shrink-0">
-                  {target}
-                </Badge>
-                <div className="flex-1" />
-                <Button
-                  type="button"
-                  variant="ghost"
-                  size="sm"
-                  onClick={() => handleRemove(source)}
-                  disabled={disabled}
-                  className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
-                >
-                  <X className="h-3 w-3 text-muted-foreground" />
-                </Button>
-              </div>
-            ))}
-          </div>
-        </div>
-      )}
-
-      {/* 添加新规则表单 */}
-      <div className="space-y-2">
-        <div className="text-xs font-medium text-muted-foreground">添加新规则</div>
-        <div className="grid grid-cols-[1fr_auto_1fr_auto] gap-2 items-end">
-          <div className="space-y-1">
-            <Label htmlFor="new-source" className="text-xs">
-              用户请求的模型
-            </Label>
-            <Input
-              id="new-source"
-              value={newSource}
-              onChange={(e) => setNewSource(e.target.value)}
-              onKeyDown={handleKeyDown}
-              placeholder="例如: claude-sonnet-4-5-20250929"
-              disabled={disabled}
-              className="font-mono text-sm"
-            />
-          </div>
-
-          <div className="text-muted-foreground pb-2">→</div>
-
-          <div className="space-y-1">
-            <Label htmlFor="new-target" className="text-xs">
-              实际转发的模型
-            </Label>
-            <Input
-              id="new-target"
-              value={newTarget}
-              onChange={(e) => setNewTarget(e.target.value)}
-              onKeyDown={handleKeyDown}
-              placeholder="例如: glm-4.6"
-              disabled={disabled}
-              className="font-mono text-sm"
-            />
-          </div>
-
-          <Button
-            type="button"
-            onClick={handleAdd}
-            disabled={disabled || !newSource.trim() || !newTarget.trim()}
-            size="default"
-            className="mb-0"
-          >
-            <Plus className="h-4 w-4 mr-1" />
-            添加
-          </Button>
-        </div>
-
-        {/* 错误提示 */}
-        {error && (
-          <div className="flex items-center gap-2 text-xs text-destructive">
-            <AlertCircle className="h-3 w-3" />
-            <span>{error}</span>
-          </div>
-        )}
-
-        {/* 帮助文本 */}
-        <p className="text-xs text-muted-foreground">
-          将 Claude Code 客户端请求的模型(如
-          claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如
-          glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。
-        </p>
-      </div>
-
-      {/* 空状态提示 */}
-      {redirects.length === 0 && (
-        <div className="text-center py-6 text-sm text-muted-foreground border border-dashed rounded-md">
-          暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 738
src/app/\[locale\]/settings/providers/_components/provider-list-item.legacy.tsx

@@ -1,738 +0,0 @@
-"use client";
-import { useState, useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import {
-  Dialog,
-  DialogContent,
-  DialogTrigger,
-  DialogHeader,
-  DialogTitle,
-  DialogDescription,
-} from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { Edit, Globe, Key, RotateCcw, Copy, CheckCircle } from "lucide-react";
-import type { ProviderDisplay } from "@/types/provider";
-import type { User } from "@/types/user";
-import { getProviderTypeConfig } from "@/lib/provider-type-utils";
-import { ProviderForm } from "./forms/provider-form";
-import { Switch } from "@/components/ui/switch";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Slider } from "@/components/ui/slider";
-import { PROVIDER_LIMITS } from "@/lib/constants/provider.constants";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-import { useProviderEdit } from "./hooks/use-provider-edit";
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogHeader,
-  AlertDialogTitle,
-  AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { resetProviderCircuit, getUnmaskedProviderKey } from "@/actions/providers";
-import { toast } from "sonner";
-import type { CurrencyCode } from "@/lib/utils/currency";
-import { formatCurrency } from "@/lib/utils/currency";
-
-interface ProviderListItemProps {
-  item: ProviderDisplay;
-  currentUser?: User;
-  healthStatus?: {
-    circuitState: "closed" | "open" | "half-open";
-    failureCount: number;
-    lastFailureTime: number | null;
-    circuitOpenUntil: number | null;
-    recoveryMinutes: number | null;
-  };
-  currencyCode?: CurrencyCode;
-  enableMultiProviderTypes: boolean;
-}
-
-export function ProviderListItem({
-  item,
-  currentUser,
-  healthStatus,
-  currencyCode = "USD",
-  enableMultiProviderTypes,
-}: ProviderListItemProps) {
-  const router = useRouter();
-  const [openEdit, setOpenEdit] = useState(false);
-  const [openClone, setOpenClone] = useState(false);
-  const [showKeyDialog, setShowKeyDialog] = useState(false);
-  const [unmaskedKey, setUnmaskedKey] = useState<string | null>(null);
-  const [copied, setCopied] = useState(false);
-  const [resetPending, startResetTransition] = useTransition();
-  const canEdit = currentUser?.role === "admin";
-
-  const {
-    enabled,
-    togglePending,
-    weight,
-    setWeight,
-    showWeight,
-    limit5hInfinite,
-    setLimit5hInfinite,
-    limit5hValue,
-    setLimit5hValue,
-    show5hLimit,
-    limitWeeklyInfinite,
-    setLimitWeeklyInfinite,
-    limitWeeklyValue,
-    setLimitWeeklyValue,
-    showWeeklyLimit,
-    limitMonthlyInfinite,
-    setLimitMonthlyInfinite,
-    limitMonthlyValue,
-    setLimitMonthlyValue,
-    showMonthlyLimit,
-    concurrentInfinite,
-    setConcurrentInfinite,
-    concurrentValue,
-    setConcurrentValue,
-    showConcurrent,
-    handleToggle,
-    handleWeightPopover,
-    handle5hLimitPopover,
-    handleWeeklyLimitPopover,
-    handleMonthlyLimitPopover,
-    handleConcurrentPopover,
-  } = useProviderEdit(item, canEdit);
-
-  // 获取供应商类型配置
-  const typeConfig = getProviderTypeConfig(item.providerType);
-  const TypeIcon = typeConfig.icon;
-
-  // 处理手动解除熔断
-  const handleResetCircuit = () => {
-    startResetTransition(async () => {
-      try {
-        const res = await resetProviderCircuit(item.id);
-        if (res.ok) {
-          toast.success("熔断器已重置", {
-            description: `供应商 "${item.name}" 的熔断状态已解除`,
-          });
-          // 刷新页面数据以同步熔断器状态
-          router.refresh();
-        } else {
-          toast.error("重置熔断器失败", {
-            description: res.error || "未知错误",
-          });
-        }
-      } catch (error) {
-        console.error("重置熔断器失败:", error);
-        toast.error("重置熔断器失败", {
-          description: "操作过程中出现异常",
-        });
-      }
-    });
-  };
-
-  // 处理查看密钥
-  const handleShowKey = async () => {
-    setShowKeyDialog(true);
-    const result = await getUnmaskedProviderKey(item.id);
-    if (result.ok) {
-      setUnmaskedKey(result.data.key);
-    } else {
-      toast.error("获取密钥失败", {
-        description: result.error || "未知错误",
-      });
-    }
-  };
-
-  // 处理复制密钥
-  const handleCopy = async () => {
-    if (unmaskedKey) {
-      try {
-        await navigator.clipboard.writeText(unmaskedKey);
-        setCopied(true);
-        toast.success("密钥已复制到剪贴板");
-        setTimeout(() => setCopied(false), 3000);
-      } catch (err) {
-        console.error("复制失败:", err);
-        toast.error("复制失败");
-      }
-    }
-  };
-
-  // 处理关闭对话框
-  const handleCloseDialog = () => {
-    setShowKeyDialog(false);
-    setUnmaskedKey(null);
-    setCopied(false);
-  };
-
-  return (
-    <div className="group relative h-full rounded-xl border border-border/70 bg-card p-4 shadow-sm transition-all duration-150 hover:shadow-md hover:border-border focus-within:ring-1 focus-within:ring-primary/20">
-      <div className="flex items-start justify-between gap-3 mb-3">
-        <div className="flex-1 min-w-0">
-          <div className="flex items-center gap-2 mb-1 flex-wrap">
-            <span
-              className={`inline-flex h-5 w-5 items-center justify-center rounded-md text-[10px] font-semibold ${enabled ? "bg-green-500/15 text-green-600" : "bg-muted text-muted-foreground"}`}
-            >
-              ●
-            </span>
-            {/* 供应商类型图标 */}
-            <span
-              className={`inline-flex h-5 w-5 items-center justify-center rounded-md ${typeConfig.bgColor}`}
-              title={typeConfig.description}
-            >
-              <TypeIcon className={`h-3 w-3 ${typeConfig.iconColor}`} />
-            </span>
-            <h3 className="text-sm font-semibold text-foreground truncate tracking-tight">
-              {item.name}
-            </h3>
-            {/* 供应商类型标签 */}
-            <Badge variant="outline" className="text-[10px] h-4 px-1.5 font-normal">
-              {typeConfig.label}
-            </Badge>
-
-            {/* 熔断器状态徽章 */}
-            {healthStatus?.circuitState === "open" && (
-              <>
-                <Badge variant="destructive" className="text-xs h-5 px-2">
-                  🔴 熔断中
-                  {healthStatus.recoveryMinutes && healthStatus.recoveryMinutes > 0 && (
-                    <span className="ml-1 opacity-80">
-                      ({healthStatus.recoveryMinutes}分钟后重试)
-                    </span>
-                  )}
-                </Badge>
-
-                {/* 手动解除熔断按钮 - 仅管理员可见 */}
-                {canEdit && (
-                  <AlertDialog>
-                    <AlertDialogTrigger asChild>
-                      <Button
-                        type="button"
-                        variant="ghost"
-                        size="icon"
-                        className="h-5 w-5 p-0 text-destructive hover:text-destructive/80 hover:bg-destructive/10"
-                        disabled={resetPending}
-                        title="手动解除熔断"
-                      >
-                        <RotateCcw
-                          className={`h-3.5 w-3.5 ${resetPending ? "animate-spin" : ""}`}
-                        />
-                      </Button>
-                    </AlertDialogTrigger>
-                    <AlertDialogContent>
-                      <AlertDialogHeader>
-                        <AlertDialogTitle>手动解除熔断</AlertDialogTitle>
-                        <AlertDialogDescription>
-                          确定要手动解除供应商 &ldquo;{item.name}&rdquo; 的熔断状态吗?
-                          <br />
-                          <span className="text-destructive font-medium">
-                            请确保上游服务已恢复正常,否则可能导致请求持续失败。
-                          </span>
-                        </AlertDialogDescription>
-                      </AlertDialogHeader>
-                      <div className="flex gap-2 justify-end">
-                        <AlertDialogCancel>取消</AlertDialogCancel>
-                        <AlertDialogAction onClick={handleResetCircuit}>确认解除</AlertDialogAction>
-                      </div>
-                    </AlertDialogContent>
-                  </AlertDialog>
-                )}
-              </>
-            )}
-            {healthStatus?.circuitState === "half-open" && (
-              <Badge
-                variant="secondary"
-                className="text-xs h-5 px-2 border-yellow-500/50 bg-yellow-500/10 text-yellow-700"
-              >
-                🟡 恢复中
-              </Badge>
-            )}
-
-            {/* 编辑和克隆按钮 - 仅管理员可见 */}
-            {canEdit && (
-              <div className="flex items-center gap-1">
-                {/* 编辑按钮 */}
-                <Dialog open={openEdit} onOpenChange={setOpenEdit}>
-                  <DialogTrigger asChild>
-                    <Button
-                      type="button"
-                      aria-label="编辑服务商"
-                      variant="ghost"
-                      size="icon"
-                      className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
-                    >
-                      <Edit className="h-3.5 w-3.5" />
-                    </Button>
-                  </DialogTrigger>
-                  <DialogContent className="sm:max-w-3xl max-h-[85vh] overflow-y-auto">
-                    <FormErrorBoundary>
-                      <ProviderForm
-                        mode="edit"
-                        provider={item}
-                        enableMultiProviderTypes={enableMultiProviderTypes}
-                        onSuccess={() => {
-                          setOpenEdit(false);
-                          // 刷新页面数据以同步所有字段
-                          router.refresh();
-                        }}
-                      />
-                    </FormErrorBoundary>
-                  </DialogContent>
-                </Dialog>
-                {/* 克隆按钮 */}
-                <Dialog open={openClone} onOpenChange={setOpenClone}>
-                  <DialogTrigger asChild>
-                    <Button
-                      type="button"
-                      aria-label="克隆服务商"
-                      variant="ghost"
-                      size="icon"
-                      className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
-                    >
-                      <Copy className="h-3.5 w-3.5" />
-                    </Button>
-                  </DialogTrigger>
-                  <DialogContent className="sm:max-w-3xl max-h-[85vh] overflow-y-auto">
-                    <FormErrorBoundary>
-                      <ProviderForm
-                        mode="create"
-                        cloneProvider={item}
-                        enableMultiProviderTypes={enableMultiProviderTypes}
-                        onSuccess={() => {
-                          setOpenClone(false);
-                          // 刷新页面数据以显示新添加的服务商
-                          router.refresh();
-                        }}
-                      />
-                    </FormErrorBoundary>
-                  </DialogContent>
-                </Dialog>
-              </div>
-            )}
-          </div>
-        </div>
-        <div className="flex items-center gap-2">
-          <div className="flex items-center gap-2 text-xs text-muted-foreground">
-            <span>启用</span>
-            <Switch
-              aria-label="启用服务商"
-              checked={enabled}
-              disabled={!canEdit || togglePending}
-              onCheckedChange={handleToggle}
-            />
-          </div>
-        </div>
-      </div>
-
-      {/* 统计信息区域 */}
-      <div className="mt-2 pt-2 border-t border-border/30 space-y-1 text-[11px] text-muted-foreground">
-        <div className="flex items-center gap-2">
-          <span className="font-medium text-foreground/80">今日用量:</span>
-          <span className="tabular-nums">
-            {formatCurrency(parseFloat(item.todayTotalCostUsd || "0"), currencyCode)} (
-            {item.todayCallCount ?? 0} 次调用)
-          </span>
-        </div>
-        <div className="flex items-center gap-2">
-          <span className="font-medium text-foreground/80">最近调用:</span>
-          <span className="tabular-nums">
-            {item.lastCallTime
-              ? new Date(item.lastCallTime).toLocaleString("zh-CN", {
-                  year: "numeric",
-                  month: "2-digit",
-                  day: "2-digit",
-                  hour: "2-digit",
-                  minute: "2-digit",
-                })
-              : "-"}
-            {item.lastCallModel && item.lastCallTime ? ` - ${item.lastCallModel}` : ""}
-          </span>
-        </div>
-        <div className="flex items-center gap-2">
-          <span className="font-medium text-foreground/80">模型白名单:</span>
-          {item.allowedModels && item.allowedModels.length > 0 ? (
-            <div className="flex items-center gap-1">
-              <Badge variant="outline" className="font-mono text-xs h-4 px-1.5">
-                {item.allowedModels.length} 个模型
-              </Badge>
-              <span className="text-muted-foreground">已启用</span>
-            </div>
-          ) : (
-            <span className="text-green-600">✓ 允许所有模型</span>
-          )}
-        </div>
-      </div>
-
-      {/* 内容区改为上下结构 */}
-      <div className="space-y-3 mb-3">
-        {/* 上:URL 与密钥 */}
-        <div className="space-y-2">
-          <div className="flex items-center gap-2 text-xs">
-            <Globe className="h-3.5 w-3.5 text-blue-500 shrink-0" />
-            <span className="font-mono text-muted-foreground truncate">{item.url}</span>
-          </div>
-          <div className="flex items-center gap-2 text-xs">
-            <Key className="h-3.5 w-3.5 text-amber-500 shrink-0" />
-            {canEdit ? (
-              <button
-                onClick={handleShowKey}
-                className="font-mono text-muted-foreground hover:text-foreground hover:underline cursor-pointer transition-colors"
-                type="button"
-              >
-                {item.maskedKey}
-              </button>
-            ) : (
-              <span className="font-mono text-muted-foreground">{item.maskedKey}</span>
-            )}
-          </div>
-        </div>
-
-        {/* 路由配置 */}
-        <div className="grid grid-cols-4 gap-2 text-[11px] pb-2 border-b border-border/40">
-          {/* 优先级 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">优先级</div>
-            <Tooltip>
-              <TooltipTrigger asChild>
-                <div className="w-full text-center font-medium tabular-nums truncate text-foreground cursor-help">
-                  <span>{item.priority}</span>
-                </div>
-              </TooltipTrigger>
-              <TooltipContent side="bottom" className="max-w-xs">
-                <p className="text-xs">
-                  数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。
-                </p>
-              </TooltipContent>
-            </Tooltip>
-          </div>
-
-          {/* 权重 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">权重</div>
-            {canEdit ? (
-              <Popover open={showWeight} onOpenChange={handleWeightPopover}>
-                <PopoverTrigger asChild>
-                  <Tooltip>
-                    <TooltipTrigger asChild>
-                      <button
-                        type="button"
-                        aria-label="编辑权重"
-                        className="w-full text-center font-medium tabular-nums truncate text-foreground cursor-pointer hover:text-primary/80 transition-colors"
-                      >
-                        <span>{weight}</span>
-                      </button>
-                    </TooltipTrigger>
-                    <TooltipContent side="bottom" className="max-w-xs">
-                      <p className="text-xs">
-                        加权随机概率。同优先级内,权重越高被选中概率越大。点击可编辑。
-                      </p>
-                    </TooltipContent>
-                  </Tooltip>
-                </PopoverTrigger>
-                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-64 p-3">
-                  <div className="mb-2 flex items-center justify-between text-[11px] text-muted-foreground">
-                    <span>调整权重</span>
-                    <span className="font-medium text-foreground">{weight}</span>
-                  </div>
-                  <Slider
-                    min={PROVIDER_LIMITS.WEIGHT.MIN}
-                    max={PROVIDER_LIMITS.WEIGHT.MAX}
-                    step={1}
-                    value={[weight]}
-                    onValueChange={(v) => setWeight(v?.[0] ?? PROVIDER_LIMITS.WEIGHT.MIN)}
-                  />
-                </PopoverContent>
-              </Popover>
-            ) : (
-              <Tooltip>
-                <TooltipTrigger asChild>
-                  <div className="w-full text-center font-medium tabular-nums truncate text-foreground cursor-help">
-                    <span>{weight}</span>
-                  </div>
-                </TooltipTrigger>
-                <TooltipContent side="bottom" className="max-w-xs">
-                  <p className="text-xs">加权随机概率。同优先级内,权重越高被选中概率越大。</p>
-                </TooltipContent>
-              </Tooltip>
-            )}
-          </div>
-
-          {/* 成本倍率 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">倍率</div>
-            <Tooltip>
-              <TooltipTrigger asChild>
-                <div className="w-full text-center font-medium tabular-nums truncate text-foreground cursor-help">
-                  <span>{item.costMultiplier.toFixed(2)}x</span>
-                </div>
-              </TooltipTrigger>
-              <TooltipContent side="bottom" className="max-w-xs">
-                <p className="text-xs">成本计算倍数。1.0x=官方价格,0.8x=便宜 20%,1.2x=贵 20%</p>
-              </TooltipContent>
-            </Tooltip>
-          </div>
-
-          {/* 分组 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">分组</div>
-            <Tooltip>
-              <TooltipTrigger asChild>
-                <div className="w-full text-center font-medium truncate text-foreground cursor-help">
-                  <span>{item.groupTag || "-"}</span>
-                </div>
-              </TooltipTrigger>
-              <TooltipContent side="bottom" className="max-w-xs">
-                <p className="text-xs">
-                  只有 providerGroup 包含此标签的用户才能使用此供应商。未设置表示所有用户可用。
-                </p>
-              </TooltipContent>
-            </Tooltip>
-          </div>
-        </div>
-
-        {/* 限流配置 */}
-        <div className="grid grid-cols-4 gap-2 text-[11px]">
-          {/* 5小时消费上限 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">5h USD</div>
-            {canEdit ? (
-              <Popover open={show5hLimit} onOpenChange={handle5hLimitPopover}>
-                <PopoverTrigger asChild>
-                  <button
-                    type="button"
-                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
-                  >
-                    <span>{limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}</span>
-                  </button>
-                </PopoverTrigger>
-                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
-                  <div className="mb-2 flex items-center justify-between text-[11px]">
-                    <span className="text-muted-foreground">5小时消费上限 (USD)</span>
-                    <div className="flex items-center gap-2 text-muted-foreground">
-                      <span>无限</span>
-                      <Switch
-                        checked={limit5hInfinite}
-                        onCheckedChange={setLimit5hInfinite}
-                        aria-label="无限"
-                      />
-                    </div>
-                  </div>
-                  <div className="flex items-center gap-3">
-                    <Slider
-                      min={PROVIDER_LIMITS.LIMIT_5H_USD.MIN}
-                      max={PROVIDER_LIMITS.LIMIT_5H_USD.MAX}
-                      step={PROVIDER_LIMITS.LIMIT_5H_USD.STEP}
-                      value={[limit5hValue]}
-                      onValueChange={(v) =>
-                        !limit5hInfinite &&
-                        setLimit5hValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN)
-                      }
-                      disabled={limit5hInfinite}
-                    />
-                    <span className="w-16 text-right text-xs font-medium">
-                      {limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}
-                    </span>
-                  </div>
-                </PopoverContent>
-              </Popover>
-            ) : (
-              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
-                <span>{limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}</span>
-              </div>
-            )}
-          </div>
-
-          {/* 周消费上限 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">Week USD</div>
-            {canEdit ? (
-              <Popover open={showWeeklyLimit} onOpenChange={handleWeeklyLimitPopover}>
-                <PopoverTrigger asChild>
-                  <button
-                    type="button"
-                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
-                  >
-                    <span>{limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}</span>
-                  </button>
-                </PopoverTrigger>
-                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
-                  <div className="mb-2 flex items-center justify-between text-[11px]">
-                    <span className="text-muted-foreground">周消费上限 (USD)</span>
-                    <div className="flex items-center gap-2 text-muted-foreground">
-                      <span>无限</span>
-                      <Switch
-                        checked={limitWeeklyInfinite}
-                        onCheckedChange={setLimitWeeklyInfinite}
-                        aria-label="无限"
-                      />
-                    </div>
-                  </div>
-                  <div className="flex items-center gap-3">
-                    <Slider
-                      min={PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN}
-                      max={PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MAX}
-                      step={PROVIDER_LIMITS.LIMIT_WEEKLY_USD.STEP}
-                      value={[limitWeeklyValue]}
-                      onValueChange={(v) =>
-                        !limitWeeklyInfinite &&
-                        setLimitWeeklyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN)
-                      }
-                      disabled={limitWeeklyInfinite}
-                    />
-                    <span className="w-16 text-right text-xs font-medium">
-                      {limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}
-                    </span>
-                  </div>
-                </PopoverContent>
-              </Popover>
-            ) : (
-              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
-                <span>{limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}</span>
-              </div>
-            )}
-          </div>
-
-          {/* 月消费上限 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">Mon USD</div>
-            {canEdit ? (
-              <Popover open={showMonthlyLimit} onOpenChange={handleMonthlyLimitPopover}>
-                <PopoverTrigger asChild>
-                  <button
-                    type="button"
-                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
-                  >
-                    <span>{limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}</span>
-                  </button>
-                </PopoverTrigger>
-                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
-                  <div className="mb-2 flex items-center justify-between text-[11px]">
-                    <span className="text-muted-foreground">月消费上限 (USD)</span>
-                    <div className="flex items-center gap-2 text-muted-foreground">
-                      <span>无限</span>
-                      <Switch
-                        checked={limitMonthlyInfinite}
-                        onCheckedChange={setLimitMonthlyInfinite}
-                        aria-label="无限"
-                      />
-                    </div>
-                  </div>
-                  <div className="flex items-center gap-3">
-                    <Slider
-                      min={PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN}
-                      max={PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MAX}
-                      step={PROVIDER_LIMITS.LIMIT_MONTHLY_USD.STEP}
-                      value={[limitMonthlyValue]}
-                      onValueChange={(v) =>
-                        !limitMonthlyInfinite &&
-                        setLimitMonthlyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN)
-                      }
-                      disabled={limitMonthlyInfinite}
-                    />
-                    <span className="w-16 text-right text-xs font-medium">
-                      {limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}
-                    </span>
-                  </div>
-                </PopoverContent>
-              </Popover>
-            ) : (
-              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
-                <span>{limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}</span>
-              </div>
-            )}
-          </div>
-
-          {/* 并发Session上限 */}
-          <div className="min-w-0 text-center">
-            <div className="text-muted-foreground">并发</div>
-            {canEdit ? (
-              <Popover open={showConcurrent} onOpenChange={handleConcurrentPopover}>
-                <PopoverTrigger asChild>
-                  <button
-                    type="button"
-                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
-                  >
-                    <span>{concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}</span>
-                  </button>
-                </PopoverTrigger>
-                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
-                  <div className="mb-2 flex items-center justify-between text-[11px]">
-                    <span className="text-muted-foreground">并发Session上限</span>
-                    <div className="flex items-center gap-2 text-muted-foreground">
-                      <span>无限</span>
-                      <Switch
-                        checked={concurrentInfinite}
-                        onCheckedChange={setConcurrentInfinite}
-                        aria-label="无限"
-                      />
-                    </div>
-                  </div>
-                  <div className="flex items-center gap-3">
-                    <Slider
-                      min={PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN}
-                      max={PROVIDER_LIMITS.CONCURRENT_SESSIONS.MAX}
-                      step={1}
-                      value={[concurrentValue]}
-                      onValueChange={(v) =>
-                        !concurrentInfinite &&
-                        setConcurrentValue(v?.[0] ?? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN)
-                      }
-                      disabled={concurrentInfinite}
-                    />
-                    <span className="w-16 text-right text-xs font-medium">
-                      {concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}
-                    </span>
-                  </div>
-                </PopoverContent>
-              </Popover>
-            ) : (
-              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
-                <span>{concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}</span>
-              </div>
-            )}
-          </div>
-        </div>
-      </div>
-
-      <div className="flex items-center justify-between text-[11px] text-muted-foreground pt-2 border-t border-border/60">
-        <span>创建 {item.createdAt}</span>
-        <span>更新 {item.updatedAt}</span>
-      </div>
-
-      {/* API Key 查看 Dialog */}
-      <Dialog open={showKeyDialog} onOpenChange={handleCloseDialog}>
-        <DialogContent className="max-w-lg">
-          <DialogHeader>
-            <DialogTitle className="flex items-center gap-2">
-              <Key className="h-5 w-5 text-amber-500" />
-              查看完整 API Key
-            </DialogTitle>
-            <DialogDescription>请妥善保管,不要泄露给他人</DialogDescription>
-          </DialogHeader>
-
-          <div className="space-y-4">
-            <div className="flex items-center gap-2">
-              <code className="flex-1 font-mono bg-muted px-3 py-2 rounded text-sm break-all border">
-                {unmaskedKey || "加载中..."}
-              </code>
-              <Button
-                size="icon"
-                variant="outline"
-                onClick={handleCopy}
-                disabled={!unmaskedKey}
-                type="button"
-              >
-                {copied ? (
-                  <CheckCircle className="h-4 w-4 text-green-500" />
-                ) : (
-                  <Copy className="h-4 w-4" />
-                )}
-              </Button>
-            </div>
-          </div>
-        </DialogContent>
-      </Dialog>
-    </div>
-  );
-}

+ 0 - 58
src/app/\[locale\]/settings/providers/_components/provider-list.tsx

@@ -1,58 +0,0 @@
-"use client";
-import { Globe } from "lucide-react";
-import type { ProviderDisplay } from "@/types/provider";
-import type { User } from "@/types/user";
-import { ProviderRichListItem } from "./provider-rich-list-item";
-import type { CurrencyCode } from "@/lib/utils/currency";
-
-interface ProviderListProps {
-  providers: ProviderDisplay[];
-  currentUser?: User;
-  healthStatus: Record<
-    number,
-    {
-      circuitState: "closed" | "open" | "half-open";
-      failureCount: number;
-      lastFailureTime: number | null;
-      circuitOpenUntil: number | null;
-      recoveryMinutes: number | null;
-    }
-  >;
-  currencyCode?: CurrencyCode;
-  enableMultiProviderTypes: boolean;
-}
-
-export function ProviderList({
-  providers,
-  currentUser,
-  healthStatus,
-  currencyCode = "USD",
-  enableMultiProviderTypes,
-}: ProviderListProps) {
-  if (providers.length === 0) {
-    return (
-      <div className="flex flex-col items-center justify-center py-12 px-4">
-        <div className="w-12 h-12 rounded-full bg-muted/50 flex items-center justify-center mb-3">
-          <Globe className="h-6 w-6 text-muted-foreground" />
-        </div>
-        <h3 className="font-medium text-foreground mb-1">暂无服务商配置</h3>
-        <p className="text-sm text-muted-foreground text-center">添加你的第一个 API 服务商</p>
-      </div>
-    );
-  }
-
-  return (
-    <div className="border rounded-lg overflow-hidden">
-      {providers.map((provider) => (
-        <ProviderRichListItem
-          key={provider.id}
-          provider={provider}
-          currentUser={currentUser}
-          healthStatus={healthStatus[provider.id]}
-          currencyCode={currencyCode}
-          enableMultiProviderTypes={enableMultiProviderTypes}
-        />
-      ))}
-    </div>
-  );
-}

+ 0 - 141
src/app/\[locale\]/settings/providers/_components/provider-manager.tsx

@@ -1,141 +0,0 @@
-"use client";
-import { useState, useMemo } from "react";
-import { Search, X } from "lucide-react";
-import { ProviderList } from "./provider-list";
-import { ProviderTypeFilter } from "./provider-type-filter";
-import { ProviderSortDropdown, type SortKey } from "./provider-sort-dropdown";
-import { Input } from "@/components/ui/input";
-import { useDebounce } from "@/lib/hooks/use-debounce";
-import type { ProviderDisplay, ProviderType } from "@/types/provider";
-import type { User } from "@/types/user";
-import type { CurrencyCode } from "@/lib/utils/currency";
-
-interface ProviderManagerProps {
-  providers: ProviderDisplay[];
-  currentUser?: User;
-  healthStatus: Record<
-    number,
-    {
-      circuitState: "closed" | "open" | "half-open";
-      failureCount: number;
-      lastFailureTime: number | null;
-      circuitOpenUntil: number | null;
-      recoveryMinutes: number | null;
-    }
-  >;
-  currencyCode?: CurrencyCode;
-  enableMultiProviderTypes: boolean;
-}
-
-export function ProviderManager({
-  providers,
-  currentUser,
-  healthStatus,
-  currencyCode = "USD",
-  enableMultiProviderTypes,
-}: ProviderManagerProps) {
-  const [typeFilter, setTypeFilter] = useState<ProviderType | "all">("all");
-  const [sortBy, setSortBy] = useState<SortKey>("priority");
-  const [searchTerm, setSearchTerm] = useState("");
-  const debouncedSearchTerm = useDebounce(searchTerm, 500);
-
-  // 统一过滤逻辑:搜索 + 类型筛选 + 排序
-  const filteredProviders = useMemo(() => {
-    let result = providers;
-
-    // 搜索过滤(name, url, groupTag)
-    if (debouncedSearchTerm) {
-      const term = debouncedSearchTerm.toLowerCase();
-      result = result.filter(
-        (p) =>
-          p.name.toLowerCase().includes(term) ||
-          p.url.toLowerCase().includes(term) ||
-          (p.groupTag && p.groupTag.toLowerCase().includes(term))
-      );
-    }
-
-    // 类型筛选
-    if (typeFilter !== "all") {
-      result = result.filter((p) => p.providerType === typeFilter);
-    }
-
-    // 排序
-    return [...result].sort((a, b) => {
-      switch (sortBy) {
-        case "name":
-          return a.name.localeCompare(b.name);
-        case "priority":
-          // 优先级:数值越小越优先(1 > 2 > 3),升序排列
-          return a.priority - b.priority;
-        case "weight":
-          // 权重:数值越大越优先,降序排列
-          return b.weight - a.weight;
-        case "createdAt": {
-          const timeA = new Date(a.createdAt).getTime();
-          const timeB = new Date(b.createdAt).getTime();
-          if (Number.isNaN(timeA) || Number.isNaN(timeB)) {
-            return b.createdAt.localeCompare(a.createdAt);
-          }
-          return timeB - timeA;
-        }
-        default:
-          return 0;
-      }
-    });
-  }, [providers, debouncedSearchTerm, typeFilter, sortBy]);
-
-  return (
-    <div className="space-y-4">
-      {/* 筛选条件 */}
-      <div className="flex flex-col gap-3">
-        <div className="flex items-center gap-2">
-          <ProviderTypeFilter value={typeFilter} onChange={setTypeFilter} />
-          <ProviderSortDropdown value={sortBy} onChange={setSortBy} />
-          <div className="relative flex-1">
-            <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-            <Input
-              type="search"
-              placeholder="搜索供应商名称、URL、备注..."
-              value={searchTerm}
-              onChange={(e) => setSearchTerm(e.target.value)}
-              className="pl-9 pr-9"
-            />
-            {searchTerm && (
-              <button
-                onClick={() => setSearchTerm("")}
-                className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
-                aria-label="清除搜索"
-              >
-                <X className="h-4 w-4" />
-              </button>
-            )}
-          </div>
-        </div>
-        {/* 搜索结果提示 */}
-        {debouncedSearchTerm && (
-          <p className="text-sm text-muted-foreground">
-            {filteredProviders.length > 0
-              ? `找到 ${filteredProviders.length} 个匹配的供应商`
-              : "未找到匹配的供应商"}
-          </p>
-        )}
-        {!debouncedSearchTerm && (
-          <div className="text-sm text-muted-foreground">
-            显示 {filteredProviders.length} / {providers.length} 个供应商
-          </div>
-        )}
-      </div>
-
-      {/* 供应商列表 */}
-      <ProviderList
-        providers={filteredProviders}
-        currentUser={currentUser}
-        healthStatus={healthStatus}
-        currencyCode={currencyCode}
-        enableMultiProviderTypes={enableMultiProviderTypes}
-      />
-    </div>
-  );
-}
-
-export type { ProviderDisplay } from "@/types/provider";

+ 0 - 490
src/app/\[locale\]/settings/providers/_components/provider-rich-list-item.tsx

@@ -1,490 +0,0 @@
-"use client";
-import { useState, useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
-  CheckCircle,
-  XCircle,
-  Edit,
-  Copy,
-  Trash,
-  Globe,
-  Key,
-  RotateCcw,
-  AlertTriangle,
-} from "lucide-react";
-import type { ProviderDisplay } from "@/types/provider";
-import type { User } from "@/types/user";
-import { getProviderTypeConfig } from "@/lib/provider-type-utils";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-  DialogDescription,
-} from "@/components/ui/dialog";
-import { ProviderForm } from "./forms/provider-form";
-import { FormErrorBoundary } from "@/components/form-error-boundary";
-import { getUnmaskedProviderKey, resetProviderCircuit, removeProvider } from "@/actions/providers";
-import { toast } from "sonner";
-import type { CurrencyCode } from "@/lib/utils/currency";
-import { formatCurrency } from "@/lib/utils/currency";
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogHeader,
-  AlertDialogTitle,
-  AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { Switch } from "@/components/ui/switch";
-import { editProvider } from "@/actions/providers";
-
-interface ProviderRichListItemProps {
-  provider: ProviderDisplay;
-  currentUser?: User;
-  healthStatus?: {
-    circuitState: "closed" | "open" | "half-open";
-    failureCount: number;
-    lastFailureTime: number | null;
-    circuitOpenUntil: number | null;
-    recoveryMinutes: number | null;
-  };
-  currencyCode?: CurrencyCode;
-  enableMultiProviderTypes: boolean;
-  onEdit?: () => void;
-  onClone?: () => void;
-  onDelete?: () => void;
-}
-
-export function ProviderRichListItem({
-  provider,
-  currentUser,
-  healthStatus,
-  currencyCode = "USD",
-  enableMultiProviderTypes,
-  onEdit: onEditProp,
-  onClone: onCloneProp,
-  onDelete: onDeleteProp,
-}: ProviderRichListItemProps) {
-  const router = useRouter();
-  const [openEdit, setOpenEdit] = useState(false);
-  const [openClone, setOpenClone] = useState(false);
-  const [showKeyDialog, setShowKeyDialog] = useState(false);
-  const [unmaskedKey, setUnmaskedKey] = useState<string | null>(null);
-  const [copied, setCopied] = useState(false);
-  const [resetPending, startResetTransition] = useTransition();
-  const [deletePending, startDeleteTransition] = useTransition();
-  const [togglePending, startToggleTransition] = useTransition();
-
-  const canEdit = currentUser?.role === "admin";
-
-  // 获取供应商类型配置
-  const typeConfig = getProviderTypeConfig(provider.providerType);
-  const TypeIcon = typeConfig.icon;
-
-  // 处理编辑
-  const handleEdit = () => {
-    if (onEditProp) {
-      onEditProp();
-    } else {
-      setOpenEdit(true);
-    }
-  };
-
-  // 处理克隆
-  const handleClone = () => {
-    if (onCloneProp) {
-      onCloneProp();
-    } else {
-      setOpenClone(true);
-    }
-  };
-
-  // 处理删除
-  const handleDelete = () => {
-    if (onDeleteProp) {
-      onDeleteProp();
-    } else {
-      startDeleteTransition(async () => {
-        try {
-          const res = await removeProvider(provider.id);
-          if (res.ok) {
-            toast.success("删除成功", {
-              description: `供应商 "${provider.name}" 已删除`,
-            });
-            router.refresh();
-          } else {
-            toast.error("删除失败", {
-              description: res.error || "未知错误",
-            });
-          }
-        } catch (error) {
-          console.error("删除供应商失败:", error);
-          toast.error("删除失败", {
-            description: "操作过程中出现异常",
-          });
-        }
-      });
-    }
-  };
-
-  // 处理查看密钥
-  const handleShowKey = async () => {
-    setShowKeyDialog(true);
-    const result = await getUnmaskedProviderKey(provider.id);
-    if (result.ok) {
-      setUnmaskedKey(result.data.key);
-    } else {
-      toast.error("获取密钥失败", {
-        description: result.error || "未知错误",
-      });
-      setShowKeyDialog(false);
-    }
-  };
-
-  // 处理复制密钥
-  const handleCopy = async () => {
-    if (unmaskedKey) {
-      try {
-        await navigator.clipboard.writeText(unmaskedKey);
-        setCopied(true);
-        toast.success("密钥已复制到剪贴板");
-        setTimeout(() => setCopied(false), 3000);
-      } catch (error) {
-        console.error("复制失败:", error);
-        toast.error("复制失败");
-      }
-    }
-  };
-
-  // 处理关闭 Dialog
-  const handleCloseDialog = () => {
-    setShowKeyDialog(false);
-    setUnmaskedKey(null);
-    setCopied(false);
-  };
-
-  // 处理手动解除熔断
-  const handleResetCircuit = () => {
-    startResetTransition(async () => {
-      try {
-        const res = await resetProviderCircuit(provider.id);
-        if (res.ok) {
-          toast.success("熔断器已重置", {
-            description: `供应商 "${provider.name}" 的熔断状态已解除`,
-          });
-          router.refresh();
-        } else {
-          toast.error("重置熔断器失败", {
-            description: res.error || "未知错误",
-          });
-        }
-      } catch (error) {
-        console.error("重置熔断器失败:", error);
-        toast.error("重置熔断器失败", {
-          description: "操作过程中出现异常",
-        });
-      }
-    });
-  };
-
-  // 处理启用/禁用切换
-  const handleToggle = () => {
-    startToggleTransition(async () => {
-      try {
-        const res = await editProvider(provider.id, {
-          is_enabled: !provider.isEnabled,
-        });
-        if (res.ok) {
-          toast.success(`供应商已${!provider.isEnabled ? "启用" : "禁用"}`, {
-            description: `供应商 "${provider.name}" 状态已更新`,
-          });
-          router.refresh();
-        } else {
-          toast.error("状态切换失败", {
-            description: res.error || "未知错误",
-          });
-        }
-      } catch (error) {
-        console.error("状态切换失败:", error);
-        toast.error("状态切换失败", {
-          description: "操作过程中出现异常",
-        });
-      }
-    });
-  };
-
-  return (
-    <>
-      <div className="flex items-center gap-4 py-3 px-4 border-b hover:bg-muted/50 transition-colors">
-        {/* 左侧:状态和类型图标 */}
-        <div className="flex items-center gap-2">
-          {/* 启用状态指示器 */}
-          {provider.isEnabled ? (
-            <CheckCircle className="h-4 w-4 text-green-500 flex-shrink-0" />
-          ) : (
-            <XCircle className="h-4 w-4 text-gray-400 flex-shrink-0" />
-          )}
-
-          {/* 类型图标 */}
-          <div
-            className={`flex items-center justify-center w-6 h-6 rounded ${typeConfig.bgColor} flex-shrink-0`}
-          >
-            <TypeIcon className="h-3.5 w-3.5" />
-          </div>
-        </div>
-
-        {/* 中间:名称、URL、官网、tag、熔断状态 */}
-        <div className="flex-1 min-w-0">
-          <div className="flex items-center gap-2 flex-wrap">
-            {/* Favicon */}
-            {provider.faviconUrl && (
-              <img
-                src={provider.faviconUrl}
-                alt=""
-                className="h-4 w-4 flex-shrink-0"
-                onError={(e) => {
-                  // 隐藏加载失败的图标
-                  (e.target as HTMLImageElement).style.display = "none";
-                }}
-              />
-            )}
-
-            {/* 名称 */}
-            <span className="font-semibold truncate">{provider.name}</span>
-
-            {/* Group Tag */}
-            {provider.groupTag && (
-              <Badge variant="outline" className="flex-shrink-0">
-                {provider.groupTag}
-              </Badge>
-            )}
-
-            {/* 熔断器警告 */}
-            {healthStatus && healthStatus.circuitState === "open" && (
-              <Badge variant="destructive" className="flex items-center gap-1 flex-shrink-0">
-                <AlertTriangle className="h-3 w-3" />
-                熔断中
-              </Badge>
-            )}
-          </div>
-
-          <div className="flex items-center gap-3 mt-1 text-sm text-muted-foreground flex-wrap">
-            {/* URL */}
-            <span className="truncate max-w-[300px]">{provider.url}</span>
-
-            {/* 官网链接 */}
-            {provider.websiteUrl && (
-              <a
-                href={provider.websiteUrl}
-                target="_blank"
-                rel="noopener noreferrer"
-                className="inline-flex items-center gap-1 hover:underline text-blue-600 hover:text-blue-700 flex-shrink-0"
-                onClick={(e) => e.stopPropagation()}
-              >
-                <Globe className="h-3 w-3" />
-                官网
-              </a>
-            )}
-
-            {/* API Key 展示(仅管理员) */}
-            {canEdit && (
-              <button
-                onClick={(e) => {
-                  e.stopPropagation();
-                  handleShowKey();
-                }}
-                className="inline-flex items-center gap-1 text-xs font-mono hover:underline flex-shrink-0"
-              >
-                <Key className="h-3 w-3" />
-                {provider.maskedKey}
-              </button>
-            )}
-          </div>
-        </div>
-
-        {/* 右侧:指标(仅桌面端) */}
-        <div className="hidden md:grid grid-cols-3 gap-4 text-center flex-shrink-0">
-          <div>
-            <div className="text-xs text-muted-foreground">优先级</div>
-            <div className="font-medium">{provider.priority}</div>
-          </div>
-          <div>
-            <div className="text-xs text-muted-foreground">权重</div>
-            <div className="font-medium">{provider.weight}</div>
-          </div>
-          <div>
-            <div className="text-xs text-muted-foreground">成本倍数</div>
-            <div className="font-medium">{provider.costMultiplier}x</div>
-          </div>
-        </div>
-
-        {/* 今日用量(仅大屏) */}
-        <div className="hidden lg:block text-center flex-shrink-0 min-w-[100px]">
-          <div className="text-xs text-muted-foreground">今日用量</div>
-          <div className="font-medium">{provider.todayCallCount || 0} 次</div>
-          <div className="text-xs font-mono text-muted-foreground mt-0.5">
-            {formatCurrency(parseFloat(provider.todayTotalCostUsd || "0"), currencyCode)}
-          </div>
-        </div>
-
-        {/* 操作按钮 */}
-        <div className="flex items-center gap-1 flex-shrink-0">
-          {/* 启用/禁用切换 */}
-          {canEdit && (
-            <Switch
-              checked={provider.isEnabled}
-              onCheckedChange={handleToggle}
-              disabled={togglePending}
-              className="data-[state=checked]:bg-green-500"
-            />
-          )}
-
-          {/* 编辑按钮 */}
-          {canEdit && (
-            <Button
-              size="icon"
-              variant="ghost"
-              onClick={(e) => {
-                e.stopPropagation();
-                handleEdit();
-              }}
-              disabled={!canEdit}
-            >
-              <Edit className="h-4 w-4" />
-            </Button>
-          )}
-
-          {/* 克隆按钮 */}
-          {canEdit && (
-            <Button
-              size="icon"
-              variant="ghost"
-              onClick={(e) => {
-                e.stopPropagation();
-                handleClone();
-              }}
-              disabled={!canEdit}
-            >
-              <Copy className="h-4 w-4" />
-            </Button>
-          )}
-
-          {/* 熔断重置按钮(仅熔断时显示) */}
-          {canEdit && healthStatus && healthStatus.circuitState === "open" && (
-            <Button
-              size="icon"
-              variant="ghost"
-              onClick={(e) => {
-                e.stopPropagation();
-                handleResetCircuit();
-              }}
-              disabled={resetPending}
-            >
-              <RotateCcw className="h-4 w-4 text-orange-600" />
-            </Button>
-          )}
-
-          {/* 删除按钮 */}
-          {canEdit && (
-            <AlertDialog>
-              <AlertDialogTrigger asChild>
-                <Button
-                  size="icon"
-                  variant="ghost"
-                  onClick={(e) => e.stopPropagation()}
-                  disabled={!canEdit}
-                >
-                  <Trash className="h-4 w-4 text-red-600" />
-                </Button>
-              </AlertDialogTrigger>
-              <AlertDialogContent>
-                <AlertDialogHeader>
-                  <AlertDialogTitle>确认删除供应商?</AlertDialogTitle>
-                  <AlertDialogDescription>
-                    确定要删除供应商 &quot;{provider.name}&quot; 吗?此操作无法撤销。
-                  </AlertDialogDescription>
-                </AlertDialogHeader>
-                <div className="flex justify-end gap-2">
-                  <AlertDialogCancel>取消</AlertDialogCancel>
-                  <AlertDialogAction
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      handleDelete();
-                    }}
-                    className="bg-red-600 hover:bg-red-700"
-                    disabled={deletePending}
-                  >
-                    删除
-                  </AlertDialogAction>
-                </div>
-              </AlertDialogContent>
-            </AlertDialog>
-          )}
-        </div>
-      </div>
-
-      {/* 编辑 Dialog */}
-      <Dialog open={openEdit} onOpenChange={setOpenEdit}>
-        <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
-          <FormErrorBoundary>
-            <ProviderForm
-              mode="edit"
-              provider={provider}
-              onSuccess={() => {
-                setOpenEdit(false);
-                router.refresh();
-              }}
-              enableMultiProviderTypes={enableMultiProviderTypes}
-            />
-          </FormErrorBoundary>
-        </DialogContent>
-      </Dialog>
-
-      {/* 克隆 Dialog */}
-      <Dialog open={openClone} onOpenChange={setOpenClone}>
-        <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
-          <FormErrorBoundary>
-            <ProviderForm
-              mode="create"
-              cloneProvider={provider}
-              onSuccess={() => {
-                setOpenClone(false);
-                router.refresh();
-              }}
-              enableMultiProviderTypes={enableMultiProviderTypes}
-            />
-          </FormErrorBoundary>
-        </DialogContent>
-      </Dialog>
-
-      {/* API Key 展示 Dialog */}
-      <Dialog open={showKeyDialog} onOpenChange={handleCloseDialog}>
-        <DialogContent className="max-w-lg">
-          <DialogHeader>
-            <DialogTitle>查看完整 API Key</DialogTitle>
-            <DialogDescription>请妥善保管,不要泄露给他人</DialogDescription>
-          </DialogHeader>
-          <div className="space-y-4">
-            <div className="flex items-center gap-2">
-              <code className="flex-1 font-mono bg-muted px-3 py-2 rounded text-sm break-all">
-                {unmaskedKey || "加载中..."}
-              </code>
-              <Button onClick={handleCopy} disabled={!unmaskedKey} size="icon" variant="outline">
-                {copied ? (
-                  <CheckCircle className="h-4 w-4 text-green-600" />
-                ) : (
-                  <Copy className="h-4 w-4" />
-                )}
-              </Button>
-            </div>
-          </div>
-        </DialogContent>
-      </Dialog>
-    </>
-  );
-}

+ 0 - 46
src/app/\[locale\]/settings/providers/_components/provider-sort-dropdown.tsx

@@ -1,46 +0,0 @@
-"use client";
-
-import { ArrowUpDown } from "lucide-react";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-
-export type SortKey = "name" | "priority" | "weight" | "createdAt";
-
-interface ProviderSortDropdownProps {
-  value: SortKey;
-  onChange: (value: SortKey) => void;
-}
-
-const SORT_OPTIONS: { value: SortKey; label: string }[] = [
-  { value: "name", label: "按名称 (A-Z)" },
-  { value: "priority", label: "按优先级 (高-低)" },
-  { value: "weight", label: "按权重 (高-低)" },
-  { value: "createdAt", label: "按创建时间 (新-旧)" },
-];
-
-export function ProviderSortDropdown({ value, onChange }: ProviderSortDropdownProps) {
-  const selectedValue = value ?? "priority";
-
-  return (
-    <div className="flex items-center gap-2">
-      <ArrowUpDown className="h-4 w-4 text-muted-foreground" />
-      <Select value={selectedValue} onValueChange={(nextValue) => onChange(nextValue as SortKey)}>
-        <SelectTrigger className="w-[200px]">
-          <SelectValue placeholder="排序供应商" />
-        </SelectTrigger>
-        <SelectContent>
-          {SORT_OPTIONS.map((option) => (
-            <SelectItem key={option.value} value={option.value}>
-              {option.label}
-            </SelectItem>
-          ))}
-        </SelectContent>
-      </Select>
-    </div>
-  );
-}

+ 0 - 44
src/app/\[locale\]/settings/providers/_components/provider-type-filter.tsx

@@ -1,44 +0,0 @@
-"use client";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { Filter } from "lucide-react";
-import type { ProviderType } from "@/types/provider";
-import { PROVIDER_TYPE_CONFIG, getAllProviderTypes } from "@/lib/provider-type-utils";
-
-interface ProviderTypeFilterProps {
-  value: ProviderType | "all";
-  onChange: (value: ProviderType | "all") => void;
-}
-
-export function ProviderTypeFilter({ value, onChange }: ProviderTypeFilterProps) {
-  return (
-    <div className="flex items-center gap-2">
-      <Filter className="h-4 w-4 text-muted-foreground" />
-      <Select value={value} onValueChange={onChange}>
-        <SelectTrigger className="w-[200px]">
-          <SelectValue placeholder="筛选供应商类型" />
-        </SelectTrigger>
-        <SelectContent>
-          <SelectItem value="all">全部供应商</SelectItem>
-          {getAllProviderTypes().map((type) => {
-            const config = PROVIDER_TYPE_CONFIG[type];
-            const Icon = config.icon;
-            return (
-              <SelectItem key={type} value={type}>
-                <div className="flex items-center gap-2">
-                  <Icon className={`h-3.5 w-3.5 ${config.iconColor}`} />
-                  <span>{config.label}</span>
-                </div>
-              </SelectItem>
-            );
-          })}
-        </SelectContent>
-      </Select>
-    </div>
-  );
-}

+ 0 - 280
src/app/\[locale\]/settings/providers/_components/scheduling-rules-dialog.tsx

@@ -1,280 +0,0 @@
-"use client";
-
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogHeader,
-  DialogTitle,
-  DialogTrigger,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { Badge } from "@/components/ui/badge";
-import { Info, ChevronDown, ChevronRight, Lightbulb } from "lucide-react";
-import { useState } from "react";
-
-interface ScenarioStep {
-  step: string;
-  description: string;
-  example: {
-    before: string;
-    after: string;
-    decision: string;
-  };
-}
-
-const scenarios: Array<{
-  title: string;
-  emoji: string;
-  description: string;
-  steps: ScenarioStep[];
-}> = [
-  {
-    title: "优先级分层选择",
-    emoji: "🎯",
-    description: "系统首先按优先级过滤,只从最高优先级的供应商中选择",
-    steps: [
-      {
-        step: "初始状态",
-        description: "有 4 个已启用的供应商,优先级各不相同",
-        example: {
-          before: "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)",
-          after: "筛选出最高优先级(0)的供应商:A, C",
-          decision: "只从 A 和 C 中选择,B 和 D 被过滤",
-        },
-      },
-      {
-        step: "成本排序",
-        description: "在同优先级内,按成本倍率从低到高排序",
-        example: {
-          before: "A (成本 1.0x), C (成本 0.8x)",
-          after: "排序后:C (0.8x), A (1.0x)",
-          decision: "成本更低的 C 有更高的被选中概率",
-        },
-      },
-      {
-        step: "加权随机",
-        description: "使用权重进行随机选择,权重越高被选中概率越大",
-        example: {
-          before: "C (权重 3), A (权重 1)",
-          after: "C 被选中概率 75%, A 被选中概率 25%",
-          decision: "最终随机选择了 C",
-        },
-      },
-    ],
-  },
-  {
-    title: "用户分组过滤",
-    emoji: "👥",
-    description: "如果用户指定了供应商组,系统会优先从该组中选择",
-    steps: [
-      {
-        step: "检查用户分组",
-        description: "用户配置了 providerGroup = 'premium'",
-        example: {
-          before: "所有供应商:A (default), B (premium), C (premium), D (economy)",
-          after: "过滤出 'premium' 组:B, C",
-          decision: "只从 B 和 C 中选择",
-        },
-      },
-      {
-        step: "分组降级",
-        description: "如果用户组内没有可用供应商,降级到所有供应商",
-        example: {
-          before: "用户组 'vip' 内的供应商全部禁用或超限",
-          after: "降级到所有启用的供应商:A, B, C, D",
-          decision: "记录警告并从全局供应商池中选择",
-        },
-      },
-    ],
-  },
-  {
-    title: "健康度过滤(熔断器 + 限流)",
-    emoji: "🛡️",
-    description: "系统自动过滤掉熔断或超限的供应商",
-    steps: [
-      {
-        step: "熔断器检查",
-        description: "连续失败 5 次后熔断器打开,60 秒内不可用",
-        example: {
-          before: "供应商 A 连续失败 5 次,熔断器状态:open",
-          after: "A 被过滤,剩余:B, C, D",
-          decision: "A 在 60 秒后自动恢复到半开状态",
-        },
-      },
-      {
-        step: "金额限流",
-        description: "检查 5 小时、7 天、30 天的消费额度是否超限",
-        example: {
-          before: "供应商 B 的 5 小时限额 $10,已消耗 $9.8",
-          after: "B 被过滤(接近限额),剩余:C, D",
-          decision: "5 小时窗口滑动后自动恢复",
-        },
-      },
-      {
-        step: "并发 Session 限制",
-        description: "检查当前活跃 Session 数是否超过配置的并发限制",
-        example: {
-          before: "供应商 C 并发限制 2,当前活跃 Session 数:2",
-          after: "C 被过滤(已满),剩余:D",
-          decision: "Session 过期(5 分钟)后自动释放",
-        },
-      },
-    ],
-  },
-  {
-    title: "会话复用机制",
-    emoji: "🔄",
-    description: "连续对话优先使用同一供应商,利用 Claude 的上下文缓存",
-    steps: [
-      {
-        step: "检查历史请求",
-        description: "查询该 API Key 最近 10 秒内使用的供应商",
-        example: {
-          before: "最近一次请求使用了供应商 B",
-          after: "检查 B 是否启用且健康",
-          decision: "B 可用,直接复用,跳过随机选择",
-        },
-      },
-      {
-        step: "复用失效",
-        description: "如果上次使用的供应商不可用,则重新选择",
-        example: {
-          before: "上次使用的供应商 B 已被禁用或熔断",
-          after: "进入正常选择流程",
-          decision: "从其他可用供应商中选择",
-        },
-      },
-    ],
-  },
-];
-
-function ScenarioCard({ title, emoji, description, steps }: (typeof scenarios)[0]) {
-  const [isOpen, setIsOpen] = useState(false);
-
-  return (
-    <Collapsible open={isOpen} onOpenChange={setIsOpen} className="border rounded-lg">
-      <CollapsibleTrigger asChild>
-        <button className="flex items-center justify-between w-full p-4 hover:bg-muted/50 transition-colors">
-          <div className="flex items-center gap-3">
-            <span className="text-2xl">{emoji}</span>
-            <div className="text-left">
-              <h3 className="font-semibold text-base">{title}</h3>
-              <p className="text-sm text-muted-foreground">{description}</p>
-            </div>
-          </div>
-          {isOpen ? (
-            <ChevronDown className="h-5 w-5 text-muted-foreground shrink-0" />
-          ) : (
-            <ChevronRight className="h-5 w-5 text-muted-foreground shrink-0" />
-          )}
-        </button>
-      </CollapsibleTrigger>
-      <CollapsibleContent>
-        <div className="px-4 pb-4 space-y-3">
-          {steps.map((step, index) => (
-            <div key={index} className="border-l-2 border-primary/30 pl-4 space-y-2">
-              <div className="flex items-baseline gap-2">
-                <Badge variant="outline" className="shrink-0">
-                  步骤 {index + 1}
-                </Badge>
-                <span className="font-medium text-sm">{step.step}</span>
-              </div>
-              <p className="text-sm text-muted-foreground">{step.description}</p>
-              <div className="bg-muted/50 rounded-md p-3 space-y-1.5 text-xs">
-                <div>
-                  <span className="font-medium">过滤前:</span>
-                  <span className="text-muted-foreground"> {step.example.before}</span>
-                </div>
-                <div>
-                  <span className="font-medium">过滤后:</span>
-                  <span className="text-muted-foreground"> {step.example.after}</span>
-                </div>
-                <div className="pt-1 border-t border-border/50">
-                  <span className="font-medium text-primary">决策:</span>
-                  <span className="text-foreground"> {step.example.decision}</span>
-                </div>
-              </div>
-            </div>
-          ))}
-        </div>
-      </CollapsibleContent>
-    </Collapsible>
-  );
-}
-
-export function SchedulingRulesDialog() {
-  return (
-    <Dialog>
-      <DialogTrigger asChild>
-        <Button variant="outline" size="sm" className="gap-2">
-          <Info className="h-4 w-4" />
-          调度规则说明
-        </Button>
-      </DialogTrigger>
-      <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
-        <DialogHeader>
-          <DialogTitle className="flex items-center gap-2 text-xl">
-            <Lightbulb className="h-5 w-5 text-primary" />
-            供应商调度规则说明
-          </DialogTitle>
-          <DialogDescription>
-            了解系统如何智能选择上游供应商,确保高可用性和成本优化
-          </DialogDescription>
-        </DialogHeader>
-
-        <div className="space-y-4 mt-4">
-          <Alert>
-            <Info className="h-4 w-4" />
-            <AlertTitle>核心原则</AlertTitle>
-            <AlertDescription className="space-y-1 text-sm">
-              <p>
-                1️⃣ <strong>优先级优先</strong>:只从最高优先级(数值最小)的供应商中选择
-              </p>
-              <p>
-                2️⃣ <strong>成本优化</strong>:同优先级内,成本倍率低的供应商有更高概率
-              </p>
-              <p>
-                3️⃣ <strong>健康过滤</strong>:自动跳过熔断或超限的供应商
-              </p>
-              <p>
-                4️⃣ <strong>会话复用</strong>:连续对话复用同一供应商,节省上下文成本
-              </p>
-            </AlertDescription>
-          </Alert>
-
-          <div className="space-y-3">
-            <h3 className="font-semibold text-sm text-muted-foreground">交互式场景演示</h3>
-            {scenarios.map((scenario, index) => (
-              <ScenarioCard key={index} {...scenario} />
-            ))}
-          </div>
-
-          <Alert variant="default" className="bg-primary/5 border-primary/20">
-            <Lightbulb className="h-4 w-4 text-primary" />
-            <AlertTitle className="text-primary">最佳实践建议</AlertTitle>
-            <AlertDescription className="space-y-1 text-sm text-foreground">
-              <p>
-                • <strong>优先级设置</strong>:核心供应商设为 0,备用供应商设为 1-3
-              </p>
-              <p>
-                • <strong>权重配置</strong>:根据供应商容量设置权重(容量大 = 权重高)
-              </p>
-              <p>
-                • <strong>成本倍率</strong>:官方倍率为 1.0,自建服务可设置为 0.8-1.2
-              </p>
-              <p>
-                • <strong>限额设置</strong>:根据预算设置 5 小时、7 天、30 天限额
-              </p>
-              <p>
-                • <strong>并发控制</strong>:根据供应商 API 限制设置 Session 并发数
-              </p>
-            </AlertDescription>
-          </Alert>
-        </div>
-      </DialogContent>
-    </Dialog>
-  );
-}

+ 0 - 144
src/app/\[locale\]/settings/sensitive-words/_components/add-word-dialog.tsx

@@ -1,144 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogFooter,
-  DialogHeader,
-  DialogTitle,
-  DialogTrigger,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { Plus } from "lucide-react";
-import { createSensitiveWordAction } from "@/actions/sensitive-words";
-import { toast } from "sonner";
-
-export function AddWordDialog() {
-  const [open, setOpen] = useState(false);
-  const [isSubmitting, setIsSubmitting] = useState(false);
-  const [word, setWord] = useState("");
-  const [matchType, setMatchType] = useState<"contains" | "exact" | "regex">("contains");
-  const [description, setDescription] = useState("");
-
-  const handleSubmit = async (e: React.FormEvent) => {
-    e.preventDefault();
-
-    if (!word.trim()) {
-      toast.error("请输入敏感词");
-      return;
-    }
-
-    setIsSubmitting(true);
-
-    try {
-      const result = await createSensitiveWordAction({
-        word: word.trim(),
-        matchType,
-        description: description.trim() || undefined,
-      });
-
-      if (result.ok) {
-        toast.success("敏感词创建成功");
-        setOpen(false);
-        // 重置表单
-        setWord("");
-        setMatchType("contains");
-        setDescription("");
-      } else {
-        toast.error(result.error);
-      }
-    } catch {
-      toast.error("创建敏感词失败");
-    } finally {
-      setIsSubmitting(false);
-    }
-  };
-
-  return (
-    <Dialog open={open} onOpenChange={setOpen}>
-      <DialogTrigger asChild>
-        <Button>
-          <Plus className="mr-2 h-4 w-4" />
-          添加敏感词
-        </Button>
-      </DialogTrigger>
-      <DialogContent>
-        <form onSubmit={handleSubmit}>
-          <DialogHeader>
-            <DialogTitle>添加敏感词</DialogTitle>
-            <DialogDescription>
-              配置敏感词过滤规则,被命中的请求将不会转发到上游。
-            </DialogDescription>
-          </DialogHeader>
-
-          <div className="grid gap-4 py-4">
-            <div className="grid gap-2">
-              <Label htmlFor="word">敏感词 *</Label>
-              <Input
-                id="word"
-                value={word}
-                onChange={(e) => setWord(e.target.value)}
-                placeholder="输入敏感词..."
-                required
-              />
-            </div>
-
-            <div className="grid gap-2">
-              <Label htmlFor="matchType">匹配类型 *</Label>
-              <Select
-                value={matchType}
-                onValueChange={(value) => setMatchType(value as "contains" | "exact" | "regex")}
-              >
-                <SelectTrigger>
-                  <SelectValue />
-                </SelectTrigger>
-                <SelectContent>
-                  <SelectItem value="contains">包含匹配 - 文本中包含该词即拦截</SelectItem>
-                  <SelectItem value="exact">精确匹配 - 完全匹配该词才拦截</SelectItem>
-                  <SelectItem value="regex">正则表达式 - 支持复杂模式匹配</SelectItem>
-                </SelectContent>
-              </Select>
-            </div>
-
-            <div className="grid gap-2">
-              <Label htmlFor="description">说明</Label>
-              <Textarea
-                id="description"
-                value={description}
-                onChange={(e) => setDescription(e.target.value)}
-                placeholder="可选:添加说明..."
-                rows={3}
-              />
-            </div>
-          </div>
-
-          <DialogFooter>
-            <Button
-              type="button"
-              variant="outline"
-              onClick={() => setOpen(false)}
-              disabled={isSubmitting}
-            >
-              取消
-            </Button>
-            <Button type="submit" disabled={isSubmitting}>
-              {isSubmitting ? "创建中..." : "创建"}
-            </Button>
-          </DialogFooter>
-        </form>
-      </DialogContent>
-    </Dialog>
-  );
-}

+ 0 - 142
src/app/\[locale\]/settings/sensitive-words/_components/edit-word-dialog.tsx

@@ -1,142 +0,0 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogFooter,
-  DialogHeader,
-  DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { updateSensitiveWordAction } from "@/actions/sensitive-words";
-import { toast } from "sonner";
-import type { SensitiveWord } from "@/repository/sensitive-words";
-
-interface EditWordDialogProps {
-  word: SensitiveWord;
-  open: boolean;
-  onOpenChange: (open: boolean) => void;
-}
-
-export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps) {
-  const [isSubmitting, setIsSubmitting] = useState(false);
-  const [wordText, setWordText] = useState("");
-  const [matchType, setMatchType] = useState<string>("");
-  const [description, setDescription] = useState("");
-
-  // 当 word 改变时更新表单
-  useEffect(() => {
-    if (word) {
-      setWordText(word.word);
-      setMatchType(word.matchType);
-      setDescription(word.description || "");
-    }
-  }, [word]);
-
-  const handleSubmit = async (e: React.FormEvent) => {
-    e.preventDefault();
-
-    if (!wordText.trim()) {
-      toast.error("请输入敏感词");
-      return;
-    }
-
-    setIsSubmitting(true);
-
-    try {
-      const result = await updateSensitiveWordAction(word.id, {
-        word: wordText.trim(),
-        matchType,
-        description: description.trim() || undefined,
-      });
-
-      if (result.ok) {
-        toast.success("敏感词更新成功");
-        onOpenChange(false);
-      } else {
-        toast.error(result.error);
-      }
-    } catch {
-      toast.error("更新敏感词失败");
-    } finally {
-      setIsSubmitting(false);
-    }
-  };
-
-  return (
-    <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent>
-        <form onSubmit={handleSubmit}>
-          <DialogHeader>
-            <DialogTitle>编辑敏感词</DialogTitle>
-            <DialogDescription>修改敏感词配置,更改后将自动刷新缓存。</DialogDescription>
-          </DialogHeader>
-
-          <div className="grid gap-4 py-4">
-            <div className="grid gap-2">
-              <Label htmlFor="edit-word">敏感词 *</Label>
-              <Input
-                id="edit-word"
-                value={wordText}
-                onChange={(e) => setWordText(e.target.value)}
-                placeholder="输入敏感词..."
-                required
-              />
-            </div>
-
-            <div className="grid gap-2">
-              <Label htmlFor="edit-matchType">匹配类型 *</Label>
-              <Select value={matchType} onValueChange={(value) => setMatchType(value)}>
-                <SelectTrigger>
-                  <SelectValue />
-                </SelectTrigger>
-                <SelectContent>
-                  <SelectItem value="contains">包含匹配 - 文本中包含该词即拦截</SelectItem>
-                  <SelectItem value="exact">精确匹配 - 完全匹配该词才拦截</SelectItem>
-                  <SelectItem value="regex">正则表达式 - 支持复杂模式匹配</SelectItem>
-                </SelectContent>
-              </Select>
-            </div>
-
-            <div className="grid gap-2">
-              <Label htmlFor="edit-description">说明</Label>
-              <Textarea
-                id="edit-description"
-                value={description}
-                onChange={(e) => setDescription(e.target.value)}
-                placeholder="可选:添加说明..."
-                rows={3}
-              />
-            </div>
-          </div>
-
-          <DialogFooter>
-            <Button
-              type="button"
-              variant="outline"
-              onClick={() => onOpenChange(false)}
-              disabled={isSubmitting}
-            >
-              取消
-            </Button>
-            <Button type="submit" disabled={isSubmitting}>
-              {isSubmitting ? "保存中..." : "保存"}
-            </Button>
-          </DialogFooter>
-        </form>
-      </DialogContent>
-    </Dialog>
-  );
-}

+ 0 - 57
src/app/\[locale\]/settings/sensitive-words/_components/refresh-cache-button.tsx

@@ -1,57 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import { RefreshCw } from "lucide-react";
-import { refreshCacheAction } from "@/actions/sensitive-words";
-import { toast } from "sonner";
-
-interface RefreshCacheButtonProps {
-  stats: {
-    containsCount: number;
-    exactCount: number;
-    regexCount: number;
-    totalCount: number;
-    lastReloadTime: number;
-  } | null;
-}
-
-export function RefreshCacheButton({ stats }: RefreshCacheButtonProps) {
-  const [isRefreshing, setIsRefreshing] = useState(false);
-
-  const handleRefresh = async () => {
-    setIsRefreshing(true);
-
-    try {
-      const result = await refreshCacheAction();
-
-      if (result.ok) {
-        const count = result.data.stats.totalCount;
-        toast.success(`缓存刷新成功,已加载 ${count} 个敏感词`);
-      } else {
-        toast.error(result.error);
-      }
-    } catch {
-      toast.error("刷新缓存失败");
-    } finally {
-      setIsRefreshing(false);
-    }
-  };
-
-  return (
-    <Button
-      variant="outline"
-      onClick={handleRefresh}
-      disabled={isRefreshing}
-      title={
-        stats
-          ? `缓存统计:包含(${stats.containsCount}) 精确(${stats.exactCount}) 正则(${stats.regexCount})`
-          : "刷新缓存"
-      }
-    >
-      <RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
-      刷新缓存
-      {stats && <span className="ml-2 text-xs text-muted-foreground">({stats.totalCount})</span>}
-    </Button>
-  );
-}

+ 0 - 137
src/app/\[locale\]/settings/sensitive-words/_components/word-list-table.tsx

@@ -1,137 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import type { SensitiveWord } from "@/repository/sensitive-words";
-import { updateSensitiveWordAction, deleteSensitiveWordAction } from "@/actions/sensitive-words";
-import { Button } from "@/components/ui/button";
-import { toast } from "sonner";
-import { Trash2, Pencil } from "lucide-react";
-import { Badge } from "@/components/ui/badge";
-import { Switch } from "@/components/ui/switch";
-import { EditWordDialog } from "./edit-word-dialog";
-
-interface WordListTableProps {
-  words: SensitiveWord[];
-}
-
-const matchTypeLabels = {
-  contains: "包含匹配",
-  exact: "精确匹配",
-  regex: "正则表达式",
-};
-
-const matchTypeColors = {
-  contains: "default" as const,
-  exact: "secondary" as const,
-  regex: "outline" as const,
-};
-
-export function WordListTable({ words }: WordListTableProps) {
-  const [selectedWord, setSelectedWord] = useState<SensitiveWord | null>(null);
-  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
-
-  const handleToggleEnabled = async (id: number, isEnabled: boolean) => {
-    const result = await updateSensitiveWordAction(id, { isEnabled });
-
-    if (result.ok) {
-      toast.success(isEnabled ? "敏感词已启用" : "敏感词已禁用");
-    } else {
-      toast.error(result.error);
-    }
-  };
-
-  const handleDelete = async (id: number, word: string) => {
-    if (!confirm(`确定要删除敏感词"${word}"吗?`)) {
-      return;
-    }
-
-    const result = await deleteSensitiveWordAction(id);
-
-    if (result.ok) {
-      toast.success("敏感词删除成功");
-    } else {
-      toast.error(result.error);
-    }
-  };
-
-  const handleEdit = (word: SensitiveWord) => {
-    setSelectedWord(word);
-    setIsEditDialogOpen(true);
-  };
-
-  if (words.length === 0) {
-    return (
-      <div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
-        暂无敏感词,点击右上角&ldquo;添加敏感词&rdquo;开始配置。
-      </div>
-    );
-  }
-
-  return (
-    <>
-      <div className="overflow-x-auto">
-        <table className="w-full border-collapse">
-          <thead>
-            <tr className="border-b bg-muted/50">
-              <th className="px-4 py-3 text-left text-sm font-medium">敏感词</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">匹配类型</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">说明</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">状态</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">创建时间</th>
-              <th className="px-4 py-3 text-right text-sm font-medium">操作</th>
-            </tr>
-          </thead>
-          <tbody>
-            {words.map((word) => (
-              <tr key={word.id} className="border-b hover:bg-muted/30">
-                <td className="px-4 py-3">
-                  <code className="rounded bg-muted px-2 py-1 text-sm">{word.word}</code>
-                </td>
-                <td className="px-4 py-3">
-                  <Badge variant={matchTypeColors[word.matchType as keyof typeof matchTypeColors]}>
-                    {matchTypeLabels[word.matchType as keyof typeof matchTypeLabels] ||
-                      word.matchType}
-                  </Badge>
-                </td>
-                <td className="px-4 py-3 text-sm text-muted-foreground">
-                  {word.description || "-"}
-                </td>
-                <td className="px-4 py-3">
-                  <Switch
-                    checked={word.isEnabled}
-                    onCheckedChange={(checked) => handleToggleEnabled(word.id, checked)}
-                  />
-                </td>
-                <td className="px-4 py-3 text-sm text-muted-foreground">
-                  {new Date(word.createdAt).toLocaleString("zh-CN")}
-                </td>
-                <td className="px-4 py-3 text-right">
-                  <div className="flex justify-end gap-2">
-                    <Button variant="ghost" size="sm" onClick={() => handleEdit(word)}>
-                      <Pencil className="h-4 w-4" />
-                    </Button>
-                    <Button
-                      variant="ghost"
-                      size="sm"
-                      onClick={() => handleDelete(word.id, word.word)}
-                    >
-                      <Trash2 className="h-4 w-4" />
-                    </Button>
-                  </div>
-                </td>
-              </tr>
-            ))}
-          </tbody>
-        </table>
-      </div>
-
-      {selectedWord && (
-        <EditWordDialog
-          word={selectedWord}
-          open={isEditDialogOpen}
-          onOpenChange={setIsEditDialogOpen}
-        />
-      )}
-    </>
-  );
-}

+ 0 - 36
src/app/\[locale\]/usage-doc/_components/quick-links.tsx

@@ -1,36 +0,0 @@
-"use client";
-
-interface QuickLinksProps {
-  isLoggedIn: boolean;
-  onBackToTop?: () => void;
-}
-
-/**
- * 快速链接组件
- * 支持桌面端和移动端复用
- */
-export function QuickLinks({ isLoggedIn, onBackToTop }: QuickLinksProps) {
-  const handleBackToTop = () => {
-    window.scrollTo({ top: 0, behavior: "smooth" });
-    onBackToTop?.();
-  };
-
-  return (
-    <div className="space-y-2">
-      {isLoggedIn && (
-        <a
-          href="/dashboard"
-          className="block text-sm text-muted-foreground hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded px-2 py-1"
-        >
-          返回仪表盘
-        </a>
-      )}
-      <button
-        onClick={handleBackToTop}
-        className="block w-full text-left text-sm text-muted-foreground hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded px-2 py-1"
-      >
-        回到顶部
-      </button>
-    </div>
-  );
-}

+ 0 - 60
src/app/\[locale\]/usage-doc/_components/toc-nav.tsx

@@ -1,60 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import { Skeleton } from "@/components/ui/skeleton";
-
-/**
- * 文档目录项
- */
-export interface TocItem {
-  id: string;
-  text: string;
-  level: number;
-}
-
-interface TocNavProps {
-  tocItems: TocItem[];
-  activeId: string;
-  tocReady: boolean;
-  onItemClick: (id: string) => void;
-}
-
-/**
- * 目录导航组件
- * 支持桌面端和移动端复用
- */
-export function TocNav({ tocItems, activeId, tocReady, onItemClick }: TocNavProps) {
-  return (
-    <nav aria-label="文档目录" className="space-y-1">
-      {!tocReady && (
-        <div className="space-y-2">
-          {Array.from({ length: 5 }).map((_, index) => (
-            <Skeleton key={index} className="h-5 w-full" />
-          ))}
-        </div>
-      )}
-      {tocReady && tocItems.length === 0 && (
-        <p className="text-xs text-muted-foreground">本页暂无可用章节</p>
-      )}
-      {tocReady &&
-        tocItems.length > 0 &&
-        tocItems.map((item) => (
-          <button
-            key={item.id}
-            onClick={() => onItemClick(item.id)}
-            className={cn(
-              "block w-full text-left text-sm px-3 py-1.5 rounded-md transition-colors",
-              "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
-              item.level === 3 && "pl-6 text-xs",
-              activeId === item.id
-                ? "bg-primary/10 text-primary font-medium"
-                : "text-muted-foreground hover:text-foreground hover:bg-muted"
-            )}
-            aria-current={activeId === item.id ? "location" : undefined}
-          >
-            {item.text}
-          </button>
-        ))}
-    </nav>
-  );
-}