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:
171
apps/admin-portal/src/app/admin/audit-logs/login-logs/page.tsx
Normal file
171
apps/admin-portal/src/app/admin/audit-logs/login-logs/page.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode, useState, useEffect } from "react";
|
||||
import { useLoginLogs, useLoginLogFilter } from "@/hooks/use-login-logs";
|
||||
import { AuditLogsTabs } from "@/components/audit-logs-tabs";
|
||||
import {
|
||||
PageHeader,
|
||||
PaperCard,
|
||||
Input,
|
||||
Select,
|
||||
LoadingState,
|
||||
ErrorState,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Pagination,
|
||||
} from "@/components/ui";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export default function LoginLogsPage(): ReactNode {
|
||||
const { filter, setPage, setSearch, setAction, setStatus } =
|
||||
useLoginLogFilter();
|
||||
const { data, loading, error } = useLoginLogs(filter);
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setSearch(searchInput), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchInput, setSearch]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 1200, margin: "0 auto" }}>
|
||||
<PageHeader
|
||||
title={t("admin.auditLogs.loginLogs")}
|
||||
description="用户登录与登出记录"
|
||||
/>
|
||||
|
||||
<AuditLogsTabs />
|
||||
|
||||
<PaperCard className="p-4 mb-4">
|
||||
<div className="flex gap-3 items-center">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder={t("admin.common.search")}
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
style={{ width: 240 }}
|
||||
/>
|
||||
<Select
|
||||
value={filter.action ?? ""}
|
||||
onChange={(e) => setAction(e.target.value)}
|
||||
>
|
||||
<option value="">{t("admin.common.all")}动作</option>
|
||||
<option value="signin">登录</option>
|
||||
<option value="signout">登出</option>
|
||||
<option value="signup">注册</option>
|
||||
</Select>
|
||||
<Select
|
||||
value={filter.status ?? ""}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
>
|
||||
<option value="">{t("admin.common.all")}状态</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="failure">失败</option>
|
||||
</Select>
|
||||
</div>
|
||||
</PaperCard>
|
||||
|
||||
<PaperCard className="p-4">
|
||||
{loading && <LoadingState />}
|
||||
{error && <ErrorState message={error.message} />}
|
||||
{!loading && !error && data && (
|
||||
<>
|
||||
{data.items.length === 0 ? (
|
||||
<EmptyState message={t("admin.common.empty")} />
|
||||
) : (
|
||||
<>
|
||||
<Table
|
||||
headers={[
|
||||
t("admin.auditLogs.time"),
|
||||
t("admin.common.name"),
|
||||
t("admin.auditLogs.loginAction"),
|
||||
t("admin.auditLogs.loginStatus"),
|
||||
t("admin.auditLogs.ip"),
|
||||
t("admin.auditLogs.userAgent"),
|
||||
t("admin.auditLogs.failureReason"),
|
||||
]}
|
||||
>
|
||||
{data.items.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell style={{ whiteSpace: "nowrap" }}>
|
||||
{new Date(log.occurredAt).toLocaleString("zh-CN")}
|
||||
</TableCell>
|
||||
<TableCell>{log.userName}</TableCell>
|
||||
<TableCell
|
||||
style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
|
||||
>
|
||||
{log.action}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "2px 8px",
|
||||
fontSize: 11,
|
||||
borderRadius: 10,
|
||||
background:
|
||||
log.status === "success"
|
||||
? "var(--color-success)20"
|
||||
: "var(--color-danger)20",
|
||||
color:
|
||||
log.status === "success"
|
||||
? "var(--color-success)"
|
||||
: "var(--color-danger)",
|
||||
border: `1px solid ${
|
||||
log.status === "success"
|
||||
? "var(--color-success)40"
|
||||
: "var(--color-danger)40"
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
{log.status === "success" ? "成功" : "失败"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
|
||||
>
|
||||
{log.ip}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 11,
|
||||
maxWidth: 200,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{log.userAgent}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 11,
|
||||
color: log.failureReason
|
||||
? "var(--color-danger)"
|
||||
: "var(--color-ink-muted)",
|
||||
}}
|
||||
>
|
||||
{log.failureReason ?? "—"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
<Pagination
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
total={data.total}
|
||||
hasNext={data.hasNext}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PaperCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user