fix(scene-fusion): refine UI layout and add result image preview modal
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-06 00:19:51 +08:00
parent 309c4c8e28
commit 8688cac064
2 changed files with 91 additions and 27 deletions

View File

@ -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">
<SceneFusionView
onBack={() => onOpenChange(false)}
onBatchAddToCanvas={onBatchAddToCanvas}
/>
</div>

View File

@ -14,6 +14,7 @@ import {
Upload,
Wand2,
Eraser,
X,
} from "lucide-react";
import { toast } from "sonner";
import { requestLongTaskResult } from "@/lib/client/long-task";
@ -22,7 +23,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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 MAX_BATCH_ITEMS = 10;
@ -47,7 +48,6 @@ interface ProcessItem {
}
interface SceneFusionViewProps {
onBack: () => void;
initialProductImage?: string | null;
initialProductName?: string;
onBatchAddToCanvas?: (imageUrls: string[]) => void | Promise<void>;
@ -121,7 +121,6 @@ function getModePrompt(mode: BatchMode) {
}
export function SceneFusionView({
onBack,
initialProductImage,
initialProductName,
onBatchAddToCanvas,
@ -154,6 +153,8 @@ export function SceneFusionView({
const [randomPrompt, setRandomPrompt] = useState("");
const [batchItems, setBatchItems] = useState<ProcessItem[]>([]);
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
const [previewImageTitle, setPreviewImageTitle] = useState<string>("处理结果预览");
const productInputRef = 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 (
<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="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="flex items-center justify-between">
<div className="flex items-center gap-2">
<Layers className="w-4 h-4 text-primary" />
<span className="font-medium">批量处理</span>
</div>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={onBack}>
返回
</Button>
<div className="border-b h-16 px-4 shrink-0 bg-white dark:bg-zinc-950 flex items-center">
<div className="flex items-center gap-2">
<Layers className="w-4 h-4 text-primary" />
<span className="font-medium">批量处理</span>
</div>
</div>
@ -1159,22 +1164,21 @@ export function SceneFusionView({
<Label className="text-base font-medium">处理模式</Label>
<Select value={mode} onValueChange={(value) => setMode(value as BatchMode)} disabled={isProcessing}>
<SelectTrigger className="h-10 bg-white dark:bg-zinc-950">
<SelectValue placeholder="请选择处理模式" />
<span className="truncate">
{selectedModeOption?.label || "请选择处理模式"}
</span>
</SelectTrigger>
<SelectContent className="z-[30000] !bg-white !dark:bg-zinc-950">
{modeOptions.map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.label}
<SelectItem key={option.id} value={option.id} className="items-start py-2">
<div className="flex flex-col">
<span className="text-sm">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</div>
</SelectItem>
))}
</SelectContent>
</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>
</div>
@ -1591,7 +1595,12 @@ export function SceneFusionView({
</div>
<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 ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
@ -1608,7 +1617,7 @@ export function SceneFusionView({
</div>
<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>
{(completedResultItems.length > 0 || (resultAllFinished && onBatchAddToCanvas)) && (
<div className="flex items-center gap-2">
@ -1661,7 +1670,28 @@ export function SceneFusionView({
</span>
</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 ? (
<img src={item.result} alt="处理结果" className="w-full h-full object-cover" />
) : (
@ -1691,11 +1721,21 @@ export function SceneFusionView({
<Button
size="icon"
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" />
</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" />
</Button>
</div>
@ -1709,6 +1749,31 @@ export function SceneFusionView({
</ScrollArea>
</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>
);
}