Просмотр исходного кода

fix: assorted UI/UX and logic small fixes

miraserver 4 недель назад
Родитель
Сommit
8a30a436fa
26 измененных файлов с 155 добавлено и 125 удалено
  1. 3 3
      src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  2. 4 6
      src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx
  3. 28 22
      src/app/[locale]/dashboard/_components/user/forms/limit-rule-picker.tsx
  4. 4 4
      src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx
  5. 4 4
      src/app/[locale]/dashboard/_components/user/forms/quick-expire-picker.tsx
  6. 10 2
      src/app/[locale]/dashboard/availability/page.tsx
  7. 10 2
      src/app/[locale]/dashboard/leaderboard/page.tsx
  8. 4 4
      src/app/[locale]/dashboard/users/users-page-client.tsx
  9. 1 1
      src/app/[locale]/settings/data/_components/database-status.tsx
  10. 3 6
      src/app/[locale]/settings/data/_components/log-cleanup-panel.tsx
  11. 2 3
      src/app/[locale]/settings/data/page.tsx
  12. 1 1
      src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx
  13. 1 1
      src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx
  14. 3 3
      src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx
  15. 2 2
      src/app/[locale]/settings/error-rules/_components/error-rules-skeleton.tsx
  16. 2 2
      src/app/[locale]/settings/error-rules/_components/override-section.tsx
  17. 1 1
      src/app/[locale]/settings/error-rules/_components/refresh-cache-button.tsx
  18. 3 3
      src/app/[locale]/settings/error-rules/_components/regex-tester.tsx
  19. 7 7
      src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx
  20. 1 1
      src/app/[locale]/settings/error-rules/page.tsx
  21. 17 15
      src/app/[locale]/settings/providers/_components/provider-type-filter.tsx
  22. 2 2
      src/components/loading/page-skeletons.test.tsx
  23. 1 1
      src/components/loading/page-skeletons.tsx
  24. 38 26
      src/components/section.tsx
  25. 1 1
      src/components/ui/data-table.tsx
  26. 2 2
      src/repository/cache-hit-rate-alert.ts

+ 3 - 3
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx

