hongjli
2025-04-28 b68687c61e43729e236783d8da37a82b13ffc302
渲染echarts图优化
已修改2个文件
266 ■■■■ 文件已修改
src/app/chat/page.tsx 237 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app/globals.css 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app/chat/page.tsx
@@ -44,8 +44,11 @@
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(() => {
@@ -85,105 +88,163 @@
      // 标记为已渲染
      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>
@@ -191,17 +252,65 @@
        </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>
  );
}
@@ -319,7 +428,7 @@
            <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">
src/app/globals.css
@@ -841,3 +841,32 @@
.no-flash-link {
  -webkit-tap-highlight-color: transparent;
}
/* 消息生成时锁定滚动 */
body.generating-message {
  overflow: hidden !important;
}
/* 滚动锁定遮罩 */
.scroll-lock-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: transparent;
  pointer-events: all;
  touch-action: none;
}
/* 图表全屏模式时的样式 */
body:has(.echart-wrapper[data-echarts-container]) {
  scroll-behavior: smooth;
}
.echart-wrapper[data-echarts-container] {
  transition: all 0.3s ease-in-out;
}
/* 当处于全屏模式时防止页面滚动 */
body:has(.echart-wrapper:has(+ .fullscreen-controls)) {
  overflow: hidden;
}