我这边的方案是通过 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);