feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -13,7 +13,12 @@ import {
|
||||
Download,
|
||||
Eye,
|
||||
X,
|
||||
ClipboardList,
|
||||
} from 'lucide-react';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import { auditApi } from '@/api';
|
||||
import { useAuditLogs } from '@/hooks/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
@@ -106,11 +111,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">
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
{t('common.success')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
{t('common.failed')}
|
||||
</Badge>
|
||||
);
|
||||
@@ -158,84 +163,80 @@ export function AuditOperationsTab() {
|
||||
const virtualRows = virtualizer.getVirtualItems();
|
||||
const totalHeight = virtualizer.getTotalSize();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<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={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"
|
||||
/>
|
||||
</div>
|
||||
const successCount = filteredLogs.filter(l => l.result === 'success').length;
|
||||
const failCount = filteredLogs.filter(l => l.result === 'failure').length;
|
||||
|
||||
{/* Module filter */}
|
||||
<select
|
||||
value={moduleFilter}
|
||||
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="">{t('audit.operations.allModules')}</option>
|
||||
{modules.map((m) => (
|
||||
<option key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Action filter */}
|
||||
<select
|
||||
value={actionFilter}
|
||||
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="">{t('audit.operations.allActions')}</option>
|
||||
{actions.map((a) => (
|
||||
<option key={a.value} value={a.value}>
|
||||
{a.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
onClick={() => refetchLogs()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
onClick={handleExport}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{t('common.export')}
|
||||
</Button>
|
||||
const toolbar = (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder={t('audit.operations.searchPlaceholder')}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="pl-9 h-9 dg-input"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={moduleFilter}
|
||||
onChange={(e) => setModuleFilter(e.target.value)}
|
||||
className="h-9 px-3 text-sm rounded-md dg-input"
|
||||
>
|
||||
<option value="">{t('audit.operations.allModules')}</option>
|
||||
{modules.map((m) => (
|
||||
<option key={m.value} value={m.value}>{m.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
className="h-9 px-3 text-sm rounded-md dg-input"
|
||||
>
|
||||
<option value="">{t('audit.operations.allActions')}</option>
|
||||
{actions.map((a) => (
|
||||
<option key={a.value} value={a.value}>{a.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Logs Table — 虚拟滚动 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : 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">{t('audit.operations.noData')}</p>
|
||||
</div>
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title={t('audit.operations.title', { defaultValue: '操作审计' })}
|
||||
subtitle={t('audit.operations.subtitle', { defaultValue: '系统操作日志筛选、导出与详情查看' })}
|
||||
icon={<ClipboardList className="h-5 w-5" />}
|
||||
loading={loading}
|
||||
metrics={(
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: t('audit.operations.allModules'), value: filteredLogs.length, color: 'blue' },
|
||||
{ label: t('common.success'), value: successCount, color: 'emerald' },
|
||||
{ label: t('common.failed'), value: failCount, color: 'red' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
toolbar={toolbar}
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={() => refetchLogs()}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={handleExport}>
|
||||
<Download className="h-4 w-4 mr-2" />{t('common.export')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<ContentPanel noPadding>
|
||||
<div>
|
||||
{!loading && filteredLogs.length === 0 ? (
|
||||
<PageEmpty icon={<Search className="h-8 w-8" />} title={t('audit.operations.noData')} />
|
||||
) : (
|
||||
<>
|
||||
{/* 表头 */}
|
||||
<div
|
||||
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"
|
||||
className="grid gap-2 px-4 py-2 border-b border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-xs font-medium dg-text-secondary sticky top-0 z-10"
|
||||
style={{ gridTemplateColumns: GRID_COLS }}
|
||||
>
|
||||
<span>{t('audit.operations.col.time')}</span>
|
||||
@@ -269,28 +270,28 @@ export function AuditOperationsTab() {
|
||||
gridTemplateColumns: GRID_COLS,
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
className="px-4 py-2.5 border-b border-slate-800 hover:bg-slate-800/50"
|
||||
className="px-4 py-2.5 border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]"
|
||||
>
|
||||
<span className="text-sm text-slate-400 truncate">
|
||||
<span className="text-sm dg-text-secondary truncate">
|
||||
{log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'}
|
||||
</span>
|
||||
<span className="text-sm text-slate-200 truncate">{log.username || '--'}</span>
|
||||
<span className="text-sm text-slate-300 truncate">{log.module || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)] truncate">{log.username || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)] truncate">{log.module || '--'}</span>
|
||||
<div>
|
||||
<Badge variant="outline" className="border-blue-500/30 text-blue-400 bg-blue-500/10">
|
||||
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10">
|
||||
{log.action || '--'}
|
||||
</Badge>
|
||||
</div>
|
||||
<code className="text-xs text-slate-400 bg-slate-800 px-2 py-0.5 rounded truncate">
|
||||
<code className="text-xs dg-text-secondary bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded truncate">
|
||||
{log.resource || '--'}
|
||||
</code>
|
||||
<span className="text-sm text-slate-400 truncate">{log.ip || '--'}</span>
|
||||
<span className="text-sm dg-text-secondary truncate">{log.ip || '--'}</span>
|
||||
<div>{getLogResultBadge(log.result)}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800"
|
||||
className="h-8 w-8 dg-text-secondary hover:dg-text-blue hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => viewDetail(log)}
|
||||
title={t('audit.operations.viewDetail')}
|
||||
>
|
||||
@@ -304,14 +305,13 @@ export function AuditOperationsTab() {
|
||||
</div>
|
||||
|
||||
{/* 行数统计 */}
|
||||
<div className="text-center py-2 text-xs text-slate-500 border-t border-slate-800">
|
||||
<div className="text-center py-2 text-xs dg-text-muted border-t border-[var(--dg-border)]">
|
||||
{t('audit.operations.totalLogs', { count: filteredLogs.length })} {filteredLogs.length > 200 && `· ${t('audit.operations.virtualScroll')}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ContentPanel>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<LogDetailDialog log={detailLog} onClose={() => setDetailLog(null)} />
|
||||
@@ -323,7 +323,7 @@ export function AuditOperationsTab() {
|
||||
loading={statsLoading}
|
||||
stats={statistics}
|
||||
/>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,13 +338,13 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<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="relative dg-card 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">{t('audit.operations.detailTitle')}</h3>
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.detailTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-slate-400 hover:text-slate-100"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -368,8 +368,8 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
variant="outline"
|
||||
className={
|
||||
log.result === 'success'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
? 'border-emerald-500/30 dg-text-green bg-emerald-500/10'
|
||||
: 'border-red-500/30 dg-text-red bg-red-500/10'
|
||||
}
|
||||
>
|
||||
{log.result === 'success' ? t('common.success') : t('common.failed')}
|
||||
@@ -382,7 +382,7 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
@@ -396,8 +396,8 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
function DetailRow({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-slate-500 w-20 flex-shrink-0">{label}</span>
|
||||
<span className={mono ? 'text-slate-300 font-mono text-xs break-all' : 'text-slate-200 break-all'}>
|
||||
<span className="dg-text-muted w-20 flex-shrink-0">{label}</span>
|
||||
<span className={mono ? 'text-[var(--dg-text-secondary)] font-mono text-xs break-all' : 'text-[var(--dg-text-primary)] break-all'}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -425,13 +425,13 @@ function StatisticsDialog({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<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="relative dg-card 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">{t('audit.operations.statsTitle')}</h3>
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.statsTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-slate-400 hover:text-slate-100"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -453,12 +453,12 @@ function StatisticsDialog({
|
||||
|
||||
{stats.top_modules && stats.top_modules.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">{t('audit.operations.topModules')}</h4>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary 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">
|
||||
<span className="text-slate-300">{m.module}</span>
|
||||
<span className="text-slate-500">{m.count}</span>
|
||||
<span className="text-[var(--dg-text-secondary)]">{m.module}</span>
|
||||
<span className="dg-text-muted">{m.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -467,12 +467,12 @@ function StatisticsDialog({
|
||||
|
||||
{stats.top_users && stats.top_users.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-400 mb-2">{t('audit.operations.topUsers')}</h4>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary 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">
|
||||
<span className="text-slate-300">{u.username}</span>
|
||||
<span className="text-slate-500">{u.count}</span>
|
||||
<span className="text-[var(--dg-text-secondary)]">{u.username}</span>
|
||||
<span className="dg-text-muted">{u.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -480,13 +480,13 @@ function StatisticsDialog({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10 text-sm">{t('audit.operations.noStats')}</p>
|
||||
<p className="dg-text-muted text-center py-10 text-sm">{t('audit.operations.noStats')}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
@@ -499,13 +499,13 @@ function StatisticsDialog({
|
||||
|
||||
function StatCard({ label, value, color }: { label: string; value: string; color: 'blue' | 'emerald' | 'red' }) {
|
||||
const colorMap = {
|
||||
blue: 'border-blue-500/20 bg-blue-500/5 text-blue-400',
|
||||
emerald: 'border-emerald-500/20 bg-emerald-500/5 text-emerald-400',
|
||||
red: 'border-red-500/20 bg-red-500/5 text-red-400',
|
||||
blue: 'border-blue-500/20 bg-blue-500/5 dg-text-blue',
|
||||
emerald: 'border-emerald-500/20 bg-emerald-500/5 dg-text-green',
|
||||
red: 'border-red-500/20 bg-red-500/5 dg-text-red',
|
||||
};
|
||||
return (
|
||||
<div className={`rounded-lg border p-3 ${colorMap[color]}`}>
|
||||
<div className="text-xs text-slate-500">{label}</div>
|
||||
<div className="text-xs dg-text-muted">{label}</div>
|
||||
<div className="text-xl font-bold mt-0.5">{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user