差距分析闭环(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 已更新
172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
"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>
|
|
);
|
|
}
|