diff --git a/src/components/usage/UsageDashboard.tsx b/src/components/usage/UsageDashboard.tsx index 9d49fcc27c..4eb2485d4a 100644 --- a/src/components/usage/UsageDashboard.tsx +++ b/src/components/usage/UsageDashboard.tsx @@ -108,9 +108,18 @@ export function UsageDashboard() { return getUsageRangePresetLabel(range.preset, t); } - return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date( - resolvedRange.endDate * 1000, - ).toLocaleString(locale)}`; + const startStr = new Date(resolvedRange.startDate * 1000).toLocaleString( + locale, + ); + + if (range.liveEndTime) { + return `${startStr} → ${t("usage.liveEndTimeNow", "现在")}`; + } + + const endStr = new Date(resolvedRange.endDate * 1000).toLocaleString( + locale, + ); + return `${startStr} - ${endStr}`; }, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]); // 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响), diff --git a/src/components/usage/UsageDateRangePicker.tsx b/src/components/usage/UsageDateRangePicker.tsx index 067e7eeb15..845795a654 100644 --- a/src/components/usage/UsageDateRangePicker.tsx +++ b/src/components/usage/UsageDateRangePicker.tsx @@ -7,6 +7,7 @@ import { ChevronRight, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Popover, @@ -117,6 +118,9 @@ export function UsageDateRangePicker({ ); const [draftStart, setDraftStart] = useState(resolvedRange.startDate); const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate); + const [draftLiveEnd, setDraftLiveEnd] = useState( + selection.preset === "custom" ? (selection.liveEndTime ?? false) : false, + ); const [displayMonth, setDisplayMonth] = useState( () => new Date( @@ -136,6 +140,9 @@ export function UsageDateRangePicker({ const r = resolveUsageRange(selection); setDraftStart(r.startDate); setDraftEnd(r.endDate); + setDraftLiveEnd( + selection.preset === "custom" ? (selection.liveEndTime ?? false) : false, + ); setDisplayMonth( new Date( fromTs(r.startDate).getFullYear(), @@ -147,6 +154,15 @@ export function UsageDateRangePicker({ setError(null); }, [open, selection]); + // Keep draftEnd ticking when live mode is active and popover is open + useEffect(() => { + if (!open || !draftLiveEnd) return; + const tick = () => setDraftEnd(Math.floor(Date.now() / 1000)); + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, [open, draftLiveEnd]); + const calendarDays = useMemo( () => getCalendarDays(displayMonth), [displayMonth], @@ -169,6 +185,14 @@ export function UsageDateRangePicker({ /* Pick a date from the calendar */ const handleDatePick = (day: Date) => { setError(null); + + // When live end time is active, calendar only controls start date + if (draftLiveEnd) { + const nextTs = setDateKeepTime(draftStart, day); + setDraftStart(nextTs); + return; + } + const nextTs = setDateKeepTime( activeField === "start" ? draftStart : draftEnd, day, @@ -211,6 +235,7 @@ export function UsageDateRangePicker({ preset: "custom", customStartDate: draftStart, customEndDate: draftEnd, + liveEndTime: draftLiveEnd, }); setOpen(false); }; @@ -222,6 +247,7 @@ export function UsageDateRangePicker({ /* ── Field card (start / end) ── */ const renderField = (field: DraftField) => { const isActive = activeField === field; + const isEndLive = field === "end" && draftLiveEnd; const ts = field === "start" ? draftStart : draftEnd; const setTs = field === "start" ? setDraftStart : setDraftEnd; const label = @@ -232,12 +258,16 @@ export function UsageDateRangePicker({ return (
setActiveField(field)} + onClick={() => { + if (!isEndLive) setActiveField(field); + }} >
{label} @@ -245,27 +275,41 @@ export function UsageDateRangePicker({
{ + if (isEndLive) return; const next = parseDateInput(ts, e.target.value); setTs(next); const d = fromTs(next); setDisplayMonth(new Date(d.getFullYear(), d.getMonth(), 1)); setError(null); }} - onFocus={() => setActiveField(field)} + onFocus={() => { + if (!isEndLive) setActiveField(field); + }} + readOnly={isEndLive} /> { + if (isEndLive) return; setTs(parseTimeInput(ts, e.target.value)); setError(null); }} - onFocus={() => setActiveField(field)} + onFocus={() => { + if (!isEndLive) setActiveField(field); + }} + readOnly={isEndLive} />
@@ -318,6 +362,23 @@ export function UsageDateRangePicker({ {renderField("start")} {renderField("end")} + + {error &&

{error}

}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d5457095fe..3f760c5065 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1472,6 +1472,8 @@ "customRangeHint": "Supports both date and time", "startTime": "Start Time", "endTime": "End Time", + "liveEndTime": "End time follows current time", + "liveEndTimeNow": "Now", "input": "Input", "output": "Output", "cacheWrite": "Creation", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index b39eb55b28..86a5c3db3f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1472,6 +1472,8 @@ "customRangeHint": "日付と時刻の両方に対応", "startTime": "開始時刻", "endTime": "終了時刻", + "liveEndTime": "終了時刻を現在時刻に追従", + "liveEndTimeNow": "現在", "input": "Input", "output": "Output", "cacheWrite": "作成", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index b6d7335f45..887e5f5185 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1444,6 +1444,8 @@ "customRangeHint": "支援日期與時間", "startTime": "開始時間", "endTime": "結束時間", + "liveEndTime": "結束時間跟隨當前時刻", + "liveEndTimeNow": "現在", "input": "Input", "output": "Output", "cacheWrite": "建立", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index ae55b2396c..b00e12df64 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1472,6 +1472,8 @@ "customRangeHint": "支持日期与时间", "startTime": "开始时间", "endTime": "结束时间", + "liveEndTime": "结束时间跟随当前时刻", + "liveEndTimeNow": "现在", "input": "Input", "output": "Output", "cacheWrite": "创建", diff --git a/src/lib/query/usage.ts b/src/lib/query/usage.ts index f0aa307447..ea95dc0cc2 100644 --- a/src/lib/query/usage.ts +++ b/src/lib/query/usage.ts @@ -26,6 +26,7 @@ type RequestLogsKey = { preset: UsageRangeSelection["preset"]; customStartDate?: number; customEndDate?: number; + liveEndTime?: boolean; appType?: string; providerName?: string; model?: string; @@ -40,6 +41,7 @@ export const usageKeys = { customStartDate: number | undefined, customEndDate: number | undefined, filters?: UsageScopeFilters, + liveEndTime?: boolean, ) => [ ...usageKeys.all, @@ -47,6 +49,7 @@ export const usageKeys = { preset, customStartDate ?? 0, customEndDate ?? 0, + liveEndTime ?? false, filters?.appType ?? null, filters?.providerName ?? null, filters?.model ?? null, @@ -55,7 +58,8 @@ export const usageKeys = { preset: UsageRangeSelection["preset"], customStartDate: number | undefined, customEndDate: number | undefined, - filters?: UsageScopeFilters, + filters?: Pick, + liveEndTime?: boolean, ) => [ ...usageKeys.all, @@ -63,6 +67,7 @@ export const usageKeys = { preset, customStartDate ?? 0, customEndDate ?? 0, + liveEndTime ?? false, filters?.providerName ?? null, filters?.model ?? null, ] as const, @@ -71,6 +76,7 @@ export const usageKeys = { customStartDate: number | undefined, customEndDate: number | undefined, filters?: UsageScopeFilters, + liveEndTime?: boolean, ) => [ ...usageKeys.all, @@ -78,6 +84,7 @@ export const usageKeys = { preset, customStartDate ?? 0, customEndDate ?? 0, + liveEndTime ?? false, filters?.appType ?? null, filters?.providerName ?? null, filters?.model ?? null, @@ -87,6 +94,7 @@ export const usageKeys = { customStartDate: number | undefined, customEndDate: number | undefined, filters?: UsageScopeFilters, + liveEndTime?: boolean, ) => [ ...usageKeys.all, @@ -94,6 +102,7 @@ export const usageKeys = { preset, customStartDate ?? 0, customEndDate ?? 0, + liveEndTime ?? false, filters?.appType ?? null, filters?.providerName ?? null, filters?.model ?? null, @@ -103,6 +112,7 @@ export const usageKeys = { customStartDate: number | undefined, customEndDate: number | undefined, filters?: UsageScopeFilters, + liveEndTime?: boolean, ) => [ ...usageKeys.all, @@ -110,6 +120,7 @@ export const usageKeys = { preset, customStartDate ?? 0, customEndDate ?? 0, + liveEndTime ?? false, filters?.appType ?? null, filters?.providerName ?? null, filters?.model ?? null, @@ -121,6 +132,7 @@ export const usageKeys = { key.preset, key.customStartDate ?? 0, key.customEndDate ?? 0, + key.liveEndTime ?? false, key.appType ?? "", key.providerName ?? "", key.model ?? "", @@ -159,6 +171,7 @@ export function useUsageSummary( range.customStartDate, range.customEndDate, effective, + range.liveEndTime, ), queryFn: () => { const { startDate, endDate } = resolveUsageRange(range); @@ -186,6 +199,7 @@ export function useUsageSummaryByApp( range.customStartDate, range.customEndDate, filters, + range.liveEndTime, ), queryFn: () => { const { startDate, endDate } = resolveUsageRange(range); @@ -213,6 +227,7 @@ export function useUsageTrends( range.customStartDate, range.customEndDate, effective, + range.liveEndTime, ), queryFn: () => { const { startDate, endDate } = resolveUsageRange(range); @@ -241,6 +256,7 @@ export function useProviderStats( range.customStartDate, range.customEndDate, effective, + range.liveEndTime, ), queryFn: () => { const { startDate, endDate } = resolveUsageRange(range); @@ -269,6 +285,7 @@ export function useModelStats( range.customStartDate, range.customEndDate, effective, + range.liveEndTime, ), queryFn: () => { const { startDate, endDate } = resolveUsageRange(range); @@ -296,6 +313,7 @@ export function useRequestLogs({ preset: range.preset, customStartDate: range.customStartDate, customEndDate: range.customEndDate, + liveEndTime: range.liveEndTime, appType: filters.appType, providerName: filters.providerName, model: filters.model, diff --git a/src/lib/usageRange.ts b/src/lib/usageRange.ts index 4b70ee1d24..6cedcf3200 100644 --- a/src/lib/usageRange.ts +++ b/src/lib/usageRange.ts @@ -49,7 +49,9 @@ export function resolveUsageRange( }; case "custom": { const startDate = selection.customStartDate ?? endDate - DAY_SECONDS; - const customEndDate = selection.customEndDate ?? endDate; + const customEndDate = selection.liveEndTime + ? endDate + : (selection.customEndDate ?? endDate); return { startDate, endDate: customEndDate, diff --git a/src/types/usage.ts b/src/types/usage.ts index 6e6f957dd8..256d58aec6 100644 --- a/src/types/usage.ts +++ b/src/types/usage.ts @@ -152,6 +152,9 @@ export interface UsageRangeSelection { preset: UsageRangePreset; customStartDate?: number; customEndDate?: number; + /** When true (custom mode only), endDate resolves to "now" instead of the + * fixed customEndDate snapshot, and the end-time field becomes read-only. */ + liveEndTime?: boolean; } /**