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);
相关推荐
jiayong235 分钟前
第 33 课:任务看板视图(按状态分列)与本地持久化
开发语言·前端·javascript·学习
Yeats_Liao1 小时前
后台 Sidebar 伸缩交互(PC + 移动端)实现
前端·javascript·css·html5
MXN_小南学前端1 小时前
computed 计算属性详解:触发时机、实战场景、Vue2 与 Vue3 对比
前端·javascript·vue.js
isNotNullX1 小时前
数据大屏怎么做?数据大屏有哪四个核心环节
开发语言·前端·javascript
竹林8181 小时前
Web3表单签名验证:我如何用 wagmi 和 siwe 让用户“无密码”登录
javascript
inksci2 小时前
使用飞帆的上传组件
前端·javascript
FOREVER-Q2 小时前
基于 Vite 的前端 SDK 工程化设计与模块化构建实践
开发语言·前端·javascript
耗子君QAQ2 小时前
为什么 AI 写代码也需要说明书?
前端·javascript·ai编程
h_65432102 小时前
公告/消息提示从右向左循环滚动vue
前端·javascript·vue.js
Lyyaoo.2 小时前
JWT 令牌(待更新)
java·前端·javascript