feat: complete TASK-WP4-05 client export

This commit is contained in:
suyx
2026-08-03 14:41:04 +08:00
parent 457e1147e4
commit d09fda13c5
9 changed files with 753 additions and 26 deletions
+38
View File
@@ -0,0 +1,38 @@
import { useState } from "react";
import { exportResultCopy, type ExportFlowStatus } from "./export-flow.js";
import { EXPORT_DIMENSIONS, type ExportFormat, type ExportRatio } from "./export-compositor.js";
export function ExportDialog(props: {
busy: boolean;
byteSize?: number;
onClose: () => void;
onExport: (input: { format: ExportFormat; quality?: number }) => void;
onRetry: () => void;
pendingEdit: boolean;
ratio: ExportRatio;
result?: ExportFlowStatus;
}) {
const [format, setFormat] = useState<ExportFormat>("jpg");
const [quality, setQuality] = useState(92);
const [pendingConfirmed, setPendingConfirmed] = useState(false);
const dimensions = EXPORT_DIMENSIONS[props.ratio];
const failure = props.result && props.result !== "downloaded_and_saved";
return <div className="editor-dialog-backdrop"><section aria-labelledby="editor-export-title" aria-modal="true" className="editor-export-dialog" role="dialog">
<header><div><p>EXPORT</p><h2 id="editor-export-title"></h2></div><button aria-label="关闭导出" disabled={props.busy} onClick={props.onClose} title="关闭" type="button">×</button></header>
{props.result ? <div className={`editor-export-result ${failure ? "failed" : "succeeded"}`}>
<p aria-live={failure ? "assertive" : "polite"} role={failure ? "alert" : "status"}>{exportResultCopy[props.result]}</p>
{props.byteSize ? <small>{(props.byteSize / 1024 / 1024).toFixed(2)} MB</small> : null}
<div>{props.result === "download_failed" ? <button disabled={props.busy} onClick={props.onRetry} type="button"></button> : null}<button disabled={props.busy} onClick={props.onClose} type="button"></button></div>
</div> : <>
<section className="editor-export-section"><h3></h3><div aria-label="导出格式" className="editor-export-segments" role="group">
<button aria-pressed={format === "jpg"} onClick={() => setFormat("jpg")} type="button">JPG</button>
<button aria-pressed={format === "png"} onClick={() => setFormat("png")} type="button">PNG</button>
</div></section>
{format === "jpg" ? <section className="editor-export-section"><div className="editor-export-label"><h3>JPG </h3><output>{quality}</output></div><div className="editor-export-quality"><input aria-label="JPG 质量" max="100" min="80" onChange={(event) => setQuality(Number(event.target.value))} step="1" type="range" value={quality} /><input aria-label="JPG 质量数值" max="100" min="80" onChange={(event) => setQuality(Math.max(80, Math.min(100, Number(event.target.value))))} step="1" type="number" value={quality} /></div></section> : null}
<section className="editor-export-summary"><span></span><strong>{dimensions.width} × {dimensions.height} px</strong><span></span><strong>sRGB</strong><span></span><strong></strong></section>
{props.pendingEdit ? <label className="editor-export-pending"><strong></strong><span><input checked={pendingConfirmed} onChange={(event) => setPendingConfirmed(event.target.checked)} type="checkbox" /></span></label> : null}
<footer><button disabled={props.busy} onClick={props.onClose} type="button"></button><button className="editor-primary" disabled={props.busy || (props.pendingEdit && !pendingConfirmed)} onClick={() => props.onExport({ format, ...(format === "jpg" ? { quality } : {}) })} type="button">{props.busy ? "正在导出" : "导出并下载"}</button></footer>
</>}
</section></div>;
}