Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions web/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
{
"semi": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 80,
"arrowParens": "avoid",
"endOfLine": "lf"
"trailingComma": "all",
"printWidth": 100,
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"jsxSingleQuote": false,
"proseWrap": "preserve",
"bracketSameLine": false
}
112 changes: 50 additions & 62 deletions web/src/components/cooldown-details-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { TFunction } from 'i18next'
import {
Dialog,
DialogContent,
} from '@/components/ui/dialog'
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import {
Snowflake,
Clock,
Expand All @@ -18,32 +15,38 @@ import {
Thermometer,
Calendar,
Activity,
} from 'lucide-react'
import type { Cooldown } from '@/lib/transport/types'
import { useCooldowns } from '@/hooks/use-cooldowns'
} from 'lucide-react';
import type { Cooldown } from '@/lib/transport/types';
import { useCooldowns } from '@/hooks/use-cooldowns';

interface CooldownDetailsDialogProps {
cooldown: Cooldown | null
open: boolean
onOpenChange: (open: boolean) => void
onClear: () => void
isClearing: boolean
onDisable: () => void
isDisabling: boolean
cooldown: Cooldown | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onClear: () => void;
isClearing: boolean;
onDisable: () => void;
isDisabling: boolean;
}

// Reason 信息和图标 - 使用翻译
const getReasonInfo = (t: TFunction) => ({
server_error: {
label: t('provider.reasons.serverError'),
description: t('provider.reasons.serverErrorDesc', '上游服务器返回 5xx 错误,系统自动进入冷却保护'),
description: t(
'provider.reasons.serverErrorDesc',
'上游服务器返回 5xx 错误,系统自动进入冷却保护',
),
icon: Server,
color: 'text-red-400',
bgColor: 'bg-red-400/10 border-red-400/20',
},
network_error: {
label: t('provider.reasons.networkError'),
description: t('provider.reasons.networkErrorDesc', '无法连接到上游服务器,可能是网络故障或服务器宕机'),
description: t(
'provider.reasons.networkErrorDesc',
'无法连接到上游服务器,可能是网络故障或服务器宕机',
),
icon: Wifi,
color: 'text-amber-400',
bgColor: 'bg-amber-400/10 border-amber-400/20',
Expand Down Expand Up @@ -76,7 +79,7 @@ const getReasonInfo = (t: TFunction) => ({
color: 'text-muted-foreground',
bgColor: 'bg-muted border-border',
},
})
});

export function CooldownDetailsDialog({
cooldown,
Expand All @@ -87,50 +90,50 @@ export function CooldownDetailsDialog({
onDisable,
isDisabling,
}: CooldownDetailsDialogProps) {
const { t, i18n } = useTranslation()
const REASON_INFO = getReasonInfo(t)
const { t, i18n } = useTranslation();
const REASON_INFO = getReasonInfo(t);
// 获取 formatRemaining 函数用于实时倒计时
const { formatRemaining } = useCooldowns()
const { formatRemaining } = useCooldowns();

// 计算初始倒计时值
const getInitialCountdown = useCallback(() => {
return cooldown ? formatRemaining(cooldown) : ''
}, [cooldown, formatRemaining])
return cooldown ? formatRemaining(cooldown) : '';
}, [cooldown, formatRemaining]);

// 实时倒计时状态
const [liveCountdown, setLiveCountdown] = useState<string>(getInitialCountdown)
const [liveCountdown, setLiveCountdown] = useState<string>(getInitialCountdown);

// 每秒更新倒计时
useEffect(() => {
if (!cooldown) return
if (!cooldown) return;

// 每秒更新
const interval = setInterval(() => {
setLiveCountdown(formatRemaining(cooldown))
}, 1000)
setLiveCountdown(formatRemaining(cooldown));
}, 1000);

return () => clearInterval(interval)
}, [cooldown, formatRemaining])
return () => clearInterval(interval);
}, [cooldown, formatRemaining]);

if (!cooldown) return null
if (!cooldown) return null;

const reasonInfo = REASON_INFO[cooldown.reason] || REASON_INFO.unknown
const Icon = reasonInfo.icon
const reasonInfo = REASON_INFO[cooldown.reason] || REASON_INFO.unknown;
const Icon = reasonInfo.icon;

const formatUntilTime = (until: string) => {
const date = new Date(until)
const date = new Date(until);
return date.toLocaleString(i18n.resolvedLanguage ?? i18n.language, {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
}
});
};

const untilDateStr = formatUntilTime(cooldown.untilTime)
const [datePart, timePart] = untilDateStr.split(' ')
const untilDateStr = formatUntilTime(cooldown.untilTime);
const [datePart, timePart] = untilDateStr.split(' ');

Comment on lines 123 to 137
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

避免通过字符串 split 解析本地化时间

Line 123-137 依赖 toLocaleString()split(' ') 拆分日期/时间,在部分语言环境(含非空格分隔符或 NBSP)下会拿到错误片段。建议直接分别格式化日期与时间,避免解析字符串。

