feat(reports): streamline reader actions and center export dialog
Independent Staging Quality Gate / validate (push) Failing after 11m16s
Independent Staging Quality Gate / publish (push) Skipped

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-24 20:43:35 +08:00
co-authored by Claude Code
parent 133aca49db
commit f125fae0a0
15 changed files with 105 additions and 215 deletions
@@ -9,10 +9,7 @@ import { SecondaryPageShell } from "@/components/secondary-page-shell";
import { GeneratePersonalReportButton } from "./generate-personal-report-button";
import { Button } from "@/components/ui/button";
import { useVisibilityAwarePoll } from "@/hooks/use-visibility-aware-poll";
import {
PERSONAL_REPORT_EXPORT_LABEL,
PERSONAL_REPORT_GENERATING_COPY,
} from "@/lib/personal-report-longform-copy";
import { PERSONAL_REPORT_GENERATING_COPY } from "@/lib/personal-report-longform-copy";
import { REPORT_DEPTH_LABELS, reportThemeLabel } from "@/lib/personal-report-progress";
import { ReportRowDelete } from "./report-row-delete";
import {
@@ -209,9 +206,6 @@ export function PersonalReportCenter() {
<Button render={<AppLink href={`/reports/${encodeURIComponent(report.id)}`} />} nativeButton={false} variant="outline">
查看报告
</Button>
<Button render={<AppLink href={`/reports/${encodeURIComponent(report.id)}#export`} />} nativeButton={false} variant="ghost">
{PERSONAL_REPORT_EXPORT_LABEL}
</Button>
</div>
) : report.status === "generating" ? (
<div className="report-center-row-actions">
@@ -476,8 +476,6 @@ export function PersonalReportPage({ reportId }: { reportId: string }) {
<style media="print">{"@page { size: A4; margin: 13mm 12mm 14mm; }"}</style>
<style media="print">{REPORT_SHELL_PRINT_CSS}</style>
<ReportActions
reportId={state.reportId || reportId}
reportTitle="个人报告"
markdown={state.markdown}
charts={state.calculationCharts}
factTables={state.factTables}
@@ -2,78 +2,33 @@
"use client";
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import { ArrowLeft, FileText, Printer, TriangleAlert } from "lucide-react";
import { ArrowLeft, Download } from "lucide-react";
import {
detectPrintRestriction,
isPrintSupported,
printPersonalReport,
safeReportFilename,
} from "@/lib/client-report-export";
import { buildReportExportBlocks } from "@/lib/report-export-blocks";
import type { ReportDocumentV2 } from "@/lib/personal-report-contract";
import type { ReportFactTable } from "@/lib/report-fact-table-schema";
import { ReportExportDrawer } from "./report-export-drawer";
import {
PERSONAL_REPORT_EXPORT_LABEL,
PERSONAL_REPORT_PRINT_LABEL,
} from "@/lib/personal-report-longform-copy";
import { Button } from "@/components/ui/button";
import { RawAppendixDownload } from "./raw-appendix-download";
interface ReportActionsProps {
reportId: string;
reportTitle?: string;
markdown: string;
charts?: ReportDocumentV2["charts"];
factTables?: readonly ReportFactTable[];
reportDate?: string | null;
}
function subscribePrintCapability(onStoreChange: () => void): () => void {
queueMicrotask(onStoreChange);
return () => undefined;
}
export function ReportActions({
reportId,
reportTitle,
markdown,
charts,
factTables,
reportDate,
}: ReportActionsProps) {
const [printBusy, setPrintBusy] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const blocks = useMemo(() => buildReportExportBlocks({ markdown, charts, factTables }), [markdown, charts, factTables]);
useEffect(() => {
// ReportActions mounts only in markdown-ready. Consume once, including StrictMode.
if (window.location.hash !== "#export") return;
window.history.replaceState(window.history.state, "", window.location.pathname + window.location.search);
queueMicrotask(() => setExportOpen(true));
}, []);
const [notice, setNotice] = useState<string | null>(null);
const printSupported = useSyncExternalStore(subscribePrintCapability, isPrintSupported, () => false);
async function handlePrint() {
if (printBusy || exportOpen || !printSupported) return;
const restriction = detectPrintRestriction();
if (restriction.restricted) {
setNotice(restriction.message ?? "当前浏览器无法可靠打印,请在系统浏览器中打开本页。");
return;
}
setPrintBusy(true);
setNotice(null);
try {
await printPersonalReport({ title: reportTitle?.trim() || safeReportFilename(reportId) });
} finally {
setPrintBusy(false);
}
}
return (
<nav aria-label="报告操作" className="personal-report-actions personal-report-screen-only">
@@ -82,13 +37,15 @@ export function ReportActions({
<ArrowLeft aria-hidden="true" />返回报告中心
</Link>
<div className="personal-report-action-end">
<RawAppendixDownload reportId={reportId} />
{notice && <span role="status"><TriangleAlert aria-hidden="true" />{notice}</span>}
<Button type="button" size="sm" variant="ghost" onClick={() => void handlePrint()} disabled={printBusy || exportOpen || !printSupported}>
<Printer aria-hidden="true" />{printBusy ? "正在准备…" : PERSONAL_REPORT_PRINT_LABEL}
</Button>
<Button type="button" size="sm" onClick={() => setExportOpen(true)} disabled={printBusy}>
<FileText aria-hidden="true" />{PERSONAL_REPORT_EXPORT_LABEL}
<Button
type="button"
size="icon"
variant="ghost"
aria-label="导出报告"
title="导出"
onClick={() => setExportOpen(true)}
>
<Download aria-hidden="true" />
</Button>
{exportOpen && <ReportExportDrawer blocks={blocks} reportDate={reportDate} onClose={() => setExportOpen(false)} />}
</div>