perf: 图片预览接入代理以提升加载速度
This commit is contained in:
parent
efa7cff431
commit
93e5493993
@ -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 && (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user