forked from EduCraft/curriculum-project-hub
feat(filelib): 导出改为下载 cph 编译的真实 PDF
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
// 已归档(软删)标记用;与"删除"区分 —— 数据仍在,只是打了 archivedAt。
|
||||
archive: "M3 8h18v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Zm1-5h16l1 5H3l1-5Zm5 9h6",
|
||||
restore: "M3 12a9 9 0 1 0 3-6.7M3 4v4.5h4.5",
|
||||
download: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof ICONS;
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
import { currentNode } from "./browser.js";
|
||||
import type { ExportJob, NodeDetail } from "./types.js";
|
||||
import Modal from "./Modal.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
let { node }: { node: NodeDetail } = $props();
|
||||
|
||||
let showEditDesc = $state(false);
|
||||
let descDraft = $state("");
|
||||
let exportJob = $state<ExportJob | null>(null);
|
||||
let exporting = $state(false);
|
||||
|
||||
const canEdit = $derived(node.role === "MANAGE" || node.role === "EDIT");
|
||||
const canManage = $derived(node.role === "MANAGE");
|
||||
@@ -34,6 +36,7 @@
|
||||
$effect(() => {
|
||||
void node.id;
|
||||
exportJob = null;
|
||||
exporting = false;
|
||||
});
|
||||
|
||||
function openEditDesc(): void {
|
||||
@@ -57,31 +60,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function submitExport(): Promise<void> {
|
||||
/**
|
||||
* 导出 PDF:提交 job → 轮询 → 完成即自动触发浏览器下载。
|
||||
*
|
||||
* 只有一个导出目标,所以不给目标选择器 —— target 由后端 adapter 固定。
|
||||
* 下载走 <a download> 而非 fetch+blob:接口是 same-origin cookie 认证,
|
||||
* 浏览器直接带上会话,不需要在 JS 里搬一遍字节。
|
||||
*/
|
||||
async function exportPdf(): Promise<void> {
|
||||
if (exporting) return;
|
||||
exporting = true;
|
||||
exportJob = null;
|
||||
try {
|
||||
const r = await api<{ jobId: string; status: string }>(`/database/api/projects/${node.id}/exports`, {
|
||||
method: "POST",
|
||||
body: { target: "manifest" },
|
||||
body: { target: "pdf" },
|
||||
});
|
||||
toastOk("导出已提交");
|
||||
void pollExport(r.jobId);
|
||||
await pollExport(r.jobId);
|
||||
} catch (e) {
|
||||
toastErr(e instanceof Error ? e.message : String(e));
|
||||
exporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollExport(jobId: string): Promise<void> {
|
||||
for (;;) {
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
let job: ExportJob;
|
||||
try {
|
||||
const job = await api<ExportJob>(`/database/api/exports/${jobId}`);
|
||||
exportJob = job;
|
||||
if (job.status === "DONE" || job.status === "FAILED") break;
|
||||
} catch {
|
||||
break;
|
||||
job = await api<ExportJob>(`/database/api/exports/${jobId}`);
|
||||
} catch (e) {
|
||||
exporting = false;
|
||||
toastErr(e instanceof Error ? e.message : String(e));
|
||||
return;
|
||||
}
|
||||
exportJob = job;
|
||||
if (job.status === "DONE") {
|
||||
exporting = false;
|
||||
toastOk("导出完成,开始下载");
|
||||
triggerDownload(`/database/api/exports/${job.id}/download`);
|
||||
return;
|
||||
}
|
||||
if (job.status === "FAILED") {
|
||||
exporting = false;
|
||||
toastErr(`导出失败:${job.error ?? "未知原因"}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function triggerDownload(url: string): void {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
@@ -124,18 +159,16 @@
|
||||
|
||||
<div class="section-title mb-2">导出</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<select class="select !w-auto"><option value="manifest">manifest(stub)</option></select>
|
||||
<button class="btn" onclick={submitExport}>开始导出</button>
|
||||
{#if exportJob}
|
||||
<button class="btn" onclick={exportPdf} disabled={exporting}>
|
||||
<Icon name="download" size={13} />
|
||||
{exporting ? "导出中…" : "导出 PDF"}
|
||||
</button>
|
||||
{#if exportJob?.status === "DONE"}
|
||||
<span class="file-meta">
|
||||
{#if exportJob.status === "DONE"}
|
||||
完成 · <a class="text-accent underline" href="/database/api/exports/{exportJob.id}/download">下载</a>
|
||||
{:else if exportJob.status === "FAILED"}
|
||||
失败:{exportJob.error ?? ""}
|
||||
{:else}
|
||||
{exportJob.status}…
|
||||
{/if}
|
||||
完成 · <a class="text-accent underline" href="/database/api/exports/{exportJob.id}/download" download>重新下载</a>
|
||||
</span>
|
||||
{:else if exportJob?.status === "FAILED"}
|
||||
<span class="file-meta text-danger">失败:{exportJob.error ?? "未知原因"}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user