fix(scene-fusion): refine UI layout and add result image preview modal
This commit is contained in:
parent
309c4c8e28
commit
8688cac064
@ -27,7 +27,6 @@ export function SceneFusionDialog({ open, onOpenChange, onBatchAddToCanvas }: Sc
|
|||||||
<div className="h-full min-h-0 flex-1 bg-white">
|
<div className="h-full min-h-0 flex-1 bg-white">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<SceneFusionView
|
<SceneFusionView
|
||||||
onBack={() => onOpenChange(false)}
|
|
||||||
onBatchAddToCanvas={onBatchAddToCanvas}
|
onBatchAddToCanvas={onBatchAddToCanvas}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Wand2,
|
Wand2,
|
||||||
Eraser,
|
Eraser,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||||
@ -22,7 +23,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
const MAX_BATCH_ITEMS = 10;
|
const MAX_BATCH_ITEMS = 10;
|
||||||
@ -47,7 +48,6 @@ interface ProcessItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SceneFusionViewProps {
|
interface SceneFusionViewProps {
|
||||||
onBack: () => void;
|
|
||||||
initialProductImage?: string | null;
|
initialProductImage?: string | null;
|
||||||
initialProductName?: string;
|
initialProductName?: string;
|
||||||
onBatchAddToCanvas?: (imageUrls: string[]) => void | Promise<void>;
|
onBatchAddToCanvas?: (imageUrls: string[]) => void | Promise<void>;
|
||||||
@ -121,7 +121,6 @@ function getModePrompt(mode: BatchMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SceneFusionView({
|
export function SceneFusionView({
|
||||||
onBack,
|
|
||||||
initialProductImage,
|
initialProductImage,
|
||||||
initialProductName,
|
initialProductName,
|
||||||
onBatchAddToCanvas,
|
onBatchAddToCanvas,
|
||||||
@ -154,6 +153,8 @@ export function SceneFusionView({
|
|||||||
const [randomPrompt, setRandomPrompt] = useState("");
|
const [randomPrompt, setRandomPrompt] = useState("");
|
||||||
|
|
||||||
const [batchItems, setBatchItems] = useState<ProcessItem[]>([]);
|
const [batchItems, setBatchItems] = useState<ProcessItem[]>([]);
|
||||||
|
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
|
||||||
|
const [previewImageTitle, setPreviewImageTitle] = useState<string>("处理结果预览");
|
||||||
|
|
||||||
const productInputRef = useRef<HTMLInputElement>(null);
|
const productInputRef = useRef<HTMLInputElement>(null);
|
||||||
const sceneInputRef = useRef<HTMLInputElement>(null);
|
const sceneInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -1137,19 +1138,23 @@ export function SceneFusionView({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openResultPreview = (imageUrl: string, title: string) => {
|
||||||
|
setPreviewImageUrl(imageUrl);
|
||||||
|
setPreviewImageTitle(title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeResultPreview = () => {
|
||||||
|
setPreviewImageUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 w-full flex-col bg-white dark:bg-zinc-950">
|
<div className="flex h-full min-h-0 w-full flex-col bg-white dark:bg-zinc-950">
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden md:flex-row">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden md:flex-row">
|
||||||
<div className="w-full md:w-[420px] border-r flex min-h-0 flex-col bg-white dark:bg-zinc-950 shrink-0">
|
<div className="w-full md:w-[420px] border-r flex min-h-0 flex-col bg-white dark:bg-zinc-950 shrink-0">
|
||||||
<div className="border-b px-4 py-3 shrink-0 bg-white dark:bg-zinc-950">
|
<div className="border-b h-16 px-4 shrink-0 bg-white dark:bg-zinc-950 flex items-center">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Layers className="w-4 h-4 text-primary" />
|
||||||
<Layers className="w-4 h-4 text-primary" />
|
<span className="font-medium">批量处理</span>
|
||||||
<span className="font-medium">批量处理</span>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={onBack}>
|
|
||||||
返回
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1159,22 +1164,21 @@ export function SceneFusionView({
|
|||||||
<Label className="text-base font-medium">处理模式</Label>
|
<Label className="text-base font-medium">处理模式</Label>
|
||||||
<Select value={mode} onValueChange={(value) => setMode(value as BatchMode)} disabled={isProcessing}>
|
<Select value={mode} onValueChange={(value) => setMode(value as BatchMode)} disabled={isProcessing}>
|
||||||
<SelectTrigger className="h-10 bg-white dark:bg-zinc-950">
|
<SelectTrigger className="h-10 bg-white dark:bg-zinc-950">
|
||||||
<SelectValue placeholder="请选择处理模式" />
|
<span className="truncate">
|
||||||
|
{selectedModeOption?.label || "请选择处理模式"}
|
||||||
|
</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="z-[30000] !bg-white !dark:bg-zinc-950">
|
<SelectContent className="z-[30000] !bg-white !dark:bg-zinc-950">
|
||||||
{modeOptions.map((option) => (
|
{modeOptions.map((option) => (
|
||||||
<SelectItem key={option.id} value={option.id}>
|
<SelectItem key={option.id} value={option.id} className="items-start py-2">
|
||||||
{option.label}
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm">{option.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{option.description}</span>
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{selectedModeOption && (
|
|
||||||
<div className="rounded-lg border bg-muted/20 px-3 py-2">
|
|
||||||
<div className="text-sm font-medium leading-5">{selectedModeOption.label}</div>
|
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">{selectedModeOption.description}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">单次处理硬限制:最多 {MAX_BATCH_ITEMS} 张图片。</p>
|
<p className="text-xs text-muted-foreground">单次处理硬限制:最多 {MAX_BATCH_ITEMS} 张图片。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1591,7 +1595,12 @@ export function SceneFusionView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t bg-background shrink-0">
|
<div className="p-4 border-t bg-background shrink-0">
|
||||||
<Button className="w-full" size="lg" onClick={startProcessing} disabled={isProcessing || !modeCanStart}>
|
<Button
|
||||||
|
className="w-full h-12 text-base font-semibold shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100"
|
||||||
|
size="lg"
|
||||||
|
onClick={startProcessing}
|
||||||
|
disabled={isProcessing || !modeCanStart}
|
||||||
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
@ -1608,7 +1617,7 @@ export function SceneFusionView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-full min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950">
|
<div className="flex h-full min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950">
|
||||||
<div className="p-4 border-b flex items-center justify-between bg-white dark:bg-zinc-950 shrink-0">
|
<div className="h-16 px-4 border-b flex items-center justify-between bg-white dark:bg-zinc-950 shrink-0">
|
||||||
<h3 className="font-medium">处理结果</h3>
|
<h3 className="font-medium">处理结果</h3>
|
||||||
{(completedResultItems.length > 0 || (resultAllFinished && onBatchAddToCanvas)) && (
|
{(completedResultItems.length > 0 || (resultAllFinished && onBatchAddToCanvas)) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -1661,7 +1670,28 @@ export function SceneFusionView({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="aspect-[3/4] bg-muted/20 relative group">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"aspect-[3/4] bg-muted/20 relative group",
|
||||||
|
item.status === "completed" && (item.result || item.originalUrl) && "cursor-zoom-in"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
const previewUrl = item.originalUrl || item.result;
|
||||||
|
if (item.status === "completed" && previewUrl) {
|
||||||
|
openResultPreview(previewUrl, `任务 ${index + 1} 预览`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
const previewUrl = item.originalUrl || item.result;
|
||||||
|
if (item.status === "completed" && previewUrl) {
|
||||||
|
event.preventDefault();
|
||||||
|
openResultPreview(previewUrl, `任务 ${index + 1} 预览`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={item.status === "completed" && (item.result || item.originalUrl) ? 0 : -1}
|
||||||
|
>
|
||||||
{item.result ? (
|
{item.result ? (
|
||||||
<img src={item.result} alt="处理结果" className="w-full h-full object-cover" />
|
<img src={item.result} alt="处理结果" className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
@ -1691,11 +1721,21 @@ export function SceneFusionView({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => downloadImage(item.originalUrl || item.result!, `batch-${index + 1}.png`)}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
downloadImage(item.originalUrl || item.result!, `batch-${index + 1}.png`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="icon" variant="secondary" onClick={() => retrySingleItem(item)}>
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
retrySingleItem(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -1709,6 +1749,31 @@ export function SceneFusionView({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{previewImageUrl && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[31000] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
|
onClick={closeResultPreview}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-6xl max-h-[90vh] bg-background rounded-lg border shadow-xl overflow-hidden"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex h-12 items-center justify-between border-b px-4">
|
||||||
|
<span className="text-sm font-medium">{previewImageTitle}</span>
|
||||||
|
<Button size="icon" variant="ghost" onClick={closeResultPreview}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center bg-black/5 p-3">
|
||||||
|
<img
|
||||||
|
src={previewImageUrl}
|
||||||
|
alt={previewImageTitle}
|
||||||
|
className="max-h-[calc(90vh-72px)] w-auto max-w-full object-contain rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user