为 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

@@ -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))',
},
}}