From 9f15f42b477c1ddf152bb053ecdb7485b638277d Mon Sep 17 00:00:00 2001
From: ryan
- 已用空间 + {t('usedSpace')}
{formatBytes(cacheStatus.total_size)} @@ -196,12 +205,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { {/* Key数量 */}
- 缓存键数量 + {t('cacheKeysCount')}
{formatNumber(cacheStatus.keys_count)}{' '} - 个文件 + {t('files')}
- 缓存基准目录 + {t('cacheBaseDirectory')}
setShowClearConfirm(true)}
>
- 立即清空缓存
+ {t('clearCacheNow')}
- SQL 执行成功 + {t('sqlExecSuccessTitle')}
- 受影响行数: {sqlResult.affected_rows} 行,耗时{' '} - {sqlResult.execution_time_ms} 毫秒。 + {t('affectedRows', { + rows: sqlResult.affected_rows, + ms: sqlResult.execution_time_ms, + })}
- 直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL - 导出为打包的 .sql 文本文件) + {t('dbBackupExportDesc')}
- 打开在线 SQL 控制终端,执行原生的 SQL - 进行数据筛选、更新、调试或性能优化 + {t('sqlQueryConsoleDesc')}
-
flex items-center gap-2
{' '}
- 作为标题行基础容器(不含下边框),外层统一使用{' '}
+ {t('rules.containerAlignment.description')}{' '}
py-6 px-1
{' '}
@@ -69,53 +71,53 @@ export default function HeaderDemoPage() {
py-6
- ) 进行上边距的视觉对齐,确保切换菜单时顶部高度感一致。
+ ) {t('rules.containerAlignment.description2')}
size-5 text-primary
{' '}
- 样式。禁止为图标包裹任何背景色小卡片、圆角或装饰性边框。
+ {t('rules.iconPresentation.description')}
{'h1 className="text-2xl font-semibold tracking-tight"'}
- ,禁用字重加粗(如{' '}
+ {t('rules.textStyle.description')}{' '}
font-bold
- )或任何渐变色,保证全站字体一致性。
+ {t('rules.textStyle.description2')}
tabs/events-tab.tsx
{' '}
- 或就近放在{' '}
+ {t('rules.tabsModular.description')}{' '}
components/
{' '}
- 下),主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。
+ {t('rules.tabsModular.description2')}
app/(main)/admin/database/components/
- ),只有真正跨页面复用的组件才进入全局{' '}
+ {t('rules.complexityDriven.description2')}{' '}
components/common/
- 。典型标杆案例参考“数据管理” (
+ {t('rules.complexityDriven.description3')}{' '}
/admin/database
@@ -145,19 +147,19 @@ export default function HeaderDemoPage() {
value='dashboard'
className='px-0 pb-2 text-xs font-semibold'
>
- 仪表盘与指标卡片
+ {t('tabs.dashboard')}
- 任务名称将用于后台执行记录的可读性显示 + {t('taskNameHint')}
- 勾选后,在当前后台任务顺利执行结束时,会关联派发多渠道通知推送。 + {t('allowNotifyDesc')}
@@ -118,32 +120,32 @@ export function ControlsTab() { {/* 按钮变体 */}
- 表单设计规范
+ {t('formDesignSpec')}
- 1. 统一控件高度与投影: + {t('spec.unifiedHeight')}
输入框 ( @@ -188,7 +190,7 @@ export function ControlsTab() {
- 2. Checkbox 与文本的对齐: + {t('spec.checkboxAlignment')}
Checkbox 应在左侧垂直偏上对齐,使用容器{' '} @@ -203,7 +205,7 @@ export function ControlsTab() {
- 3. 按钮使用策略: + {t('spec.buttonStrategy')}
普通主表单及新增/保存按钮,一律使用默认 Primary 按钮或次要{' '} diff --git a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx index fcc81455..9837d581 100644 --- a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx +++ b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx @@ -4,11 +4,13 @@ 'use client'; import * as React from 'react'; +import { useTranslations } from 'next-intl'; import { Code, Cpu, Database, HardDrive } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; export function DashboardTab() { + const t = useTranslations('admin.demo.dashboard'); return (
- 容器运行正常,负载平稳 + {t('cpuStatus')}
- 数据已同步远端 S3 存储桶,支持自动过期 + {t('storageStatus')}
@@ -54,7 +56,7 @@ export function DashboardTab() {- 链接池自动维护中,最大连接限制为 100 -
+{t('dbStatus')}
- 指标卡片 & 仪表盘设计规范
+ {t('dashboardDesignSpec')}
border-dashed shadow-none
{' '}
- 作为边框和投影。
+ {t('spec.dashedBorder2')}
flex flex-row items-center justify-between pb-2
{' '}
- 布局,左侧为标题小字,右侧为直接呈现的 Lucide 图标。
+ {t('spec.headerLayout2')}
text-2xl font-semibold tracking-tight
{' '}
- 展示。
+ {t('spec.valueStyle2')}
h-1.5
{' '}
- 细条高度,不喧宾夺主。
+ {t('spec.progressBar2')}
text-[10px] text-muted-foreground
{' '}
- 等辅助类。
+ {t('spec.secondaryText2')}
diff --git a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
index f5a60c16..29f71b56 100644
--- a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
+++ b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
@@ -4,6 +4,7 @@
'use client';
import * as React from 'react';
+import { useTranslations } from 'next-intl';
import { Code, Edit, ListFilter, Plus, Search, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -19,27 +20,28 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export function TableTab() {
+ const t = useTranslations('admin.demo.table');
const mockTableData = [
{
id: 'TX-1002',
- name: '用户信息批量同步导出',
- type: '数据同步',
+ name: t('mockData.syncExport'),
+ type: t('mockType.dataSync'),
status: 'success',
duration: '1.2s',
time: '2026-06-15 14:22',
},
{
id: 'TX-1003',
- name: '系统数据库每周自动备份归档',
- type: '数据库运维',
+ name: t('mockData.dbBackup'),
+ type: t('mockType.dbOps'),
status: 'running',
- duration: '在执行中',
+ duration: t('mockDuration.running'),
time: '2026-06-15 15:00',
},
{
id: 'TX-1004',
- name: '远端S3过期缓存对象自动清理任务',
- type: '磁盘清理',
+ name: t('mockData.s3Cleanup'),
+ type: t('mockType.diskCleanup'),
status: 'failed',
duration: '0.4s',
time: '2026-06-15 10:05',
@@ -54,7 +56,7 @@ export function TableTab() {
@@ -64,12 +66,12 @@ export function TableTab() {
className='h-8 border-dashed text-xs shadow-none'
>
- 过滤
+ {t('filter')}
@@ -79,25 +81,25 @@ export function TableTab() {
- 任务编号
+ {t('columns.taskId')}
- 执行任务名称
+ {t('columns.taskName')}
- 任务类型
+ {t('columns.taskType')}
- 状态
+ {t('columns.status')}
- 执行耗时
+ {t('columns.duration')}
- 触发时间
+ {t('columns.triggerTime')}
- 操作
+ {t('columns.actions')}
@@ -123,7 +125,7 @@ export function TableTab() {
className='text-[10px] bg-emerald-500/10 border-emerald-500/20 text-emerald-600 rounded-full py-0 px-2 font-medium'
>
- 成功
+ {t('status.success')}
)}
{row.status === 'running' && (
@@ -132,7 +134,7 @@ export function TableTab() {
className='text-[10px] bg-blue-500/10 border-blue-500/20 text-blue-600 rounded-full py-0 px-2 font-medium'
>
- 运行中
+ {t('status.running')}
)}
{row.status === 'failed' && (
@@ -141,7 +143,7 @@ export function TableTab() {
className='text-[10px] bg-destructive/10 border-destructive/20 text-destructive rounded-full py-0 px-2 font-medium'
>
- 失败
+ {t('status.failed')}
)}
@@ -180,13 +182,13 @@ export function TableTab() {
- 表格设计规范与代码模板
+ {t('tableDesignSpec')}
-
- 表格容器必须统一使用带有虚线边框的容器,即类名:
+ {t('spec.container')}
border border-dashed shadow-none rounded-lg overflow-hidden
bg-background
@@ -194,40 +196,40 @@ export function TableTab() {
。
-
- 表头背景设为{' '}
+ {t('spec.headerBg')}
bg-muted/40
- 。表头表项字号统一为小字粗体{' '}
+ {t('spec.headerStyle')}
text-xs font-semibold
。
-
- 单元格行与列之间使用虚线分割(
+ {t('spec.cellDivider')}
border-dashed
- )。表格行悬浮交互采用温和过渡色{' '}
+ {t('spec.rowHover')}
hover:bg-muted/10 transition-colors
。
-
- 编码、主键或唯一 ID 类字段一律采用等宽字体呈现:
+ {t('spec.monoFont')}
font-mono text-xs
。
-
- 状态标签(Badge)尽量圆角化(
+ {t('spec.badgeStyle')}
rounded-full py-0 px-2
- ),并以浅背景配以各自对应的图标圆点,以实现现代、高对比度的视觉质感。
+ {t('spec.badgeStyle2')}
diff --git a/frontend/app/(main)/admin/files/components/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx
index eea4f8a3..134cc3bc 100644
--- a/frontend/app/(main)/admin/files/components/file-list.tsx
+++ b/frontend/app/(main)/admin/files/components/file-list.tsx
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
+import { useTranslations } from 'next-intl';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AnimatePresence, motion } from 'motion/react';
import {
@@ -81,6 +82,7 @@ function formatDate(dateStr: string) {
}
export function FileList() {
+ const t = useTranslations('admin.files');
const queryClient = useQueryClient();
const [keyword, setKeyword] = React.useState('');
const [debouncedKeyword, setDebouncedKeyword] = React.useState('');
@@ -157,10 +159,10 @@ export function FileList() {
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['files', 'my'] });
void queryClient.invalidateQueries({ queryKey: ['files', 'stats'] });
- toast.success('文件已删除');
+ toast.success(t('list.deleteSuccess'));
setDeleteTarget(null);
},
- onError: (err: Error) => toast.error(err.message || '删除失败'),
+ onError: (err: Error) => toast.error(err.message || t('list.deleteFailed')),
});
// 批量 ZIP 下载
@@ -173,9 +175,9 @@ export function FileList() {
a.download = 'batch_download.zip';
a.click();
URL.revokeObjectURL(url);
- toast.success('批量下载已开始');
+ toast.success(t('list.batchDownloadSuccess'));
},
- onError: () => toast.error('批量下载失败'),
+ onError: () => toast.error(t('list.batchDownloadFailed')),
});
const toggleSelect = (id: string) => {
@@ -209,7 +211,7 @@ export function FileList() {
setKeyword(e.target.value)}
@@ -232,7 +234,9 @@ export function FileList() {
selectedIds.size === files.length ? clearSelection : selectAll
}
>
- {selectedIds.size === files.length ? '取消全选' : '全选本页'}
+ {selectedIds.size === files.length
+ ? t('list.deselectAll')
+ : t('list.selectAll')}
)}
@@ -247,7 +251,7 @@ export function FileList() {
className='flex items-center gap-2'
>
- 已选 {selectedIds.size} 个
+ {t('list.selectedCount', { count: selectedIds.size })}
)}
- 打包下载
+ {t('list.batchDownload')}
@@ -292,7 +296,7 @@ export function FileList() {
- {debouncedKeyword ? '没有匹配的文件' : '您还没有上传任何文件'}
+ {debouncedKeyword ? t('list.noMatch') : t('list.noFiles')}
) : (
@@ -309,18 +313,28 @@ export function FileList() {
onCheckedChange={handleSelectAll}
/>
- 预览
- ID
- 文件名
-
- 路径
+
+ {t('list.colPreview')}
+
+ ID
+ {t('list.colName')}
+
+ {t('list.colPath')}
+
+
+ {t('list.colType')}
+
+
+ {t('list.colMime')}
+
+
+ {t('list.colSize')}
+
+
+ {t('list.colUploadedAt')}
- 业务类别
- MIME类型
- 大小
- 上传时间
- 操作
+ {t('list.colActions')}
@@ -396,7 +410,7 @@ export function FileList() {
size='icon'
variant='ghost'
className='h-7 w-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground'
- title='查看详情'
+ title={t('list.viewDetail')}
onClick={() => setDetailTarget(file)}
>
@@ -405,7 +419,7 @@ export function FileList() {
size='icon'
variant='ghost'
className='h-7 w-7 rounded-md hover:bg-muted'
- title='下载'
+ title={t('list.download')}
onClick={() => handleDownload(file)}
>
@@ -414,7 +428,7 @@ export function FileList() {
size='icon'
variant='ghost'
className='h-7 w-7 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive'
- title='删除'
+ title={t('list.delete')}
onClick={() => setDeleteTarget(file)}
>
@@ -439,7 +453,7 @@ export function FileList() {
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
- 上一页
+ {t('list.prevPage')}
{page} / {totalPages}
@@ -451,7 +465,7 @@ export function FileList() {
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
- 下一页
+ {t('list.nextPage')}
)}
@@ -463,10 +477,8 @@ export function FileList() {
>
- 文件详情
-
- 查看文件的完整元数据与配置属性。
-
+ {t('list.detailTitle')}
+ {t('list.detailDesc')}
{detailTarget && (
@@ -495,7 +507,7 @@ export function FileList() {
- 文件 ID
+ {t('list.detailId')}
{detailTarget.id}
@@ -504,7 +516,7 @@ export function FileList() {
- 文件名
+ {t('list.detailName')}
{detailTarget.file_name}
@@ -513,7 +525,7 @@ export function FileList() {
- 存储路径
+ {t('list.detailPath')}
{detailTarget.file_path}
@@ -522,17 +534,17 @@ export function FileList() {
- 大小
+ {t('list.detailSize')}
{formatFileSize(detailTarget.file_size)} (
- {detailTarget.file_size.toLocaleString()} 字节)
+ {detailTarget.file_size.toLocaleString()} {t('list.bytes')})
- MIME 类型
+ {t('list.detailMime')}
{detailTarget.mime_type}
@@ -541,7 +553,7 @@ export function FileList() {
- 文件后缀
+ {t('list.detailExtension')}
{detailTarget.extension}
@@ -559,7 +571,7 @@ export function FileList() {
- 业务类型
+ {t('list.detailBizType')}
- 存储驱动
+ {t('list.detailDriver')}
{storageDriverQuery.data ?? 'local'}
@@ -582,7 +594,7 @@ export function FileList() {
- 上传者 ID
+ {t('list.detailUploaderId')}
{detailTarget.user_id}
@@ -591,7 +603,7 @@ export function FileList() {
- 上传时间
+ {t('list.detailUploadedAt')}
{formatDate(detailTarget.created_at)}
@@ -602,7 +614,7 @@ export function FileList() {
Object.keys(detailTarget.metadata).length > 0 && (
- 额外元数据
+ {t('list.detailMetadata')}
{JSON.stringify(detailTarget.metadata, null, 2)}
@@ -622,18 +634,14 @@ export function FileList() {
>
- 确认删除文件
+ {t('list.deleteTitle')}
- 确定要删除文件{' '}
-
- 「{deleteTarget?.file_name}」
- {' '}
- 吗?此操作不可撤销。
+ {t('list.deleteConfirm', { name: deleteTarget?.file_name || '' })}
- 取消
+ {t('list.cancel')}
@@ -645,7 +653,7 @@ export function FileList() {
{deleteMutation.isPending && (
)}
- 确认删除
+ {t('list.confirmDelete')}
diff --git a/frontend/app/(main)/admin/files/components/file-stats.tsx b/frontend/app/(main)/admin/files/components/file-stats.tsx
index ab608bf4..6000c6a0 100644
--- a/frontend/app/(main)/admin/files/components/file-stats.tsx
+++ b/frontend/app/(main)/admin/files/components/file-stats.tsx
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
+import { useTranslations } from 'next-intl';
import { useQuery } from '@tanstack/react-query';
import {
Database,
@@ -51,40 +52,41 @@ const categoryMap: Record = {
const statsChartConfig = {
images: {
- label: '图片',
+ label: 'images',
color: 'hsl(var(--chart-1))',
},
videos: {
- label: '视频',
+ label: 'videos',
color: 'hsl(var(--chart-2))',
},
audio: {
- label: '音频',
+ label: 'audio',
color: 'hsl(var(--chart-3))',
},
documents: {
- label: '文档',
+ label: 'documents',
color: 'hsl(var(--chart-4))',
},
archives: {
- label: '压缩包',
+ label: 'archives',
color: 'hsl(var(--chart-5))',
},
others: {
- label: '其他',
+ label: 'others',
color: 'hsl(var(--chart-6))',
},
count: {
- label: '新增文件数',
+ label: 'count',
color: 'hsl(var(--primary))',
},
size: {
- label: '新增大小',
+ label: 'size',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
export function FileStats() {
+ const t = useTranslations('admin.files');
const [trendMetric, setTrendMetric] = React.useState<'count' | 'size'>(
'count',
);
@@ -182,9 +184,9 @@ export function FileStats() {
- 暂无上传数据
+ {t('stats.noData')}
- 上传文件后即可在此查看容量与文件分布分析。
+ {t('stats.noDataDesc')}
@@ -198,7 +200,7 @@ export function FileStats() {
- 总文件数
+ {t('stats.totalFiles')}
@@ -206,11 +208,11 @@ export function FileStats() {
{stats.total_count.toLocaleString()}{' '}
- 个
+ {t('stats.countUnit')}
- 您上传的可用文件数量汇总
+ {t('stats.totalFilesDesc')}
@@ -218,7 +220,7 @@ export function FileStats() {
- 占用存储容量
+ {t('stats.totalSize')}
@@ -227,7 +229,7 @@ export function FileStats() {
{formatFileSize(stats.total_size)}
- 文件实际在磁盘/S3中占用的总空间
+ {t('stats.totalSizeDesc')}
@@ -235,7 +237,7 @@ export function FileStats() {
- 近 7 天新增文件
+ {t('stats.weeklyNewFiles')}
@@ -243,11 +245,11 @@ export function FileStats() {
+{trendSummary.count.toLocaleString()}{' '}
- 个
+ {t('stats.countUnit')}
- 最近 7 天上传成功的文件总数
+ {t('stats.weeklyNewFilesDesc')}
@@ -255,7 +257,7 @@ export function FileStats() {
- 近 7 天新增大小
+ {t('stats.weeklyNewSize')}
@@ -264,7 +266,7 @@ export function FileStats() {
+{formatFileSize(trendSummary.size)}
- 最近 7 天上传所消耗的存储带宽
+ {t('stats.weeklyNewSizeDesc')}
@@ -275,10 +277,10 @@ export function FileStats() {
- 最近 7 天新增趋势
+ {t('stats.trendTitle')}
- 按日统计近一周期内的上传波动
+ {t('stats.trendDesc')}
@@ -288,7 +290,7 @@ export function FileStats() {
className='h-7 text-xs px-2.5 rounded-md'
onClick={() => setTrendMetric('count')}
>
- 新增数量 (个)
+ {t('stats.trendCount')}
@@ -361,7 +363,9 @@ export function FileStats() {
hideLabel
formatter={(value) => {
const label =
- trendMetric === 'count' ? '新增文件数' : '新增大小';
+ trendMetric === 'count'
+ ? t('stats.chartLabelCount')
+ : t('stats.chartLabelSize');
const formattedValue =
trendMetric === 'size'
? formatFileSize(Number(value))
@@ -419,16 +423,16 @@ export function FileStats() {
- 文件格式数量分布
+ {t('stats.countDistTitle')}
- 各种格式类型的文件个数占比
+ {t('stats.countDistDesc')}
{categoryCountData.length === 0 ? (
- 暂无分类数据
+ {t('stats.noCategoryData')}
) : (
@@ -494,15 +498,10 @@ export function FileStats() {
- 文件类型中数量最多的是{' '}
-
- 「{maxStats.maxCountName}」
-
- ,累计上传了{' '}
-
- {maxStats.maxCount}
- {' '}
- 个文件。
+ {t('stats.maxCountHint', {
+ name: maxStats.maxCountName,
+ count: maxStats.maxCount,
+ })}
@@ -512,16 +511,16 @@ export function FileStats() {
- 文件占用容量分布
+ {t('stats.sizeDistTitle')}
- 各种格式类型的文件大小占比
+ {t('stats.sizeDistDesc')}
{categorySizeData.length === 0 ? (
- 暂无容量数据
+ {t('stats.noSizeData')}
) : (
@@ -587,15 +586,10 @@ export function FileStats() {
- 文件类型中占用存储最大的是{' '}
-
- 「{maxStats.maxSizeName}」
-
- ,共消耗了{' '}
-
- {formatFileSize(maxStats.maxSize)}
- {' '}
- 存储容量。
+ {t('stats.maxSizeHint', {
+ name: maxStats.maxSizeName,
+ size: formatFileSize(maxStats.maxSize),
+ })}
diff --git a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx
index d6a1b741..62da26bc 100644
--- a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx
+++ b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
+import { useTranslations } from 'next-intl';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Database, Loader2, Play, Save } from 'lucide-react';
import { toast } from 'sonner';
@@ -44,13 +45,13 @@ import type {
const storageConfigKey = 'storage_config';
const storageMigrationTaskType = 'storage_migration';
-const driverLabels: Record = {
- local: '本地文件系统',
- s3: 'AWS S3',
- r2: 'Cloudflare R2',
- minio: 'MinIO',
- oss: '阿里云 OSS',
- webdav: 'WebDAV',
+const driverLabelKeys: Record = {
+ local: 'driverLocal',
+ s3: 'driverS3',
+ r2: 'driverR2',
+ minio: 'driverMinio',
+ oss: 'driverOss',
+ webdav: 'driverWebdav',
};
const emptyObjectConfig: ObjectStorageConfig = {
@@ -112,8 +113,8 @@ function parseMigrationPayload(payload: string): StorageConfig | undefined {
}
}
-function driverLabel(driver?: StorageDriver): string {
- return driver ? driverLabels[driver] : '未知';
+function driverLabelKey(driver?: StorageDriver): string {
+ return driver ? driverLabelKeys[driver] : 'driverUnknown';
}
function latestMigration(
@@ -136,6 +137,7 @@ function latestMigration(
}
export function StorageConfigTab() {
+ const t = useTranslations('admin.files');
const queryClient = useQueryClient();
const [config, setConfig] = React.useState(null);
const query = useQuery({
@@ -186,13 +188,13 @@ export function StorageConfigTab() {
});
},
onSuccess: () => {
- toast.success('存储配置已保存');
+ toast.success(t('storage.saveSuccess'));
void queryClient.invalidateQueries({
queryKey: ['admin', 'storage-config'],
});
},
onError: (error: Error) => {
- toast.error(error.message || '保存存储配置失败');
+ toast.error(error.message || t('storage.saveFailed'));
void queryClient.invalidateQueries({
queryKey: ['admin', 'storage-config'],
});
@@ -208,13 +210,13 @@ export function StorageConfigTab() {
});
},
onSuccess: () => {
- toast.success('存储迁移任务已下发');
+ toast.success(t('storage.migrateSuccess'));
void queryClient.invalidateQueries({
queryKey: ['admin', 'storage-config'],
});
},
onError: (error: Error) => {
- toast.error(error.message || '下发存储迁移任务失败');
+ toast.error(error.message || t('storage.migrateFailed'));
void queryClient.invalidateQueries({
queryKey: ['admin', 'storage-config'],
});
@@ -224,12 +226,13 @@ export function StorageConfigTab() {
mutationFn: (executionID: string) =>
services.adminTask.retryTaskExecution(executionID),
onSuccess: () => {
- toast.success('存储迁移任务已重新下发');
+ toast.success(t('storage.retrySuccess'));
void queryClient.invalidateQueries({
queryKey: ['admin', 'storage-config'],
});
},
- onError: (error: Error) => toast.error(error.message || '运行存储迁移失败'),
+ onError: (error: Error) =>
+ toast.error(error.message || t('storage.retryFailed')),
});
if (query.isPending || !config) {
@@ -269,7 +272,7 @@ export function StorageConfigTab() {
- 存储维护模式
+ {t('storage.maintenanceMode')}
- {driverLabel(migration.source_driver)} →{' '}
- {driverLabel(migration.target_driver)}
- 。迁移期间文件只允许读取,禁止上传、删除和清理。
+ {t('storage.maintenanceDesc', {
+ source: t(`storage.${driverLabelKey(migration.source_driver)}`),
+ target: t(`storage.${driverLabelKey(migration.target_driver)}`),
+ })}
@@ -289,7 +293,9 @@ export function StorageConfigTab() {
value={migration.state === 'succeeded' ? 100 : undefined}
/>
- 迁移进度请查看任务执行日志:{migration.task_id || '尚未下发'}
+ {t('storage.migrationProgress', {
+ taskId: migration.task_id || t('storage.notDispatched'),
+ })}
{migration.error && (
{migration.error}
@@ -310,7 +316,7 @@ export function StorageConfigTab() {
) : (
)}
- 重试迁移
+ {t('storage.retryMigration')}
@@ -324,10 +330,10 @@ export function StorageConfigTab() {
- 文件存储
+ {t('storage.fileStorage')}
- 默认使用本地存储。配置系统文件的存储媒介,切换存储类型且已有文件时,系统会自动进入维护模式并迁移文件。
+ {t('storage.fileStorageDesc')}
@@ -337,7 +343,7 @@ export function StorageConfigTab() {
- 存储类型
+ {t('storage.storageType')}
- 系统记录的所有成功认证的访问总频次
+ {t('totalRequests7dDesc')}
- 活跃终端分类数
+ {t('activeBrowserTypes')}
@@ -190,14 +190,14 @@ export function AccessAnalytics() {
{browsers.length}
- 在一周内发起请求的浏览器代理大类统计
+ {t('activeBrowserTypesDesc')}
- 活跃独立用户数
+ {t('activeUsers')}
@@ -206,7 +206,7 @@ export function AccessAnalytics() {
{topUsers.length}
- 一周内累计发起高频请求的注册账户总量
+ {t('activeUsersDesc')}
@@ -217,10 +217,10 @@ export function AccessAnalytics() {
- 一周访问量趋势
+ {t('weeklyAccessTrend')}
- 展现系统最近 7 天内的每日 API 请求曲线
+ {t('weeklyAccessTrendDesc')}
) : (
@@ -305,17 +305,17 @@ export function AccessAnalytics() {
- 使用的浏览器排行
+ {t('browserRanking')}
- 基于请求头 User-Agent 智能分类的一周占比统计
+ {t('browserRankingDesc')}
{browsers.length === 0 ? (
) : (
@@ -360,17 +360,17 @@ export function AccessAnalytics() {
- 最活跃的用户排行 (Top 10)
+ {t('topActiveUsers')}
- 统计最近一周发起接口访问请求数量最多的账户
+ {t('topActiveUsersDesc')}
{topUsers.length === 0 ? (
) : (
@@ -385,11 +385,13 @@ export function AccessAnalytics() {
- {user.username || '未知'}
+ {user.username || t('unknown')}
- {user.nickname ? `(${user.nickname})` : '(无昵称)'} |
- ID: {user.user_id}
+ {user.nickname
+ ? `(${user.nickname})`
+ : t('noNickname')}{' '}
+ | ID: {user.user_id}
@@ -398,7 +400,7 @@ export function AccessAnalytics() {
{user.count.toLocaleString()}
- 次请求
+ {t('requests')}
diff --git a/frontend/app/(main)/admin/logs/components/access-logs.tsx b/frontend/app/(main)/admin/logs/components/access-logs.tsx
index dcba3fc4..251970b4 100644
--- a/frontend/app/(main)/admin/logs/components/access-logs.tsx
+++ b/frontend/app/(main)/admin/logs/components/access-logs.tsx
@@ -32,6 +32,7 @@ import {
} from 'lucide-react';
import services from '@/lib/services';
+import { useTranslations } from 'next-intl';
import { ErrorInline } from '@/components/layout/error';
import { LoadingStateWithBorder } from '@/components/layout/loading';
import { EmptyStateWithBorder } from '@/components/layout/empty';
@@ -97,6 +98,7 @@ function isClickhouseDisabledError(error: Error) {
}
export function AccessLogs() {
+ const t = useTranslations('admin.logs.accessLogs');
const [page, setPage] = useState(1);
// 搜索过滤条件
@@ -202,11 +204,9 @@ export function AccessLogs() {
return (
- ClickHouse 未启用
+ {t('clickhouseDisabled')}
- 当前系统配置未启用 ClickHouse
- 存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml`
- 配置文件中配置并启用 ClickHouse。
+ {t('clickhouseDisabledDesc')}
);
@@ -227,11 +227,11 @@ export function AccessLogs() {
>
setUsernameFilter(e.target.value)}
className='h-8 text-xs'
@@ -239,11 +239,11 @@ export function AccessLogs() {
setPathFilter(e.target.value)}
className='h-8 text-xs'
@@ -251,7 +251,7 @@ export function AccessLogs() {
@@ -295,26 +295,27 @@ export function AccessLogs() {
{loading && logs.length === 0 ? (
) : logs.length === 0 ? (
-
+
) : (
- 请求方法
- 路径
- 用户
- IP
- 状态
- 耗时
- 请求时间
- 详情
+ {t('colMethod')}
+ {t('colPath')}
+ {t('colUser')}
+ {t('colIp')}
+ {t('colStatus')}
+ {t('colLatency')}
+
+ {t('colRequestTime')}
+
+
+ {t('colDetail')}
+
@@ -334,7 +335,7 @@ export function AccessLogs() {
- {log.username || '未知用户'}
+ {log.username || t('unknownUser')}
{log.nickname && (
@@ -381,7 +382,7 @@ export function AccessLogs() {
{logs.length > 0 && (
- 共 {total} 条记录,当前第 {page}/{totalPages} 页
+ {t('paginationInfo', { total, page, totalPages })}
@@ -412,7 +413,9 @@ export function AccessLogs() {
- 访问日志详情
+
+ {t('accessLogDetail')}
+
访问 ID: {selectedLog?.id}
@@ -424,7 +427,7 @@ export function AccessLogs() {
- 请求方法
+ {t('method')}
{selectedLog.method}
@@ -432,7 +435,7 @@ export function AccessLogs() {
- 响应状态
+ {t('responseStatus')}
@@ -442,7 +445,7 @@ export function AccessLogs() {
- 耗时
+ {t('latency')}
{formatLatency(selectedLog.latency)}
@@ -450,18 +453,18 @@ export function AccessLogs() {
- IP 地址
+ {t('ipAddress')}
{selectedLog.ip}
- 用户
+ {t('user')}
{selectedLog.username
- ? `${selectedLog.username} (${selectedLog.nickname || '无昵称'})`
- : '未知/游客'}
+ ? `${selectedLog.username} (${selectedLog.nickname || t('noNickname')})`
+ : t('unknownGuest')}
ID: {selectedLog.user_id}
@@ -472,7 +475,7 @@ export function AccessLogs() {
{/* Path */}
{selectedLog.path}
@@ -482,7 +485,7 @@ export function AccessLogs() {
{/* Request Time */}
{formatDateTime(selectedLog.created_at)}
@@ -542,7 +545,7 @@ export function AccessLogs() {
)}
diff --git a/frontend/app/(main)/admin/logs/components/app-logs.tsx b/frontend/app/(main)/admin/logs/components/app-logs.tsx
index bd51880a..2429166d 100644
--- a/frontend/app/(main)/admin/logs/components/app-logs.tsx
+++ b/frontend/app/(main)/admin/logs/components/app-logs.tsx
@@ -22,6 +22,7 @@ import { toast } from 'sonner';
import { ArrowDown, ChevronUp, Loader2 } from 'lucide-react';
import services from '@/lib/services';
+import { useTranslations } from 'next-intl';
import { ErrorInline } from '@/components/layout/error';
import { LoadingStateWithBorder } from '@/components/layout/loading';
import { Button } from '@/components/ui/button';
@@ -92,6 +93,7 @@ function parseLogLevel(
}
export function AppLogs() {
+ const t = useTranslations('admin.logs.appLogs');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -300,12 +302,12 @@ export function AppLogs() {
{loadingMore ? (
<>
- 加载中...
+ {t('loading')}
>
) : (
<>
- 加载更早日志
+ {t('loadOlderLogs')}
>
)}
@@ -313,7 +315,9 @@ export function AppLogs() {
)}
{logs.length === 0 ? (
- 暂无日志
+
+ {t('noLogs')}
+
) : (
- 回到最新
+ {t('backToLatest')}
)}
diff --git a/frontend/app/(main)/admin/logs/page-client.tsx b/frontend/app/(main)/admin/logs/page-client.tsx
index 3f6a5a8f..5e65ffee 100644
--- a/frontend/app/(main)/admin/logs/page-client.tsx
+++ b/frontend/app/(main)/admin/logs/page-client.tsx
@@ -2,6 +2,7 @@
import dynamic from 'next/dynamic';
import { Activity, BarChart3, Terminal } from 'lucide-react';
+import { useTranslations } from 'next-intl';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -26,13 +27,16 @@ const AppLogs = dynamic(
);
export function LogsPageClient() {
+ const t = useTranslations('admin.logs');
return (
{/* Header */}
- 系统日志
+
+ {t('pageTitle')}
+
{/* Tabs Layout */}
@@ -43,21 +47,21 @@ export function LogsPageClient() {
className='px-0 pb-2 text-xs font-semibold flex items-center gap-1.5'
>
- 访问分析
+ {t('accessAnalytics')}
- 用户访问日志
+ {t('userAccessLogs')}
- 应用运行日志
+ {t('appRuntimeLogs')}
diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx
index 79f52e92..bbfad4d9 100644
--- a/frontend/app/(main)/admin/push/components/events-tab.tsx
+++ b/frontend/app/(main)/admin/push/components/events-tab.tsx
@@ -57,6 +57,8 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
+import { useTranslations } from 'next-intl';
+
import { ErrorInline } from '@/components/layout/error';
import { LoadingStateWithBorder } from '@/components/layout/loading';
import { EmptyStateWithBorder } from '@/components/layout/empty';
@@ -70,6 +72,7 @@ import type {
import { PushService } from '@/lib/services/push';
export function EventsTab() {
+ const t = useTranslations('admin.push.events');
const queryClient = useQueryClient();
// --- 获取所有自定义消息通道 ---
@@ -107,12 +110,12 @@ export function EventsTab() {
mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) =>
PushService.updateEvent(id, data),
onSuccess: () => {
- toast.success('事件更新成功');
+ toast.success(t('eventUpdateSuccess'));
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
setEditEventOpen(false);
},
onError: (err: unknown) => {
- toast.error('事件更新失败: ' + (err as Error).message);
+ toast.error(t('eventUpdateFailed') + ': ' + (err as Error).message);
},
});
@@ -122,14 +125,14 @@ export function EventsTab() {
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
},
onError: (err: unknown) => {
- toast.error('操作失败: ' + (err as Error).message);
+ toast.error(t('operationFailed') + ': ' + (err as Error).message);
},
});
const createEventMutation = useMutation({
mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data),
onSuccess: () => {
- toast.success('事件创建成功');
+ toast.success(t('eventCreateSuccess'));
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
setCreateEventOpen(false);
setNewEventKey('');
@@ -139,18 +142,18 @@ export function EventsTab() {
setNewEventEnabled(true);
},
onError: (err: unknown) => {
- toast.error('事件创建失败: ' + (err as Error).message);
+ toast.error(t('eventCreateFailed') + ': ' + (err as Error).message);
},
});
const deleteEventMutation = useMutation({
mutationFn: (id: number) => PushService.deleteEvent(id),
onSuccess: () => {
- toast.success('配置删除成功');
+ toast.success(t('configDeleteSuccess'));
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
},
onError: (err: unknown) => {
- toast.error('配置删除失败: ' + (err as Error).message);
+ toast.error(t('configDeleteFailed') + ': ' + (err as Error).message);
},
});
@@ -198,7 +201,7 @@ export function EventsTab() {
try {
JSON.parse(eventTemplate);
} catch {
- toast.error('消息模板不是合法的 JSON 格式');
+ toast.error(t('templateInvalidJson'));
return;
}
@@ -258,11 +261,11 @@ export function EventsTab() {
const handleCreateEvent = () => {
if (newEventType === 'builtin' && !newEventKey) {
- toast.error('请选择系统事件');
+ toast.error(t('selectSystemEvent'));
return;
}
if (newEventType === 'task' && !newEventTaskType) {
- toast.error('请选择异步任务');
+ toast.error(t('selectAsyncTask'));
return;
}
@@ -270,7 +273,7 @@ export function EventsTab() {
try {
JSON.parse(newEventTemplate);
} catch {
- toast.error('内容渲染模板不是合法的 JSON 格式');
+ toast.error(t('templateInvalidJson'));
return;
}
}
@@ -295,11 +298,14 @@ export function EventsTab() {
{eventsQuery.isLoading ? (
-
+
) : eventsQuery.isError ? (
) : (eventsQuery.data ?? []).length === 0 ? (
-
+
) : (
@@ -322,19 +325,19 @@ export function EventsTab() {
ID
- 通知事件
+ {t('colEvent')}
- 关联渠道
+ {t('colChannels')}
- 推送目标
+ {t('colTargets')}
- 状态
+ {t('colStatus')}
- 操作
+ {t('colActions')}
@@ -362,7 +365,7 @@ export function EventsTab() {
variant='outline'
className='text-[8px] h-3.5 px-1 bg-blue-50/50 text-blue-600 border-blue-200'
>
- 任务
+ {t('taskBadge')}
)}
@@ -375,7 +378,7 @@ export function EventsTab() {
{(event.channels ?? []).length === 0 ? (
- 未指定渠道
+ {t('noChannelSpecified')}
) : (
event.channels.map((ch) => (
@@ -384,7 +387,7 @@ export function EventsTab() {
variant='secondary'
className='text-[10px] py-0 px-1.5 h-4.5'
>
- {ch === 'email' ? '邮件' : ch}
+ {ch === 'email' ? t('emailChannel') : ch}
))
)}
@@ -439,7 +442,7 @@ export function EventsTab() {
- 配置
+ {t('configure')}
@@ -453,7 +456,7 @@ export function EventsTab() {
className='h-6 w-6 text-muted-foreground hover:text-destructive hover:bg-destructive/10'
disabled={deleteEventMutation.isPending}
onClick={() => {
- if (confirm('确定要删除该通知事件配置吗?')) {
+ if (confirm(t('deleteEventConfirm'))) {
deleteEventMutation.mutate(event.id);
}
}}
@@ -462,7 +465,7 @@ export function EventsTab() {
- 删除
+ {t('delete')}
@@ -479,15 +482,13 @@ export function EventsTab() {