From 186ece6495770bfa8b628352c61018ddd33bc85b Mon Sep 17 00:00:00 2001 From: attacker Date: Mon, 29 Jun 2026 20:44:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E5=AE=89=E5=85=A8=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E6=A8=A1=E5=9D=97=E5=85=A8=E9=87=8F=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=20=E2=80=94=207=20=E6=96=87=E4=BB=B6=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E4=B8=AD=E6=96=87=E6=94=B9=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E9=94=AE=20+=20audit=20=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4(89?= =?UTF-8?q?=20=E9=94=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移文件: - AuditOperationsTab.tsx — 操作审计(搜索/筛选/虚拟滚动表格/详情弹窗/统计弹窗) - TerminalAuditTab.tsx — 终端审计子标签(在线/历史/命令) - LoginLogsTab.tsx — 登录日志(DataTable + 状态筛选 + 登录方式) - ActiveSessionsPanel.tsx — 在线会话(自动刷新 + 阻断确认 + 实时监控) - HistorySessionsPanel.tsx — 历史会话(回放/下载录制) - SessionCommandsPanel.tsx — 会话命令(虚拟滚动) - helpers.tsx — formatDuration/getStatusBadge (i18n.t 非组件模式) 翻译键: audit 命名空间 89 键 (operations/login/terminal 三子域) + common.duration 时长单位(秒/分/时) 总键数: 481→610, 命名空间: 8→9 (common/menu/header/login/dashboard/server/alert/approval/audit) zh-CN/en-US 完全对齐, tsc -b=0, npm build✓, 17/17 测试通过 --- ROADMAP.md | 2 +- frontend/src/locales/en-US.json | 111 +++++++++++++++++- frontend/src/locales/zh-CN.json | 111 +++++++++++++++++- .../src/pages/audit/AuditOperationsTab.tsx | 84 ++++++------- frontend/src/pages/audit/LoginLogsTab.tsx | 50 ++++---- frontend/src/pages/audit/TerminalAuditTab.tsx | 48 ++++---- .../terminal-audit/ActiveSessionsPanel.tsx | 41 ++++--- .../terminal-audit/HistorySessionsPanel.tsx | 32 ++--- .../terminal-audit/SessionCommandsPanel.tsx | 22 ++-- .../pages/audit/terminal-audit/helpers.tsx | 15 +-- 10 files changed, 375 insertions(+), 141 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e8b57f8..794e4ab 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -280,7 +280,7 @@ services/api.ts (1324L) - ✅ **可扩展架构** — `MenuItem.labelKey` 模式支持增量迁移,后续页面只需添加翻译键 + `useTranslation()` 即可 **后续增量迁移路径**(已建立完整模式,各页面可独立迁移): -- Dashboard / ServerManager / Kubernetes 各页面 / AlertCenter 等页面内容翻译 +- ✅ Dashboard / ServerManager / Kubernetes 各页面 / AlertCenter / **安全审计(audit)** 等页面内容翻译 - 添加更多语言(如日语 `ja-JP`)只需在 `SUPPORTED_LANGUAGES` + 新 JSON 文件中注册 **验收标准**: diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 1a1b440..7afeec6 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -71,7 +71,14 @@ "operationSuccess": "Operation succeeded", "operationFailed": "Operation failed", "loadFailed": "Failed to load", - "saveFailed": "Failed to save" + "saveFailed": "Failed to save", + "duration": { + "sec": "{{count}}s", + "min": "{{count}}m", + "minSec": "{{m}}m {{s}}s", + "hour": "{{count}}h", + "hourMin": "{{h}}h {{m}}m" + } }, "menu": { "dashboard": "Dashboard", @@ -545,5 +552,107 @@ "templateNotLoaded": "Template not loaded", "pleaseFill": "Please fill in \"{{label}}\"" } + }, + "audit": { + "operations": { + "searchPlaceholder": "Search audit logs...", + "allModules": "All Modules", + "allActions": "All Actions", + "noData": "No audit logs", + "viewDetail": "View Details", + "totalLogs": "{{count}} logs", + "virtualScroll": "virtual scroll", + "detailTitle": "Audit Log Details", + "statsTitle": "Audit Statistics", + "noStats": "No statistics available", + "topModules": "Top Modules", + "topUsers": "Active Users", + "col": { + "time": "Time", + "user": "User", + "module": "Module", + "actionType": "Action", + "resource": "Resource", + "ip": "IP", + "result": "Result", + "detail": "Detail" + }, + "detailCol": { + "userId": "User ID", + "ipAddress": "IP Address", + "userAgent": "User-Agent", + "message": "Message" + }, + "stats": { + "totalLogs": "Total Logs", + "todayLogs": "Today", + "successCount": "Success", + "failureCount": "Failed" + } + }, + "login": { + "searchPlaceholder": "Search user/IP/location...", + "noData": "No login logs", + "all": "All", + "col": { + "loginTime": "Login Time", + "user": "User", + "loginType": "Method", + "ip": "IP", + "location": "Location", + "browser": "Browser/OS", + "status": "Status" + }, + "type": { + "password": "Password", + "ldap": "LDAP", + "token": "Token", + "oauth": "OAuth" + } + }, + "terminal": { + "searchUserServer": "Search user/server...", + "searchUserServerIp": "Search user/server/IP...", + "searchUserCommand": "Search user/command...", + "sessionIdPlaceholder": "Session ID (optional)", + "noActive": "No active sessions", + "noHistory": "No history sessions", + "noCommands": "No command records", + "totalCommands": "{{count}} commands", + "virtualScroll": "virtual scroll", + "autoRefresh": "Auto Refresh", + "autoRefreshHintOn": "Auto-refresh every {{sec}}s · click to disable", + "autoRefreshHintOff": "Click to enable auto-refresh", + "killTitle": "Terminate Session", + "killConfirm": "Force disconnect user \"{{user}}\"'s current session?", + "killConfirmBtn": "Terminate", + "liveMonitor": "Live Monitor", + "replay": "Replay", + "downloadRecord": "Download Recording", + "tab": { + "active": "Active Sessions", + "history": "History Sessions", + "commands": "Session Commands" + }, + "col": { + "id": "ID", + "user": "User", + "targetServer": "Target Server", + "port": "Port", + "startTime": "Start Time", + "endTime": "End Time", + "duration": "Duration", + "status": "Status", + "actions": "Actions", + "command": "Command", + "outputSize": "Output Size", + "sessionRef": "Session" + }, + "status": { + "active": "Active", + "failed": "Failed", + "closed": "Closed" + } + } } } diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index d1a24b3..8341b09 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -71,7 +71,14 @@ "operationSuccess": "操作成功", "operationFailed": "操作失败", "loadFailed": "加载失败", - "saveFailed": "保存失败" + "saveFailed": "保存失败", + "duration": { + "sec": "{{count}}秒", + "min": "{{count}}分", + "minSec": "{{m}}分{{s}}秒", + "hour": "{{count}}时", + "hourMin": "{{h}}时{{m}}分" + } }, "menu": { "dashboard": "仪表盘", @@ -545,5 +552,107 @@ "templateNotLoaded": "模板未加载", "pleaseFill": "请填写「{{label}}」" } + }, + "audit": { + "operations": { + "searchPlaceholder": "搜索操作日志...", + "allModules": "全部模块", + "allActions": "全部操作", + "noData": "暂无操作日志", + "viewDetail": "查看详情", + "totalLogs": "共 {{count}} 条日志", + "virtualScroll": "虚拟滚动渲染", + "detailTitle": "操作日志详情", + "statsTitle": "审计统计", + "noStats": "暂无统计数据", + "topModules": "热门模块", + "topUsers": "活跃用户", + "col": { + "time": "时间", + "user": "用户", + "module": "模块", + "actionType": "操作类型", + "resource": "资源", + "ip": "IP", + "result": "结果", + "detail": "详情" + }, + "detailCol": { + "userId": "用户ID", + "ipAddress": "IP地址", + "userAgent": "User-Agent", + "message": "详情" + }, + "stats": { + "totalLogs": "总日志数", + "todayLogs": "今日日志", + "successCount": "成功数", + "failureCount": "失败数" + } + }, + "login": { + "searchPlaceholder": "搜索用户/IP/归属地...", + "noData": "暂无登录日志", + "all": "全部", + "col": { + "loginTime": "登录时间", + "user": "用户", + "loginType": "登录方式", + "ip": "IP", + "location": "归属地", + "browser": "浏览器/OS", + "status": "状态" + }, + "type": { + "password": "密码登录", + "ldap": "LDAP", + "token": "Token", + "oauth": "OAuth" + } + }, + "terminal": { + "searchUserServer": "搜索用户/服务器...", + "searchUserServerIp": "搜索用户/服务器/IP...", + "searchUserCommand": "搜索用户/命令...", + "sessionIdPlaceholder": "会话 ID(可选)", + "noActive": "暂无在线会话", + "noHistory": "暂无历史会话", + "noCommands": "暂无会话命令记录", + "totalCommands": "共 {{count}} 条命令", + "virtualScroll": "虚拟滚动渲染", + "autoRefresh": "自动刷新", + "autoRefreshHintOn": "每 {{sec}}s 自动刷新 · 点击关闭", + "autoRefreshHintOff": "点击开启自动刷新", + "killTitle": "阻断会话", + "killConfirm": "确认强制断开用户「{{user}}」的当前会话吗?", + "killConfirmBtn": "阻断", + "liveMonitor": "实时查看", + "replay": "回放", + "downloadRecord": "下载录制", + "tab": { + "active": "在线会话", + "history": "历史会话", + "commands": "会话命令" + }, + "col": { + "id": "ID", + "user": "用户", + "targetServer": "目标服务器", + "port": "端口", + "startTime": "开始时间", + "endTime": "结束时间", + "duration": "时长", + "status": "状态", + "actions": "操作", + "command": "命令", + "outputSize": "输出大小", + "sessionRef": "关联会话" + }, + "status": { + "active": "活动中", + "failed": "失败", + "closed": "已关闭" + } + } } } diff --git a/frontend/src/pages/audit/AuditOperationsTab.tsx b/frontend/src/pages/audit/AuditOperationsTab.tsx index 057f34e..9135edc 100644 --- a/frontend/src/pages/audit/AuditOperationsTab.tsx +++ b/frontend/src/pages/audit/AuditOperationsTab.tsx @@ -1,6 +1,7 @@ // 操作审计 Tab — 增强版审计日志列表,含筛选/导出/统计/清除 // v2: 虚拟滚动渲染,支持万级日志流畅查看 import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -46,6 +47,7 @@ interface AuditStatistics { } export function AuditOperationsTab() { + const { t } = useTranslation(); const _confirm = useConfirm(); // Filters const [keyword, setKeyword] = useState(''); @@ -105,11 +107,11 @@ export function AuditOperationsTab() { const getLogResultBadge = (result: OperationLog['result']) => { return result === 'success' ? ( - 成功 + {t('common.success')} ) : ( - 失败 + {t('common.failed')} ); }; @@ -163,7 +165,7 @@ export function AuditOperationsTab() {
setKeyword(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" @@ -176,7 +178,7 @@ export function AuditOperationsTab() { onChange={(e) => setModuleFilter(e.target.value)} className="h-9 px-3 text-sm rounded-md bg-slate-800/50 border border-slate-700 text-slate-300 focus:outline-none focus:border-blue-500" > - + {modules.map((m) => ( + {actions.map((a) => (
@@ -227,7 +229,7 @@ export function AuditOperationsTab() { ) : filteredLogs.length === 0 ? (
-

暂无操作日志

+

{t('audit.operations.noData')}

) : ( <> @@ -236,14 +238,14 @@ export function AuditOperationsTab() { className="grid gap-2 px-4 py-2 border-b border-slate-800 bg-slate-900 text-xs font-medium text-slate-400 sticky top-0 z-10" style={{ gridTemplateColumns: GRID_COLS }} > - 时间 - 用户 - 模块 - 操作类型 - 资源 - IP - 结果 - 详情 + {t('audit.operations.col.time')} + {t('audit.operations.col.user')} + {t('audit.operations.col.module')} + {t('audit.operations.col.actionType')} + {t('audit.operations.col.resource')} + {t('audit.operations.col.ip')} + {t('audit.operations.col.result')} + {t('audit.operations.col.detail')} {/* 虚拟滚动列表 */} @@ -290,7 +292,7 @@ export function AuditOperationsTab() { size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800" onClick={() => viewDetail(log)} - title="查看详情" + title={t('audit.operations.viewDetail')} > @@ -303,7 +305,7 @@ export function AuditOperationsTab() { {/* 行数统计 */}
- 共 {filteredLogs.length} 条日志 {filteredLogs.length > 200 && '· 虚拟滚动渲染'} + {t('audit.operations.totalLogs', { count: filteredLogs.length })} {filteredLogs.length > 200 && `· ${t('audit.operations.virtualScroll')}`}
)} @@ -330,6 +332,7 @@ export function AuditOperationsTab() { // ========================================================================= function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose: () => void }) { + const { t } = useTranslation(); if (!log) return null; return ( @@ -337,7 +340,7 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
-

操作日志详情

+

{t('audit.operations.detailTitle')}

@@ -382,7 +385,7 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose: className="border-slate-700 text-slate-300 hover:bg-slate-800" onClick={onClose} > - 关闭 + {t('common.close')}
@@ -416,6 +419,7 @@ function StatisticsDialog({ loading: boolean; stats: AuditStatistics | null; }) { + const { t } = useTranslation(); if (!open) return null; return ( @@ -423,7 +427,7 @@ function StatisticsDialog({
-

审计统计

+

{t('audit.operations.statsTitle')}

diff --git a/frontend/src/pages/audit/LoginLogsTab.tsx b/frontend/src/pages/audit/LoginLogsTab.tsx index 96d9bf3..52014bb 100644 --- a/frontend/src/pages/audit/LoginLogsTab.tsx +++ b/frontend/src/pages/audit/LoginLogsTab.tsx @@ -1,5 +1,6 @@ // 登录日志 Tab — 用户登录记录列表(参考 opsx-web-lite) import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -40,17 +41,8 @@ function normalizeLoginLog(raw: JsonData): LoginLog { }; } -function getLoginTypeLabel(type: string) { - switch (type) { - case 'password': return '密码登录'; - case 'ldap': return 'LDAP'; - case 'token': return 'Token'; - case 'oauth': return 'OAuth'; - default: return type || '密码登录'; - } -} - export function LoginLogsTab() { + const { t } = useTranslation(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [statusFilter, setStatusFilter] = useState(''); @@ -74,18 +66,24 @@ export function LoginLogsTab() { fetchLogs(); }, [fetchLogs]); + const getLoginTypeLabel = (type: string) => { + const key = `audit.login.type.${type}`; + const label = t(key); + return label === key ? (type || t('audit.login.type.password')) : label; + }; + const columns: DataTableColumn[] = [ { - key: 'login_at', header: '登录时间', sortable: true, width: 180, + key: 'login_at', header: t('audit.login.col.loginTime'), sortable: true, width: 180, accessor: (r) => r.login_at, render: (r) => {r.login_at ? new Date(r.login_at).toLocaleString() : '—'}, }, { - key: 'username', header: '用户', sortable: true, width: 120, + key: 'username', header: t('audit.login.col.user'), sortable: true, width: 120, render: (r) => {r.username || '—'}, }, { - key: 'login_type', header: '登录方式', width: 100, + key: 'login_type', header: t('audit.login.col.loginType'), width: 100, render: (r) => ( {getLoginTypeLabel(r.login_type)} @@ -93,11 +91,11 @@ export function LoginLogsTab() { ), }, { - key: 'ip', header: 'IP', width: 140, + key: 'ip', header: t('audit.login.col.ip'), width: 140, render: (r) => {r.ip || '—'}, }, { - key: 'location', header: '归属地', width: 140, + key: 'location', header: t('audit.login.col.location'), width: 140, render: (r) => ( @@ -106,7 +104,7 @@ export function LoginLogsTab() { ), }, { - key: 'browser', header: '浏览器/OS', + key: 'browser', header: t('audit.login.col.browser'), render: (r) => ( @@ -119,12 +117,12 @@ export function LoginLogsTab() { ), }, { - key: 'status', header: '状态', sortable: true, width: 80, align: 'center', + key: 'status', header: t('audit.login.col.status'), sortable: true, width: 80, align: 'center', render: (r) => r.status === 'success' ? ( - 成功 + {t('common.success')} ) : ( - {r.fail_reason || '失败'} + {r.fail_reason || t('common.failed')} ), }, @@ -136,9 +134,9 @@ export function LoginLogsTab() {
{[ - { value: '', label: '全部' }, - { value: 'success', label: '成功' }, - { value: 'fail', label: '失败' }, + { value: '', label: t('audit.login.all') }, + { value: 'success', label: t('common.success') }, + { value: 'fail', label: t('common.failed') }, ].map(({ value, label }) => (
@@ -175,7 +173,7 @@ export function LoginLogsTab() { rowKey={(r) => r.id} loading={loading} searchable - searchPlaceholder="搜索用户/IP/归属地..." + searchPlaceholder={t('audit.login.searchPlaceholder')} searchKeys={['username', 'ip', 'location', 'user_agent']} sortable pagination @@ -183,11 +181,11 @@ export function LoginLogsTab() { exportable exportFilename="login-logs.csv" columnToggle - emptyText="暂无登录日志" + emptyText={t('audit.login.noData')} emptyRender={() => (
-

暂无登录日志

+

{t('audit.login.noData')}

)} /> diff --git a/frontend/src/pages/audit/TerminalAuditTab.tsx b/frontend/src/pages/audit/TerminalAuditTab.tsx index b779801..8573601 100644 --- a/frontend/src/pages/audit/TerminalAuditTab.tsx +++ b/frontend/src/pages/audit/TerminalAuditTab.tsx @@ -1,6 +1,7 @@ // 终端审计 Tab — 子标签编排 // Logan.Li import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { Activity, Monitor, Search } from 'lucide-react'; import { ActiveSessionsPanel } from './terminal-audit/ActiveSessionsPanel'; @@ -8,34 +9,39 @@ import { HistorySessionsPanel } from './terminal-audit/HistorySessionsPanel'; import { SessionCommandsPanel } from './terminal-audit/SessionCommandsPanel'; import type { SubTabId } from './terminal-audit/types'; -const subTabs: { id: SubTabId; name: string; icon: React.ElementType }[] = [ - { id: 'active', name: '在线会话', icon: Activity }, - { id: 'history', name: '历史会话', icon: Monitor }, - { id: 'commands', name: '会话命令', icon: Search }, -]; +const subTabIds: SubTabId[] = ['active', 'history', 'commands']; +const subTabIcons: Record = { + active: Activity, + history: Monitor, + commands: Search, +}; export function TerminalAuditTab() { + const { t } = useTranslation(); const [subTab, setSubTab] = useState('active'); const [userToggledOff, setUserToggledOff] = useState(false); return (
- {subTabs.map((tab) => ( - - ))} + {subTabIds.map((id) => { + const Icon = subTabIcons[id]; + return ( + + ); + })}
{subTab === 'active' && ( @@ -48,4 +54,4 @@ export function TerminalAuditTab() { {subTab === 'commands' && }
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/audit/terminal-audit/ActiveSessionsPanel.tsx b/frontend/src/pages/audit/terminal-audit/ActiveSessionsPanel.tsx index 36605d6..9f12009 100644 --- a/frontend/src/pages/audit/terminal-audit/ActiveSessionsPanel.tsx +++ b/frontend/src/pages/audit/terminal-audit/ActiveSessionsPanel.tsx @@ -1,6 +1,7 @@ // 终端审计 — 在线会话子面板 // Logan.Li import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -48,6 +49,7 @@ function ActiveSessionRow({ onKill: (id: number, username: string) => void; onMonitor: (id: number) => void; }) { + const { t } = useTranslation(); const elapsed = useElapsedTimer(session.start_time); return ( @@ -83,7 +85,7 @@ function ActiveSessionRow({ size="icon" className="h-8 w-8 text-slate-400 hover:text-red-400 hover:bg-slate-800" onClick={() => onKill(session.id, session.username)} - title="阻断会话" + title={t('audit.terminal.killTitle')} > @@ -92,7 +94,7 @@ function ActiveSessionRow({ size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800" onClick={() => onMonitor(session.id)} - title="实时查看" + title={t('audit.terminal.liveMonitor')} > @@ -103,6 +105,7 @@ function ActiveSessionRow({ } export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: ActiveSessionsPanelProps) { + const { t } = useTranslation(); const confirm = useConfirm(); const { killMutation } = useTerminalSessionMutations(); const { data: sessions = [], isLoading, refetch } = useTerminalSessions({ @@ -139,9 +142,9 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active const handleKill = async (id: number, username: string) => { const ok = await confirm({ - title: '阻断会话', - description: `确认强制断开用户「${username}」的当前会话吗?`, - confirmText: '阻断', + title: t('audit.terminal.killTitle'), + description: t('audit.terminal.killConfirm', { user: username }), + confirmText: t('audit.terminal.killConfirmBtn'), variant: 'destructive', }); if (!ok) return; @@ -163,7 +166,7 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
setKeyword(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" @@ -175,7 +178,7 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active onClick={() => refetch()} > - 刷新 + {t('common.refresh')} @@ -214,20 +217,20 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active ) : filtered.length === 0 ? (
-

暂无在线会话

+

{t('audit.terminal.noActive')}

) : ( - ID - 用户 - 目标服务器 - 端口 - 开始时间 - 时长 - 状态 - 操作 + {t('audit.terminal.col.id')} + {t('audit.terminal.col.user')} + {t('audit.terminal.col.targetServer')} + {t('audit.terminal.col.port')} + {t('audit.terminal.col.startTime')} + {t('audit.terminal.col.duration')} + {t('audit.terminal.col.status')} + {t('audit.terminal.col.actions')} @@ -244,4 +247,4 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/audit/terminal-audit/HistorySessionsPanel.tsx b/frontend/src/pages/audit/terminal-audit/HistorySessionsPanel.tsx index a11e940..c2f28eb 100644 --- a/frontend/src/pages/audit/terminal-audit/HistorySessionsPanel.tsx +++ b/frontend/src/pages/audit/terminal-audit/HistorySessionsPanel.tsx @@ -1,6 +1,7 @@ // 终端审计 — 历史会话子面板 // Logan.Li import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -19,6 +20,7 @@ import { useTerminalSessions } from '@/hooks/api'; import { formatDuration, getStatusBadge } from './helpers'; export function HistorySessionsPanel() { + const { t } = useTranslation(); const { data: sessions = [], isLoading, refetch } = useTerminalSessions({ page: 1, page_size: 100, @@ -58,7 +60,7 @@ export function HistorySessionsPanel() {
setKeyword(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" @@ -70,7 +72,7 @@ export function HistorySessionsPanel() { onClick={() => refetch()} > - 刷新 + {t('common.refresh')}
@@ -84,21 +86,21 @@ export function HistorySessionsPanel() { ) : filtered.length === 0 ? (
-

暂无历史会话

+

{t('audit.terminal.noHistory')}

) : (
- ID - 用户 - 目标服务器 - 端口 - 开始时间 - 结束时间 - 时长 - 状态 - 操作 + {t('audit.terminal.col.id')} + {t('audit.terminal.col.user')} + {t('audit.terminal.col.targetServer')} + {t('audit.terminal.col.port')} + {t('audit.terminal.col.startTime')} + {t('audit.terminal.col.endTime')} + {t('audit.terminal.col.duration')} + {t('audit.terminal.col.status')} + {t('audit.terminal.col.actions')} @@ -140,7 +142,7 @@ export function HistorySessionsPanel() { size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800" onClick={() => openReplay(s.id)} - title="回放" + title={t('audit.terminal.replay')} > @@ -149,7 +151,7 @@ export function HistorySessionsPanel() { size="icon" className="h-8 w-8 text-slate-400 hover:text-green-400 hover:bg-slate-800" onClick={() => handleDownload(s.id)} - title="下载录制" + title={t('audit.terminal.downloadRecord')} > @@ -167,4 +169,4 @@ export function HistorySessionsPanel() { ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/audit/terminal-audit/SessionCommandsPanel.tsx b/frontend/src/pages/audit/terminal-audit/SessionCommandsPanel.tsx index 59bf5f3..1412bac 100644 --- a/frontend/src/pages/audit/terminal-audit/SessionCommandsPanel.tsx +++ b/frontend/src/pages/audit/terminal-audit/SessionCommandsPanel.tsx @@ -2,6 +2,7 @@ // v2: 虚拟滚动,支持万级命令记录流畅查看 import { useMemo, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -13,6 +14,7 @@ import { formatSize } from './helpers'; const GRID_COLS = 'minmax(150px,1fr) minmax(80px,0.8fr) minmax(200px,2fr) minmax(100px,0.8fr) minmax(80px,0.5fr)'; export function SessionCommandsPanel() { + const { t } = useTranslation(); const [keyword, setKeyword] = useState(''); const [sessionIdInput, setSessionIdInput] = useState(''); @@ -53,14 +55,14 @@ export function SessionCommandsPanel() {
setKeyword(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" />
setSessionIdInput(e.target.value)} className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" @@ -71,7 +73,7 @@ export function SessionCommandsPanel() { onClick={() => refetch()} > - 刷新 + {t('common.refresh')} @@ -85,7 +87,7 @@ export function SessionCommandsPanel() { ) : filtered.length === 0 ? (
-

暂无会话命令记录

+

{t('audit.terminal.noCommands')}

) : ( <> @@ -94,11 +96,11 @@ export function SessionCommandsPanel() { className="grid gap-2 px-4 py-2 border-b border-slate-800 bg-slate-900 text-xs font-medium text-slate-400" style={{ gridTemplateColumns: GRID_COLS }} > - 时间 - 用户 - 命令 - 输出大小 - 关联会话 + {t('audit.terminal.col.startTime')} + {t('audit.terminal.col.user')} + {t('audit.terminal.col.command')} + {t('audit.terminal.col.outputSize')} + {t('audit.terminal.col.sessionRef')} {/* 虚拟滚动列表 */} @@ -141,7 +143,7 @@ export function SessionCommandsPanel() { {/* 行数统计 */}
- 共 {filtered.length} 条命令 {filtered.length > 200 && '· 虚拟滚动渲染'} + {t('audit.terminal.totalCommands', { count: filtered.length })} {filtered.length > 200 && `· ${t('audit.terminal.virtualScroll')}`}
)} diff --git a/frontend/src/pages/audit/terminal-audit/helpers.tsx b/frontend/src/pages/audit/terminal-audit/helpers.tsx index 4a08228..72550de 100644 --- a/frontend/src/pages/audit/terminal-audit/helpers.tsx +++ b/frontend/src/pages/audit/terminal-audit/helpers.tsx @@ -1,16 +1,17 @@ // 终端审计 — 展示辅助 // Logan.Li import { Badge } from '@/components/ui/badge'; +import i18n from '@/i18n'; export function formatDuration(sec: number): string { if (!sec || sec < 0) return '--'; - if (sec < 60) return `${sec}秒`; + if (sec < 60) return i18n.t('common.duration.sec', { count: sec }); const m = Math.floor(sec / 60); const s = sec % 60; - if (m < 60) return `${m}分${s > 0 ? `${s}秒` : ''}`; + if (m < 60) return s > 0 ? i18n.t('common.duration.minSec', { m, s }) : i18n.t('common.duration.min', { count: m }); const h = Math.floor(m / 60); const rm = m % 60; - return `${h}时${rm > 0 ? `${rm}分` : ''}`; + return rm > 0 ? i18n.t('common.duration.hourMin', { h, m: rm }) : i18n.t('common.duration.hour', { count: h }); } export function formatSize(bytes: number): string { @@ -25,20 +26,20 @@ export function getStatusBadge(status: string) { return ( - 活动中 + {i18n.t('audit.terminal.status.active')} ); } if (status === 'failed') { return ( - 失败 + {i18n.t('audit.terminal.status.failed')} ); } return ( - 已关闭 + {i18n.t('audit.terminal.status.closed')} ); -} \ No newline at end of file +}