修复画布预览放大交互与触控板横向滑动翻页
This commit is contained in:
parent
be1ad6be1d
commit
1556826c1b
@ -488,34 +488,64 @@ async function normalizeAssistantImageMarkdown(
|
||||
if (!text || !text.includes("data:image")) return text;
|
||||
|
||||
const markdownDataUrlRegex = /!\[([^\]]*)\]\s*\(\s*(data:image\/[a-z0-9+.-]+;base64,[^)]+)\s*\)/gim;
|
||||
const matches = [...text.matchAll(markdownDataUrlRegex)];
|
||||
if (matches.length === 0) {
|
||||
return text;
|
||||
}
|
||||
const markdownMatches = [...text.matchAll(markdownDataUrlRegex)];
|
||||
let normalizedText = text;
|
||||
const uploadedUrls: string[] = [];
|
||||
|
||||
let normalizedText = "";
|
||||
let cursor = 0;
|
||||
if (markdownMatches.length > 0) {
|
||||
let rebuilt = "";
|
||||
let cursor = 0;
|
||||
|
||||
for (const match of matches) {
|
||||
const matchText = match[0];
|
||||
const dataUrl = match[2];
|
||||
const start = match.index ?? -1;
|
||||
if (start < 0) continue;
|
||||
for (const match of markdownMatches) {
|
||||
const matchText = match[0];
|
||||
const dataUrl = match[2];
|
||||
const start = match.index ?? -1;
|
||||
if (start < 0) continue;
|
||||
|
||||
normalizedText += text.slice(cursor, start);
|
||||
rebuilt += normalizedText.slice(cursor, start);
|
||||
|
||||
try {
|
||||
const uploadedUrl = await uploadDataUrlImage(dataUrl);
|
||||
normalizedText += ``;
|
||||
} catch (error) {
|
||||
console.error("[Chat API] Failed to upload inline data URL image:", error);
|
||||
normalizedText += matchText;
|
||||
try {
|
||||
const uploadedUrl = await uploadDataUrlImage(dataUrl);
|
||||
uploadedUrls.push(uploadedUrl);
|
||||
rebuilt += ``;
|
||||
} catch (error) {
|
||||
console.error("[Chat API] Failed to upload inline data URL image:", error);
|
||||
rebuilt += matchText;
|
||||
}
|
||||
|
||||
cursor = start + matchText.length;
|
||||
}
|
||||
|
||||
cursor = start + matchText.length;
|
||||
rebuilt += normalizedText.slice(cursor);
|
||||
normalizedText = rebuilt;
|
||||
}
|
||||
|
||||
if (normalizedText.includes("data:image")) {
|
||||
const rawDataUrlRegex = /data:image\/[a-z0-9+.-]+;base64,[a-z0-9+/=\s]+/gim;
|
||||
const rawMatches = normalizedText.match(rawDataUrlRegex) ?? [];
|
||||
|
||||
for (const rawDataUrl of rawMatches) {
|
||||
try {
|
||||
const uploadedUrl = await uploadDataUrlImage(rawDataUrl);
|
||||
uploadedUrls.push(uploadedUrl);
|
||||
normalizedText = normalizedText.replace(rawDataUrl, uploadedUrl);
|
||||
} catch (error) {
|
||||
console.error("[Chat API] Failed to normalize raw data URL image:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasMarkdownImage = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/i.test(normalizedText);
|
||||
if (!hasMarkdownImage && uploadedUrls.length > 0) {
|
||||
const uniqueUrls = [...new Set(uploadedUrls)];
|
||||
console.log(
|
||||
`[Chat API] Converted ${uniqueUrls.length} data URL image(s) to hosted link(s)`
|
||||
);
|
||||
return uniqueUrls
|
||||
.map((url) => ``)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
normalizedText += text.slice(cursor);
|
||||
return normalizedText;
|
||||
}
|
||||
|
||||
|
||||
@ -465,7 +465,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="relative h-dvh w-full overflow-hidden bg-background">
|
||||
<div className="relative h-dvh w-full overflow-hidden overscroll-none bg-background">
|
||||
|
||||
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */}
|
||||
<div className="absolute inset-0 z-0" key={`workspace-${id}`}>
|
||||
|
||||
@ -161,6 +161,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
// 🆕 标签悬停预览状态
|
||||
const [tagPreview, setTagPreview] = useState<TagPreviewInfo | null>(null);
|
||||
const previewTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [fullscreenPreview, setFullscreenPreview] = useState<{
|
||||
url: string;
|
||||
alt?: string;
|
||||
} | null>(null);
|
||||
|
||||
// 🆕 图片参数状态
|
||||
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
||||
@ -192,6 +196,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fullscreenPreview) return;
|
||||
const handleEsc = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setFullscreenPreview(null);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
return () => window.removeEventListener("keydown", handleEsc);
|
||||
}, [fullscreenPreview]);
|
||||
|
||||
// Canvas store
|
||||
const addNode = useCanvasStore((state) => state.addNode);
|
||||
const nodes = useCanvasStore((state) => state.nodes);
|
||||
@ -1419,9 +1434,14 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
className="max-w-full h-auto cursor-pointer hover:scale-105 transition-transform duration-300"
|
||||
className="max-w-full h-auto cursor-zoom-in hover:scale-105 transition-transform duration-300"
|
||||
style={{ maxHeight: '200px' }}
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
onClick={() => setFullscreenPreview({ url })}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setFullscreenPreview({ url });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -1466,6 +1486,35 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{fullscreenPreview && createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] bg-black/90 flex items-center justify-center cursor-zoom-out"
|
||||
onClick={() => setFullscreenPreview(null)}
|
||||
>
|
||||
<button
|
||||
className="absolute top-4 right-4 p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
|
||||
onClick={() => setFullscreenPreview(null)}
|
||||
aria-label="关闭预览"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<img
|
||||
src={fullscreenPreview.url}
|
||||
alt={fullscreenPreview.alt || "图片预览"}
|
||||
className="max-w-[95vw] max-h-[95vh] object-contain"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/60 text-sm">
|
||||
点击空白处或按 Esc 关闭
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
|
||||
<div className="p-4 bg-background/80 backdrop-blur-md border-t">
|
||||
|
||||
@ -77,6 +77,21 @@ export function CanvasWorkspace() {
|
||||
return () => window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
|
||||
// macOS 触控板横向双指滑动会触发浏览器前进/后退,必须使用非 passive 监听阻止默认行为
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const preventTrackpadHistorySwipe = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
container.addEventListener("wheel", preventTrackpadHistorySwipe, { passive: false });
|
||||
return () => {
|
||||
container.removeEventListener("wheel", preventTrackpadHistorySwipe);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isFileDrag = useCallback((dataTransfer?: DataTransfer | null) => {
|
||||
if (!dataTransfer) return false;
|
||||
const types = Array.from(dataTransfer.types || []);
|
||||
@ -599,7 +614,7 @@ export function CanvasWorkspace() {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative w-full h-full overflow-hidden bg-background",
|
||||
"relative w-full h-full overflow-hidden overscroll-none touch-none bg-background",
|
||||
tool === 'pan' && "cursor-grab",
|
||||
isPanning && "cursor-grabbing"
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user