From 7486023ddecb9bdaf1e3b361477d3eba5ed53e6c Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 11 Apr 2026 15:37:24 +0800 Subject: [PATCH] refactor(usage): streamline dashboard layout and stats components --- src/components/usage/ModelStatsTable.tsx | 5 +- src/components/usage/ProviderStatsTable.tsx | 5 +- src/components/usage/UsageDashboard.tsx | 216 ++++++++++++-------- src/components/usage/UsageSummaryCards.tsx | 7 +- src/components/usage/UsageTrendChart.tsx | 24 +-- 5 files changed, 153 insertions(+), 104 deletions(-) diff --git a/src/components/usage/ModelStatsTable.tsx b/src/components/usage/ModelStatsTable.tsx index 9f629d70b..b74b70f49 100644 --- a/src/components/usage/ModelStatsTable.tsx +++ b/src/components/usage/ModelStatsTable.tsx @@ -9,18 +9,21 @@ import { } from "@/components/ui/table"; import { useModelStats } from "@/lib/query/usage"; import { fmtUsd } from "./format"; +import type { UsageRangeSelection } from "@/types/usage"; interface ModelStatsTableProps { + range: UsageRangeSelection; appType?: string; refreshIntervalMs: number; } export function ModelStatsTable({ + range, appType, refreshIntervalMs, }: ModelStatsTableProps) { const { t } = useTranslation(); - const { data: stats, isLoading } = useModelStats(appType, { + const { data: stats, isLoading } = useModelStats(range, appType, { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, }); diff --git a/src/components/usage/ProviderStatsTable.tsx b/src/components/usage/ProviderStatsTable.tsx index 993a436ba..64c18213f 100644 --- a/src/components/usage/ProviderStatsTable.tsx +++ b/src/components/usage/ProviderStatsTable.tsx @@ -9,18 +9,21 @@ import { } from "@/components/ui/table"; import { useProviderStats } from "@/lib/query/usage"; import { fmtUsd } from "./format"; +import type { UsageRangeSelection } from "@/types/usage"; interface ProviderStatsTableProps { + range: UsageRangeSelection; appType?: string; refreshIntervalMs: number; } export function ProviderStatsTable({ + range, appType, refreshIntervalMs, }: ProviderStatsTableProps) { const { t } = useTranslation(); - const { data: stats, isLoading } = useProviderStats(appType, { + const { data: stats, isLoading } = useProviderStats(range, appType, { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, }); diff --git a/src/components/usage/UsageDashboard.tsx b/src/components/usage/UsageDashboard.tsx index d0ad19a7e..14dd815c0 100644 --- a/src/components/usage/UsageDashboard.tsx +++ b/src/components/usage/UsageDashboard.tsx @@ -1,12 +1,15 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { UsageSummaryCards } from "./UsageSummaryCards"; import { UsageTrendChart } from "./UsageTrendChart"; import { RequestLogTable } from "./RequestLogTable"; import { ProviderStatsTable } from "./ProviderStatsTable"; import { ModelStatsTable } from "./ModelStatsTable"; -import type { AppTypeFilter, TimeRange } from "@/types/usage"; +import type { + AppTypeFilter, + UsageRangePreset, + UsageRangeSelection, +} from "@/types/usage"; import { useUsageSummary } from "@/lib/query/usage"; import { motion } from "framer-motion"; import { @@ -27,7 +30,10 @@ import { } from "@/components/ui/accordion"; import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel"; import { cn } from "@/lib/utils"; -import { fmtUsd, parseFiniteNumber } from "./format"; +import { fmtUsd, getLocaleFromLanguage, parseFiniteNumber } from "./format"; +import { resolveUsageRange } from "@/lib/usageRange"; +import { UsageDateRangePicker } from "./UsageDateRangePicker"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const APP_FILTER_OPTIONS: AppTypeFilter[] = [ "all", @@ -36,10 +42,32 @@ const APP_FILTER_OPTIONS: AppTypeFilter[] = [ "gemini", ]; +const RANGE_PRESETS: UsageRangePreset[] = ["today", "1d", "7d", "14d", "30d"]; + +function getPresetLabel( + preset: UsageRangePreset, + t: (key: string, options?: { defaultValue?: string }) => string, +): string { + switch (preset) { + case "today": + return t("usage.presetToday", { defaultValue: "当天" }); + case "1d": + return t("usage.preset1d", { defaultValue: "1d" }); + case "7d": + return t("usage.preset7d", { defaultValue: "7d" }); + case "14d": + return t("usage.preset14d", { defaultValue: "14d" }); + case "30d": + return t("usage.preset30d", { defaultValue: "30d" }); + case "custom": + return t("usage.customRange", { defaultValue: "日历筛选" }); + } +} + export function UsageDashboard() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); - const [timeRange, setTimeRange] = useState("1d"); + const [range, setRange] = useState({ preset: "today" }); const [appType, setAppType] = useState("all"); const [refreshIntervalMs, setRefreshIntervalMs] = useState(30000); @@ -48,17 +76,27 @@ export function UsageDashboard() { const currentIndex = refreshIntervalOptionsMs.indexOf( refreshIntervalMs as (typeof refreshIntervalOptionsMs)[number], ); - const safeIndex = currentIndex >= 0 ? currentIndex : 3; // default 30s + const safeIndex = currentIndex >= 0 ? currentIndex : 3; const nextIndex = (safeIndex + 1) % refreshIntervalOptionsMs.length; const next = refreshIntervalOptionsMs[nextIndex]; setRefreshIntervalMs(next); queryClient.invalidateQueries({ queryKey: usageKeys.all }); }; - const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30; + const language = i18n.resolvedLanguage || i18n.language || "en"; + const locale = getLocaleFromLanguage(language); + const resolvedRange = useMemo(() => resolveUsageRange(range), [range]); + const rangeLabel = useMemo(() => { + if (range.preset !== "custom") { + return getPresetLabel(range.preset, t); + } - // Summary data for the app filter bar - const { data: summaryData } = useUsageSummary(days, appType, { + return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date( + resolvedRange.endDate * 1000, + ).toLocaleString(locale)}`; + }, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]); + + const { data: summaryData } = useUsageSummary(range, appType, { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, }); @@ -69,93 +107,94 @@ export function UsageDashboard() { transition={{ duration: 0.4 }} className="space-y-8 pb-8" > -
-
-

{t("usage.title")}

-

{t("usage.subtitle")}

-
- - setTimeRange(v as TimeRange)} - className="w-full sm:w-auto" - > -
- - - - {t("usage.today")} - - - {t("usage.last7days")} - - - {t("usage.last30days")} - - +
+
+
+

{t("usage.title")}

+

+ {t("usage.subtitle")} +

- -
- - {/* App type filter bar (replaces DataSourceBar) */} -
-
- {APP_FILTER_OPTIONS.map((type) => ( - - ))}
-
- - {(summaryData?.totalRequests ?? 0).toLocaleString()}{" "} - {t("usage.requestsLabel")} - - | - - {fmtUsd(parseFiniteNumber(summaryData?.totalCost) ?? 0, 4)}{" "} - {t("usage.costLabel")} - + +
+
+
+ {APP_FILTER_OPTIONS.map((type) => ( + + ))} +
+ +
+ + + {RANGE_PRESETS.map((preset) => ( + + ))} + + setRange(nextRange)} + /> +
+
+ +
+ {rangeLabel} + | + + {(summaryData?.totalRequests ?? 0).toLocaleString()}{" "} + {t("usage.requestsLabel")} + + | + + {fmtUsd(parseFiniteNumber(summaryData?.totalCost) ?? 0, 4)}{" "} + {t("usage.costLabel")} + +
@@ -186,6 +225,8 @@ export function UsageDashboard() { > @@ -193,6 +234,7 @@ export function UsageDashboard() { @@ -200,6 +242,7 @@ export function UsageDashboard() { @@ -208,7 +251,6 @@ export function UsageDashboard() {
- {/* Pricing Configuration */} 0 ? refreshIntervalMs : false, }); diff --git a/src/components/usage/UsageTrendChart.tsx b/src/components/usage/UsageTrendChart.tsx index 7344c62b7..976e3db28 100644 --- a/src/components/usage/UsageTrendChart.tsx +++ b/src/components/usage/UsageTrendChart.tsx @@ -17,20 +17,25 @@ import { getLocaleFromLanguage, parseFiniteNumber, } from "./format"; +import { resolveUsageRange } from "@/lib/usageRange"; +import type { UsageRangeSelection } from "@/types/usage"; interface UsageTrendChartProps { - days: number; + range: UsageRangeSelection; + rangeLabel: string; appType?: string; refreshIntervalMs: number; } export function UsageTrendChart({ - days, + range, + rangeLabel, appType, refreshIntervalMs, }: UsageTrendChartProps) { const { t, i18n } = useTranslation(); - const { data: trends, isLoading } = useUsageTrends(days, appType, { + const { startDate, endDate } = resolveUsageRange(range); + const { data: trends, isLoading } = useUsageTrends(range, appType, { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, }); @@ -42,7 +47,8 @@ export function UsageTrendChart({ ); } - const isToday = days === 1; + const durationSeconds = Math.max(endDate - startDate, 0); + const isHourly = durationSeconds <= 24 * 60 * 60; const language = i18n.resolvedLanguage || i18n.language || "en"; const dateLocale = getLocaleFromLanguage(language); const chartData = @@ -51,7 +57,7 @@ export function UsageTrendChart({ const cost = parseFiniteNumber(stat.totalCost); return { rawDate: stat.date, - label: isToday + label: isHourly ? pointDate.toLocaleString(dateLocale, { month: "2-digit", day: "2-digit", @@ -108,13 +114,7 @@ export function UsageTrendChart({

{t("usage.trends", "使用趋势")}

-

- {isToday - ? t("usage.rangeToday", "今天 (按小时)") - : days === 7 - ? t("usage.rangeLast7Days", "过去 7 天") - : t("usage.rangeLast30Days", "过去 30 天")} -

+

{rangeLabel}