feat: 为审批记录增加翻页 (#115)

This commit is contained in:
RuoJi6 authored and GitHub committed 2026-09-15 00:52:48 +08:00
1 parent 5db71f7c53
commit 71fa919599
5 files changed
+179 -14

No files matched your search

+33 -6
View File
@@ -15,6 +15,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { TablePagination } from "@/components/table-pagination";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -464,6 +465,9 @@ function ApprovalTable({
export function ApprovalRecords({ taskId }: { taskId?: string }) {
const [rows, setRows] = React.useState<InterceptApprovalRow[]>([]);
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(20);
const [total, setTotal] = React.useState(0);
const [loading, setLoading] = React.useState(true);
const [refreshing, setRefreshing] = React.useState(false);
const [error, setError] = React.useState("");
@@ -472,14 +476,24 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
const request = React.useRef(0);
const decisionLock = React.useRef(false);
// A task can stay mounted while the user switches between task details.
// Reset the cursor so the new scope always starts at its newest records.
// biome-ignore lint/correctness/useExhaustiveDependencies: taskId intentionally resets pagination when scope changes.
React.useEffect(() => {
setPage(1);
}, [taskId]);
const load = React.useCallback(
async (manual = false) => {
const id = ++request.current;
if (manual) setRefreshing(true);
try {
const items = taskId ? await api.interceptTask(taskId) : await api.interceptHistory();
const result = taskId
? await api.interceptTaskPage(taskId, page, pageSize)
: await api.interceptHistoryPage(page, pageSize);
if (id !== request.current) return;
setRows(items);
setRows(result.items);
setTotal(result.total);
setError("");
if (manual) setRevision((v) => v + 1);
} catch (e) {
@@ -491,7 +505,7 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
}
}
},
[taskId],
[taskId, page, pageSize],
);
React.useEffect(() => {
@@ -503,6 +517,11 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
};
}, [load]);
const changePageSize = (next: number) => {
setPageSize(next);
setPage(1);
};
const decide: Decide = async (id, decision) => {
if (decisionLock.current) return;
decisionLock.current = true;
@@ -556,9 +575,7 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
</section>
) : null}
<section className="overflow-hidden rounded-xl border">
<div className="border-b px-4 py-3 font-medium text-sm">
{taskId ? "全部记录" : "最近记录"}({rows.length})
</div>
<div className="border-b px-4 py-3 font-medium text-sm">全部记录({total})</div>
{loading ? (
<div className="flex flex-col gap-3 p-4">
<Skeleton className="h-10 w-full" />
@@ -580,6 +597,16 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
{rows.length ? (
<ApprovalTable rows={rows} busy={deciding} decide={decide} revision={revision} label="审批记录列表" />
) : null}
{!loading && !error ? (
<TablePagination
page={page}
pageSize={pageSize}
total={total}
onPageChange={setPage}
onPageSizeChange={changePageSize}
pageSizeOptions={[10, 20, 50]}
/>
) : null}
</section>
</div>
);
+14
View File
@@ -1063,8 +1063,22 @@ export const api = {
post<{ ok: boolean }>(`/intercept/pending/${id}/decide`, { decision }),
interceptDetail: (id: number) => get<InterceptDetail>(`/intercept/history/${id}`),
interceptHistory: () => get<{ items: InterceptApprovalRow[] }>("/intercept/history").then((r) => arr(r.items)),
interceptHistoryPage: (page = 1, size = 20) =>
get<{ items: InterceptApprovalRow[]; total?: number }>(`/intercept/history?page=${page}&size=${size}`).then(
(r) => ({
items: arr(r.items),
total: r.total ?? r.items?.length ?? 0,
}),
),
interceptTask: (taskId: string) =>
get<{ items: InterceptApprovalRow[] }>(`/intercept/task/${taskId}`).then((r) => arr(r.items)),
interceptTaskPage: (taskId: string, page = 1, size = 20) =>
get<{ items: InterceptApprovalRow[]; total?: number }>(
`/intercept/task/${encodeURIComponent(taskId)}?page=${page}&size=${size}`,
).then((r) => ({
items: arr(r.items),
total: r.total ?? r.items?.length ?? 0,
})),
// ---- intercept tool-config (全局工具拦截范围) ----
interceptGetToolConfig: async (): Promise<{ enabled_tools: string[] }> => {
+26 -4
View File
@@ -2056,7 +2056,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (seg[0] === "intercept" && seg[1] === "pending" && seg[3] === "decide") {
const id = Number(seg[2]);
const row = mockInterceptHistory.find((r) => r.id === id) ?? mockInterceptPending.find((r) => r.id === id);
if (!row || row.status !== "pending") throw new Error("审批已处理或不存在,请刷新记录");
if (row?.status !== "pending") throw new Error("审批已处理或不存在,请刷新记录");
if (b.decision !== "allowed" && b.decision !== "denied") throw new Error("无效审批动作");
row.status = b.decision;
row.decided_at = new Date().toISOString();
@@ -2076,9 +2076,31 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "intercept" && seg[1] === "pending" && seg.length === 3 && m === "GET")
return mockInterceptPending.find((p) => p.id === Number(seg[2])) ?? null;
if (path === "/intercept/history") return { items: mockInterceptHistory };
if (seg[0] === "intercept" && seg[1] === "task")
return { items: mockInterceptHistory.filter((r) => r.task_id === seg[2]) };
if (path === "/intercept/history") {
if (!q.has("page") && !q.has("size")) return { items: mockInterceptHistory, total: mockInterceptHistory.length };
const page = Math.max(1, Number(q.get("page")) || 1);
const size = Math.min(100, Math.max(1, Number(q.get("size")) || 20));
const offset = (page - 1) * size;
return {
items: mockInterceptHistory.slice(offset, offset + size),
total: mockInterceptHistory.length,
page,
page_size: size,
};
}
if (seg[0] === "intercept" && seg[1] === "task") {
const filtered = mockInterceptHistory.filter((r) => r.task_id === seg[2]);
if (!q.has("page") && !q.has("size")) return { items: filtered, total: filtered.length };
const page = Math.max(1, Number(q.get("page")) || 1);
const size = Math.min(100, Math.max(1, Number(q.get("size")) || 20));
const offset = (page - 1) * size;
return {
items: filtered.slice(offset, offset + size),
total: filtered.length,
page,
page_size: size,
};
}
if (path === "/intercept/tool-config") return { enabled_tools: ["bash"] };
if (path === "/intercept/judge" && m === "GET")
return {