为 WebUI 首页支持 i18n

This commit is contained in:
DrSmoothl
2026-03-07 21:32:09 +08:00
parent 4565bd94b4
commit 2e4be9bd77
5 changed files with 440 additions and 74 deletions

View File

@@ -473,5 +473,96 @@
"sidebarNav": "Main navigation",
"closeMenu": "Close menu",
"navigatedTo": "Navigated to {{page}}"
},
"home": {
"title": "Live Monitor",
"subtitle": "MaiBot runtime status and statistics overview",
"loading": "Loading statistics...",
"loadingHint": "Fetching MaiBot runtime data",
"timeRange": {
"24h": "24 Hours",
"7d": "7 Days",
"30d": "30 Days"
},
"autoRefresh": "Auto Refresh",
"botStatus": {
"title": "MaiBot Status",
"running": "Running",
"stopped": "Stopped",
"uptime": "Up {{time}}"
},
"quickActions": {
"title": "Quick Actions",
"restart": "Restart MaiBot",
"restarting": "Restarting...",
"expressionReview": "Expression Review",
"viewLogs": "View Logs",
"pluginManage": "Plugins",
"systemSettings": "Settings"
},
"survey": {
"title": "Feedback Survey",
"description": "Help us improve the experience",
"webui": "WebUI Feedback",
"maibot": "MaiBot Feedback"
},
"stats": {
"totalRequests": "Total Requests",
"totalCost": "Total Cost",
"tokenUsage": "Token Usage",
"avgResponse": "Avg Response",
"avgResponseDesc": "Avg API latency",
"onlineTime": "Online Time",
"messageProcessing": "Messages",
"costEfficiency": "Cost Efficiency",
"recentPeriod": "Last {{range}}",
"perHour": "{{value}}/hr",
"noData": "No data",
"replied": "{{num}} replies",
"per100Messages": "Per 100 messages",
"seconds": "sec",
"hours": "hours",
"days": "days"
},
"charts": {
"tabs": {
"trends": "Trends",
"models": "Models",
"activity": "Activity",
"daily": "Daily"
},
"requestTrend": "Request Trend",
"requestTrendDesc": "Request volume over the last {{hours}} hours",
"costTrend": "Cost Trend",
"costTrendDesc": "API call cost over time",
"tokenUsage": "Token Usage",
"tokenUsageDesc": "Token consumption over time",
"modelDistribution": "Model Request Distribution",
"modelDistributionDesc": "Usage share per model ({{count}} models)",
"modelDetails": "Model Details",
"modelDetailsDesc": "Requests, cost and performance",
"recentActivity": "Recent Activity",
"recentActivityDesc": "Latest API call records",
"dailyStats": "Daily Statistics",
"dailyStatsDesc": "Data summary for the last 7 days",
"requests": "Requests",
"cost": "Cost (¥)",
"requestCount": "Requests",
"costLabel": "Cost",
"avgTime": "Avg Time",
"timeCost": "Duration",
"status": "Status"
},
"time": {
"hoursMinutes": "{{hours}}h {{minutes}}m"
},
"hitokotoFallback": "Life is like a box of chocolates, you never know what you're gonna get.",
"hitokotoFallbackFrom": "Forrest Gump",
"unknownSource": "Unknown",
"ariaLabel": {
"requestTrend": "Hourly request trend chart showing request count changes over recent hours",
"costTrend": "API cost trend chart showing API call cost changes over recent hours",
"tokenUsage": "Token usage trend chart showing token consumption over recent hours"
}
}
}

View File

