perf: 图片预览接入代理以提升加载速度

This commit is contained in:
Your Name 2026-03-04 00:06:14 +08:00
parent efa7cff431
commit 93e5493993
4 changed files with 123 additions and 17 deletions

View File

@ -44,6 +44,7 @@ import {
extractOriginalImageUrlFromAlt,
inferOriginalImageUrl,
} from "@/lib/image-url";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import type { Attachment, ChatMessage } from "@/lib/types";
import { type CanvasLaunchPayload, getCanvasLaunchStorageKey } from "@/lib/canvas-launch";
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
@ -140,6 +141,14 @@ function extractPlainImageUrls(text: string): string[] {
return matches ? Array.from(new Set(matches)) : [];
}
function buildPreviewImageUrl(url: string, width: number): string {
return buildImageProxyUrl(url, {
width,
quality: 86,
format: "webp",
});
}
function normalizeMediaUrl(url: string): string {
const raw = String(url || "").trim();
if (!raw) return "";
@ -6015,7 +6024,18 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
<span className="flex min-w-0 items-center gap-2">
<span className="h-5 w-5 overflow-hidden rounded-full border border-[#d4d9df] bg-white">
{model.avatarUrl ? (
<img src={model.avatarUrl} alt={model.displayName} className="h-full w-full object-cover" />
<img
src={buildPreviewImageUrl(model.avatarUrl, 40)}
alt={model.displayName}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== model.avatarUrl) {
event.currentTarget.src = model.avatarUrl || "";
}
}}
/>
) : (
<span className="flex h-full w-full items-center justify-center text-[9px] font-medium text-[#6b7280]">
{model.id === "auto" ? "A" : model.displayName.slice(0, 1)}
@ -6370,9 +6390,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
className="inline-flex items-center gap-1 rounded-xl bg-white/90 px-2 py-1 text-xs"
>
<img
src={fileUrl}
src={buildPreviewImageUrl(fileUrl, 48)}
alt=""
className="w-5 h-5 object-cover rounded"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== fileUrl) {
event.currentTarget.src = fileUrl;
}
}}
/>
<span className="max-w-[80px] truncate text-foreground">{part.name || 'Image'}</span>
</span>
@ -6403,9 +6430,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
className="inline-flex items-center gap-1 rounded-xl bg-white/90 px-2 py-1 text-xs"
>
<img
src={att.url}
src={buildPreviewImageUrl(att.url, 48)}
alt=""
className="w-5 h-5 object-cover rounded"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== att.url) {
event.currentTarget.src = att.url;
}
}}
/>
<span className="max-w-[80px] truncate text-foreground">{att.name || 'Image'}</span>
</span>
@ -6580,10 +6614,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
)}
>
<img
src={url}
src={buildPreviewImageUrl(url, 960)}
alt=""
className="block max-w-full h-auto cursor-zoom-in transition-transform duration-300 group-hover:scale-[1.02]"
style={{ maxHeight: "260px" }}
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== url) {
event.currentTarget.src = url;
}
}}
onClick={() => setFullscreenPreview({ url })}
onDoubleClick={(e) => {
e.preventDefault();
@ -7218,9 +7259,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
onClick={() => detailSlotInputRefs.current[index]?.click()}
>
<img
src={slotAttachment.url}
src={buildPreviewImageUrl(slotAttachment.url, 520)}
alt={config.label}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== slotAttachment.url) {
event.currentTarget.src = slotAttachment.url;
}
}}
/>
</button>
<div className="absolute inset-x-0 bottom-0 bg-black/60 px-1 py-1 text-center text-[10px] text-white backdrop-blur-sm">
@ -7591,9 +7639,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
{slotAttachment ? (
<>
<img
src={slotAttachment.url}
src={buildPreviewImageUrl(slotAttachment.url, 256)}
alt={slotAttachment.name || label}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== slotAttachment.url) {
event.currentTarget.src = slotAttachment.url;
}
}}
/>
<div className="absolute inset-x-0 bottom-0 bg-black/55 px-1 py-0.5 text-center text-[11px] text-white">
{label}
@ -7782,9 +7837,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
>
{agentAvatarUrl ? (
<img
src={agentAvatarUrl}
src={buildPreviewImageUrl(agentAvatarUrl, 64)}
alt={agentDisplayName}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== agentAvatarUrl) {
event.currentTarget.src = agentAvatarUrl;
}
}}
/>
) : (
<Sparkles className="h-4 w-4 text-[#8b95a7]" />
@ -7825,9 +7887,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
<span className="h-6 w-6 overflow-hidden rounded-full bg-white flex items-center justify-center">
{selectDisplayModel?.avatarUrl ? (
<img
src={selectDisplayModel.avatarUrl}
src={buildPreviewImageUrl(selectDisplayModel.avatarUrl, 64)}
alt={selectDisplayModel.displayName || "模型头像"}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== selectDisplayModel.avatarUrl) {
event.currentTarget.src = selectDisplayModel.avatarUrl || "";
}
}}
/>
) : (
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-[#4b5563]">
@ -7931,9 +8000,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
<div className="flex justify-center bg-zinc-100 dark:bg-zinc-800">
<div className="relative inline-block">
<img
src={tagPreview.url}
src={buildPreviewImageUrl(tagPreview.url, 720)}
alt={tagPreview.name}
className="block max-w-full max-h-[180px] h-auto"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== tagPreview.url) {
event.currentTarget.src = tagPreview.url;
}
}}
/>
{/* 如果是物品标签,显示标注位置 */}
{tagPreview.pickedObject && (

View File

@ -17,6 +17,7 @@ import {
import { canvasService } from "@/services/canvas";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import type { CanvasProject } from "@/types/canvas";
interface CanvasFileListProps {
@ -139,9 +140,20 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
<div className="h-12 w-12 rounded-lg bg-muted/50 overflow-hidden shrink-0 border">
{project.thumbnail ? (
<img
src={project.thumbnail}
src={buildImageProxyUrl(project.thumbnail, {
width: 144,
quality: 82,
format: "webp",
})}
alt={project.title}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== project.thumbnail) {
event.currentTarget.src = project.thumbnail || "";
}
}}
/>
) : (
<div className="h-full w-full flex items-center justify-center">

View File

@ -10,6 +10,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { canvasService } from "@/services/canvas";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import type { CanvasProject } from "@/types/canvas";
interface CanvasFileListPanelProps {
@ -138,9 +139,20 @@ export function CanvasFileListPanel({ currentId, onClose }: CanvasFileListPanelP
<div className="h-14 w-14 rounded-lg bg-muted/50 overflow-hidden shrink-0 border">
{project.thumbnail ? (
<img
src={project.thumbnail}
src={buildImageProxyUrl(project.thumbnail, {
width: 160,
quality: 82,
format: "webp",
})}
alt={project.title}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== project.thumbnail) {
event.currentTarget.src = project.thumbnail || "";
}
}}
/>
) : (
<div className="h-full w-full flex items-center justify-center">

View File

@ -5,6 +5,7 @@ import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maxi
import { toast } from "sonner";
import { ImageAnnotator } from "./image-annotator";
import { AngleEditDialog } from "./angle-edit-dialog";
import { buildImageProxyUrl } from "@/lib/image-delivery";
export function ImagePreview({
src,
@ -174,11 +175,12 @@ export function ImagePreview({
};
const { thumbnail, original, maxWidth, is4K } = parseImageUrls();
const preferOriginalForPreview = Boolean(original && original !== thumbnail);
const previewSrc = preferOriginalForPreview ? original : thumbnail;
const previewSrcSet = original
? `${thumbnail} 1x, ${original} 2x`
: undefined;
const previewWidth = Math.min(2200, Math.max(640, Math.round(maxWidth * 2)));
const previewSrc = buildImageProxyUrl(thumbnail || original, {
width: previewWidth,
quality: 86,
format: "webp",
});
// 下载图片函数(下载原图)
const handleDownload = async () => {
@ -252,7 +254,6 @@ export function ImagePreview({
<div className="group relative inline-block max-w-full mr-3 mb-3">
<img
src={previewSrc}
srcSet={previewSrcSet}
alt="Generated Image"
className="cursor-pointer rounded-lg transition-opacity hover:opacity-90"
style={{
@ -263,6 +264,11 @@ export function ImagePreview({
}}
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== (thumbnail || original)) {
event.currentTarget.src = thumbnail || original;
}
}}
onClick={() => setIsOpen(true)}
/>