Files
goods/admin-frontend/src/components/AuditLogPage.tsx
T
sulaimaannaasif6866 01593dcbdc
CI / Go (api) (pull_request) Successful in 8s
CI / Python (ingestion) (pull_request) Successful in 13s
CI / Migrations (postgres) (pull_request) Successful in 14s
style(admin): 后台各页容器统一居中 (mx-auto)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 03:05:26 +00:00

116 lines
3.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { api, ApiError } from "../api";
import type { AuditLogRow } from "../types";
import { ScrollText } from "lucide-react";
const ACTION_LABEL: Record<string, string> = {
create: "新建",
update: "修改",
delete: "删除",
add_image: "添加图片",
delete_image: "删除图片",
bulk_status: "批量改状态",
bulk_category: "批量改分类",
};
const ENTITY_LABEL: Record<string, string> = {
product: "商品",
category: "分类",
brand: "品牌",
};
export default function AuditLogPage() {
const [rows, setRows] = useState<AuditLogRow[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const size = 30;
const [error, setError] = useState("");
useEffect(() => {
setError("");
api
.auditLog(page, size)
.then((r) => {
setRows(r.items);
setTotal(r.total);
})
.catch((e) => setError(e instanceof ApiError ? e.message : "加载失败"));
}, [page]);
const pages = Math.max(1, Math.ceil(total / size));
return (
<div className="mx-auto max-w-5xl">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-800">
<ScrollText className="h-5 w-5 text-emerald-600" />
<span className="text-sm font-normal text-gray-400"> {total} </span>
</h2>
{error && (
<div className="mb-3 rounded bg-red-50 px-4 py-2 text-sm text-red-700">{error}</div>
)}
<div className="overflow-hidden rounded-lg border bg-white">
<table className="w-full text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
</tr>
</thead>
<tbody className="divide-y">
{rows.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-gray-400">
</td>
</tr>
) : (
rows.map((e) => (
<tr key={e.id} className="hover:bg-gray-50">
<td className="whitespace-nowrap px-4 py-2 text-gray-500">
{new Date(e.created_at).toLocaleString()}
</td>
<td className="px-4 py-2 text-gray-700">{e.actor}</td>
<td className="px-4 py-2 text-gray-700">
{ACTION_LABEL[e.action] || e.action}
</td>
<td className="px-4 py-2 text-gray-600">
{ENTITY_LABEL[e.entity] || e.entity}
</td>
<td className="px-4 py-2 text-xs text-gray-400">
{e.fields.length ? e.fields.join(", ") : "—"}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div className="mt-4 flex items-center justify-end gap-2 text-sm text-gray-600">
<button
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
className="rounded border border-gray-300 px-3 py-1 disabled:opacity-50"
>
</button>
<span>
{page} / {pages}
</span>
<button
disabled={page >= pages}
onClick={() => setPage((p) => p + 1)}
className="rounded border border-gray-300 px-3 py-1 disabled:opacity-50"
>
</button>
</div>
</div>
);
}