@@ -473,5 +473,96 @@
"sidebarNav": "メインナビゲーション",
"closeMenu": "メニューを閉じる",
"navigatedTo": "{{page}} に移動しました"
},
"home": {
"title": "リアルタイムモニター",
"subtitle": "MaiBotの稼働状況と統計データの一覧",
"loading": "統計データを読み込み中...",
"loadingHint": "MaiBotの稼働データを取得しています",
"timeRange": {
"24h": "24時間",
"7d": "7日間",
"30d": "30日間"
},
"autoRefresh": "自動更新",
"botStatus": {
"title": "MaiBot ステータス",
"running": "稼働中",
"stopped": "停止中",
"uptime": "稼働 {{time}}"
},
"quickActions": {
"title": "クイック操作",
"restart": "MaiBotを再起動",
"restarting": "再起動中...",
"expressionReview": "表現レビュー",
"viewLogs": "ログを見る",
"pluginManage": "プラグイン管理",
"systemSettings": "システム設定"
},
"survey": {
"title": "フィードバック",
"description": "製品体験の改善にご協力ください",
"webui": "WebUI フィードバック",
"maibot": "MaiBot フィードバック"
},
"stats": {
"totalRequests": "総リクエスト数",
"totalCost": "総コスト",
"tokenUsage": "トークン消費",
"avgResponse": "平均応答",
"avgResponseDesc": "API平均レイテンシ",
"onlineTime": "オンライン時間",
"messageProcessing": "メッセージ処理",
"costEfficiency": "コスト効率",
"recentPeriod": "直近{{range}}",
"perHour": "{{value}}/時",
"noData": "データなし",
"replied": "返信 {{num}} 件",
"per100Messages": "100メッセージあたり",
"seconds": "秒",
"hours": "時間",
"days": "日"
},
"charts": {
"tabs": {
"trends": "トレンド",
"models": "モデル",
"activity": "アクティビティ",
"daily": "日次統計"
},
"requestTrend": "リクエストトレンド",
"requestTrendDesc": "直近{{hours}}時間のリクエスト量の変化",
"costTrend": "コストトレンド",
"costTrendDesc": "API呼び出しコストの変化",
"tokenUsage": "トークン消費",
"tokenUsageDesc": "トークン使用量の変化",
"modelDistribution": "モデルリクエスト分布",
"modelDistributionDesc": "各モデルの使用割合({{count}}モデル)",
"modelDetails": "モデル詳細統計",
"modelDetailsDesc": "リクエスト数、コスト、パフォーマンス",
"recentActivity": "最近のアクティビティ",
"recentActivityDesc": "最新のAPI呼び出し記録",
"dailyStats": "日次統計",
"dailyStatsDesc": "直近7日間のデータサマリー",
"requests": "リクエスト数",
"cost": "コスト(¥)",
"requestCount": "リクエスト数",
"costLabel": "コスト",
"avgTime": "平均所要時間",
"timeCost": "所要時間",
"status": "ステータス"
},
"time": {
"hoursMinutes": "{{hours}}時間{{minutes}}分"
},
"hitokotoFallback": "人生はチョコレートの箱のようなもの、次に何が出てくるか分からない。",
"hitokotoFallbackFrom": "フォレスト・ガンプ",
"unknownSource": "不明",
"ariaLabel": {
"requestTrend": "時間ごとのリクエスト数トレンドチャート、直近のリクエスト数変化を表示",
"costTrend": "APIコストトレンドチャート、直近のAPI呼び出しコスト変化を表示",
"tokenUsage": "Token消費トレンドチャート、直近のToken使用量変化を表示"
}
}
}

View File

