| | |
| | | function EchartsRenderer({ code }: { code: string }) { |
| | | const [error, setError] = useState<string | null>(null); |
| | | const [isLoading, setIsLoading] = useState(true); |
| | | const [isModalOpen, setIsModalOpen] = useState(false); |
| | | const chartContainerRef = useRef<HTMLDivElement>(null); |
| | | const modalChartRef = useRef<HTMLDivElement>(null); |
| | | const hasRenderedRef = useRef<boolean>(false); |
| | | const chartInstanceRef = useRef<any>(null); |
| | | |
| | | // 初始化DOM容器 |
| | | useEffect(() => { |
| | |
| | | // 标记为已渲染 |
| | | hasRenderedRef.current = true; |
| | | |
| | | // 简化的图表初始化逻辑 |
| | | const loadChart = async () => { |
| | | // 初始化图表 |
| | | const initializeChart = async () => { |
| | | try { |
| | | // 动态导入echarts |
| | | const echarts = await import('echarts'); |
| | | |
| | | // 再次确认DOM元素存在并具有有效尺寸 |
| | | if (!chartContainerRef.current || |
| | | !chartContainerRef.current.offsetWidth || |
| | | !chartContainerRef.current.offsetHeight) { |
| | | console.error('图表容器尺寸无效,跳过初始化'); |
| | | setError('图表容器尺寸无效'); |
| | | setIsLoading(false); |
| | | return; |
| | | } |
| | | // 初始化图表 |
| | | const chartInstance = echarts.init(chartContainerRef.current); |
| | | chartInstanceRef.current = chartInstance; |
| | | |
| | | // 初始化图表 - 添加渲染器类型参数 |
| | | const chartInstance = echarts.init( |
| | | chartContainerRef.current, |
| | | undefined, |
| | | { renderer: 'canvas', devicePixelRatio: window.devicePixelRatio || 1 } |
| | | ); |
| | | |
| | | // 尝试解析和设置图表选项 |
| | | try { |
| | | // 安全处理代码 |
| | | const safeCode = code.replace(/window\.option/g, '_uniqueOptionVar'); |
| | | |
| | | // 执行代码获取配置 |
| | | const getFinalOption = new Function(` |
| | | const safeCode = code.replace(/window\.option/g, 'option'); |
| | | const safeFunc = new Function(` |
| | | "use strict"; |
| | | let option; |
| | | let _uniqueOptionVar; |
| | | try { |
| | | ${safeCode} |
| | | return option || _uniqueOptionVar; |
| | | return option; |
| | | } catch (e) { |
| | | console.error("ECharts内部执行错误:", e); |
| | | console.error("图表代码执行错误:", e); |
| | | return null; |
| | | } |
| | | `); |
| | | |
| | | const chartOption = getFinalOption(); |
| | | const chartOption = safeFunc(); |
| | | |
| | | // 应用配置 |
| | | if (chartOption) { |
| | | chartInstance.setOption(chartOption); |
| | | setError(null); |
| | | } else { |
| | | // 应用默认配置 |
| | | chartInstance.setOption({ |
| | | title: { text: '图表数据解析失败' }, |
| | | xAxis: { type: 'category', data: ['无数据'] }, |
| | | yAxis: { type: 'value' }, |
| | | series: [{ data: [0], type: 'bar' }] |
| | | }); |
| | | throw new Error("无法获取图表配置"); |
| | | } |
| | | |
| | | setIsLoading(false); |
| | | } catch (e) { |
| | | console.error('图表代码执行错误:', e); |
| | | setError('图表解析失败'); |
| | | setIsLoading(false); |
| | | setError('图表配置错误'); |
| | | |
| | | // 设置一个默认图表以显示错误 |
| | | chartInstance.setOption({ |
| | | title: { text: '图表配置错误' }, |
| | | xAxis: { type: 'category', data: ['错误'] }, |
| | | yAxis: { type: 'value' }, |
| | | series: [{ data: [0], type: 'bar' }] |
| | | }); |
| | | } |
| | | |
| | | // 窗口大小改变时,重设图表大小 |
| | | // 添加窗口大小变化监听器 |
| | | const handleResize = () => chartInstance.resize(); |
| | | window.addEventListener('resize', handleResize); |
| | | |
| | | // 组件卸载时清理 |
| | | // 清理函数 |
| | | return () => { |
| | | window.removeEventListener('resize', handleResize); |
| | | try { |
| | | chartInstance.dispose(); |
| | | } catch (e) {} |
| | | chartInstance.dispose(); |
| | | }; |
| | | } catch (e) { |
| | | console.error('图表加载失败:', e); |
| | | console.error('ECharts加载失败:', e); |
| | | setError('图表库加载失败'); |
| | | } finally { |
| | | setIsLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 执行加载 |
| | | loadChart(); |
| | | initializeChart(); |
| | | }, 500); // 增加延迟,确保DOM完全渲染 |
| | | |
| | | return () => clearTimeout(timer); |
| | | }, [code]); |
| | | |
| | | // 当全屏状态变化时重新调整图表大小 |
| | | useEffect(() => { |
| | | if (chartInstanceRef.current) { |
| | | setTimeout(() => { |
| | | chartInstanceRef.current.resize(); |
| | | }, 300); // 给DOM一些时间来更新 |
| | | } |
| | | }, [isModalOpen]); |
| | | |
| | | // 处理全屏切换 |
| | | const toggleModal = useCallback(() => { |
| | | setIsModalOpen(prev => !prev); |
| | | }, []); |
| | | |
| | | // 处理模态窗口的图表 |
| | | useEffect(() => { |
| | | if (!isModalOpen || !modalChartRef.current) return; |
| | | |
| | | const initModalChart = async () => { |
| | | try { |
| | | const echarts = await import('echarts'); |
| | | |
| | | // 初始化模态窗口中的图表 |
| | | const modalChartInstance = echarts.init(modalChartRef.current); |
| | | |
| | | // 如果主图表已经初始化,复用其配置 |
| | | if (chartInstanceRef.current) { |
| | | const option = chartInstanceRef.current.getOption(); |
| | | modalChartInstance.setOption(option); |
| | | } else { |
| | | // 如果主图表没有初始化,尝试从代码初始化 |
| | | try { |
| | | const safeCode = code.replace(/window\.option/g, 'option'); |
| | | const safeFunc = new Function(` |
| | | "use strict"; |
| | | let option; |
| | | try { |
| | | ${safeCode} |
| | | return option; |
| | | } catch (e) { |
| | | console.error("模态图表代码执行错误:", e); |
| | | return null; |
| | | } |
| | | `); |
| | | |
| | | const chartOption = safeFunc(); |
| | | |
| | | if (chartOption) { |
| | | modalChartInstance.setOption(chartOption); |
| | | } else { |
| | | throw new Error("无法获取模态图表配置"); |
| | | } |
| | | } catch (e) { |
| | | console.error('模态图表代码执行错误:', e); |
| | | |
| | | // 设置一个默认图表 |
| | | modalChartInstance.setOption({ |
| | | title: { text: '图表配置错误' }, |
| | | xAxis: { type: 'category', data: ['错误'] }, |
| | | yAxis: { type: 'value' }, |
| | | series: [{ data: [0], type: 'bar' }] |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // 添加窗口大小变化监听器 |
| | | const handleResize = () => modalChartInstance.resize(); |
| | | window.addEventListener('resize', handleResize); |
| | | |
| | | // 返回清理函数 |
| | | return () => { |
| | | window.removeEventListener('resize', handleResize); |
| | | modalChartInstance.dispose(); |
| | | }; |
| | | } catch (e) { |
| | | console.error('模态图表初始化失败:', e); |
| | | } |
| | | }; |
| | | |
| | | // 延迟一点执行,确保DOM已更新完成 |
| | | const timer = setTimeout(initModalChart, 100); |
| | | return () => clearTimeout(timer); |
| | | }, [isModalOpen, code]); |
| | | |
| | | // 简化的渲染逻辑,确保容器有明确的尺寸 |
| | | return ( |
| | | <div className="relative my-4 echart-container"> |
| | | {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} |
| | | </div> |
| | | )} |
| | | |
| | | <div className="relative my-4"> |
| | | {isLoading && ( |
| | | <div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-70 z-10"> |
| | | <div className="absolute inset-0 flex items-center justify-center bg-white/70 z-10"> |
| | | <div className="flex flex-col items-center"> |
| | | <div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500 mb-3"></div> |
| | | <p className="text-gray-500">图表加载中...</p> |
| | |
| | | </div> |
| | | )} |
| | | |
| | | <div |
| | | ref={chartContainerRef} |
| | | {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} |
| | | </div> |
| | | )} |
| | | |
| | | {/* 图表容器 */} |
| | | <div |
| | | ref={chartContainerRef} |
| | | className="w-full bg-white border border-gray-200 rounded-lg overflow-hidden chart-container" |
| | | style={{ |
| | | height: '400px', |
| | | minHeight: '300px', // 确保有最小高度 |
| | | visibility: 'visible', // 确保容器可见 |
| | | position: 'relative' // 创建BFC |
| | | height: '300px', |
| | | minHeight: '300px', |
| | | visibility: 'visible', |
| | | position: 'relative' |
| | | }} |
| | | data-echarts-container="true" // 添加标识属性 |
| | | data-echarts-container="true" |
| | | /> |
| | | |
| | | {/* 打开弹窗按钮 */} |
| | | <button |
| | | onClick={() => setIsModalOpen(true)} |
| | | className="absolute top-2 right-2 bg-white/90 hover:bg-white p-2 rounded-full shadow-md z-20 transition-all duration-300 hover:scale-110 cursor-pointer" |
| | | title="在弹窗中查看" |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 3h6v6m0-6L14 9M9 21H3v-6m0 6l7-7" /> |
| | | </svg> |
| | | </button> |
| | | |
| | | {/* 弹出式对话框 */} |
| | | {isModalOpen && ( |
| | | <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4"> |
| | | <div className="bg-white rounded-lg w-[90vw] max-w-6xl h-[80vh] relative flex flex-col"> |
| | | {/* 对话框头部 */} |
| | | <div className="flex justify-between items-center p-4 border-b"> |
| | | <h3 className="text-xl font-semibold text-gray-800">数据可视化图表</h3> |
| | | <button |
| | | onClick={() => setIsModalOpen(false)} |
| | | className="p-1 rounded-full hover:bg-gray-100 cursor-pointer" |
| | | aria-label="关闭" |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | |
| | | {/* 对话框主体 - 图表 */} |
| | | <div className="flex-1 overflow-hidden p-4"> |
| | | <div |
| | | ref={modalChartRef} |
| | | className="w-full h-full" |
| | | data-echarts-modal |
| | | ></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </div> |
| | | ); |
| | | } |
| | |
| | | <button |
| | | onClick={handleSend} |
| | | disabled={isStreaming || !inputText.trim() || !isMessageComplete} |
| | | className="h-12 px-5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none" |
| | | className="h-12 px-5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none cursor-pointer" |
| | | > |
| | | <span>{isStreaming ? '回复中...' : (isMessageComplete ? '发送' : '处理中...')}</span> |
| | | <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 transform rotate-45 ${isStreaming ? 'animate-pulse' : ''}`} viewBox="0 0 20 20" fill="currentColor"> |