feat: add downloads for style pro and ai wash results
This commit is contained in:
parent
0f27bf1b10
commit
77174f4e21
@ -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<string, number>;
|
||||
@ -456,6 +463,9 @@ export function AiWashWorkspace({
|
||||
useState<Record<string, number>>({});
|
||||
const [historySourceOverride, setHistorySourceOverride] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [downloadingResultId, setDownloadingResultId] = useState<string | null>(
|
||||
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({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedResult?.imageUrl ? (
|
||||
<button
|
||||
className="absolute bottom-2 right-2 z-30 inline-flex h-10 items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/92 px-4 text-xs font-medium text-[#333333] shadow-[0_10px_24px_rgba(0,0,0,0.07)] backdrop-blur transition hover:border-[#111111] hover:text-[#111111] disabled:cursor-not-allowed disabled:opacity-55"
|
||||
disabled={Boolean(downloadingResultId)}
|
||||
onClick={() => {
|
||||
void handleDownloadSelectedResult();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{downloadingResultId ===
|
||||
(selectedResult.presetId || selectedResult.imageUrl) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
下载
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="absolute bottom-2 left-2 z-30 inline-flex h-10 items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/90 px-4 text-xs text-[#666666] shadow-[0_10px_24px_rgba(0,0,0,0.06)] backdrop-blur transition hover:border-[#111111] hover:text-[#111111]"
|
||||
onClick={() => setIsHistoryExpanded((current) => !current)}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Clock3, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { ArrowLeft, Clock3, Download, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
type CSSProperties,
|
||||
@ -29,6 +29,7 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
import { downloadCanvasImageAsPng } from "@/lib/client/canvas-image-download";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import type {
|
||||
PublicStyleProCategory,
|
||||
@ -457,6 +458,15 @@ function formatStyleProHistoryDateTime(value?: string | null) {
|
||||
return `${dateText} ${timeText}`;
|
||||
}
|
||||
|
||||
function buildStyleProDownloadName(item: StyleProHistoryItem) {
|
||||
const mode =
|
||||
item.generationMode === "realistic" ? "style-pro-realistic" : "style-pro";
|
||||
const createdAt = item.createdAt
|
||||
? item.createdAt.replace(/[T:.]/g, "-").slice(0, 19)
|
||||
: String(Date.now());
|
||||
return `${mode}-${createdAt}`;
|
||||
}
|
||||
|
||||
function formatStyleProEstimatedWait(estimatedWaitMs?: number | null) {
|
||||
const numeric = Number(estimatedWaitMs || 0);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
@ -817,10 +827,16 @@ function GeneratingImagePlaceholder({
|
||||
function BeforeAfterSlider({
|
||||
afterUrl,
|
||||
beforeUrl,
|
||||
downloadDisabled,
|
||||
downloading,
|
||||
onDownload,
|
||||
taskState,
|
||||
}: {
|
||||
afterUrl?: string | null;
|
||||
beforeUrl?: string | null;
|
||||
downloadDisabled?: boolean;
|
||||
downloading?: boolean;
|
||||
onDownload?: () => void;
|
||||
taskState?: StyleProTaskState | null;
|
||||
}) {
|
||||
const showTaskState =
|
||||
@ -946,6 +962,24 @@ function BeforeAfterSlider({
|
||||
<div className="absolute right-4 top-4 rounded-full bg-emerald-500/90 px-3 py-1 text-xs font-semibold text-black">
|
||||
结果
|
||||
</div>
|
||||
{afterUrl ? (
|
||||
<button
|
||||
className="absolute bottom-4 right-4 z-20 inline-flex h-11 items-center gap-2 rounded-full border border-white/25 bg-black/72 px-4 text-[13px] font-semibold text-white shadow-[0_14px_32px_rgba(0,0,0,0.24)] backdrop-blur transition hover:bg-black disabled:cursor-not-allowed disabled:opacity-55"
|
||||
disabled={downloadDisabled || downloading}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDownload?.();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{downloading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
下载
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1021,6 +1055,9 @@ export function StyleProWorkspace({
|
||||
useState<StyleProGenerationMode>("style");
|
||||
const [selectedModelId, setSelectedModelId] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [downloadingImageId, setDownloadingImageId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [generationTaskState, setGenerationTaskState] =
|
||||
useState<StyleProTaskState | null>(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({
|
||||
<BeforeAfterSlider
|
||||
afterUrl={currentResult?.resultImageUrl}
|
||||
beforeUrl={currentResult?.sourceImageUrl || sourceImageUrl}
|
||||
downloadDisabled={!currentResult?.resultImageUrl}
|
||||
downloading={Boolean(
|
||||
currentResult &&
|
||||
downloadingImageId ===
|
||||
(currentResult.id || currentResult.resultImageUrl)
|
||||
)}
|
||||
onDownload={() => {
|
||||
void handleDownloadResult(currentResult);
|
||||
}}
|
||||
taskState={generationTaskState}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user