@@ -473,5 +473,96 @@
"sidebarNav": "메인 내비게이션",
"closeMenu": "메뉴 닫기",
"navigatedTo": "{{page}}으로 이동했습니다"
},
"home": {
"title": "실시간 모니터",
"subtitle": "MaiBot 실행 상태 및 통계 데이터 개요",
"loading": "통계 데이터 로딩 중...",
"loadingHint": "MaiBot 실행 데이터를 가져오는 중",
"timeRange": {
"24h": "24시간",
"7d": "7일",
"30d": "30일"
},
"autoRefresh": "자동 새로고침",
"botStatus": {
"title": "MaiBot 상태",
"running": "실행 중",
"stopped": "중지됨",
"uptime": "가동 {{time}}"
},
"quickActions": {
"title": "빠른 작업",
"restart": "MaiBot 재시작",
"restarting": "재시작 중...",
"expressionReview": "표현 검토",
"viewLogs": "로그 보기",
"pluginManage": "플러그인 관리",
"systemSettings": "시스템 설정"
},
"survey": {
"title": "피드백 설문",
"description": "제품 경험 개선에 도움을 주세요",
"webui": "WebUI 피드백",
"maibot": "MaiBot 피드백"
},
"stats": {
"totalRequests": "총 요청 수",
"totalCost": "총 비용",
"tokenUsage": "토큰 소비",
"avgResponse": "평균 응답",
"avgResponseDesc": "API 평균 지연",
"onlineTime": "온라인 시간",
"messageProcessing": "메시지 처리",
"costEfficiency": "비용 효율",
"recentPeriod": "최근 {{range}}",
"perHour": "{{value}}/시간",
"noData": "데이터 없음",
"replied": "답장 {{num}}건",
"per100Messages": "100건당",
"seconds": "초",
"hours": "시간",
"days": "일"
},
"charts": {
"tabs": {
"trends": "트렌드",
"models": "모델",
"activity": "활동",
"daily": "일간 통계"
},
"requestTrend": "요청 트렌드",
"requestTrendDesc": "최근 {{hours}}시간의 요청량 변화",
"costTrend": "비용 트렌드",
"costTrendDesc": "API 호출 비용 변화",
"tokenUsage": "토큰 소비",
"tokenUsageDesc": "토큰 사용량 변화",
"modelDistribution": "모델 요청 분포",
"modelDistributionDesc": "모델별 사용 비율 ({{count}}개 모델)",
"modelDetails": "모델 상세 통계",
"modelDetailsDesc": "요청 수, 비용 및 성능",
"recentActivity": "최근 활동",
"recentActivityDesc": "최신 API 호출 기록",
"dailyStats": "일간 통계",
"dailyStatsDesc": "최근 7일 데이터 요약",
"requests": "요청 수",
"cost": "비용(¥)",
"requestCount": "요청 수",
"costLabel": "비용",
"avgTime": "평균 소요",
"timeCost": "소요 시간",
"status": "상태"
},
"time": {
"hoursMinutes": "{{hours}}시간 {{minutes}}분"
},
"hitokotoFallback": "인생은 초콜릿 상자와 같아서, 다음에 무엇이 나올지 모릅니다.",
"hitokotoFallbackFrom": "포레스트 검프",
"unknownSource": "알 수 없음",
"ariaLabel": {
"requestTrend": "시간별 요청 트렌드 차트, 최근 시간대별 요청 수 변화 표시",
"costTrend": "API 비용 트렌드 차트, 최근 시간대별 API 호출 비용 변화 표시",
"tokenUsage": "토큰 소비 트렌드 차트, 최근 시간대별 토큰 사용량 변화 표시"
}
}
}

View File

@@ -473,5 +473,96 @@
"sidebarNav": "主导航",
"closeMenu": "关闭菜单",
"navigatedTo": "已导航至 {{page}}"
},
"home": {
"title": "实时监控面板",
"subtitle": "麦麦运行状态和统计数据一览",
"loading": "加载统计数据中...",
"loadingHint": "正在获取麦麦运行数据",
"timeRange": {
"24h": "24小时",
"7d": "7天",
"30d": "30天"
},
"autoRefresh": "自动刷新",
"botStatus": {
"title": "麦麦状态",
"running": "运行中",
"stopped": "已停止",
"uptime": "运行 {{time}}"
},
"quickActions": {
"title": "快速操作",
"restart": "重启麦麦",
"restarting": "重启中...",
"expressionReview": "表达审核",
"viewLogs": "查看日志",
"pluginManage": "插件管理",
"systemSettings": "系统设置"
},
"survey": {
"title": "反馈问卷",
"description": "帮助我们改进产品体验",
"webui": "WebUI 反馈",
"maibot": "麦麦反馈"
},
"stats": {
"totalRequests": "总请求数",
"totalCost": "总花费",
"tokenUsage": "Token消耗",
"avgResponse": "平均响应",
"avgResponseDesc": "API平均耗时",
"onlineTime": "在线时长",
"messageProcessing": "消息处理",
"costEfficiency": "成本效率",
"recentPeriod": "最近{{range}}",
"perHour": "{{value}}/小时",
"noData": "暂无数据",
"replied": "回复 {{num}} 条",
"per100Messages": "每100条消息",
"seconds": "秒",
"hours": "小时",
"days": "天"
},
"charts": {
"tabs": {
"trends": "趋势",
"models": "模型",
"activity": "活动",
"daily": "日统计"
},
"requestTrend": "请求趋势",
"requestTrendDesc": "最近{{hours}}小时的请求量变化",
"costTrend": "花费趋势",
"costTrendDesc": "API调用成本变化",
"tokenUsage": "Token消耗",
"tokenUsageDesc": "Token使用量变化",
"modelDistribution": "模型请求分布",
"modelDistributionDesc": "各模型使用占比 (共 {{count}} 个模型)",
"modelDetails": "模型详细统计",
"modelDetailsDesc": "请求数、花费和性能",
"recentActivity": "最近活动",
"recentActivityDesc": "最新的API调用记录",
"dailyStats": "每日统计",
"dailyStatsDesc": "最近7天的数据汇总",
"requests": "请求数",
"cost": "花费(¥)",
"requestCount": "请求数",
"costLabel": "花费",
"avgTime": "平均耗时",
"timeCost": "耗时",
"status": "状态"
},
"time": {
"hoursMinutes": "{{hours}}小时{{minutes}}分钟"
},
"hitokotoFallback": "人生就像一盒巧克力,你永远不知道下一颗是什么味道。",
"hitokotoFallbackFrom": "阿甘正传",
"unknownSource": "未知",
"ariaLabel": {
"requestTrend": "每小时请求量趋势图,显示最近若干小时的请求次数变化",
"costTrend": "API花费趋势图显示最近若干小时的API调用成本变化",
"tokenUsage": "Token消耗趋势图显示最近若干小时的Token使用量变化"
}
}
}

