From e9b0e1af02b05de795420ccac832823e62eb8516 Mon Sep 17 00:00:00 2001 From: hongjli <3117313295@qq.com> Date: 星期日, 27 四月 2025 20:21:52 +0800 Subject: [PATCH] 渲染echarts图 --- src/app/chat/page.tsx | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 303 insertions(+), 4 deletions(-) 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