feat(reports): add professional reference export

This commit is contained in:
Jesse_Chen
2026-09-04 00:19:28 +08:00
parent c2f23131f7
commit 3b09bbbee0
11 changed files with 715 additions and 48 deletions
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { GeneratePersonalReportButton } from "./generate-personal-report-button";
import { Button } from "@/components/ui/button";
import { useVisibilityAwarePoll } from "@/hooks/use-visibility-aware-poll";
import { downloadMarkdownReport } from "@/lib/consultation-report-export";
const LIST_POLL_INTERVAL_MS = 3000;
@@ -85,6 +86,8 @@ function StatusIcon({ status }: { status: ReportListItem["status"] }) {
export function PersonalReportCenter() {
const [state, setState] = useState<CenterState>({ phase: "loading", reports: [] });
const [exportingReportId, setExportingReportId] = useState<string | null>(null);
const [exportError, setExportError] = useState<{ reportId: string; message: string } | null>(null);
const cancelled = useRef(false);
const load = useCallback(async (showLoading = false) => {
@@ -136,6 +139,31 @@ export function PersonalReportCenter() {
[state.reports],
);
const downloadProfessionalReference = useCallback(async (reportId: string) => {
setExportingReportId(reportId);
setExportError(null);
try {
const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}/professional-reference`, {
method: "POST",
credentials: "same-origin",
headers: { Accept: "application/json" },
});
const result: unknown = await response.json().catch(() => null);
const payload = result && typeof result === "object" ? result as Record<string, unknown> : {};
if (!response.ok || payload.format !== "markdown" || typeof payload.markdown !== "string") {
throw new Error(typeof payload.error === "string" ? payload.error : "专业参考版暂时无法生成");
}
downloadMarkdownReport("个人专业参考版", payload.markdown);
} catch (error) {
setExportError({
reportId,
message: error instanceof Error ? error.message : "专业参考版暂时无法生成",
});
} finally {
setExportingReportId(null);
}
}, []);
if (state.phase === "unauthorized") {
return (
<main className="report-center-shell report-center-message">
@@ -201,9 +229,25 @@ export function PersonalReportCenter() {
<small>{formatDate(report.createdAt)} · {report.depth} · {report.themes.join(" / ") || "综合主题"}</small>
</div>
{report.status === "ready" ? (
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="outline">
</Button>
<div className="flex flex-wrap items-center justify-end gap-2 max-[720px]:justify-start">
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="outline">
</Button>
<Button
type="button"
variant="ghost"
disabled={exportingReportId === report.id}
onClick={() => void downloadProfessionalReference(report.id)}
>
{exportingReportId === report.id ? <InlineSpinner size={14} /> : null}
</Button>
{exportError?.reportId === report.id ? (
<span className="basis-full text-right text-sm text-destructive max-[720px]:text-left" role="alert">
{exportError.message}
</span>
) : null}
</div>
) : report.status === "generating" ? (
<Button render={<Link href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="ghost">