Files
botgroup.chat/src/components/SharePoster.tsx
2025-02-26 08:43:29 +08:00

199 lines
6.1 KiB
TypeScript

import React, { useRef, useEffect } from 'react';
import domtoimage from 'dom-to-image';
import { Dialog, DialogContent, DialogClose } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Share2, Download } from 'lucide-react';
import { toast } from 'sonner';
interface SharePosterProps {
isOpen: boolean;
onClose: () => void;
chatAreaRef: React.RefObject<HTMLDivElement>;
}
export function SharePoster({ isOpen, onClose, chatAreaRef }: SharePosterProps) {
const posterRef = useRef<HTMLDivElement>(null);
const [posterImage, setPosterImage] = React.useState<string>('');
useEffect(() => {
if (isOpen && chatAreaRef.current) {
generatePoster();
}
}, [isOpen]);
const generatePoster = async () => {
if (!chatAreaRef.current) return;
await document.fonts.ready;
try {
const messageContainer = chatAreaRef.current.querySelector('.space-y-4');
if (!messageContainer) return;
const qrCode = messageContainer.querySelector('#qrcode');
if (qrCode) {
qrCode.classList.remove('hidden');
}
// 预处理所有图片
const preloadImages = async () => {
const images = Array.from(messageContainer.getElementsByTagName('img'));
await Promise.all(images.map(async (img) => {
try {
const response = await fetch(img.src, {
mode: 'cors',
credentials: 'omit'
});
const blob = await response.blob();
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
img.src = base64 as string;
} catch (error) {
console.error('图片预处理失败:', error);
}
}));
};
await preloadImages();
const originalScroll = chatAreaRef.current.scrollTop;
chatAreaRef.current.scrollTop = 0;
const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth);
const extraSpace = 6;
const targetWidth = viewportWidth * 0.95 - (extraSpace * 2);
const currentWidth = messageContainer.getBoundingClientRect().width;
const scale = targetWidth / currentWidth;
const currentHeight = messageContainer.scrollHeight;
const adjustedHeight = currentHeight * scale;
const dataUrl = await domtoimage.toSvg(messageContainer as HTMLElement, {
bgcolor: '#f3f4f6',
scale: 1, // 回到较安全的值
width: targetWidth + (extraSpace * 5),
height: adjustedHeight + (extraSpace * 5),
style: {
padding: `${extraSpace}px`,
margin: '0 auto',
width: '120%',
height: '110%',
transform: `scale(${scale})`,
transformOrigin: 'top left',
background: '#f3f4f6',
boxSizing: 'border-box'
},
quality: 1.0
});
chatAreaRef.current.scrollTop = originalScroll;
setPosterImage(dataUrl);
} catch (error) {
console.error('生成海报失败:', error);
}
};
const handleDownload = async () => {
if (!posterImage) return;
try {
// 创建一个新的图片对象
const img = new Image();
img.crossOrigin = 'anonymous';
// 等待图片加载完成
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = posterImage;
});
// 创建高分辨率canvas
const scale = 2;
const canvas = document.createElement('canvas');
canvas.width = img.width * scale;
canvas.height = img.height * scale;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建canvas上下文');
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
// 转换为Blob
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
resolve(blob!);
}, 'image/png', 1.0);
});
// 检查是否为移动设备
if (/Android|iPhone|iPad|iPod/i.test(navigator.userAgent) && navigator.share) {
// 使用系统分享
await navigator.share({
files: [new File([blob], 'chat-history.png', { type: 'image/png' })],
title: '聊天记录',
});
} else {
// PC端使用传统下载方式
const pngUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = pngUrl;
a.download = 'chat-history.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(pngUrl);
}
} catch (error) {
console.error('转换图片失败:', error);
toast.error('保存图片失败,请重试');
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => {
if (!open) {
// Hide QR code when dialog closes
const qrCode = chatAreaRef.current?.querySelector('#qrcode');
if (qrCode) {
qrCode.classList.add('hidden');
}
onClose();
}
}}>
<DialogContent className="max-w-[100vw] w-full sm:max-w-[50vw] max-h-[90vh] flex flex-col p-0">
{/* 图片容器 */}
<div className="flex-1 overflow-auto ">
{posterImage && (
<div className="flex items-center justify-center min-h-full">
<img
src={posterImage}
alt="Share Poster"
className="w-full w-auto h-auto"
style={{
objectFit: 'contain',
imageRendering: 'crisp-edges',
WebkitFontSmoothing: 'antialiased'
}}
/>
</div>
)}
</div>
<div className="flex items-center justify-center gap-2 p-2 sm:p-4 border-t">
<Button onClick={handleDownload}>
</Button>
</div>
</DialogContent>
</Dialog>
);
}