@@ -555,9 +555,9 @@ export function StatisticsChartCard({
 
       {/* Legend */}
       {enableUserFilter && (
-        <div ref={legendRef} className="relative px-4 pb-2">
+        <div ref={legendRef} className="relative px-4 pb-2 min-h-[20px]">
           {/* Control buttons (floating, does not take extra vertical space) */}
-          <div className="absolute right-4 rtl:right-auto rtl:left-4 top-0.5 z-10 w-24 flex flex-wrap justify-end gap-x-2 gap-y-0.5">
+          <div className="absolute right-4 rtl:right-auto rtl:left-4 top-0.5 z-10 w-auto flex flex-nowrap justify-end gap-x-2 gap-y-0.5">
             <button
               onClick={() => setSelectedUserIds(new Set(data.users.map((u) => u.id)))}
               disabled={selectedUserIds.size === data.users.length}
@@ -594,7 +594,7 @@ export function StatisticsChartCard({
             </button>
           </div>
           {/* User list with max 3 rows (3 * 24px = 72px) and scroll - only show users with non-zero usage */}
-          <div className="max-h-[72px] overflow-y-auto pr-24 rtl:pr-0 rtl:pl-24">
+          <div className="max-h-[72px] overflow-y-auto pr-36 rtl:pr-0 rtl:pl-36">
             <div className="flex flex-wrap gap-1.5 justify-center">
               {data.users
                 .map((user, originalIndex) => ({ user, originalIndex }))

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

@@ -64,12 +64,10 @@ export function EditKeyForm({ keyData, user, isAdmin = false, onSuccess }: EditK
   useEffect(() => {
     // providerGroup 为 admin-only 字段:仅管理员允许编辑 Key.providerGroup
     if (!isAdmin) return;
-    if (user?.id) {
-      getAvailableProviderGroups(user.id).then(setProviderGroupSuggestions);
-    } else {
-      getAvailableProviderGroups().then(setProviderGroupSuggestions);
-    }
-  }, [isAdmin, user?.id]);
+    getAvailableProviderGroups()
+      .then(setProviderGroupSuggestions)
+      .catch(() => {});
+  }, [isAdmin]);
 
   const formatExpiresAt = (expiresAt: string) => {
     if (!expiresAt) return "";

+ 28 - 22
src/app/[locale]/dashboard/_components/user/forms/limit-rule-picker.tsx

@@ -55,13 +55,13 @@ export interface LimitRulePickerProps {
 }
 
 const LIMIT_TYPE_OPTIONS: Array<{ type: LimitType; fallbackLabel: string }> = [
-  { type: "limitRpm", fallbackLabel: "RPM 限额" },
-  { type: "limit5h", fallbackLabel: "5小时限额" },
-  { type: "limitDaily", fallbackLabel: "每日限额" },
-  { type: "limitWeekly", fallbackLabel: "周限额" },
-  { type: "limitMonthly", fallbackLabel: "月限额" },
-  { type: "limitTotal", fallbackLabel: "总限额" },
-  { type: "limitSessions", fallbackLabel: "并发 Session" },
+  { type: "limitRpm", fallbackLabel: "RPM limit" },
+  { type: "limit5h", fallbackLabel: "5h limit" },
+  { type: "limitDaily", fallbackLabel: "Daily limit" },
+  { type: "limitWeekly", fallbackLabel: "Weekly limit" },
+  { type: "limitMonthly", fallbackLabel: "Monthly limit" },
+  { type: "limitTotal", fallbackLabel: "Total limit" },
+  { type: "limitSessions", fallbackLabel: "Concurrent sessions" },
 ];
 
 const QUICK_VALUES = [10, 50, 100, 500] as const;
@@ -132,17 +132,19 @@ export function LimitRulePicker({
     setError(null);
 
     if (!type) {
-      setError(getTranslation(translations, "errors.missingType", "请选择限额类型"));
+      setError(getTranslation(translations, "errors.missingType", "Please select a limit type"));
       return;
     }
 
     if (!Number.isFinite(numericValue) || numericValue < 0) {
-      setError(getTranslation(translations, "errors.invalidValue", "请输入有效数值"));
+      setError(getTranslation(translations, "errors.invalidValue", "Please enter a valid value"));
       return;
     }
 
     if (needsTime && !isValidTime(dailyTime)) {
-      setError(getTranslation(translations, "errors.invalidTime", "请输入有效时间 (HH:mm)"));
+      setError(
+        getTranslation(translations, "errors.invalidTime", "Please enter a valid time (HH:mm)")
+      );
       return;
     }
 
@@ -158,9 +160,9 @@ export function LimitRulePicker({
     <Dialog open={open} onOpenChange={onOpenChange}>
       <DialogContent className="sm:max-w-[680px]">
         <DialogHeader>
-          <DialogTitle>{getTranslation(translations, "title", "添加限额规则")}</DialogTitle>
+          <DialogTitle>{getTranslation(translations, "title", "Add limit rule")}</DialogTitle>
           <DialogDescription>
-            {getTranslation(translations, "description", "选择限额类型并设置数值")}
+            {getTranslation(translations, "description", "Select limit type and set value")}
           </DialogDescription>
         </DialogHeader>
 
@@ -174,11 +176,11 @@ export function LimitRulePicker({
         >
           <div className="grid gap-4 sm:grid-cols-2">
             <div className="space-y-2">
-              <Label>{getTranslation(translations, "fields.type.label", "限额类型")}</Label>
+              <Label>{getTranslation(translations, "fields.type.label", "Limit type")}</Label>
               <Select value={type} onValueChange={(val) => setType(val as LimitType)}>
                 <SelectTrigger>
                   <SelectValue
-                    placeholder={getTranslation(translations, "fields.type.placeholder", "请选择")}
+                    placeholder={getTranslation(translations, "fields.type.placeholder", "Select")}
                   />
                 </SelectTrigger>
                 <SelectContent>
@@ -196,7 +198,7 @@ export function LimitRulePicker({
                     {getTranslation(
                       translations,
                       "overwriteHint",
-                      "此类型已存在,保存将覆盖原有值"
+                      "This type already exists, saving will overwrite"
                     )}
                   </span>
                 </div>
@@ -204,7 +206,7 @@ export function LimitRulePicker({
             </div>
 
             <div className="space-y-2">
-              <Label>{getTranslation(translations, "fields.value.label", "数值")}</Label>
+              <Label>{getTranslation(translations, "fields.value.label", "Value")}</Label>
               <Input
                 type="number"
                 min={0}
@@ -213,7 +215,11 @@ export function LimitRulePicker({
                 autoFocus
                 value={rawValue}
                 onChange={(e) => setRawValue(e.target.value)}
-                placeholder={getTranslation(translations, "fields.value.placeholder", "请输入")}
+                placeholder={getTranslation(
+                  translations,
+                  "fields.value.placeholder",
+                  "Enter value"
+                )}
                 aria-invalid={Boolean(error)}
               />
 
@@ -232,7 +238,7 @@ export function LimitRulePicker({
                     onClick={() => setRawValue(String(v))}
                   >
                     {v === 0
-                      ? getTranslation(translations, "quickValues.unlimited", "无限")
+                      ? getTranslation(translations, "quickValues.unlimited", "Unlimited")
                       : type === "limitSessions" || type === "limitRpm"
                         ? v
                         : `$${v}`}
@@ -245,7 +251,7 @@ export function LimitRulePicker({
           {isDaily && (
             <div className={cn("grid gap-4", dailyMode === "fixed" ? "sm:grid-cols-2" : "")}>
               <div className="space-y-2">
-                <Label>{getTranslation(translations, "daily.mode.label", "每日模式")}</Label>
+                <Label>{getTranslation(translations, "daily.mode.label", "Daily mode")}</Label>
                 <Select
                   value={dailyMode}
                   onValueChange={(val) => setDailyMode(val as DailyResetMode)}
@@ -266,7 +272,7 @@ export function LimitRulePicker({
 
               {dailyMode === "fixed" && (
                 <div className="space-y-2">
-                  <Label>{getTranslation(translations, "daily.time.label", "重置时间")}</Label>
+                  <Label>{getTranslation(translations, "daily.time.label", "Reset time")}</Label>
                   <Input
                     type="time"
                     step={60}
@@ -290,10 +296,10 @@ export function LimitRulePicker({
 
           <DialogFooter>
             <Button type="button" variant="outline" onClick={handleCancel}>
-              {getTranslation(translations, "cancel", "取消")}
+              {getTranslation(translations, "cancel", "Cancel")}
             </Button>
             <Button type="submit" disabled={!canConfirm}>
-              {getTranslation(translations, "confirm", "保存")}
+              {getTranslation(translations, "confirm", "Save")}
             </Button>
           </DialogFooter>
         </form>

+ 4 - 4
src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx

@@ -45,7 +45,7 @@ export function ProviderGroupSelect({
   const [groups, setGroups] = useState<Array<{ group: string; providerCount: number }>>([]);
   const [isLoading, setIsLoading] = useState(false);
   const loadFailedText = useMemo(
-    () => getTranslation(translations, "errors.loadFailed", "加载失败"),
+    () => getTranslation(translations, "errors.loadFailed", "Load failed"),
     [translations]
   );
 
@@ -99,7 +99,7 @@ export function ProviderGroupSelect({
   const description = useMemo(() => {
     const base = getTranslation(translations, "description", "");
     if (isLoading && !base) {
-      return getTranslation(translations, "loadingText", "加载中...");
+      return getTranslation(translations, "loadingText", "Loading...");
     }
     return base;
   }, [translations, isLoading]);
@@ -124,8 +124,8 @@ export function ProviderGroupSelect({
 
   return (
     <TagInputField
-      label={getTranslation(translations, "label", "供应商分组")}
-      placeholder={getTranslation(translations, "placeholder", "输入分组并回车")}
+      label={getTranslation(translations, "label", "Provider group")}
+      placeholder={getTranslation(translations, "placeholder", "Enter group and press Enter")}
       description={description}
       maxTagLength={200}
       maxTags={20}

+ 4 - 4
src/app/[locale]/dashboard/_components/user/forms/quick-expire-picker.tsx

@@ -41,7 +41,7 @@ export function QuickExpirePicker({ onSelect, translations }: QuickExpirePickerP
   return (
     <div className="flex flex-wrap gap-2">
       <Button type="button" variant="outline" size="sm" onClick={() => onSelect(addDays(base, 7))}>
-        {getTranslation(translations, "week", "一周后")}
+        {getTranslation(translations, "week", "1 week")}
       </Button>
       <Button
         type="button"
@@ -49,7 +49,7 @@ export function QuickExpirePicker({ onSelect, translations }: QuickExpirePickerP
         size="sm"
         onClick={() => onSelect(addMonths(base, 1))}
       >
-        {getTranslation(translations, "month", "一月后")}
+        {getTranslation(translations, "month", "1 month")}
       </Button>
       <Button
         type="button"
@@ -57,10 +57,10 @@ export function QuickExpirePicker({ onSelect, translations }: QuickExpirePickerP
         size="sm"
         onClick={() => onSelect(addMonths(base, 3))}
       >
-        {getTranslation(translations, "threeMonths", "三月后")}
+        {getTranslation(translations, "threeMonths", "3 months")}
       </Button>
       <Button type="button" variant="outline" size="sm" onClick={() => onSelect(addYears(base, 1))}>
-        {getTranslation(translations, "year", "一年后")}
+        {getTranslation(translations, "year", "1 year")}
       </Button>
     </div>
   );

+ 10 - 2
src/app/[locale]/dashboard/availability/page.tsx

@@ -20,7 +20,11 @@ export default async function AvailabilityPage() {
   if (!isAdmin) {
     return (
       <div className="space-y-6">
-        <Section title={t("availability.title")} description={t("availability.description")}>
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">{t("availability.title")}</h1>
+          <p className="mt-2 text-muted-foreground">{t("availability.description")}</p>
+        </div>
+        <Section>
           <Card>
             <CardHeader>
               <CardTitle className="flex items-center gap-2">
@@ -43,7 +47,11 @@ export default async function AvailabilityPage() {
 
   return (
     <div className="space-y-6">
-      <Section title={t("availability.title")} description={t("availability.description")}>
+      <div>
+        <h1 className="text-3xl font-bold tracking-tight">{t("availability.title")}</h1>
+        <p className="mt-2 text-muted-foreground">{t("availability.description")}</p>
+      </div>
+      <Section>
         <Suspense fallback={<AvailabilityDashboardSkeleton />}>
           <AvailabilityDashboard />
         </Suspense>

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

@@ -24,7 +24,11 @@ export default async function LeaderboardPage() {
   if (!hasPermission) {
     return (
       <div className="space-y-6">
-        <Section title={t("title.costRanking")} description={t("title.costRankingDescription")}>
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">{t("title.costRanking")}</h1>
+          <p className="mt-2 text-muted-foreground">{t("title.costRankingDescription")}</p>
+        </div>
+        <Section>
           <Card>
             <CardHeader>
               <CardTitle className="flex items-center gap-2">
@@ -60,7 +64,11 @@ export default async function LeaderboardPage() {
   // 有权限时渲染排行榜
   return (
     <div className="space-y-6">
-      <Section title={t("title.costRanking")} description={t("title.costRankingDescription")}>
+      <div>
+        <h1 className="text-3xl font-bold tracking-tight">{t("title.costRanking")}</h1>
+        <p className="mt-2 text-muted-foreground">{t("title.costRankingDescription")}</p>
+      </div>
+      <Section>
         <LeaderboardView isAdmin={isAdmin} />
       </Section>
     </div>

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

@@ -574,17 +574,17 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) {
 
   return (
     <div className="space-y-4">
-      <div className="flex items-center justify-between">
+      <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
         <div>
-          <h3 className="text-lg font-medium">{t("title")}</h3>
-          <p className="text-sm text-muted-foreground">
+          <h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
+          <p className="mt-2 text-muted-foreground">
             {isInitialLoading
               ? tCommon("loading")
               : t("description", { count: visibleUsers.length })}
           </p>
         </div>
         {isAdmin && (
-          <Button onClick={handleCreateUser}>
+          <Button onClick={handleCreateUser} className="shrink-0">
             <Plus className="mr-2 h-4 w-4" />
             {t("toolbar.createUser")}
           </Button>

+ 1 - 1
src/app/[locale]/settings/data/_components/database-status.tsx

@@ -77,7 +77,7 @@ export function DatabaseStatusDisplay() {
       <div className="flex items-center justify-between">
         <div className="flex items-center gap-3">
           {status.isAvailable ? (
-            <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide border bg-green-500/10 text-green-400 border-green-500/20">
+            <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide border bg-green-500/15 text-green-500 border-green-500/30">
               {t("connected")}
             </span>
           ) : (

+ 3 - 6
src/app/[locale]/settings/data/_components/log-cleanup-panel.tsx

@@ -146,13 +146,10 @@ export function LogCleanupPanel() {
         </strong>
       </p>
 
-      <div className="flex flex-col gap-3 p-4 rounded-xl bg-white/[0.02] border border-white/5">
+      <div className="flex flex-col gap-3 p-4 rounded-xl bg-card/80 border border-border/50">
         <Label htmlFor="time-range">{t("rangeLabel")}</Label>
         <Select value={timeRange} onValueChange={setTimeRange}>
-          <SelectTrigger
-            id="time-range"
-            className="w-full sm:w-[300px] border-white/10 bg-white/[0.02]"
-          >
+          <SelectTrigger id="time-range" className="w-full sm:w-[300px]">
             <SelectValue />
           </SelectTrigger>
           <SelectContent>
@@ -183,7 +180,7 @@ export function LogCleanupPanel() {
               <p>{t("confirmWarning", { range: getTimeRangeDescription() })}</p>
 
               {/* Preview info */}
-              <div className="bg-white/[0.02] border border-white/5 p-3 rounded-xl">
+              <div className="bg-card/80 border border-border/50 p-3 rounded-xl">
                 {isPreviewLoading ? (
                   <div className="flex items-center gap-2 text-sm">
                     <Loader2 className="h-4 w-4 animate-spin" />

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

@@ -17,7 +17,7 @@ export default function SettingsDataPage() {
   const [isUsageGuideOpen, setIsUsageGuideOpen] = useState(false);
 
   return (
-    <>
+    <div className="space-y-4">
       <SettingsPageHeader
         title={t("data.title")}
         description={t("data.description")}
@@ -37,7 +37,6 @@ export default function SettingsDataPage() {
         title={t("data.section.cleanup.title")}
         description={t("data.section.cleanup.description")}
         icon="trash"
-        variant="warning"
       >
         <LogCleanupPanel />
       </Section>
@@ -108,6 +107,6 @@ export default function SettingsDataPage() {
           </CollapsibleContent>
         </Collapsible>
       </div>
-    </>
+    </div>
   );
 }

+ 1 - 1
src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx

@@ -244,7 +244,7 @@ export function AddRuleDialog() {
               variant="ghost"
               onClick={() => setOpen(false)}
               disabled={isSubmitting}
-              className="hover:bg-white/10"
+              className="hover:bg-muted"
             >
               {t("common.cancel")}
             </Button>

+ 1 - 1
src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx

@@ -263,7 +263,7 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
               variant="ghost"
               onClick={() => onOpenChange(false)}
               disabled={isSubmitting}
-              className="hover:bg-white/10"
+              className="hover:bg-muted"
             >
               {t("common.cancel")}
             </Button>

+ 3 - 3
src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx

@@ -91,7 +91,7 @@ export function ErrorRuleTester() {
       </Button>
 
       {result && (
-        <div className="space-y-4 rounded-xl bg-white/[0.02] border border-border/50 p-4">
+        <div className="space-y-4 rounded-xl bg-card/80 border border-border/50 p-4">
           {/* Match Status */}
           <div className="flex flex-wrap items-center gap-2">
             {result.matched ? (
@@ -105,7 +105,7 @@ export function ErrorRuleTester() {
               </>
             ) : (
               <>
-                <div className="p-1.5 rounded-lg bg-white/5">
+                <div className="p-1.5 rounded-lg bg-muted/50">
                   <XCircle className="h-4 w-4 text-muted-foreground" />
                 </div>
                 <span className="text-sm font-medium text-muted-foreground">
@@ -129,7 +129,7 @@ export function ErrorRuleTester() {
                     </span>
                     <Badge
                       variant="secondary"
-                      className="bg-white/5 text-foreground border-border text-[10px]"
+                      className="bg-muted/50 text-foreground border-border text-[10px]"
                     >
                       {result.rule.category}
                     </Badge>

+ 2 - 2
src/app/[locale]/settings/error-rules/_components/error-rules-skeleton.tsx

@@ -4,7 +4,7 @@ export function ErrorRulesSkeleton() {
   return (
     <div className="space-y-6">
       {/* Tester Section Skeleton */}
-      <div className="rounded-xl border border-white/5 bg-card/30 backdrop-blur-sm p-5 md:p-6 space-y-4">
+      <div className="rounded-xl border border-border/50 bg-card/50 backdrop-blur-sm p-5 md:p-6 space-y-4">
         <div className="flex items-start gap-3">
           <Skeleton className="h-9 w-9 rounded-lg shrink-0" />
           <div className="space-y-2 flex-1">
@@ -40,7 +40,7 @@ export function ErrorRulesTableSkeleton() {
       {Array.from({ length: 5 }).map((_, i) => (
         <div
           key={i}
-          className="p-4 rounded-xl bg-white/[0.02] border border-white/5 flex items-center justify-between gap-4"
+          className="p-4 rounded-xl bg-card/80 border border-border/50 flex items-center justify-between gap-4"
         >
           <div className="flex items-start gap-3 flex-1">
             <Skeleton className="h-8 w-8 rounded-lg shrink-0" />

+ 2 - 2
src/app/[locale]/settings/error-rules/_components/override-section.tsx

@@ -100,7 +100,7 @@ export function OverrideSection({
   );
 
   return (
-    <div className="rounded-xl bg-white/[0.02] border border-border/50 p-4 space-y-4">
+    <div className="rounded-xl bg-card/80 border border-border/50 p-4 space-y-4">
       <div className="flex items-center space-x-2">
         <Checkbox
           id={`${idPrefix}-enableOverride`}
@@ -146,7 +146,7 @@ export function OverrideSection({
                       type="button"
                       variant="ghost"
                       size="sm"
-                      className="h-6 text-xs hover:bg-white/10"
+                      className="h-6 text-xs hover:bg-muted"
                     >
                       {t("errorRules.dialog.useTemplate")}
                       <ChevronDown className="ml-1 h-3 w-3" />

+ 1 - 1
src/app/[locale]/settings/error-rules/_components/refresh-cache-button.tsx

@@ -47,7 +47,7 @@ export function RefreshCacheButton({ stats }: RefreshCacheButtonProps) {
       variant="outline"
       onClick={handleRefresh}
       disabled={isRefreshing}
-      className="bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"
+      className="bg-muted/50 border-border hover:bg-muted hover:border-border"
       title={
         stats
           ? t("errorRules.cacheStats", {

+ 3 - 3
src/app/[locale]/settings/error-rules/_components/regex-tester.tsx

@@ -45,7 +45,7 @@ export function RegexTester({ pattern }: RegexTesterProps) {
   }, [pattern, testMessage]);
 
   return (
-    <div className="space-y-3 rounded-xl bg-white/[0.02] border border-border/50 p-4">
+    <div className="space-y-3 rounded-xl bg-card/80 border border-border/50 p-4">
       <div className="space-y-2">
         <label
           htmlFor="test-message"
@@ -82,12 +82,12 @@ export function RegexTester({ pattern }: RegexTesterProps) {
                 </>
               ) : (
                 <>
-                  <div className="p-1 rounded-md bg-white/5">
+                  <div className="p-1 rounded-md bg-muted/50">
                     <XCircle className="h-3.5 w-3.5 text-muted-foreground" />
                   </div>
                   <Badge
                     variant="secondary"
-                    className="bg-white/5 text-muted-foreground border-border text-[10px]"
+                    className="bg-muted/50 text-muted-foreground border-border text-[10px]"
                   >
                     {t("errorRules.dialog.matchFailed")}
                   </Badge>

+ 7 - 7
src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx

@@ -71,7 +71,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
   if (rules.length === 0) {
     return (
       <div className="flex flex-col items-center justify-center py-12 text-center">
-        <div className="p-3 rounded-xl bg-white/[0.02] border border-white/5 mb-4">
+        <div className="p-3 rounded-xl bg-card/80 border border-border/50 mb-4">
           <AlertTriangle className="h-8 w-8 text-muted-foreground" />
         </div>
         <p className="text-sm text-muted-foreground">{t("errorRules.emptyState")}</p>
@@ -92,9 +92,9 @@ export function RuleListTable({ rules }: RuleListTableProps) {
             <div
               key={rule.id}
               className={cn(
-                "p-4 rounded-xl bg-white/[0.02] border border-white/5",
+                "p-4 rounded-xl bg-card/80 border border-border/50",
                 "flex flex-col sm:flex-row sm:items-center justify-between gap-4",
-                "hover:bg-white/[0.04] hover:border-white/10 transition-colors group"
+                "hover:bg-card hover:border-border transition-colors group"
               )}
             >
               <div className="flex items-start gap-3 min-w-0 flex-1">
@@ -122,7 +122,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
                     {rule.isDefault && (
                       <Badge
                         variant="secondary"
-                        className="text-[10px] bg-white/5 text-muted-foreground border-white/10"
+                        className="text-[10px] bg-muted/50 text-muted-foreground border-border"
                       >
                         {t("errorRules.table.default")}
                       </Badge>
@@ -130,7 +130,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
                     {rule.category && (
                       <Badge
                         variant="outline"
-                        className={cn("text-[10px] border-white/10", colors.text)}
+                        className={cn("text-[10px] border-border", colors.text)}
                       >
                         {rule.category}
                       </Badge>
@@ -141,7 +141,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
                       {rule.description}
                     </p>
                   )}
-                  <p className="text-[10px] text-muted-foreground/60 mt-1">
+                  <p className="text-[10px] text-muted-foreground mt-1">
                     {formatInTimeZone(new Date(rule.createdAt), timeZone, "yyyy-MM-dd HH:mm:ss")}
                   </p>
                 </div>
@@ -156,7 +156,7 @@ export function RuleListTable({ rules }: RuleListTableProps) {
                   <Button
                     variant="ghost"
                     size="icon"
-                    className="h-8 w-8 hover:bg-white/10"
+                    className="h-8 w-8 hover:bg-muted"
                     onClick={() => handleEdit(rule)}
                   >
                     <Pencil className="h-4 w-4" />

+ 1 - 1
src/app/[locale]/settings/error-rules/page.tsx

@@ -36,7 +36,7 @@ export default async function ErrorRulesPage() {
           title={t("errorRules.section.title")}
           icon="alert-triangle"
           iconColor="text-orange-400"
-          variant="warning"
+          variant="default"
           actions={
             <div className="flex gap-2">
               <Suspense fallback={<Skeleton className="h-9 w-24" />}>

+ 17 - 15
src/app/[locale]/settings/providers/_components/provider-type-filter.tsx

@@ -29,26 +29,28 @@ export function ProviderTypeFilter({ value, onChange, disabled = false }: Provid
     <div className="flex items-center gap-2">
       <Filter className="h-4 w-4 text-muted-foreground" />
       <Select value={value} onValueChange={onChange} disabled={disabled}>
-        <SelectTrigger className="w-[200px]" disabled={disabled}>
+        <SelectTrigger className="w-[160px]" disabled={disabled}>
           <SelectValue placeholder={tForm("filterByType")} />
         </SelectTrigger>
         <SelectContent>
           <SelectItem value="all">{tForm("filterAllProviders")}</SelectItem>
-          {getAllProviderTypes().map((type) => {
-            const config = PROVIDER_TYPE_CONFIG[type];
-            const Icon = config.icon;
-            const typeKey = getProviderTypeTranslationKey(type);
-            const label = tTypes(`${typeKey}.label`);
+          {getAllProviderTypes()
+            .filter((type) => !["claude-auth", "gemini-cli"].includes(type)) // internal/system types
+            .map((type) => {
+              const config = PROVIDER_TYPE_CONFIG[type];
+              const Icon = config.icon;
+              const typeKey = getProviderTypeTranslationKey(type);
+              const label = tTypes(`${typeKey}.label`);
 
-            return (
-              <SelectItem key={type} value={type}>
-                <div className="flex items-center gap-2">
-                  <Icon className={`h-3.5 w-3.5 ${config.iconColor}`} />
-                  <span>{label}</span>
-                </div>
-              </SelectItem>
-            );
-          })}
+              return (
+                <SelectItem key={type} value={type}>
+                  <div className="flex items-center gap-2">
+                    <Icon className={`h-3.5 w-3.5 ${config.iconColor}`} />
+                    <span className="truncate max-w-[100px]">{label}</span>
+                  </div>
+                </SelectItem>
+              );
+            })}
         </SelectContent>
       </Select>
     </div>

+ 2 - 2
src/components/loading/page-skeletons.test.tsx

@@ -8,8 +8,8 @@ import {
 
 describe("page-skeletons", () => {
   test("LoadingState renders label and aria-busy", () => {
-    const html = renderToStaticMarkup(<LoadingState label="加载中" />);
-    expect(html).toContain("加载中");
+    const html = renderToStaticMarkup(<LoadingState label="Loading" />);
+    expect(html).toContain("Loading");
     expect(html).toContain('aria-busy="true"');
   });
 

+ 1 - 1
src/components/loading/page-skeletons.tsx

@@ -7,7 +7,7 @@ interface LoadingStateProps {
   className?: string;
 }
 
-export function LoadingState({ label = "加载中", className }: LoadingStateProps) {
+export function LoadingState({ label = "Loading", className }: LoadingStateProps) {
   return (
     <div
       role="status"

+ 38 - 26
src/components/section.tsx

@@ -69,7 +69,7 @@ const SECTION_ICON_MAP: Record<SectionIconName, LucideIcon> = {
 };
 
 export type SectionProps = {
-  title: string;
+  title?: string;
   description?: string;
   icon?: SectionIconName;
   iconColor?: string;
@@ -117,27 +117,35 @@ export function Section({
       )}
 
       <div className="relative z-10">
-        <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
-          <div className="flex items-start gap-3 min-w-0">
-            {Icon && (
-              <div
-                className={cn(
-                  "flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5",
-                  variant === "highlight" ? "bg-primary/20" : "bg-muted/50"
+        {(title || description || Icon || actions) && (
+          <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+            <div className="flex items-start gap-3 min-w-0">
+              {Icon && (
+                <div
+                  className={cn(
+                    "flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5",
+                    variant === "highlight" ? "bg-primary/20" : "bg-muted/50"
+                  )}
+                >
+                  <Icon className={cn("h-4 w-4", iconColor)} />
+                </div>
+              )}
+              <div className="min-w-0">
+                {title && (
+                  <h2 className="text-base font-semibold text-foreground tracking-tight">
+                    {title}
+                  </h2>
+                )}
+                {description && (
+                  <p className="text-sm text-muted-foreground mt-1 leading-relaxed">
+                    {description}
+                  </p>
                 )}
-              >
-                <Icon className={cn("h-4 w-4", iconColor)} />
               </div>
-            )}
-            <div className="min-w-0">
-              <h2 className="text-base font-semibold text-foreground tracking-tight">{title}</h2>
-              {description && (
-                <p className="text-sm text-muted-foreground mt-1 leading-relaxed">{description}</p>
-              )}
             </div>
+            {actions && <div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>}
           </div>
-          {actions && <div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>}
-        </div>
+        )}
         {children}
       </div>
     </motion.section>
@@ -159,15 +167,19 @@ export function SectionStatic({
         className
       )}
     >
-      <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
-        <div className="min-w-0">
-          <h2 className="text-base font-semibold text-foreground tracking-tight">{title}</h2>
-          {description && (
-            <p className="text-sm text-muted-foreground mt-1 leading-relaxed">{description}</p>
-          )}
+      {(title || description || actions) && (
+        <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+          <div className="min-w-0">
+            {title && (
+              <h2 className="text-base font-semibold text-foreground tracking-tight">{title}</h2>
+            )}
+            {description && (
+              <p className="text-sm text-muted-foreground mt-1 leading-relaxed">{description}</p>
+            )}
+          </div>
+          {actions && <div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>}
         </div>
-        {actions && <div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>}
-      </div>
+      )}
       {children}
     </section>
   );

+ 1 - 1
src/components/ui/data-table.tsx

@@ -271,7 +271,7 @@ export const TableColumnTypes = {
    * 操作列
    */
   actions: <T extends TableData>(
-    title: string = "操作",
+    title: string = "Actions",
     render: (value: any, record: T, index: number) => ReactNode,
     options?: Partial<TableColumn<T>>
   ): TableColumn<T> => ({

+ 2 - 2
src/repository/cache-hit-rate-alert.ts

@@ -82,10 +82,10 @@ function normalizeTtlFallbackSeconds(config: CacheHitRateAlertQueryConfig): {
   const base: Record<ProviderType, number> = {
     claude: defaultSeconds,
     "claude-auth": defaultSeconds,
-    codex: 6 * 3600,
+    codex: 600,
     gemini: defaultSeconds,
     "gemini-cli": defaultSeconds,
-    "openai-compatible": 6 * 3600,
+    "openai-compatible": 600,
   };
   const overrides = config.ttlFallbackSecondsByProviderType ?? {};
   const byType: Record<ProviderType, number> = { ...base };