View File

@@ -1,4 +1,5 @@
import { useEffect, useState, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import axios from 'axios'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
@@ -129,6 +130,7 @@ const generatePieColors = (count: number): string[] => {
// 内部实现组件
function IndexPageContent() {
const { t } = useTranslation()
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true)
const [loadingProgress, setLoadingProgress] = useState(0)
@@ -179,15 +181,15 @@ function IndexPageContent() {
if (isMountedRef.current) {
setHitokoto({
hitokoto: response.data.hitokoto,
from: response.data.from || response.data.from_who || '未知'
from: response.data.from || response.data.from_who || t('home.unknownSource')
})
}
} catch (error) {
console.error('获取一言失败:', error)
if (isMountedRef.current) {
setHitokoto({
hitokoto: '人生就像一盒巧克力,你永远不知道下一颗是什么味道。',
from: '阿甘正传'
hitokoto: t('home.hitokotoFallback'),
from: t('home.hitokotoFallbackFrom')
})
}
} finally {
@@ -195,7 +197,7 @@ function IndexPageContent() {
setHitokotoLoading(false)
}
}
}, [])
}, [t])
// 获取机器人状态
const fetchBotStatus = useCallback(async () => {
@@ -310,8 +312,8 @@ function IndexPageContent() {
<div className="text-center space-y-6 w-full max-w-md px-4">
<RefreshCw className="h-12 w-12 animate-spin mx-auto text-primary" />
<div className="space-y-2">
<p className="text-lg font-medium">...</p>
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-medium">{t('home.loading')}</p>
<p className="text-sm text-muted-foreground">{t('home.loadingHint')}</p>
</div>
<div className="space-y-2">
<Progress value={loadingProgress} className="h-2" />
@@ -348,7 +350,7 @@ function IndexPageContent() {
const formatTime = (seconds: number) => {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${hours}小时${minutes}分钟`
return t('home.time.hoursMinutes', { hours, minutes })
}
// 格式化大数字(自动选择合适单位)
@@ -403,11 +405,11 @@ function IndexPageContent() {
// 图表配置
const chartConfig = {
requests: {
label: '请求数',
label: t('home.charts.requests'),
color: 'hsl(var(--color-chart-1))',
},
cost: {
label: '花费(¥)',
label: t('home.charts.cost'),
color: 'hsl(var(--color-chart-2))',
},
tokens: {
@@ -422,17 +424,17 @@ function IndexPageContent() {
{/* 标题和控制栏 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold"></h1>
<h1 className="text-2xl sm:text-3xl font-bold">{t('home.title')}</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('home.subtitle')}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Tabs value={timeRange.toString()} onValueChange={(v) => setTimeRange(Number(v))}>
<TabsList className="grid grid-cols-3 w-full sm:w-auto">
<TabsTrigger value="24">24</TabsTrigger>
<TabsTrigger value="168">7</TabsTrigger>
<TabsTrigger value="720">30</TabsTrigger>
<TabsTrigger value="24">{t('home.timeRange.24h')}</TabsTrigger>
<TabsTrigger value="168">{t('home.timeRange.7d')}</TabsTrigger>
<TabsTrigger value="720">{t('home.timeRange.30d')}</TabsTrigger>
</TabsList>
</Tabs>
<Button
@@ -442,7 +444,7 @@ function IndexPageContent() {
className="gap-2"
>
<RefreshCw className={`h-4 w-4 ${autoRefresh ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline"></span>
<span className="hidden sm:inline">{t('home.autoRefresh')}</span>
</Button>
<Button variant="outline" size="sm" onClick={fetchDashboardData}>
<RefreshCw className="h-4 w-4" />
@@ -477,7 +479,7 @@ function IndexPageContent() {
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Power className="h-4 w-4" />
{t('home.botStatus.title')}
</CardTitle>
</CardHeader>
<CardContent>
@@ -488,7 +490,7 @@ function IndexPageContent() {
<div className="h-3 w-3 rounded-full bg-green-500 animate-pulse" />
<Badge variant="outline" className="text-green-600 border-green-300 bg-green-50">
<CheckCircle2 className="h-3 w-3 mr-1" />
{t('home.botStatus.running')}
</Badge>
</>
) : (
@@ -496,7 +498,7 @@ function IndexPageContent() {
<div className="h-3 w-3 rounded-full bg-red-500" />
<Badge variant="outline" className="text-red-600 border-red-300 bg-red-50">
<AlertCircle className="h-3 w-3 mr-1" />
{t('home.botStatus.stopped')}
</Badge>
</>
)}
@@ -505,7 +507,7 @@ function IndexPageContent() {
<div className="text-xs text-muted-foreground">
<span>v{botStatus.version}</span>
<span className="mx-2">|</span>
<span> {formatTime(botStatus.uptime)}</span>
<span>{t('home.botStatus.uptime', { time: formatTime(botStatus.uptime) })}</span>
</div>
)}
</div>
@@ -517,7 +519,7 @@ function IndexPageContent() {
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Zap className="h-4 w-4" />
{t('home.quickActions.title')}
</CardTitle>
</CardHeader>
<CardContent>
@@ -530,7 +532,7 @@ function IndexPageContent() {
className="gap-2"
>
<RotateCcw className={`h-4 w-4 ${isRestarting ? 'animate-spin' : ''}`} />
{isRestarting ? '重启中...' : '重启麦麦'}
{isRestarting ? t('home.quickActions.restarting') : t('home.quickActions.restart')}
</Button>
<Button
variant="outline"
@@ -539,7 +541,7 @@ function IndexPageContent() {
className="gap-2"
>
<ClipboardCheck className="h-4 w-4" />
{t('home.quickActions.expressionReview')}
{uncheckedCount > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-orange-500 text-white">
{uncheckedCount > 99 ? '99+' : uncheckedCount}
@@ -549,19 +551,19 @@ function IndexPageContent() {
<Button variant="outline" size="sm" asChild className="gap-2">
<Link to="/logs">
<FileText className="h-4 w-4" />
{t('home.quickActions.viewLogs')}
</Link>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link to="/plugins">
<Puzzle className="h-4 w-4" />
{t('home.quickActions.pluginManage')}
</Link>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link to="/settings">
<Settings className="h-4 w-4" />
{t('home.quickActions.systemSettings')}
</Link>
</Button>
</div>
@@ -573,10 +575,10 @@ function IndexPageContent() {
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<ClipboardList className="h-4 w-4" />
{t('home.survey.title')}
</CardTitle>
<CardDescription className="text-xs">
{t('home.survey.description')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -584,13 +586,13 @@ function IndexPageContent() {
<Button variant="outline" size="sm" asChild className="gap-2">
<Link to="/survey/webui-feedback">
<FileText className="h-4 w-4" />
WebUI
{t('home.survey.webui')}
</Link>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link to="/survey/maibot-feedback">
<MessageSquare className="h-4 w-4" />
{t('home.survey.maibot')}
</Link>
</Button>
</div>
@@ -602,7 +604,7 @@ function IndexPageContent() {
<div className="grid gap-4 grid-cols-1 xs:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.totalRequests')}</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -613,14 +615,14 @@ function IndexPageContent() {
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{timeRange < 48 ? timeRange + '小时' : Math.floor(timeRange / 24) + '天'}
{t('home.stats.recentPeriod', { range: timeRange < 48 ? timeRange + t('home.stats.hours') : Math.floor(timeRange / 24) + t('home.stats.days') })}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.totalCost')}</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -631,14 +633,14 @@ function IndexPageContent() {
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{summary.cost_per_hour > 0 ? `¥${summary.cost_per_hour.toFixed(2)}/小时` : '暂无数据'}
{summary.cost_per_hour > 0 ? t('home.stats.perHour', { value: `¥${summary.cost_per_hour.toFixed(2)}` }) : t('home.stats.noData')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Token消耗</CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.tokenUsage')}</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -650,20 +652,20 @@ function IndexPageContent() {
</div>
<p className="text-xs text-muted-foreground mt-1">
{summary.tokens_per_hour > 0
? `${formatNumber(summary.tokens_per_hour).display}/小时`
: '暂无数据'}
? t('home.stats.perHour', { value: formatNumber(summary.tokens_per_hour).display })
: t('home.stats.noData')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.avgResponse')}</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{summary.avg_response_time.toFixed(2)}s</div>
<p className="text-xs text-muted-foreground mt-1">API平均耗时</p>
<p className="text-xs text-muted-foreground mt-1">{t('home.stats.avgResponseDesc')}</p>
</CardContent>
</Card>
</div>
@@ -672,20 +674,20 @@ function IndexPageContent() {
<div className="grid gap-4 grid-cols-1 sm:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">线</CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.onlineTime')}</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl font-bold">
{formatTime(summary.online_time)}
<span className="text-xs font-normal text-muted-foreground ml-1">({summary.online_time.toLocaleString()})</span>
<span className="text-xs font-normal text-muted-foreground ml-1">({summary.online_time.toLocaleString()}{t('home.stats.seconds')})</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.messageProcessing')}</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -696,17 +698,17 @@ function IndexPageContent() {
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{formatNumber(summary.total_replies).display}
{t('home.stats.replied', { num: formatNumber(summary.total_replies).display })}
{formatNumber(summary.total_replies).needsExact && (
<span>({formatNumber(summary.total_replies).exact})</span>
)}
)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CardTitle className="text-sm font-medium">{t('home.stats.costEfficiency')}</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -715,7 +717,7 @@ function IndexPageContent() {
? `¥${((summary.total_cost / summary.total_messages) * 100).toFixed(2)}`
: '¥0.00'}
</div>
<p className="text-xs text-muted-foreground mt-1">100</p>
<p className="text-xs text-muted-foreground mt-1">{t('home.stats.per100Messages')}</p>
</CardContent>
</Card>
</div>
@@ -723,21 +725,21 @@ function IndexPageContent() {
{/* 图表区域 */}
<Tabs defaultValue="trends" className="space-y-4">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4">
<TabsTrigger value="trends"></TabsTrigger>
<TabsTrigger value="models"></TabsTrigger>
<TabsTrigger value="activity"></TabsTrigger>
<TabsTrigger value="daily"></TabsTrigger>
<TabsTrigger value="trends">{t('home.charts.tabs.trends')}</TabsTrigger>
<TabsTrigger value="models">{t('home.charts.tabs.models')}</TabsTrigger>
<TabsTrigger value="activity">{t('home.charts.tabs.activity')}</TabsTrigger>
<TabsTrigger value="daily">{t('home.charts.tabs.daily')}</TabsTrigger>
</TabsList>
{/* 趋势图表 */}
<TabsContent value="trends" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>{timeRange}</CardDescription>
<CardTitle>{t('home.charts.requestTrend')}</CardTitle>
<CardDescription>{t('home.charts.requestTrendDesc', { hours: timeRange })}</CardDescription>
</CardHeader>
<CardContent>
<ZoomableChart aria-label="每小时请求量趋势图,显示最近若干小时的请求次数变化">
<ZoomableChart aria-label={t('home.ariaLabel.requestTrend')}>
<ChartContainer config={chartConfig} className="h-[300px] sm:h-[400px] w-full aspect-auto">
<LineChart data={hourly_data}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--color-muted-foreground) / 0.2)" />
@@ -769,11 +771,11 @@ function IndexPageContent() {
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>API调用成本变化</CardDescription>
<CardTitle>{t('home.charts.costTrend')}</CardTitle>
<CardDescription>{t('home.charts.costTrendDesc')}</CardDescription>
</CardHeader>
<CardContent>
<ZoomableChart aria-label="API花费趋势图显示最近若干小时的API调用成本变化">
<ZoomableChart aria-label={t('home.ariaLabel.costTrend')}>
<ChartContainer config={chartConfig} className="h-[250px] sm:h-[300px] w-full aspect-auto">
<BarChart data={hourly_data}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--color-muted-foreground) / 0.2)" />
@@ -799,11 +801,11 @@ function IndexPageContent() {
<Card>
<CardHeader>
<CardTitle>Token消耗</CardTitle>
<CardDescription>Token使用量变化</CardDescription>
<CardTitle>{t('home.charts.tokenUsage')}</CardTitle>
<CardDescription>{t('home.charts.tokenUsageDesc')}</CardDescription>
</CardHeader>
<CardContent>
<ZoomableChart aria-label="Token消耗趋势图显示最近若干小时的Token使用量变化">
<ZoomableChart aria-label={t('home.ariaLabel.tokenUsage')}>
<ChartContainer config={chartConfig} className="h-[250px] sm:h-[300px] w-full aspect-auto">
<BarChart data={hourly_data}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--color-muted-foreground) / 0.2)" />
@@ -834,8 +836,8 @@ function IndexPageContent() {
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>使 ( {model_stats.length} )</CardDescription>
<CardTitle>{t('home.charts.modelDistribution')}</CardTitle>
<CardDescription>{t('home.charts.modelDistributionDesc', { count: model_stats.length })}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
@@ -878,8 +880,8 @@ function IndexPageContent() {
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
<CardTitle>{t('home.charts.modelDetails')}</CardTitle>
<CardDescription>{t('home.charts.modelDetailsDesc')}</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[300px] sm:h-[400px]">
@@ -902,13 +904,13 @@ function IndexPageContent() {
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.requestCount')}:</span>
<span className="ml-1 font-medium">
{stat.request_count.toLocaleString()}
</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.costLabel')}:</span>
<span className="ml-1 font-medium">¥{stat.total_cost.toFixed(2)}</span>
</div>
<div>
@@ -918,7 +920,7 @@ function IndexPageContent() {
</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.avgTime')}:</span>
<span className="ml-1 font-medium">
{stat.avg_response_time.toFixed(2)}s
</span>
@@ -935,8 +937,8 @@ function IndexPageContent() {
<TabsContent value="activity">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>API调用记录</CardDescription>
<CardTitle>{t('home.charts.recentActivity')}</CardTitle>
<CardDescription>{t('home.charts.recentActivityDesc')}</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] sm:h-[500px]">
@@ -963,15 +965,15 @@ function IndexPageContent() {
<span className="ml-1">{activity.tokens}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.costLabel')}:</span>
<span className="ml-1">¥{activity.cost.toFixed(4)}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.timeCost')}:</span>
<span className="ml-1">{activity.time_cost.toFixed(2)}s</span>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{t('home.charts.status')}:</span>
<span
className={`ml-1 ${activity.status === 'success' ? 'text-green-600' : 'text-red-600'}`}
>
@@ -991,18 +993,18 @@ function IndexPageContent() {
<TabsContent value="daily">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>7</CardDescription>
<CardTitle>{t('home.charts.dailyStats')}</CardTitle>
<CardDescription>{t('home.charts.dailyStatsDesc')}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={{
requests: {
label: '请求数',
label: t('home.charts.requests'),
color: 'hsl(var(--color-chart-1))',
},
cost: {
label: '花费(¥)',
label: t('home.charts.cost'),
color: 'hsl(var(--color-chart-2))',
},
}}