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);
相关推荐
To_OC5 分钟前
LC 208 实现 Trie 前缀树:曾被名字劝退,写完发现是送分题
javascript·算法·leetcode
天渺工作室42 分钟前
实现一个adblock/adblock plus等浏览器广告拦截器检测插件
前端·javascript
YFF菲菲兔8 小时前
useState 源码解析
react.js
kyriewen9 小时前
2026 年了,还在用 Node.js?Bun 迁移实战:20 分钟搞定,附踩坑记录
前端·javascript·node.js
minglie14 小时前
一个置换问题
javascript
默_笙15 小时前
🌀 别再手动写 Prompt 了!我让 AI 自己循环改到满意为止
javascript
Flynt15 小时前
我的Next.js项目升级到16之后,dev倒是快了,但build差点让我回退
react.js·next.js·turbopack
To_OC1 天前
LC 994 腐烂的橘子:人人都说是 BFS 入门题,我却写了三遍才过
javascript·算法·leetcode
To_OC1 天前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
labixiong1 天前
实现一个能跑的迷你版Promise(一)
前端·javascript·面试