feat: rebuild CSP practice workflow, UX and automation

这个提交包含在:
Codex CLI
2026-02-13 15:49:05 +08:00
父节点 d33deed4c5
当前提交 e2ab522b78
修改 105 个文件,包含 15669 行新增428 行删除

查看文件

@@ -0,0 +1,119 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
type Submission = {
id: number;
user_id: number;
problem_id: number;
contest_id: number | null;
status: string;
score: number;
time_ms: number;
created_at: number;
};
type ListResp = { items: Submission[]; page: number; page_size: number };
export default function SubmissionsPage() {
const [userId, setUserId] = useState("");
const [problemId, setProblemId] = useState("");
const [contestId, setContestId] = useState("");
const [items, setItems] = useState<Submission[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const load = async () => {
setLoading(true);
setError("");
try {
const params = new URLSearchParams();
if (userId) params.set("user_id", userId);
if (problemId) params.set("problem_id", problemId);
if (contestId) params.set("contest_id", contestId);
const data = await apiFetch<ListResp>(`/api/v1/submissions?${params.toString()}`);
setItems(data.items);
} catch (e: unknown) {
setError(String(e));
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<main className="mx-auto max-w-6xl px-6 py-8">
<h1 className="text-2xl font-semibold"></h1>
<div className="mt-4 grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-4">
<input
className="rounded border px-3 py-2"
placeholder="user_id"
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<input
className="rounded border px-3 py-2"
placeholder="problem_id"
value={problemId}
onChange={(e) => setProblemId(e.target.value)}
/>
<input
className="rounded border px-3 py-2"
placeholder="contest_id"
value={contestId}
onChange={(e) => setContestId(e.target.value)}
/>
<button
className="rounded bg-zinc-900 px-4 py-2 text-white disabled:opacity-50"
onClick={() => void load()}
disabled={loading}
>
{loading ? "加载中..." : "筛选"}
</button>
</div>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
<div className="mt-4 overflow-x-auto rounded-xl border bg-white">
<table className="min-w-full text-sm">
<thead className="bg-zinc-100 text-left">
<tr>
<th className="px-3 py-2">ID</th>
<th className="px-3 py-2"></th>
<th className="px-3 py-2"></th>
<th className="px-3 py-2"></th>
<th className="px-3 py-2"></th>
<th className="px-3 py-2">(ms)</th>
<th className="px-3 py-2"></th>
</tr>
</thead>
<tbody>
{items.map((s) => (
<tr key={s.id} className="border-t">
<td className="px-3 py-2">{s.id}</td>
<td className="px-3 py-2">{s.user_id}</td>
<td className="px-3 py-2">{s.problem_id}</td>
<td className="px-3 py-2">{s.status}</td>
<td className="px-3 py-2">{s.score}</td>
<td className="px-3 py-2">{s.time_ms}</td>
<td className="px-3 py-2">
<Link className="text-blue-600 underline" href={`/submissions/${s.id}`}>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
);
}