"use client"; /** * 归档页的**交互动作**(数据在服务端渲染,这里只管「页面之外」的动作)。 * * 拆出来的原因:归档详情页改为 Server Component(只读内容必须能被 SSR 直出, * 否则不带 JS 的抓取/分享预览看不到「选股条件」与「交易执行依据」); * 而删除、导出、折叠 spec 这些必须有事件处理,就集中放在这个客户端组件里。 * * 删除的确认文案必须写清后果:结果只存归档一份(`job.result_json` 对新记录为 NULL, * `GET /api/jobs/{id}` 是从归档回读的),所以删归档 = 该次回测结果彻底消失。 * 先前的文案写成「不影响作业记录」,那是**误导**。 */ import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { apiDelete, apiGetText } from "@/lib/api"; import { Banner, Btn, Card } from "@/components/ui"; /** * 刻意只收 `id` / `spec`(**不接收完整 result**):归档结果可达数 MB,而服务端已经把它 * 传给图表组件了;再传一份给本组件会让 RSC 载荷翻倍。导出时按需拉取。 */ export function ArchiveActions({ id, spec }: { id: string; spec: unknown }) { const router = useRouter(); const [deleting, setDeleting] = useState(false); const [error, setError] = useState(""); const [showSpec, setShowSpec] = useState(false); async function downloadJson() { setError(""); try { const text = await apiGetText(`/experiments/${encodeURIComponent(id)}`); const blob = new Blob([text], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${id}-archive.json`; a.click(); URL.revokeObjectURL(url); } catch (e) { setError(`导出失败:${(e as Error).message}`); } } async function remove() { const ok = window.confirm( `删除归档 ${id}?\n\n` + `· 删除后这份快照(结果 + spec)无法再查看,也不可恢复;\n` + `· 结果只存归档这一份 —— 删除后连该次执行的作业记录也读不回结果\n` + ` (完整存档上线前的早期归档在作业表里另有副本,但不应依赖);\n` + `· 想留底请先点左侧「导出完整 JSON」。\n\n确认删除?` ); if (!ok) return; setDeleting(true); setError(""); try { await apiDelete(`/experiments/${encodeURIComponent(id)}`); router.push("/experiments"); } catch (e) { setError((e as Error).message); setDeleting(false); } } return ( <> {error ? {error} : null}
实验对比 加入对比 以此参数再跑 导出完整 JSON setShowSpec((v) => !v)}> {showSpec ? "收起原始 spec" : "查看原始 spec(JSON)"} 删除归档
{showSpec ? (
{JSON.stringify(spec ?? {}, null, 2)}
) : null} ); }