feat(canvas): preview generated task images in app

This commit is contained in:
2026-09-15 14:58:21 +08:00
parent 80e52496d2
commit 41664b2ae3
6 changed files with 357 additions and 3 deletions

View File

@@ -0,0 +1,103 @@
import { useEffect, useState } from 'react';
import { ImageIcon, Loader2, Maximize2, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { resolveImageWorkspaceAssetUrl } from '@/lib/image-workspace';
import type { DesignAsset } from '../../../shared/image-workspace';
function ImagePreviewContent({ asset }: { asset: DesignAsset }) {
const [source, setSource] = useState<string | null>(null);
const [loaded, setLoaded] = useState(false);
const [failed, setFailed] = useState(false);
const [originalSize, setOriginalSize] = useState(false);
useEffect(() => {
let active = true;
void resolveImageWorkspaceAssetUrl(asset.contentPath)
.then((url) => { if (active) setSource(url); })
.catch(() => { if (active) setFailed(true); });
return () => { active = false; };
}, [asset.contentPath]);
return (
<>
<header className="flex items-center justify-between gap-3 border-b border-border/70 px-4 py-3">
<div className="min-w-0">
<DialogTitle className="text-base">图片预览</DialogTitle>
<DialogDescription className="mt-1 text-xs">
{asset.width} × {asset.height}
</DialogDescription>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={!loaded || failed}
onClick={() => setOriginalSize((value) => !value)}
>
{originalSize ? '适应窗口' : '原始尺寸'}
</Button>
<DialogClose asChild>
<Button type="button" variant="ghost" size="icon" aria-label="关闭图片预览">
<X className="h-4 w-4" aria-hidden="true" />
</Button>
</DialogClose>
</div>
</header>
<div className="relative min-h-0 overflow-auto bg-surface-subtle/50" data-testid="design-image-preview-viewport">
{failed ? (
<div role="alert" className="flex h-full flex-col items-center justify-center gap-3 p-6 text-sm text-muted-foreground">
<ImageIcon className="h-8 w-8" aria-hidden="true" />
图片暂时无法加载,请关闭后重试。
</div>
) : (
<>
{!loaded && (
<div role="status" className="absolute inset-0 flex items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" />
正在加载图片…
</div>
)}
{source && (
<img
src={source}
alt="AI 作品大图"
className={originalSize ? 'block max-w-none' : 'h-full w-full object-contain'}
onLoad={() => setLoaded(true)}
onError={() => setFailed(true)}
/>
)}
</>
)}
</div>
</>
);
}
export function DesignImagePreviewButton({ asset }: { asset: DesignAsset }) {
return (
<Dialog>
<DialogTrigger asChild>
<button
type="button"
aria-label="放大查看图片"
title="放大查看图片"
className="absolute bottom-1 left-1 flex h-7 w-7 items-center justify-center rounded-lg bg-background/90 text-muted-foreground shadow-sm hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
>
<Maximize2 className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</DialogTrigger>
<DialogContent className="h-[85vh] max-w-[min(94vw,100rem)] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden p-0">
<ImagePreviewContent key={asset.contentPath} asset={asset} />
</DialogContent>
</Dialog>
);
}

View File

@@ -4,6 +4,7 @@ import { saveImageWorkspaceAsset } from '@/lib/image-workspace';
import { cn } from '@/lib/utils';
import type { DesignGenerationTask, DesignWorkspace } from '../../../shared/image-workspace';
import { DesignAssetThumbnail } from './DesignAssetThumbnail';
import { DesignImagePreviewButton } from './DesignImagePreview';
import { youthTaskIssueMessage } from './youth-issue-copy';
const STATUS_LABELS: Record<DesignGenerationTask['status'], string> = {
@@ -41,7 +42,7 @@ export function DesignPlanHistory({ workspace }: { workspace: DesignWorkspace })
if (tasks.length === 0) return null;
return (
<div className="space-y-2" data-testid="design-plan-history">
<div key={workspace.workspace.workspaceId} className="space-y-2" data-testid="design-plan-history">
{tasks.map((task) => {
const resultAssets = task.resultAssetIds
.map((assetId) => workspace.assets.find((asset) => asset.assetId === assetId) ?? null)
@@ -111,6 +112,7 @@ export function DesignPlanHistory({ workspace }: { workspace: DesignWorkspace })
{resultAssets.map((asset) => (
<div key={asset.assetId} className="group/result relative">
<DesignAssetThumbnail asset={asset} className="h-20 w-20 rounded-xl" />
{asset.mediaType === 'image' && <DesignImagePreviewButton asset={asset} />}
<button
type="button"
aria-label="保存作品到电脑"