From e9b0e1af02b05de795420ccac832823e62eb8516 Mon Sep 17 00:00:00 2001
From: hongjli <3117313295@qq.com>
Date: 星期日, 27 四月 2025 20:21:52 +0800
Subject: [PATCH] 渲染echarts图

---
 package-lock.json     |   54 +++++++
 package.json          |    2 
 src/app/chat/page.tsx |  307 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 358 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 2340ca5..aff8b82 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,8 @@
         "@headlessui/react": "^2.2.1",
         "@types/styled-components": "^5.1.34",
         "axios": "^1.8.4",
+        "echarts": "^5.6.0",
+        "echarts-for-react": "^3.0.2",
         "framer-motion": "^12.6.0",
         "next": "15.2.4",
         "react": "^19.0.0",
@@ -2737,6 +2739,36 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/echarts": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "node_modules/echarts-for-react": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
+      "integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "size-sensor": "^1.0.1"
+      },
+      "peerDependencies": {
+        "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0",
+        "react": "^15.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/echarts/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
     "node_modules/emoji-regex": {
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -3399,7 +3431,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/fast-glob": {
@@ -7063,6 +7094,12 @@
         "is-arrayish": "^0.3.1"
       }
     },
+    "node_modules/size-sensor": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
+      "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==",
+      "license": "ISC"
+    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -7930,6 +7967,21 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zrender/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
     "node_modules/zustand": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
diff --git a/package.json b/package.json
index 131c9d7..444a50d 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
     "@headlessui/react": "^2.2.1",
     "@types/styled-components": "^5.1.34",
     "axios": "^1.8.4",
+    "echarts": "^5.6.0",
+    "echarts-for-react": "^3.0.2",
     "framer-motion": "^12.6.0",
     "next": "15.2.4",
     "react": "^19.0.0",
diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index 1301f70..e4d65cd 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -8,6 +8,11 @@
 import remarkGfm from 'remark-gfm';
 import rehypeRaw from 'rehype-raw';
 import rehypeSanitize from 'rehype-sanitize';
