Grafana 表格自定义下载样式。

我这边的方案是通过 grafana嵌套在iframe中,然后获取数据postmessage 给父页面 调用 excel.js 下载。

增加一个html panel , 在 onlint 添加如下代码。

该代码会在目标panel的标题上 增加一个 按钮,点击后触发。

复制代码
var targetPanelId = 8;

setTimeout(function() {
  var panel = $('.react-grid-item[data-panelid="' + targetPanelId + '"]');
  var header = panel.find('[data-testid="header-container"]');
  if (header.length === 0) header = panel.find('header');

  var btn = $('<button>⬇ Download</button>')
    .attr('contenteditable', 'false')
    .css({
      'padding': '2px 12px',
      'font-size': '12px',
      'background': '#000',
      'color': '#fff',
      'border': 'none',
      'border-radius': '4px',
      'cursor': 'pointer',
      'user-select': 'none',
      'pointer-events': 'auto',
      'z-index': '99999'
    });

  btn.on('click', function(e) {
    e.stopPropagation();
    e.stopImmediatePropagation();

    // ★ 从 React 内部直接取当前显示的完整数据
    function extractData(el) {
      var fiberKey = Object.keys(el).find(function(k) {
        return k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$');
      });
      if (!fiberKey) return null;

      var fiber = el[fiberKey];
      var node = fiber;
      var depth = 100;

      while (node && depth-- > 0) {
        var p = node.memoizedProps || {};

        // Grafana 把查询结果放在 props.data.series
        if (p.data && p.data.series && p.data.series.length > 0) {
          return p.data.series;
        }
        if (p.data && Array.isArray(p.data.fields)) {
          return [p.data];
        }
        if (p.frames && p.frames.length > 0) {
          return p.frames;
        }

        node = node.return;
      }
      return null;
    }

    // 从 panel 容器开始找
    var frames = null;
    var dom = panel[0];

    // 先从 panel 根元素找
    frames = extractData(dom);

    // 没找到就遍历子元素
    if (!frames) {
      var children = dom.querySelectorAll('div');
      for (var i = 0; i < children.length && !frames; i++) {
        frames = extractData(children[i]);
      }
    }

    if (!frames || frames.length === 0) {
      console.error('❌ 未找到数据');
      return;
    }

    // 解析 DataFrame → headers + rows
    var headers = [];
    var rows = [];

    frames.forEach(function(frame) {
      var fields = frame.fields || [];
      if (fields.length === 0) return;

      if (headers.length === 0) {
        headers = fields.map(function(f) {
          return f.config && f.config.displayName
            ? f.config.displayName
            : f.name || '';
        });
      }

      // values 可能是数组、ArrayVector、TypedArray
      var columns = fields.map(function(f) {
        var v = f.values;
        if (Array.isArray(v)) return v;
        if (v && v.buffer) return Array.from(v.buffer);
        if (v && v.toArray) return v.toArray();
        if (v && typeof v.length === 'number') return Array.from(v);
        return [];
      });

      var rowCount = columns[0] ? columns[0].length : 0;
      for (var i = 0; i < rowCount; i++) {
        var row = [];
        for (var j = 0; j < columns.length; j++) {
          var cell = columns[j][i];
          // 时间戳转可读
          if (fields[j].type === 'time' && typeof cell === 'number') {
            cell = new Date(cell > 1e12 ? cell : cell * 1000)
              .toISOString().replace('T', ' ').slice(0, 19);
          }
          row.push(cell != null ? cell : '');
        }
        rows.push(row);
      }
    });

    console.log('📊 表头:', headers);
    console.log('📊 行数:', rows.length);
    console.log('📊 前3行:', rows.slice(0, 3));

    // postMessage 给父页面
    var message = {
      type: 'GRAFANA_PANEL_DATA',
      panelId: targetPanelId,
      timestamp: new Date().toISOString(),
      data: { headers: headers, rows: rows, rowCount: rows.length }
    };

    if (window.parent && window.parent !== window) {
      window.parent.postMessage(message, '*');
    } else {
      window.postMessage(message, '*');
    }

    console.log('✅ 已发送 ' + rows.length + ' 行');
  });

  btn.on('mousedown pointerdown dragstart', function(e) {
    e.stopPropagation();
    e.stopImmediatePropagation();
  });

  header.css({ 'display': 'flex', 'align-items': 'center' });
  header.append(btn);

}, 1000);
相关推荐
用户9385156350737 分钟前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端
油丶酸萝卜别吃1 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang1 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方2 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript
半个落月2 小时前
React useRef 详解:DOM 引用、持久化值与 Worker 实例
前端·react.js
汉堡大王95272 小时前
Vue 3 + TS + Element Plus 实战:如何从零搭建企业级违章记录管理 SaaS 前端
前端·javascript·vue.js
海天鹰3 小时前
屏幕颜色检测
javascript·html5
用户7917286761973 小时前
opencode-plugin-peers完全指南:OpenCode如何实现 Claude Code式跨会话消息
javascript
光影少年5 小时前
React Native网络、存储与原生能力
前端·react native·react.js
To_OC5 小时前
绕开层层 props 搬运:我把 useContext 和自定义 Hook 跑通了
前端·react.js·前端框架