feat(admin-portal): 完成参考项目差距闭环 - 4 批次补齐 + Vitest 测试
差距分析闭环(workline §7-§8): - P0:审计子模块 3 页 + 学校组织 7 页 - P1:系统设置 4 Card + 公告管理 + 邀请码管理 - P2:文件管理 + AI 配置 - P6:5 测试文件 / 69 用例全过 共享基础设施: - permissions +11 权限点 +14 路由 - view-models +20 接口 - i18n +100 key - graphql-client +25 operation - fixtures/handlers +15 mock +20 handler 质量:typecheck + lint 零错误,vitest 69/69 通过,arch.db 已更新
This commit is contained in:
199
apps/admin-portal/src/app/admin/audit-logs/overview/page.tsx
Normal file
199
apps/admin-portal/src/app/admin/audit-logs/overview/page.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import {
|
||||
useAuditOverviewStats,
|
||||
useAuditTrend,
|
||||
} from "@/hooks/use-audit-overview";
|
||||
import { AuditLogsTabs } from "@/components/audit-logs-tabs";
|
||||
import {
|
||||
PageHeader,
|
||||
PaperCard,
|
||||
LoadingState,
|
||||
ErrorState,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@/components/ui";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
function StatCard({ label, value }: StatCardProps): ReactNode {
|
||||
return (
|
||||
<PaperCard className="p-4">
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--color-ink-muted)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
fontSize: 28,
|
||||
color: "var(--color-ink)",
|
||||
margin: "8px 0 0 0",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</PaperCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuditOverviewPage(): ReactNode {
|
||||
const { data: stats, loading, error } = useAuditOverviewStats();
|
||||
const { data: trend } = useAuditTrend(7);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 1200, margin: "0 auto" }}>
|
||||
<PageHeader
|
||||
title={t("admin.auditLogs.overview")}
|
||||
description="审计事件统计与趋势"
|
||||
/>
|
||||
|
||||
<AuditLogsTabs />
|
||||
|
||||
{loading && <LoadingState />}
|
||||
{error && <ErrorState message={error.message} />}
|
||||
{stats && (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label={t("admin.auditLogs.totalEvents")}
|
||||
value={stats.totalEvents}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("admin.auditLogs.todayEvents")}
|
||||
value={stats.todayEvents}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("admin.auditLogs.uniqueActors")}
|
||||
value={stats.uniqueActors}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("admin.auditLogs.failedActions")}
|
||||
value={stats.failedActions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 热门操作 & 热门资源 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<PaperCard className="p-4">
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
fontSize: 16,
|
||||
color: "var(--color-ink)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{t("admin.auditLogs.topActions")}
|
||||
</h3>
|
||||
{stats.topActions.length === 0 ? (
|
||||
<EmptyState message={t("admin.common.empty")} />
|
||||
) : (
|
||||
<Table headers={["操作", "次数"]}>
|
||||
{stats.topActions.map((item) => (
|
||||
<TableRow key={item.action}>
|
||||
<TableCell
|
||||
style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
|
||||
>
|
||||
{item.action}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: "right" }}>
|
||||
{item.count}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</PaperCard>
|
||||
|
||||
<PaperCard className="p-4">
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
fontSize: 16,
|
||||
color: "var(--color-ink)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{t("admin.auditLogs.topResources")}
|
||||
</h3>
|
||||
{stats.topResources.length === 0 ? (
|
||||
<EmptyState message={t("admin.common.empty")} />
|
||||
) : (
|
||||
<Table headers={["资源", "次数"]}>
|
||||
{stats.topResources.map((item) => (
|
||||
<TableRow key={item.resource}>
|
||||
<TableCell
|
||||
style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
|
||||
>
|
||||
{item.resource}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: "right" }}>
|
||||
{item.count}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</PaperCard>
|
||||
</div>
|
||||
|
||||
{/* 7天趋势 */}
|
||||
<PaperCard className="p-4">
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
fontSize: 16,
|
||||
color: "var(--color-ink)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{t("admin.auditLogs.trend")}
|
||||
</h3>
|
||||
{!trend || trend.length === 0 ? (
|
||||
<EmptyState message={t("admin.common.empty")} />
|
||||
) : (
|
||||
<Table
|
||||
headers={[
|
||||
t("admin.auditLogs.time"),
|
||||
t("admin.auditLogs.action"),
|
||||
t("admin.common.total"),
|
||||
]}
|
||||
>
|
||||
{trend.map((point) => (
|
||||
<TableRow key={`${point.date}-${point.action}`}>
|
||||
<TableCell style={{ whiteSpace: "nowrap" }}>
|
||||
{point.date}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
|
||||
>
|
||||
{point.action}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: "right" }}>
|
||||
{point.count}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</PaperCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user