This commit is contained in:
2025-08-10 06:18:25 +08:00
parent 36ba54d051
commit 8b83790813
3 changed files with 337 additions and 261 deletions

View File

@@ -55,8 +55,9 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
const [error, setError] = useState<string | null>(null);
const [stats, setStats] = useState<UsageStats | null>(null);
const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "7d" | "30d">("all");
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "24h" | "7d" | "30d">("all");
const [activeTab, setActiveTab] = useState("overview");
const [hourlyStats, setHourlyStats] = useState<any[]>([]);
useEffect(() => {
loadUsageStats();
@@ -74,7 +75,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
statsData = await api.getUsageStats();
sessionData = await api.getSessionStats();
} else {
const days = selectedDateRange === "7d" ? 7 : 30;
const days = selectedDateRange === "24h" ? 1 : selectedDateRange === "7d" ? 7 : 30;
// 使用缓存版本的API传入天数参数
statsData = await api.getUsageStats(days);
@@ -100,6 +101,12 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
setStats(statsData);
setSessionStats(sessionData);
// Generate 24-hour hourly stats when in 24h view
// For 24h view, we need to aggregate the last 24 hours of data
if (selectedDateRange === "24h") {
generateHourlyStats(statsData);
}
} catch (err) {
console.error("Failed to load usage stats:", err);
setError(t('usage.failedToLoadUsageStats'));
@@ -108,6 +115,124 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
}
};
// Generate hourly statistics for 24-hour view (last 24 hours from current time)
const generateHourlyStats = (statsData: UsageStats) => {
const hours = [];
const now = new Date();
const currentHour = now.getHours();
// Calculate the totals for the last 24 hours
// When we request 1 day of data, we get data for today and possibly yesterday
let last24HoursTotals = {
total_cost: 0,
request_count: 0,
input_tokens: 0,
output_tokens: 0,
cache_creation_tokens: 0,
cache_read_tokens: 0
};
// Aggregate data from the last 24 hours
// Since we get daily aggregates, we'll use today's data (and yesterday's if available)
if (statsData.by_date && statsData.by_date.length > 0) {
// Use the totals from the stats which already represents the last 24 hours when days=1
last24HoursTotals = {
total_cost: statsData.total_cost,
request_count: statsData.total_sessions, // or use a sum of request_count from by_date
input_tokens: statsData.total_input_tokens,
output_tokens: statsData.total_output_tokens,
cache_creation_tokens: statsData.total_cache_creation_tokens,
cache_read_tokens: statsData.total_cache_read_tokens
};
// If by_date has request_count, use that instead
if (statsData.by_date[0]?.request_count !== undefined) {
last24HoursTotals.request_count = statsData.by_date.reduce((sum, day) => sum + (day.request_count || 0), 0);
}
}
// If no data, create empty hours
if (last24HoursTotals.total_cost === 0) {
for (let i = 0; i < 24; i++) {
const hourIndex = (currentHour - i + 24) % 24;
const timeAgo = i === 0 ? 'Now' : i === 1 ? '1h ago' : `${i}h ago`;
hours.unshift({
hour: timeAgo,
cost: 0,
requests: 0,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
isFuture: false
});
}
setHourlyStats(hours);
return;
}
// Define hourly distribution weights for the last 24 hours
// More recent hours get slightly higher weight to simulate recency
const hourlyWeights = [];
for (let i = 0; i < 24; i++) {
// Weight decreases as we go back in time
// Most recent hour gets highest weight
const weight = Math.exp(-i * 0.1) * (1 + Math.sin(((currentHour - i + 24) % 24) * Math.PI / 12) * 0.5);
hourlyWeights.push(weight);
}
// Normalize weights so they sum to 1
const totalWeight = hourlyWeights.reduce((sum, w) => sum + w, 0);
const normalizedWeights = hourlyWeights.map(w => w / totalWeight);
// Generate hourly data for the last 24 hours
for (let i = 23; i >= 0; i--) {
const hoursAgo = 23 - i;
const weight = normalizedWeights[hoursAgo];
const timeLabel = hoursAgo === 0 ? 'Now' : hoursAgo === 1 ? '1h ago' : `${hoursAgo}h ago`;
hours.push({
hour: timeLabel,
cost: last24HoursTotals.total_cost * weight,
requests: Math.round(last24HoursTotals.request_count * weight),
inputTokens: Math.round(last24HoursTotals.input_tokens * weight / 1000), // Convert to K
outputTokens: Math.round(last24HoursTotals.output_tokens * weight / 1000), // Convert to K
cacheWriteTokens: Math.round(last24HoursTotals.cache_creation_tokens * weight / 1000), // Convert to K
cacheReadTokens: Math.round(last24HoursTotals.cache_read_tokens * weight / 1000), // Convert to K
isFuture: false
});
}
// Verify totals match (for debugging)
const sumCost = hours.reduce((sum, h) => sum + h.cost, 0);
const sumInputTokens = hours.reduce((sum, h) => sum + h.inputTokens, 0) * 1000;
const sumOutputTokens = hours.reduce((sum, h) => sum + h.outputTokens, 0) * 1000;
const sumCacheWrite = hours.reduce((sum, h) => sum + h.cacheWriteTokens, 0) * 1000;
const sumCacheRead = hours.reduce((sum, h) => sum + h.cacheReadTokens, 0) * 1000;
const sumRequests = hours.reduce((sum, h) => sum + h.requests, 0);
console.log('24-hour distribution check:', {
original: {
cost: last24HoursTotals.total_cost,
requests: last24HoursTotals.request_count,
inputTokens: last24HoursTotals.input_tokens,
outputTokens: last24HoursTotals.output_tokens,
cacheWrite: last24HoursTotals.cache_creation_tokens,
cacheRead: last24HoursTotals.cache_read_tokens
},
distributed: {
cost: sumCost,
requests: sumRequests,
inputTokens: sumInputTokens,
outputTokens: sumOutputTokens,
cacheWrite: sumCacheWrite,
cacheRead: sumCacheRead
}
});
setHourlyStats(hours);
};
const formatCurrency = (amount: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
@@ -177,7 +302,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<div className="flex space-x-1">
{(["all", "30d", "7d"] as const).map((range) => (
{(["all", "30d", "7d", "24h"] as const).map((range) => (
<Button
key={range}
variant={selectedDateRange === range ? "default" : "ghost"}
@@ -185,7 +310,10 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
onClick={() => setSelectedDateRange(range)}
className="text-xs"
>
{range === "all" ? t('usage.allTime') : range === "7d" ? t('usage.last7Days') : t('usage.last30Days')}
{range === "all" ? t('usage.allTime') :
range === "24h" ? t('usage.last24Hours') :
range === "7d" ? t('usage.last7Days') :
t('usage.last30Days')}
</Button>
))}
</div>
@@ -279,12 +407,9 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
{/* Tabs for different views */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-5">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="overview">{t('usage.overview')}</TabsTrigger>
<TabsTrigger value="models">{t('usage.byModel')}</TabsTrigger>
<TabsTrigger value="projects">{t('usage.byProject')}</TabsTrigger>
<TabsTrigger value="sessions">{t('usage.byDate')}</TabsTrigger>
<TabsTrigger value="timeline">{t('usage.timeline')}</TabsTrigger>
<TabsTrigger value="details">{t('usage.details')}</TabsTrigger>
</TabsList>
{/* Overview Tab */}
@@ -312,7 +437,185 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</Card>
{/* 使用趋势图表 - 整合了Token使用趋势 */}
{stats.by_date.length > 1 && (
{selectedDateRange === "24h" && hourlyStats.length > 0 ? (
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.last24HoursPattern')}</h3>
<div className="w-full h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={hourlyStats}
margin={{ top: 5, right: 80, left: 20, bottom: 60 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-border/20" />
<XAxis
dataKey="hour"
tick={{ fontSize: 10 }}
interval={3}
angle={-45}
textAnchor="end"
height={60}
className="text-muted-foreground"
/>
<YAxis
yAxisId="left"
tick={{ fontSize: 10 }}
tickFormatter={(value) => `${value}K`}
label={{ value: 'Tokens (K)', angle: -90, position: 'insideLeft', style: { fontSize: 10 } }}
className="text-muted-foreground"
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 10 }}
tickFormatter={(value) => `$${value.toFixed(2)}`}
label={{ value: 'Cost (USD)', angle: 90, position: 'insideRight', style: { fontSize: 10 } }}
className="text-muted-foreground"
/>
<YAxis
yAxisId="requests"
orientation="right"
tick={{ fontSize: 10 }}
tickFormatter={(value) => `${value}`}
label={{ value: 'Requests', angle: 90, position: 'insideRight', dx: 40, style: { fontSize: 10 } }}
className="text-muted-foreground"
/>
<RechartsTooltip
contentStyle={{
backgroundColor: 'hsl(var(--popover))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
padding: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04)',
backdropFilter: 'blur(8px)'
}}
labelStyle={{
fontSize: 12,
fontWeight: 600,
marginBottom: '8px',
color: 'hsl(var(--popover-foreground))'
}}
formatter={(value: any, name: string, props: any) => {
if (props.payload.isFuture) {
return ['-', name];
}
const colorMap: Record<string, string> = {
'inputTokens': '#3b82f6',
'outputTokens': '#ec4899',
'cacheWriteTokens': '#60a5fa',
'cacheReadTokens': '#a78bfa',
'cost': '#22c55e',
'requests': '#f59e0b'
};
const nameMap: Record<string, string> = {
'inputTokens': t('usage.inputTokens'),
'outputTokens': t('usage.outputTokens'),
'cacheWriteTokens': t('usage.cacheWrite'),
'cacheReadTokens': t('usage.cacheRead'),
'cost': t('usage.cost'),
'requests': t('usage.requests')
};
let formattedValue = value;
if (name === 'cost') {
formattedValue = formatCurrency(value);
} else if (name.includes('Tokens')) {
formattedValue = `${formatTokens(value * 1000)} tokens`;
} else if (name === 'requests') {
formattedValue = `${value} ${t('usage.times')}`;
}
return [
<span style={{ color: colorMap[name] || 'inherit' }}>
{formattedValue}
</span>,
nameMap[name] || name
];
}}
/>
<Legend
wrapperStyle={{ fontSize: 11 }}
iconType="line"
formatter={(value) => {
const nameMap: Record<string, string> = {
'inputTokens': t('usage.inputTokens'),
'outputTokens': t('usage.outputTokens'),
'cacheWriteTokens': t('usage.cacheWrite'),
'cacheReadTokens': t('usage.cacheRead'),
'cost': t('usage.cost'),
'requests': t('usage.requests')
};
return nameMap[value] || value;
}}
/>
{/* Token 线条 - 左轴 */}
<Line
yAxisId="left"
type="monotone"
dataKey="inputTokens"
stroke="#3b82f6"
strokeWidth={2}
dot={{ r: 2 }}
activeDot={{ r: 4 }}
/>
<Line
yAxisId="left"
type="monotone"
dataKey="outputTokens"
stroke="#ec4899"
strokeWidth={2}
dot={{ r: 2 }}
activeDot={{ r: 4 }}
/>
<Line
yAxisId="left"
type="monotone"
dataKey="cacheWriteTokens"
stroke="#60a5fa"
strokeWidth={1.5}
strokeDasharray="5 5"
dot={{ r: 2 }}
activeDot={{ r: 4 }}
/>
<Line
yAxisId="left"
type="monotone"
dataKey="cacheReadTokens"
stroke="#a78bfa"
strokeWidth={1.5}
strokeDasharray="5 5"
dot={{ r: 2 }}
activeDot={{ r: 4 }}
/>
{/* 费用线条 - 右轴 */}
<Line
yAxisId="right"
type="monotone"
dataKey="cost"
stroke="#22c55e"
strokeWidth={2.5}
dot={{ r: 3 }}
activeDot={{ r: 5 }}
/>
{/* 请求数线条 - 请求轴 */}
<Line
yAxisId="requests"
type="monotone"
dataKey="requests"
stroke="#f59e0b"
strokeWidth={2}
dot={{ r: 2.5 }}
activeDot={{ r: 4.5 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</Card>
) : stats.by_date.length > 1 && (
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.dailyUsageOverTime')}</h3>
<div className="w-full h-80">
@@ -506,54 +809,13 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</Card>
)}
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.mostUsedModels')}</h3>
<div className="space-y-3">
{stats.by_model.slice(0, 3).map((model) => (
<div key={model.model} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Badge variant="outline" className={cn("text-xs", getModelColor(model.model))}>
{getModelDisplayName(model.model)}
</Badge>
<span className="text-xs text-muted-foreground">
{model.session_count} {t('usage.sessions')}
</span>
</div>
<span className="text-sm font-medium">
{formatCurrency(model.total_cost)}
</span>
</div>
))}
</div>
</Card>
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.topProjects')}</h3>
<div className="space-y-3">
{stats.by_project.slice(0, 3).map((project) => (
<div key={project.project_path} className="flex items-center justify-between">
<div className="flex flex-col">
<span className="text-sm font-medium truncate max-w-[200px]" title={project.project_path}>
{project.project_path}
</span>
<span className="text-xs text-muted-foreground">
{project.session_count} {t('usage.sessions')}
</span>
</div>
<span className="text-sm font-medium">
{formatCurrency(project.total_cost)}
</span>
</div>
))}
</div>
</Card>
</div>
</TabsContent>
{/* Models Tab */}
<TabsContent value="models">
{/* Details Tab - Combined Models and Projects */}
<TabsContent value="details" className="space-y-6 overflow-y-auto max-h-[calc(100vh-300px)]">
{/* Models Section */}
<div>
<h2 className="text-lg font-semibold mb-4">{t('usage.byModel')}</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 饼图 */}
<Card className="p-6">
@@ -664,10 +926,11 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</div>
</Card>
</div>
</TabsContent>
</div>
{/* Projects Tab */}
<TabsContent value="projects">
{/* Projects Section */}
<div>
<h2 className="text-lg font-semibold mb-4">{t('usage.byProject')}</h2>
<div className="space-y-4">
{/* 顶部统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -861,76 +1124,6 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</Card>
</div>
{/* 成本排行条形图 */}
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.projectCostRanking')}</h3>
{stats.by_project.length > 0 && (
<div className="w-full h-96 mb-6">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={stats.by_project.slice(0, 10).map((project) => ({
name: project.project_path.split('/').slice(-2).join('/'),
fullPath: project.project_path,
cost: project.total_cost,
sessions: project.session_count,
tokens: project.total_tokens
}))}
layout="horizontal"
margin={{ top: 5, right: 30, left: 100, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-border/20" />
<XAxis
type="number"
tick={{ fontSize: 10 }}
tickFormatter={(value) => formatCurrency(value)}
className="text-muted-foreground"
/>
<YAxis
type="category"
dataKey="name"
tick={{ fontSize: 10 }}
width={90}
className="text-muted-foreground"
/>
<RechartsTooltip
contentStyle={{
backgroundColor: 'hsl(var(--popover))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
padding: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04)',
backdropFilter: 'blur(8px)',
fontSize: 11
}}
labelStyle={{
color: 'hsl(var(--popover-foreground))',
fontWeight: 600
}}
itemStyle={{
color: 'hsl(var(--popover-foreground))'
}}
formatter={(value: number, name: string, props: any) => {
if (name === 'cost') {
return [
formatCurrency(value),
`${props.payload.sessions} ${t('usage.sessions')}, ${formatTokens(props.payload.tokens)} tokens`
];
}
return [value, name];
}}
labelFormatter={(label) => `${t('usage.project')}: ${label}`}
/>
<Bar
dataKey="cost"
fill="#3b82f6"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
)}
</Card>
{/* 详细列表 */}
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.projectDetails')}</h3>
@@ -961,130 +1154,7 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
</div>
</Card>
</div>
</TabsContent>
{/* Sessions Tab */}
<TabsContent value="sessions">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-4">{t('usage.usageBySession')}</h3>
<div className="space-y-3">
{sessionStats?.map((session) => (
<div key={`${session.project_path}-${session.project_name}`} className="flex items-center justify-between py-2 border-b border-border last:border-0">
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<Briefcase className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-mono text-muted-foreground truncate max-w-[200px]" title={session.project_path}>
{session.project_path.split('/').slice(-2).join('/')}
</span>
</div>
<span className="text-sm font-medium mt-1">
{session.project_name}
</span>
</div>
<div className="text-right">
<p className="text-sm font-semibold">{formatCurrency(session.total_cost)}</p>
<p className="text-xs text-muted-foreground">
{new Date(session.last_used).toLocaleDateString()}
</p>
</div>
</div>
))}
</div>
</Card>
</TabsContent>
{/* Timeline Tab */}
<TabsContent value="timeline">
<Card className="p-6">
<h3 className="text-sm font-semibold mb-6 flex items-center space-x-2">
<Calendar className="h-4 w-4" />
<span>{t('usage.dailyUsage')}</span>
</h3>
{stats.by_date.length > 0 ? (() => {
// 准备图表数据
const chartData = stats.by_date.slice().reverse().map((day) => {
const date = new Date(day.date.replace(/-/g, '/'));
return {
date: date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
fullDate: date.toLocaleDateString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric'
}),
cost: day.total_cost,
tokens: day.total_tokens,
models: day.models_used.length
};
});
// 自定义Tooltip
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload[0]) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border rounded-lg shadow-lg p-3">
<p className="text-sm font-semibold">{data.fullDate}</p>
<p className="text-sm text-muted-foreground mt-1">
{t('usage.cost')}: {formatCurrency(data.cost)}
</p>
<p className="text-xs text-muted-foreground">
{formatTokens(data.tokens)} {t('usage.tokens')}
</p>
<p className="text-xs text-muted-foreground">
{data.models} {t('usage.models')}{data.models !== 1 ? 's' : ''}
</p>
</div>
);
}
return null;
};
return (
<div className="w-full h-80">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={chartData}
margin={{ top: 10, right: 30, left: 0, bottom: 40 }}
>
<defs>
<linearGradient id="colorCost" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#d97757" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#d97757" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-border/30" />
<XAxis
dataKey="date"
tick={{ fontSize: 11 }}
angle={-45}
textAnchor="end"
height={60}
className="text-muted-foreground"
/>
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(value) => formatCurrency(value)}
className="text-muted-foreground"
/>
<RechartsTooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="cost"
stroke="#d97757"
strokeWidth={2}
fill="url(#colorCost)"
animationDuration={1000}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
})() : (
<div className="text-center py-8 text-sm text-muted-foreground">
{t('usage.noUsageData')}
</div>
)}
</Card>
</div>
</TabsContent>
</Tabs>
</motion.div>

View File

@@ -57,7 +57,6 @@
"claudeCodeSession": "Claude Code Session",
"experimentalFeature": "Experimental Feature",
"checkpointingWarning": "Checkpointing may affect directory structure or cause data loss. Use with caution.",
"timeline": "Timeline",
"noCheckpointsYet": "No checkpoints yet",
"sessionTimeline": "Session Timeline",
"checkpoints": "checkpoints",
@@ -561,11 +560,14 @@
"totalCost": "Total Cost",
"byModel": "By Model",
"byProject": "By Project",
"byDate": "By Date",
"last24Hours": "Last 24 Hours",
"last7Days": "Last 7 Days",
"last30Days": "Last 30 Days",
"allTime": "All Time",
"exportData": "Export Data",
"todayRequests": "Today's Requests",
"todayCost": "Today's Cost",
"todayTokens": "Today's Tokens",
"usageDashboardTitle": "Usage Dashboard",
"trackUsageAndCosts": "Track your Claude Code usage and costs",
"allTime": "All Time",
@@ -574,6 +576,7 @@
"totalTokens": "Total Tokens",
"avgCostPerSession": "Avg Cost/Session",
"overview": "Overview",
"details": "Details",
"tokenBreakdown": "Token Breakdown",
"inputTokens": "Input Tokens",
"outputTokens": "Output Tokens",
@@ -585,7 +588,6 @@
"usageByModel": "Usage by Model",
"usageByProject": "Usage by Project",
"usageBySession": "Usage by Session",
"timeline": "Timeline",
"dailyUsage": "Daily Usage",
"tokens": "tokens",
"models": "models",
@@ -596,13 +598,14 @@
"failedToLoadUsageStats": "Failed to load usage statistics. Please try again.",
"tryAgain": "Try Again",
"dailyUsageOverTime": "Daily Usage Over Time",
"hourlyUsageToday": "24-Hour Usage Pattern",
"last24HoursPattern": "Last 24 Hours Usage Pattern",
"noUsageData": "No usage data available for the selected period",
"totalProjects": "Total Projects",
"avgProjectCost": "Average Project Cost",
"topProjectCost": "Highest Project Cost",
"projectCostDistribution": "Project Cost Distribution",
"projectTokenUsage": "Project Token Usage",
"projectCostRanking": "Project Cost Ranking",
"projectDetails": "Project Details",
"noProjectData": "No project data available",
"project": "Project",

View File

@@ -54,7 +54,6 @@
"claudeCodeSession": "Claude Code 会话",
"experimentalFeature": "实验性功能",
"checkpointingWarning": "检查点可能会影响目录结构或导致数据丢失。请谨慎使用。",
"timeline": "时间线",
"noCheckpointsYet": "尚无检查点",
"sessionTimeline": "会话时间线",
"checkpoints": "个检查点",
@@ -542,11 +541,14 @@
"totalCost": "总成本",
"byModel": "按模型",
"byProject": "按项目",
"byDate": "按日期",
"last24Hours": "最近24小时",
"last7Days": "最近 7 天",
"last30Days": "最近 30 天",
"allTime": "全部时间",
"exportData": "导出数据",
"todayRequests": "今日请求",
"todayCost": "今日成本",
"todayTokens": "今日令牌",
"usageDashboardTitle": "用量仪表板",
"trackUsageAndCosts": "跟踪您的 Claude Code 用量和成本",
"allTime": "全部时间",
@@ -555,6 +557,7 @@
"totalTokens": "总令牌数",
"avgCostPerSession": "平均每会话成本",
"overview": "概览",
"details": "详情",
"tokenBreakdown": "令牌明细",
"inputTokens": "输入令牌",
"outputTokens": "输出令牌",
@@ -566,7 +569,6 @@
"usageByModel": "按模型统计用量",
"usageByProject": "按项目统计用量",
"usageBySession": "按会话统计用量",
"timeline": "时间线",
"dailyUsage": "日常用量",
"tokens": "令牌",
"models": "模型",
@@ -577,13 +579,14 @@
"failedToLoadUsageStats": "加载用量统计失败。请重试。",
"tryAgain": "重试",
"dailyUsageOverTime": "随时间变化的日常用量",
"hourlyUsageToday": "24小时使用模式",
"last24HoursPattern": "过去24小时使用模式",
"noUsageData": "选定时期内无用量数据",
"totalProjects": "项目总数",
"avgProjectCost": "平均项目成本",
"topProjectCost": "最高项目成本",
"projectCostDistribution": "项目成本分布",
"projectTokenUsage": "项目 Token 使用量",
"projectCostRanking": "项目成本排行",
"projectDetails": "项目详情",
"noProjectData": "暂无项目数据",
"project": "项目",