feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
1 parent 1acb0c49af
commit e363ded445
213 files changed
+7148 -6818

No files matched your search

+108 -108
View File
@@ -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>
);