diff --git a/package-lock.json b/package-lock.json
index 844bf86..34493dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,8 @@
"@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dom-to-image": "^2.6.0",
+ "dom-to-image-more": "^3.5.0",
"html2canvas": "^1.4.1",
"katex": "^0.16.21",
"lucide-react": "^0.263.1",
@@ -26,6 +28,7 @@
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
+ "sonner": "^2.0.0",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
@@ -2615,6 +2618,16 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
+ "node_modules/dom-to-image": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmmirror.com/dom-to-image/-/dom-to-image-2.6.0.tgz",
+ "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA=="
+ },
+ "node_modules/dom-to-image-more": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmmirror.com/dom-to-image-more/-/dom-to-image-more-3.5.0.tgz",
+ "integrity": "sha512-VF/vwfHsPNMHJb5W/5sAmco3UIlEWSEFLppInQwqwN4joUvBULDwE3CqVcUDkUWleke/nZ5KwIVSrrFlGw7WPA=="
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -5617,6 +5630,15 @@
"node": ">=8"
}
},
+ "node_modules/sonner": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/sonner/-/sonner-2.0.0.tgz",
+ "integrity": "sha512-3WeSl3WrEdhmdiTniT8JsCiVRVDOdn7k+4MG2drqzSMOeqrExm03HIwDBPKuq52JBqL7wRLG17QBIiSleUbljw==",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
diff --git a/package.json b/package.json
index 059eae2..66e8518 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,8 @@
"@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dom-to-image": "^2.6.0",
+ "dom-to-image-more": "^3.5.0",
"html2canvas": "^1.4.1",
"katex": "^0.16.21",
"lucide-react": "^0.263.1",
@@ -26,6 +28,7 @@
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
+ "sonner": "^2.0.0",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
diff --git a/src/components/ChatUI.tsx b/src/components/ChatUI.tsx
index b41ac6d..4081d65 100644
--- a/src/components/ChatUI.tsx
+++ b/src/components/ChatUI.tsx
@@ -448,8 +448,8 @@ const ChatUI = () => {
{/* Main Chat Area */}
-
-
+
+
{messages.map((message) => (
diff --git a/src/components/SharePoster.tsx b/src/components/SharePoster.tsx
index 8871b4d..1025a76 100644
--- a/src/components/SharePoster.tsx
+++ b/src/components/SharePoster.tsx
@@ -1,8 +1,9 @@
import React, { useRef, useEffect } from 'react';
-import html2canvas from 'html2canvas';
-import { Dialog, DialogContent } from "@/components/ui/dialog";
+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;
@@ -22,132 +23,148 @@ export function SharePoster({ isOpen, onClose, chatAreaRef }: SharePosterProps)
const generatePoster = async () => {
if (!chatAreaRef.current) return;
-
+ await document.fonts.ready;
+
try {
- // 克隆原始聊天区域以保持样式
- const clonedChat = chatAreaRef.current.cloneNode(true) as HTMLElement;
-
- // 创建临时容器并设置与原始容器相同的样式
- const tempContainer = document.createElement('div');
- tempContainer.style.position = 'absolute';
- tempContainer.style.left = '-9999px';
- tempContainer.style.width = chatAreaRef.current.offsetWidth + 'px';
- document.body.appendChild(tempContainer);
- tempContainer.appendChild(clonedChat);
+ const messageContainer = chatAreaRef.current.querySelector('.space-y-4');
+ if (!messageContainer) return;
- // 确保所有图片都已加载
- const images = Array.from(clonedChat.getElementsByTagName('img'));
- await Promise.all(images.map(img => {
- if (img.complete) return Promise.resolve();
- return new Promise((resolve) => {
- img.onload = resolve;
- img.onerror = resolve;
- });
- }));
+ // 预处理所有图片
+ 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);
+ }
+ }));
+ };
- const canvas = await html2canvas(clonedChat, {
- backgroundColor: '#f3f4f6',
- scale: 2,
- useCORS: true, // 允许跨域图片
- logging: false,
- onclone: (document) => {
- // 确保所有样式都被正确应用
- const styles = Array.from(document.styleSheets);
- styles.forEach(styleSheet => {
- try {
- if (styleSheet.cssRules) {
- Array.from(styleSheet.cssRules).forEach(rule => {
- clonedChat.style.cssText += rule.cssText;
- });
- }
- } catch (e) {
- console.warn('无法访问样式表:', e);
- }
- });
- }
+ await preloadImages();
+
+ const originalScroll = chatAreaRef.current.scrollTop;
+ chatAreaRef.current.scrollTop = 0;
+
+ const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth);
+ const extraSpace = 20;
+ 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 * 2),
+ height: adjustedHeight + (extraSpace * 2),
+ style: {
+ padding: `${extraSpace}px`,
+ margin: '0',
+ width: '110%',
+ height: '110%',
+ transform: `scale(${scale})`,
+ transformOrigin: 'top left',
+ background: '#f3f4f6',
+ boxSizing: 'border-box'
+ },
+ quality: 1.0
});
-
- // 清理临时元素
- document.body.removeChild(tempContainer);
-
- setPosterImage(canvas.toDataURL('image/png'));
+
+ chatAreaRef.current.scrollTop = originalScroll;
+ setPosterImage(dataUrl);
} catch (error) {
console.error('生成海报失败:', error);
}
};
- const handleShare = async () => {
- if (!posterImage) return;
- try {
- const blob = await fetch(posterImage).then(r => r.blob());
- const filesArray = [
- new File([blob], 'chat-history.png', { type: 'image/png' })
- ];
-
- if (navigator.share && navigator.canShare({ files: filesArray })) {
- await navigator.share({
- files: filesArray,
- title: '聊天记录',
- });
- } else {
- throw new Error('不支持系统分享');
- }
- } catch (error) {
- console.error('分享失败:', error);
- alert('分享失败,请尝试保存图片');
- }
- };
-
- const handleDownload = () => {
+ const handleDownload = async () => {
if (!posterImage) return;
- const a = document.createElement('a');
- a.href = posterImage;
- a.download = 'chat-history.png';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
+ 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; // 将分辨率提高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);
+
+ // 使用更高质量的PNG导出
+ const pngUrl = canvas.toDataURL('image/png', 1.0);
+ const a = document.createElement('a');
+ a.href = pngUrl;
+ a.download = 'chat-history.png';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ } catch (error) {
+ console.error('转换图片失败:', error);
+ toast.error('保存图片失败,请重试');
+ }
};
return (