feat: wire auth and CRUD UI to APIs
This commit is contained in:
176
modules/settings/components/SettingsOverview.tsx
Normal file
176
modules/settings/components/SettingsOverview.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { AuditLog, PublicAccount, RoleDefinition } from "@/modules/core/types";
|
||||
import { ROLE_LABELS } from "@/modules/core/types";
|
||||
|
||||
type SettingsOverviewProps = {
|
||||
accounts: PublicAccount[];
|
||||
roles: RoleDefinition[];
|
||||
auditLogs: AuditLog[];
|
||||
};
|
||||
|
||||
export function SettingsOverview({
|
||||
accounts,
|
||||
roles,
|
||||
auditLogs,
|
||||
}: SettingsOverviewProps): React.ReactElement {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
||||
<section className="border-b pb-5">
|
||||
<Badge variant="success">RBAC + Audit</Badge>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-normal">权限设置</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">内置角色、账号状态和关键操作审计。</p>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>系统账号</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{accounts.length}</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">服务端持久化账号</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>角色数量</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{roles.length}</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">内置权限组</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>审计日志</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-semibold">{auditLogs.length}</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">最近 100 条</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>内置角色</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{roles.map((role) => (
|
||||
<div key={role.id} className="rounded-md border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="font-medium">{role.label}</p>
|
||||
<Badge variant="secondary">{role.permissions.length} 权限</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{role.description}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{role.permissions.map((permission) => (
|
||||
<Badge key={permission} variant="outline">
|
||||
{permission}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>账号列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[620px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">账号</th>
|
||||
<th className="px-4 py-3 font-medium">角色</th>
|
||||
<th className="px-4 py-3 font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-right font-medium">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
{accounts.map((account) => (
|
||||
<tr key={account.id}>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium">{account.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{account.email}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">{ROLE_LABELS[account.role]}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={account.status === "active" ? "success" : "danger"}>
|
||||
{account.status === "active" ? "启用" : "停用"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-muted-foreground">
|
||||
{new Date(account.createdAt).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>审计日志</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<table className="w-full min-w-[860px] text-sm">
|
||||
<thead className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">时间</th>
|
||||
<th className="px-4 py-3 font-medium">操作者</th>
|
||||
<th className="px-4 py-3 font-medium">动作</th>
|
||||
<th className="px-4 py-3 font-medium">对象</th>
|
||||
<th className="px-4 py-3 font-medium">结果</th>
|
||||
<th className="px-4 py-3 font-medium">原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y bg-card">
|
||||
{auditLogs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{new Date(log.timestamp).toLocaleString("zh-CN")}
|
||||
</td>
|
||||
<td className="px-4 py-3">{log.actorEmail ?? "系统"}</td>
|
||||
<td className="px-4 py-3">{log.action}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{log.targetType}
|
||||
{log.targetId ? ` / ${log.targetId.slice(0, 8)}` : ""}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge
|
||||
variant={
|
||||
log.result === "success" ? "success" : log.result === "denied" ? "warning" : "danger"
|
||||
}
|
||||
>
|
||||
{log.result}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{log.reason}</td>
|
||||
</tr>
|
||||
))}
|
||||
{auditLogs.length === 0 ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||
暂无审计日志
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user