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 @@
|
||||
// 操作审计 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' ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
成功
|
||||
{t('common.success')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
失败
|
||||
{t('common.failed')}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -163,7 +165,7 @@ export function AuditOperationsTab() {
|
||||
<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.operations.searchPlaceholder')}
|
||||
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"
|
||||
@@ -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"
|
||||
>
|
||||
<option value="">全部模块</option>
|
||||
<option value="">{t('audit.operations.allModules')}</option>
|
||||
{modules.map((m) => (
|
||||
<option key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
@@ -190,7 +192,7 @@ export function AuditOperationsTab() {
|
||||
onChange={(e) => setActionFilter(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"
|
||||
>
|
||||
<option value="">全部操作</option>
|
||||
<option value="">{t('audit.operations.allActions')}</option>
|
||||
{actions.map((a) => (
|
||||
<option key={a.value} value={a.value}>
|
||||
{a.label}
|
||||
@@ -204,7 +206,7 @@ export function AuditOperationsTab() {
|
||||
onClick={() => refetchLogs()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -212,7 +214,7 @@ export function AuditOperationsTab() {
|
||||
onClick={handleExport}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
{t('common.export')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +229,7 @@ export function AuditOperationsTab() {
|
||||
) : filteredLogs.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.operations.noData')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -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 }}
|
||||
>
|
||||
<span>时间</span>
|
||||
<span>用户</span>
|
||||
<span>模块</span>
|
||||
<span>操作类型</span>
|
||||
<span>资源</span>
|
||||
<span>IP</span>
|
||||
<span>结果</span>
|
||||
<span className="text-right">详情</span>
|
||||
<span>{t('audit.operations.col.time')}</span>
|
||||
<span>{t('audit.operations.col.user')}</span>
|
||||
<span>{t('audit.operations.col.module')}</span>
|
||||
<span>{t('audit.operations.col.actionType')}</span>
|
||||
<span>{t('audit.operations.col.resource')}</span>
|
||||
<span>{t('audit.operations.col.ip')}</span>
|
||||
<span>{t('audit.operations.col.result')}</span>
|
||||
<span className="text-right">{t('audit.operations.col.detail')}</span>
|
||||
</div>
|
||||
|
||||
{/* 虚拟滚动列表 */}
|
||||
@@ -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')}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -303,7 +305,7 @@ export function AuditOperationsTab() {
|
||||
|
||||
{/* 行数统计 */}
|
||||
<div className="text-center py-2 text-xs text-slate-500 border-t border-slate-800">
|
||||
共 {filteredLogs.length} 条日志 {filteredLogs.length > 200 && '· 虚拟滚动渲染'}
|
||||
{t('audit.operations.totalLogs', { count: filteredLogs.length })} {filteredLogs.length > 200 && `· ${t('audit.operations.virtualScroll')}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -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:
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative bg-slate-900 border border-slate-800 rounded-lg p-6 max-w-lg w-full mx-4 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-100">操作日志详情</h3>
|
||||
<h3 className="text-lg font-semibold text-slate-100">{t('audit.operations.detailTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -350,16 +353,16 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<DetailRow label="ID" value={log.id} />
|
||||
<DetailRow label="时间" value={log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'} />
|
||||
<DetailRow label="用户" value={log.username || '--'} />
|
||||
<DetailRow label="用户ID" value={log.userId || '--'} />
|
||||
<DetailRow label="模块" value={log.module || '--'} />
|
||||
<DetailRow label="操作类型" value={log.action || '--'} />
|
||||
<DetailRow label="资源" value={log.resource || '--'} />
|
||||
<DetailRow label="IP地址" value={log.ip || '--'} />
|
||||
<DetailRow label="User-Agent" value={log.userAgent || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.time')} value={log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'} />
|
||||
<DetailRow label={t('audit.operations.col.user')} value={log.username || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userId')} value={log.userId || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.module')} value={log.module || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.actionType')} value={log.action || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.resource')} value={log.resource || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.ipAddress')} value={log.ip || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userAgent')} value={log.userAgent || '--'} />
|
||||
<DetailRow
|
||||
label="结果"
|
||||
label={t('audit.operations.col.result')}
|
||||
value={
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -369,11 +372,11 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
: 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
}
|
||||
>
|
||||
{log.result === 'success' ? '成功' : '失败'}
|
||||
{log.result === 'success' ? t('common.success') : t('common.failed')}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
{log.message && <DetailRow label="详情" value={log.message} mono />}
|
||||
{log.message && <DetailRow label={t('audit.operations.detailCol.message')} value={log.message} mono />}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
@@ -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')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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({
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative bg-slate-900 border border-slate-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-100">审计统计</h3>
|
||||
<h3 className="text-lg font-semibold text-slate-100">{t('audit.operations.statsTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -441,15 +445,15 @@ function StatisticsDialog({
|
||||
) : stats ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<StatCard label="总日志数" value={String(stats.total_logs ?? '--')} color="blue" />
|
||||
<StatCard label="今日日志" value={String(stats.today_count ?? '--')} color="emerald" />
|
||||
<StatCard label="成功数" value={String(stats.success_count ?? '--')} color="emerald" />
|
||||
<StatCard label="失败数" value={String(stats.failure_count ?? '--')} color="red" />
|
||||
<StatCard label={t('audit.operations.stats.totalLogs')} value={String(stats.total_logs ?? '--')} color="blue" />
|
||||
<StatCard label={t('audit.operations.stats.todayLogs')} value={String(stats.today_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.successCount')} value={String(stats.success_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.failureCount')} value={String(stats.failure_count ?? '--')} color="red" />
|
||||
</div>
|
||||
|
||||
{stats.top_modules && stats.top_modules.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">热门模块</h4>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">{t('audit.operations.topModules')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_modules.slice(0, 5).map((m, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
@@ -463,7 +467,7 @@ function StatisticsDialog({
|
||||
|
||||
{stats.top_users && stats.top_users.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">活跃用户</h4>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">{t('audit.operations.topUsers')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_users.slice(0, 5).map((u, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
@@ -476,7 +480,7 @@ function StatisticsDialog({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10 text-sm">暂无统计数据</p>
|
||||
<p className="text-slate-500 text-center py-10 text-sm">{t('audit.operations.noStats')}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
@@ -485,7 +489,7 @@ function StatisticsDialog({
|
||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user