feat(export): add server-side AI export
This commit is contained in:
@@ -31,6 +31,7 @@ export default function ExportPanel({
|
||||
const [exportW, setExportW] = useState('1920');
|
||||
const [exportH, setExportH] = useState('1080');
|
||||
const [isSavingSticker, setIsSavingSticker] = useState(false);
|
||||
const [isExportingAi, setIsExportingAi] = useState(false);
|
||||
|
||||
const [stroke, setStroke] = useState(false);
|
||||
const [fillMode, setFillMode] = useState<FillMode>('fill');
|
||||
@@ -77,6 +78,27 @@ export default function ExportPanel({
|
||||
triggerDownload(url, 'wordcloud.svg');
|
||||
};
|
||||
|
||||
const handleExportAi = async () => {
|
||||
if (isExportingAi) return;
|
||||
const url = buildCustomSvgUrl();
|
||||
if (!url) return;
|
||||
setIsExportingAi(true);
|
||||
try {
|
||||
const svgResponse = await ensureOk(await fetch(url), '读取 SVG 失败');
|
||||
const svg = await svgResponse.text();
|
||||
const aiResponse = await ensureOk(await fetch(apiUrl('/api/exports/ai'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ svg, filename: 'wordcloud.ai' }),
|
||||
}), '导出 AI 失败');
|
||||
triggerDownloadBlob(await aiResponse.blob(), 'wordcloud.ai');
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : '导出 AI 失败');
|
||||
} finally {
|
||||
setIsExportingAi(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAsSticker = async (mode: 'add' | 'replace' = 'add') => {
|
||||
if (isSavingSticker) return;
|
||||
if (mode === 'replace' && !replaceTarget) {
|
||||
@@ -145,6 +167,12 @@ export default function ExportPanel({
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
const triggerDownloadBlob = (blob: Blob, filename: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
triggerDownload(url, filename);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const hasResult = !!jobId;
|
||||
|
||||
return (
|
||||
@@ -339,6 +367,16 @@ export default function ExportPanel({
|
||||
>
|
||||
导出 SVG
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm btn-block"
|
||||
disabled={!hasResult || isExportingAi || fillMode === 'dot' || fillMode === 'ring'}
|
||||
onClick={handleExportAi}
|
||||
>
|
||||
{isExportingAi ? '正在生成 AI...' : '导出 AI'}
|
||||
</button>
|
||||
{(fillMode === 'dot' || fillMode === 'ring') && (
|
||||
<div className="note-text">AI 导出暂不支持点阵和空心圆填充;请改用填充或横线模式。</div>
|
||||
)}
|
||||
{replaceTarget ? (
|
||||
<>
|
||||
<div className="settings-note" style={{ marginTop: 8 }}>
|
||||
|
||||
@@ -471,6 +471,16 @@ export default function CanvasStudio({
|
||||
);
|
||||
}, [normalizedDocument, stickerById]);
|
||||
|
||||
const exportAi = useCallback(async (addRegistrationMarks: boolean) => {
|
||||
const svg = await serializeDocument(normalizedDocument, stickerById, { addRegistrationMarks });
|
||||
const response = await ensureOk(await fetch(apiUrl('/api/exports/ai'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ svg, filename: 'canvas-design.ai' }),
|
||||
}), '导出 AI 失败');
|
||||
downloadBlob(await response.blob(), 'canvas-design.ai');
|
||||
}, [normalizedDocument, stickerById]);
|
||||
|
||||
const exportLayerZip = async (layerIds: string[], folderIds: string[], addRegistrationMarks: boolean) => {
|
||||
const blob = await createLayerExportZip(normalizedDocument, stickerById, layerIds, folderIds, { addRegistrationMarks });
|
||||
downloadBlob(blob, 'canvas-layers.zip');
|
||||
@@ -892,6 +902,7 @@ export default function CanvasStudio({
|
||||
stickerById={stickerById}
|
||||
onUpdateDocument={updateDocument}
|
||||
onExportSvg={exportSvg}
|
||||
onExportAi={exportAi}
|
||||
onExportLayerZip={exportLayerZip}
|
||||
onReset={resetDesign}
|
||||
/>
|
||||
@@ -1414,6 +1425,7 @@ function CanvasExportPanel({
|
||||
stickerById,
|
||||
onUpdateDocument,
|
||||
onExportSvg,
|
||||
onExportAi,
|
||||
onExportLayerZip,
|
||||
onReset,
|
||||
}: {
|
||||
@@ -1421,6 +1433,7 @@ function CanvasExportPanel({
|
||||
stickerById: Map<string, StickerAsset>;
|
||||
onUpdateDocument: (partial: Partial<CanvasDocument>) => void;
|
||||
onExportSvg: (addRegistrationMarks: boolean) => void;
|
||||
onExportAi: (addRegistrationMarks: boolean) => Promise<void>;
|
||||
onExportLayerZip: (layerIds: string[], folderIds: string[], addRegistrationMarks: boolean) => void;
|
||||
onReset: () => void;
|
||||
}) {
|
||||
@@ -1431,6 +1444,7 @@ function CanvasExportPanel({
|
||||
const [templateDescription, setTemplateDescription] = useState('');
|
||||
const [referenceFiles, setReferenceFiles] = useState<File[]>([]);
|
||||
const [addRegistrationMarks, setAddRegistrationMarks] = useState(false);
|
||||
const [exportingAi, setExportingAi] = useState(false);
|
||||
const [exportingPackage, setExportingPackage] = useState(false);
|
||||
const layers = documentModel.layers || [];
|
||||
const folders = documentModel.layerFolders || [];
|
||||
@@ -1556,6 +1570,24 @@ function CanvasExportPanel({
|
||||
<div className="note-text">在导出的总图和每个分层文件左上角、右下角添加对齐点</div>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-block" onClick={() => onExportSvg(addRegistrationMarks)}>导出总图 SVG</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={exportingAi}
|
||||
onClick={async () => {
|
||||
if (exportingAi) return;
|
||||
setExportingAi(true);
|
||||
try {
|
||||
await onExportAi(addRegistrationMarks);
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : '导出 AI 失败');
|
||||
} finally {
|
||||
setExportingAi(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{exportingAi ? '正在生成 AI...' : '导出总图 AI'}
|
||||
</button>
|
||||
<div className="note-text">AI 导出当前支持路径和基础形状;含图片贴纸或普通文字的画布会提示不支持。</div>
|
||||
|
||||
<div className="section-divider" />
|
||||
<div className="section-title">分层打包导出</div>
|
||||
|
||||
Reference in New Issue
Block a user