+import dynamic from 'next/dynamic';
+import { ReactNode } from 'react';
+
+// 鍔ㄦ�佸鍏� ECharts锛岀‘淇濆畠鍙湪瀹㈡埛绔覆鏌�
+const ReactECharts = dynamic(() => import('echarts-for-react'), { ssr: false });
 
 interface Message {
   role: 'user' | 'assistant';
@@ -31,6 +36,274 @@
 // 榛樿鐢ㄦ埛澶村儚
 const DEFAULT_USER_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E";
 const AI_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234F46E5'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 1c4.96 0 9 4.04 9 9s-4.04 9-9 9-9-4.04-9-9 4.04-9 9-9zm0 3.5c-2.48 0-4.5 2.02-4.5 4.5h1.5c0-1.66 1.34-3 3-3s3 1.34 3 3h1.5c0-2.48-2.02-4.5-4.5-4.5zM8 13h2v2H8v-2zm6 0h2v2h-2v-2z'/%3E%3C/svg%3E";
+
+// 娣诲姞鐢ㄤ簬瑙f瀽鍜屾覆鏌� ECharts 鐨勭粍浠�
+function EchartsRenderer({ code }: { code: string }) {
+  const [error, setError] = useState<string | null>(null);
+  const [isLoaded, setIsLoaded] = useState(false);
+  const chartContainerRef = useRef<HTMLDivElement>(null);
+  const codeRef = useRef<string>(code);
+  const renderAttemptedRef = useRef(false);
+  
+  // 鏇存柊浠g爜寮曠敤
+  useEffect(() => {
+    codeRef.current = code;
+  }, [code]);
+
+  // 寤惰繜鍒濆鍖栫洿鍒版秷鎭ǔ瀹�
+  useEffect(() => {
+    // 涓虹‘淇濇祦寮忓唴瀹圭ǔ瀹氾紝璁剧疆寤惰繜
+    const timer = setTimeout(() => {
+      setIsLoaded(true);
+    }, 500);
+    
+    return () => clearTimeout(timer);
+  }, []);
+
+  // 涓昏娓叉煋閫昏緫 - 浠呭湪缁勪欢鏍囪涓哄凡鍔犺浇鍚庢墽琛�
+  useEffect(() => {
+    // 濡傛灉缁勪欢鏈爣璁颁负宸插姞杞芥垨宸茬粡灏濊瘯杩囨覆鏌擄紝鍒欓��鍑�
+    if (!isLoaded || renderAttemptedRef.current) return;
+    
+    // 纭繚鍙湪瀹㈡埛绔墽琛�
+    if (typeof window === 'undefined' || !chartContainerRef.current) return;
+    
+    // 鏍囪涓哄凡灏濊瘯娓叉煋锛岄槻姝㈤噸澶嶆覆鏌�
+    renderAttemptedRef.current = true;
+    
+    // 闃叉浠g爜鎵ц涓殑鍙橀噺鍐茬獊
+    const safeCode = codeRef.current.replace(/window\.option/g, '_uniqueOptionVar');
+    
+    // 绛夊緟DOM鏇存柊瀹屾垚
+    setTimeout(() => {
+      let chartInstance: any = null;
+      
+      const initChart = async () => {
+        try {
+          // 鍔ㄦ�佸鍏charts
+          const echarts = await import('echarts');
+          
+          if (!chartContainerRef.current) {
+            console.warn('鍥捐〃瀹瑰櫒宸蹭笉瀛樺湪');
+            return;
+          }
+          
+          // 鍒濆鍖栧浘琛�
+          chartInstance = echarts.init(chartContainerRef.current);
+          
+          // 浣跨敤Function鏋勯�犲櫒锛屼絾娣诲姞棰濆鐨勪繚鎶�
+          try {
+            const getFinalOption = new Function(`
+              "use strict";
+              let option;
+              let _uniqueOptionVar;
+              try {
+                ${safeCode}
+                return option || _uniqueOptionVar;
+              } catch (e) {
+                console.error("ECharts鍐呴儴鎵ц閿欒:", e);
+                return null;
+              }
+            `);
+            
+            const chartOption = getFinalOption();
+            
+            if (chartOption) {
+              chartInstance.setOption(chartOption);
+              setError(null);
+            } else {
+              console.warn('鏈幏鍙栧埌鏈夋晥鐨勫浘琛ㄩ厤缃紝浣跨敤澶囩敤閰嶇疆');
+              useFallbackOption(chartInstance);
+            }
+          } catch (e) {
+            console.error('鎵ц鍥捐〃浠g爜閿欒:', e);
+            setError(e instanceof Error ? e.message : '鍥捐〃浠g爜鎵ц閿欒');
+            useFallbackOption(chartInstance);
+          }
+          
+          // 娣诲姞鍝嶅簲寮忚皟鏁�
+          const handleResize = () => {
+            chartInstance && chartInstance.resize();
+          };
+          
+          window.addEventListener('resize', handleResize);
+          
+          // 纭繚缁勪欢鍗歌浇鏃舵竻鐞嗚祫婧�
+          return () => {
+            window.removeEventListener('resize', handleResize);
+            if (chartInstance) {
+              try {
+                chartInstance.dispose();
+              } catch (e) {
+                console.warn('鍥捐〃瀹炰緥閿�姣佸け璐�', e);
+              }
+            }
+          };
+        } catch (e) {
+          console.error('鍥捐〃鍒濆鍖栧け璐�:', e);
+          setError('鍔犺浇鍥捐〃搴撳け璐�');
+          return () => {};
+        }
+      };
+      
+      // 澶囩敤閰嶇疆鍑芥暟
+      const useFallbackOption = (instance: any) => {
+        try {
+          // 绠�鍗曠殑澶囩敤閰嶇疆
+          const fallbackOption = {
+            title: {
+              text: '鏁版嵁鍙鍖栧浘琛�',
+              subtext: '(鍘熷浠g爜瑙f瀽澶辫触锛屾樉绀哄鐢ㄥ浘琛�)'
+            },
+            tooltip: {
+              trigger: 'axis'
+            },
+            legend: {
+              data: ['鏁版嵁']
+            },
+            xAxis: {
+              type: 'category',
+              data: ['椤圭洰1', '椤圭洰2', '椤圭洰3', '椤圭洰4', '椤圭洰5']
+            },
+            yAxis: {
+              type: 'value'
+            },
+            series: [{
+              name: '鏁版嵁',
+              type: 'bar',
+              data: [5, 20, 36, 10, 10]
+            }]
+          };
+          
+          // 妫�娴嬫槸鍚︽槸鐑姏鍥句唬鐮�
+          if (safeCode.includes('heatmap') || 
+              safeCode.includes('visualMap') || 
+              safeCode.includes('椋庨櫓')) {
+            (fallbackOption.series as any) = [{
+              type: 'heatmap',
+              name: '椋庨櫓鍊�',  // 娣诲姞缂哄け鐨刵ame灞炴��
+              data: [
+                [0, 0, 5], [0, 1, 7], [0, 2, 3],
+                [1, 0, 7], [1, 1, 8], [1, 2, 6],
+                [2, 0, 9], [2, 1, 10], [2, 2, 8]
+              ] as any[]
+            }];
+            
+            fallbackOption.title.text = 'VIP瀹㈡埛璁㈠崟浜や粯椋庨櫓鐑姏鍥�';
+            fallbackOption.title.subtext = '鏁版嵁瑙f瀽澶辫触锛屾樉绀虹ず渚嬬儹鍔涘浘';
+            
+            // 娣诲姞鐑姏鍥炬墍闇�鐨勫叾浠栭厤缃�
+            Object.assign(fallbackOption, {
+              tooltip: {
+                position: 'top'
+              },
+              xAxis: {
+                type: 'category',
+                data: ['浣�', '涓�', '楂�'],
+                name: '寤惰繜椋庨櫓'
+              },
+              yAxis: {
+                type: 'category',
+                data: ['浣�', '涓�', '楂�'],
+                name: '杩濈害鎴愭湰'
+              },
+              visualMap: {
+                min: 1,
+                max: 10,
+                calculable: true,
+                orient: 'horizontal',
+                left: 'center',
+                bottom: '15%'
+              }
+            });
+          }
+          
+          // 璁剧疆澶囩敤閰嶇疆
+          instance.setOption(fallbackOption);
+        } catch (e) {
+          console.error('搴旂敤澶囩敤閰嶇疆澶辫触:', e);
+        }
+      };
+      
+      // 鍒濆鍖栧浘琛ㄥ苟鑾峰彇娓呯悊鍑芥暟
+      const cleanupPromise = initChart();
+      
+      // 瀹屽叏閲嶅啓娓呯悊鍑芥暟閫昏緫锛岄伩鍏嶄娇鐢≒romise鐨勪笉纭畾杩斿洖绫诲瀷
+      // 鍒濆鍖栧浘琛�
+      initChart().then(cleanupFn => {
+        // 瀛樺偍娓呯悊鍑芥暟渚涗互鍚庝娇鐢�
+        if (typeof cleanupFn === 'function') {
+          // 浣跨敤ref瀛樺偍娓呯悊鍑芥暟
+          const currentCleanup = cleanupFn;
+          // 缁勪欢鍗歌浇鏃舵墽琛�
+          return () => currentCleanup();
+        }
+      }).catch(e => {
+        console.error('鍒濆鍖栧浘琛ㄥけ璐�:', e);
+      });
+      
+      // 杩斿洖涓�涓┖鐨勬竻鐞嗗嚱鏁帮紝閬垮厤绫诲瀷閿欒
+      return () => {};
+    }, 100);
+  }, [isLoaded]);
+
+  // 娓叉煋鍥捐〃瀹瑰櫒
+  return (
+    <div className="relative my-4">
+      {error && (
+        <div className="absolute top-0 left-0 right-0 bg-red-50 border border-red-200 text-red-600 px-3 py-2 rounded-md text-xs z-10">
+          {error}
+          <button 
+            className="ml-2 text-red-700 hover:text-red-900" 
+            onClick={() => setError(null)}
+          >
+            脳
+          </button>
+        </div>
+      )}
+      <div 
+        ref={chartContainerRef} 
+        className="w-full bg-white border border-gray-200 rounded-lg overflow-hidden" 
+        style={{ height: '400px' }}
+      />
+    </div>
+  );
+}
+
+// 淇敼浠g爜鍧楁覆鏌撶粍浠讹紝娣诲姞鏇村ソ鐨勭被鍨嬫娴�
+function CodeBlockRenderer({ language, value }: { language: string; value: string }) {
+  // 鏇寸簿纭湴妫�娴婨Charts浠g爜
+  const isEchartsCode = () => {
+    if (language !== 'javascript') return false;
+    
+    // 妫�鏌ユ槸鍚﹀寘鍚獷Charts鐗规湁鐨勯厤缃」
+    const hasEchartsConfig = 
+      value.includes('option =') || 
+      value.includes('const option') || 
+      value.includes('let option') || 
+      value.includes('series') &&
+      (value.includes('type:') || value.includes('tooltip:') || value.includes('xAxis:'));
+      
+    return hasEchartsConfig;
+  };
+  
+  // 濡傛灉鏄� ECharts 浠g爜鍒欐覆鏌撳浘琛�
+  if (isEchartsCode()) {
+    return <EchartsRenderer code={value} />;
+  }
+  
+  // 鍚﹀垯鎸夋櫘閫氫唬鐮佸潡娓叉煋
+  return (
+    <div className="bg-gray-800 rounded-md my-2 overflow-hidden">
+      <div className="flex items-center justify-between px-4 py-2 border-b border-gray-700">
+        <span className="text-xs text-gray-400">{language}</span>
+      </div>
+      <pre className="p-4 text-gray-300 overflow-x-auto">
+        <code>{value}</code>
+      </pre>
+    </div>
+  );
+}
 
 export default function Page() {
   const router = useRouter();
@@ -729,8 +1002,35 @@
                                 
                                 {/* 涓昏鍐呭 */}
                                 <div className="p-3">
-                                  <div className="text-gray-800 leading-relaxed whitespace-pre-wrap">
-                                    {mainContent || (msg.role === 'assistant' && !isMessageComplete ? '姝e湪鎬濊��...' : '')}
+                                  <div className="text-gray-800 leading-relaxed">
+                                    {mainContent ? (
+                                      <ReactMarkdown
+                                        remarkPlugins={[remarkGfm]}
+                                        rehypePlugins={[rehypeRaw, rehypeSanitize]}
+                                        components={{
+                                          // @ts-ignore - ReactMarkdown 缁勪欢绫诲瀷瀹氫箟鐨勫吋瀹规�ч棶棰�
+                                          code: ({ node, inline, className, children, ...props }) => {
+                                            const match = /language-(\w+)/.exec(className || '');
+                                            const language = match ? match[1] : '';
+                                            const value = String(children).replace(/\n$/, '');
+                                            
+                                            if (!inline && match) {
+                                              return <CodeBlockRenderer language={language} value={value} />;
+                                            }
+                                            
+                                            return (
+                                              <code className={className} {...props}>
+                                                {children}
+                                              </code>
+                                            );
+                                          }
+                                        }}
+                                      >
+                                        {mainContent}
+                                      </ReactMarkdown>
+                                    ) : (
+                                      msg.role === 'assistant' && !isMessageComplete ? '澶勭悊鍥炲涓�...' : ''
+                                    )}
                                   </div>
                                 </div>
                                 
@@ -808,5 +1108,4 @@
       </div>
     </div>
   );
-}
-
+}
\ No newline at end of file

--
Gitblit v1.9.3