feat(i18n): 安全审计模块全量国际化 — 7 文件硬编码中文改翻译键 + audit 命名空间(89 键)
迁移文件: - 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 测试通过
This commit is contained in:
1 parent
fef4997391
commit
186ece6495
10 files changed
+375
-141
No files matched your search
@@ -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')}
|
||||
>
|
||||
<XOctagon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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')}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索用户/服务器..."
|
||||
placeholder={t('audit.terminal.searchUserServer')}
|
||||
value={keyword}
|
||||
onChange={(e) => 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()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={autoRefresh ? 'default' : 'outline'}
|
||||
@@ -188,7 +191,7 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
|
||||
if (autoRefresh) onUserToggledOff();
|
||||
setAutoRefresh(!autoRefresh);
|
||||
}}
|
||||
title={autoRefresh ? `每 ${REFRESH_SEC}s 自动刷新 · 点击关闭` : '点击开启自动刷新'}
|
||||
title={autoRefresh ? t('audit.terminal.autoRefreshHintOn', { sec: REFRESH_SEC }) : t('audit.terminal.autoRefreshHintOff')}
|
||||
>
|
||||
{autoRefresh ? (
|
||||
<>
|
||||
@@ -198,7 +201,7 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
|
||||
) : (
|
||||
<>
|
||||
<TimerOff className="h-4 w-4 mr-2" />
|
||||
自动刷新
|
||||
{t('audit.terminal.autoRefresh')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -214,20 +217,20 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<Activity className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无在线会话</p>
|
||||
<p className="text-sm">{t('audit.terminal.noActive')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">ID</TableHead>
|
||||
<TableHead className="text-slate-400">用户</TableHead>
|
||||
<TableHead className="text-slate-400">目标服务器</TableHead>
|
||||
<TableHead className="text-slate-400">端口</TableHead>
|
||||
<TableHead className="text-slate-400">开始时间</TableHead>
|
||||
<TableHead className="text-slate-400">时长</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">操作</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.id')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.user')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.targetServer')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.port')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.startTime')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.duration')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.status')}</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">{t('audit.terminal.col.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -244,4 +247,4 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
|
||||
<LiveMonitorDialog open={monitorOpen} onOpenChange={setMonitorOpen} sessionId={monitorSessionId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索用户/服务器/IP..."
|
||||
placeholder={t('audit.terminal.searchUserServerIp')}
|
||||
value={keyword}
|
||||
onChange={(e) => 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()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -84,21 +86,21 @@ export function HistorySessionsPanel() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<Monitor className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无历史会话</p>
|
||||
<p className="text-sm">{t('audit.terminal.noHistory')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">ID</TableHead>
|
||||
<TableHead className="text-slate-400">用户</TableHead>
|
||||
<TableHead className="text-slate-400">目标服务器</TableHead>
|
||||
<TableHead className="text-slate-400">端口</TableHead>
|
||||
<TableHead className="text-slate-400">开始时间</TableHead>
|
||||
<TableHead className="text-slate-400">结束时间</TableHead>
|
||||
<TableHead className="text-slate-400">时长</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">操作</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.id')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.user')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.targetServer')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.port')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.startTime')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.endTime')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.duration')}</TableHead>
|
||||
<TableHead className="text-slate-400">{t('audit.terminal.col.status')}</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">{t('audit.terminal.col.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -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')}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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')}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -167,4 +169,4 @@ export function HistorySessionsPanel() {
|
||||
<ReplayDialog open={replayOpen} onOpenChange={setReplayOpen} sessionId={replayId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索用户/命令..."
|
||||
placeholder={t('audit.terminal.searchUserCommand')}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="会话 ID(可选)"
|
||||
placeholder={t('audit.terminal.sessionIdPlaceholder')}
|
||||
value={sessionIdInput}
|
||||
onChange={(e) => 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()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +87,7 @@ export function SessionCommandsPanel() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无会话命令记录</p>
|
||||
<p className="text-sm">{t('audit.terminal.noCommands')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -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 }}
|
||||
>
|
||||
<span>时间</span>
|
||||
<span>用户</span>
|
||||
<span>命令</span>
|
||||
<span>输出大小</span>
|
||||
<span>关联会话</span>
|
||||
<span>{t('audit.terminal.col.startTime')}</span>
|
||||
<span>{t('audit.terminal.col.user')}</span>
|
||||
<span>{t('audit.terminal.col.command')}</span>
|
||||
<span>{t('audit.terminal.col.outputSize')}</span>
|
||||
<span>{t('audit.terminal.col.sessionRef')}</span>
|
||||
</div>
|
||||
|
||||
{/* 虚拟滚动列表 */}
|
||||
@@ -141,7 +143,7 @@ export function SessionCommandsPanel() {
|
||||
|
||||
{/* 行数统计 */}
|
||||
<div className="text-center py-2 text-xs text-slate-500 border-t border-slate-800">
|
||||
共 {filtered.length} 条命令 {filtered.length > 200 && '· 虚拟滚动渲染'}
|
||||
{t('audit.terminal.totalCommands', { count: filtered.length })} {filtered.length > 200 && `· ${t('audit.terminal.virtualScroll')}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mr-1.5 inline-block animate-pulse" />
|
||||
活动中
|
||||
{i18n.t('audit.terminal.status.active')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
失败
|
||||
{i18n.t('audit.terminal.status.failed')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
|
||||
已关闭
|
||||
{i18n.t('audit.terminal.status.closed')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user