🔧 建议修改
-  const formatUntilTime = (until: string) => {
-    const date = new Date(until);
-    return date.toLocaleString(i18n.resolvedLanguage ?? i18n.language, {
-      month: '2-digit',
-      day: '2-digit',
-      hour: '2-digit',
-      minute: '2-digit',
-      second: '2-digit',
-      hour12: false,
-    });
-  };
-
-  const untilDateStr = formatUntilTime(cooldown.untilTime);
-  const [datePart, timePart] = untilDateStr.split(' ');
+  const formatUntilTimeParts = (until: string) => {
+    const date = new Date(until);
+    const locale = i18n.resolvedLanguage ?? i18n.language;
+    return {
+      datePart: date.toLocaleDateString(locale, { month: '2-digit', day: '2-digit' }),
+      timePart: date.toLocaleTimeString(locale, {
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: false,
+      }),
+    };
+  };
+
+  const { datePart, timePart } = formatUntilTimeParts(cooldown.untilTime);
🤖 Prompt for AI Agents
In `@web/src/components/cooldown-details-dialog.tsx` around lines 123 - 137, The
current formatUntilTime uses toLocaleString and then the code splits that string
(untilDateStr.split(' ')) which breaks for locales that don't separate date/time
with a space; update formatUntilTime into two helpers (e.g., formatDatePart and
formatTimePart) or use two Intl.DateTimeFormat instances to produce the date and
time independently (respectively using options for month/day and for
hour/minute/second with hour12 false) and replace the untilDateStr + split logic
with direct calls to these helpers when computing datePart and timePart for the
component; change references to formatUntilTime and untilDateStr accordingly so
no string split is performed.

return (
<Dialog open={open} onOpenChange={onOpenChange}>
Expand All @@ -149,15 +152,10 @@ export function CooldownDetailsDialog({

<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 rounded-2xl bg-cyan-500/10 border border-cyan-400/20 shadow-[0_0_15px_-3px_rgba(6,182,212,0.2)]">
<Snowflake
size={28}
className="text-cyan-400 animate-spin-slow"
/>
<Snowflake size={28} className="text-cyan-400 animate-spin-slow" />
</div>
<div>
<h2 className="text-xl font-bold text-text-primary">
{t('cooldown.title')}
</h2>
<h2 className="text-xl font-bold text-text-primary">{t('cooldown.title')}</h2>
<p className="text-xs text-cyan-500/80 font-medium uppercase tracking-wider mt-1">
Frozen Protocol Active
</p>
Expand Down Expand Up @@ -193,9 +191,7 @@ export function CooldownDetailsDialog({
<Icon size={20} />
</div>
<div>
<h3 className={`text-sm font-bold ${reasonInfo.color} mb-1`}>
{reasonInfo.label}
</h3>
<h3 className={`text-sm font-bold ${reasonInfo.color} mb-1`}>{reasonInfo.label}</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
{reasonInfo.description}
</p>
Expand All @@ -210,9 +206,7 @@ export function CooldownDetailsDialog({
<div className="absolute inset-0 bg-cyan-400/5 opacity-50 group-hover:opacity-100 transition-opacity" />
<div className="relative flex items-center gap-1.5 text-cyan-500 mb-1">
<Thermometer size={14} />
<span className="text-[10px] font-bold uppercase tracking-widest">
Remaining
</span>
<span className="text-[10px] font-bold uppercase tracking-widest">Remaining</span>
</div>
<div className="relative font-mono text-4xl font-bold text-cyan-400 tracking-widest tabular-nums drop-shadow-[0_0_8px_rgba(34,211,238,0.3)]">
{liveCountdown}
Expand All @@ -224,18 +218,14 @@ export function CooldownDetailsDialog({
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-bold flex items-center gap-1.5">
<Clock size={10} /> Resume
</span>
<div className="font-mono text-sm font-semibold text-foreground">
{timePart}
</div>
<div className="font-mono text-sm font-semibold text-foreground">{timePart}</div>
</div>

<div className="p-3 rounded-xl bg-muted border border-border flex flex-col items-center justify-center gap-1">
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-bold flex items-center gap-1.5">
<Calendar size={10} /> Date
</span>
<div className="font-mono text-sm font-semibold text-foreground">
{datePart}
</div>
<div className="font-mono text-sm font-semibold text-foreground">{datePart}</div>
</div>
</div>

Expand All @@ -251,9 +241,7 @@ export function CooldownDetailsDialog({
{isClearing ? (
<>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
<span className="text-sm font-bold text-white">
Thawing...
</span>
<span className="text-sm font-bold text-white">Thawing...</span>
</>
) : (
<>
Expand Down Expand Up @@ -290,5 +278,5 @@ export function CooldownDetailsDialog({
</div>
</DialogContent>
</Dialog>
)
);
}
Loading