diff --git a/components/canvas/AiWashWorkspace.tsx b/components/canvas/AiWashWorkspace.tsx index 5e878afe9..10690b970 100644 --- a/components/canvas/AiWashWorkspace.tsx +++ b/components/canvas/AiWashWorkspace.tsx @@ -3,6 +3,7 @@ import { ChevronDown, Clock3, + Download, History as HistoryIcon, ImageIcon, Loader2, @@ -45,6 +46,7 @@ import { type AiWashControlLevel, type AiWashPreset, } from "@/lib/ai-wash/config"; +import { downloadCanvasImageAsPng } from "@/lib/client/canvas-image-download"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { cn } from "@/lib/utils"; import { imageGenerationService } from "@/services/canvas"; @@ -368,6 +370,11 @@ function getAiWashStatusLabel(status?: AiWashResultStatus) { } } +function buildAiWashDownloadName(result: AiWashResultCard) { + const createdAt = result.createdAt || Date.now(); + return `ai-wash-${result.title || result.presetId || "result"}-${createdAt}`; +} + function composePresetPrompt(params: { preset: AiWashPreset; selectedLevelIndexByControl: Record; @@ -456,6 +463,9 @@ export function AiWashWorkspace({ useState>({}); const [historySourceOverride, setHistorySourceOverride] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [downloadingResultId, setDownloadingResultId] = useState( + null + ); const [isHistoryExpanded, setIsHistoryExpanded] = useState(false); const [lastErrorMessage, setLastErrorMessage] = useState(""); const [comparePositionById, setComparePositionById] = useState< @@ -977,6 +987,30 @@ export function AiWashWorkspace({ }); }, [compareSourceUrl, onApplyResult, selectedResult]); + const handleDownloadSelectedResult = useCallback(async () => { + if (!selectedResult?.imageUrl) { + toast.error("请先选择一张洗图结果"); + return; + } + + const downloadId = selectedResult.presetId || selectedResult.imageUrl; + setDownloadingResultId(downloadId); + try { + await downloadCanvasImageAsPng({ + url: selectedResult.imageUrl, + name: buildAiWashDownloadName(selectedResult), + candidates: [selectedResult.imageUrl], + }); + toast.success("图片已开始下载"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "下载失败"); + } finally { + setDownloadingResultId((current) => + current === downloadId ? null : current + ); + } + }, [selectedResult]); + const handleLoadHistoryBatch = useCallback((entry: AiWashHistoryBatch) => { const loadedPresetId = (entry.items.find((item) => item.isDefault) || entry.items[0])?.presetId || ""; @@ -1430,6 +1464,25 @@ export function AiWashWorkspace({ + {selectedResult?.imageUrl ? ( + + ) : null} + + ) : null} ); } @@ -1021,6 +1055,9 @@ export function StyleProWorkspace({ useState("style"); const [selectedModelId, setSelectedModelId] = useState(""); const [uploading, setUploading] = useState(false); + const [downloadingImageId, setDownloadingImageId] = useState( + null + ); const [generationTaskState, setGenerationTaskState] = useState(null); const generating = @@ -1458,6 +1495,34 @@ export function StyleProWorkspace({ [config] ); + const handleDownloadResult = useCallback( + async (item?: StyleProHistoryItem | null) => { + const target = item || currentResult; + if (!target?.resultImageUrl) { + toast.error("暂无可下载的结果图"); + return; + } + + const downloadId = target.id || target.resultImageUrl; + setDownloadingImageId(downloadId); + try { + await downloadCanvasImageAsPng({ + url: target.resultImageUrl, + name: buildStyleProDownloadName(target), + candidates: [target.resultImageUrl], + }); + toast.success("图片已开始下载"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "下载失败"); + } finally { + setDownloadingImageId((current) => + current === downloadId ? null : current + ); + } + }, + [currentResult] + ); + const disabledReason = !sourceImageUrl ? "请先上传图片" : generationMode === "realistic" && config?.realisticModeAvailable === false @@ -1875,6 +1940,15 @@ export function StyleProWorkspace({ { + void handleDownloadResult(currentResult); + }} taskState={generationTaskState} />