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);
相关推荐
lifewange2 小时前
JavaScript是什么
开发语言·javascript·ecmascript
Armouy2 小时前
Nuxt.js 学习复盘:核心概念与实战要点
前端·javascript·学习
safestar20123 小时前
React 19实战:Action、并发与性能,一次告别“意大利面状态”的升级
开发语言·javascript·vue.js
ZhaoJuFei3 小时前
React生态学习路线
前端·学习·react.js
早點睡3903 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-calendar-events(读取不到日历里新增的事件,待排查)
javascript·react native·react.js
837927397@QQ.COM3 小时前
个人理解无界原理
开发语言·前端·javascript
冰暮流星3 小时前
javascript之Dom查询操作1
java·前端·javascript
Cxiaomu3 小时前
像ChatGPT一样逐字输出:React + TypeScript 流式接收与“打字机”效果实现方案
人工智能·react.js·chatgpt·typescript
取码网3 小时前
2025最新口红机防篡改版本源码
android·java·javascript