跳到主要内容

导出: 班次 / OEE / 报警

Darra HMI 提供设备/班次级汇总的导出能力。浏览器内通过 jsPDF + SheetJS 直接生成 PDF/Excel, 不依赖服务端任何业务 CRUD API, 也不对接业务管理系统。

本章范围是设备运行汇总 — 班次产量、OEE、报警统计、PID 曲线、质量趋势。不是业务财务/销售/对账汇总, 那些应由 ERP/BI 系统另行处理。

导出范围

导出类型内容周期用途
班次生产汇总产量/废品/OEE/停机时间每班班组绩效/交接班参考
日度汇总产线全天指标汇总每日车间早会
OEE 月度月度 OEE 趋势与分解每月设备改善依据
报警统计报警 TOP10 + 频次/时长分布按需设备可靠性分析
PID 调参记录过程变量曲线按需工艺优化
质量趋势合格率 + SPC 控制图按需质量追踪
能耗电/气/水累计日/月能源管理

数据来源

数据来源仅限:

  • Historian (设备级时序库) — 通过 Service 提供的查询 API /api/historian/*
  • PLC 当前变量 — darra.get() / darra.bindGroup()
  • 本地 IndexedDB 缓存 — 离线时使用

不从任何业务系统拉数据, 所有数据的含义都是设备变量值。

jsPDF: PDF 导出

基础引入

<script src="/static/js/jspdf.umd.min.js"></script>
<script src="/static/js/jspdf.autotable.min.js"></script>

班次生产汇总

async function exportShiftSummary(shiftStart, shiftEnd) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit: 'mm', format: 'a4' });

// 1. 从 Historian 查询班次数据
const url = '/api/historian/query?vars=MW.Yield,MW.Scrap,MD.OEE'
+ '&start=' + shiftStart + '&end=' + shiftEnd
+ '&agg=avg&interval=5m';
const data = await fetch(url).then(r => r.json());

// 2. 标题
doc.setFont('NotoSansSC', 'normal');
doc.setFontSize(18);
doc.text('班次生产汇总', 105, 20, { align: 'center' });

// 3. 基本信息
doc.setFontSize(11);
doc.text([
'产线: ' + data.line,
'开始: ' + new Date(shiftStart).toLocaleString(),
'结束: ' + new Date(shiftEnd).toLocaleString(),
'当班: ' + data.operator
], 15, 35);

// 4. KPI 表
doc.autoTable({
startY: 60,
head: [['指标', '数值', '单位']],
body: [
['产量', data.totalYield, '件'],
['废品', data.totalScrap, '件'],
['合格率', (data.quality * 100).toFixed(2), '%'],
['平均 OEE', (data.avgOEE * 100).toFixed(1), '%'],
['故障次数', data.faultCount, '次'],
['停机时长', data.downtime, '分钟']
]
});

// 5. 趋势图 (Canvas 转图片)
const trendCanvas = document.getElementById('shift-trend');
const imgData = trendCanvas.toDataURL('image/png');
doc.addImage(imgData, 'PNG', 15, doc.lastAutoTable.finalY + 10, 180, 80);

// 6. 保存
doc.save('班次汇总_' + data.line + '_'
+ new Date(shiftEnd).toLocaleDateString() + '.pdf');
}

中文字体

jsPDF 默认不支持中文, 需要嵌入字体:

const fontData = await fetch('/static/fonts/NotoSansSC.ttf.base64').then(r => r.text());
doc.addFileToVFS('NotoSansSC.ttf', fontData);
doc.addFont('NotoSansSC.ttf', 'NotoSansSC', 'normal');
doc.setFont('NotoSansSC');

字体文件较大 (2-5 MB), 首次加载后缓存到 Service Worker.

SheetJS: Excel 导出

基础引入

<script src="/static/js/xlsx.full.min.js"></script>

OEE 月度

async function exportOeeMonthly(year, month) {
const start = new Date(year, month - 1, 1).getTime();
const end = new Date(year, month, 0, 23, 59, 59).getTime();

const url = '/api/historian/query?vars=MD.OEE,MD.Availability,MD.Performance,MD.Quality'
+ '&start=' + start + '&end=' + end + '&agg=avg&interval=1d';
const raw = await fetch(url).then(r => r.json());

const rows = raw.points.map(p => ({
日期: new Date(p.ts).toLocaleDateString(),
OEE: (p.oee * 100).toFixed(2),
稼动率: (p.availability * 100).toFixed(2),
性能率: (p.performance * 100).toFixed(2),
合格率: (p.quality * 100).toFixed(2)
}));

const ws = XLSX.utils.json_to_sheet(rows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, year + '年' + month + '月');
ws['!cols'] = [ { wch: 12 }, { wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 } ];
XLSX.writeFile(wb, 'OEE月度_' + year + '-' + month + '.xlsx');
}

多 Sheet 报警统计

function exportAlarmStats(alarms) {
const wb = XLSX.utils.book_new();

const byCode = {};
alarms.forEach(a => {
byCode[a.code] = byCode[a.code]
|| { code: a.code, msg: a.message, count: 0, duration: 0 };
byCode[a.code].count++;
byCode[a.code].duration += (a.resetTime - a.firstTime);
});
const top10 = Object.values(byCode)
.sort((a, b) => b.count - a.count)
.slice(0, 10);

XLSX.utils.book_append_sheet(wb,
XLSX.utils.json_to_sheet(top10), 'TOP10 报警');

XLSX.utils.book_append_sheet(wb,
XLSX.utils.json_to_sheet(alarms.map(a => ({
时间: new Date(a.firstTime).toLocaleString(),
代码: a.code,
级别: a.severity,
来源: a.source,
消息: a.message,
确认人: a.ackBy,
持续秒数: Math.floor((a.resetTime - a.firstTime) / 1000)
}))), '完整列表');

XLSX.writeFile(wb, '报警统计_'
+ new Date().toISOString().slice(0, 10) + '.xlsx');
}

Canvas 图表转 PDF 图片

Chart.js / ECharts 渲染的趋势图可直接嵌入 PDF:

const canvas = document.getElementById('pid-trend');
const imgData = canvas.toDataURL('image/png', 1.0);
doc.addImage(imgData, 'PNG', 15, 60, 180, 80);

<darra-trend> 控件内部 Canvas 可以通过 trendEl.getCanvas() 暴露:

const trendEl = document.querySelector('darra-trend');
const canvas = trendEl.getCanvas();
const imgData = canvas.toDataURL('image/png');

浏览器原生打印

建议把导出 UI 写成纯前端的预览页, 使用浏览器原生打印:

<div id="preview">
<header>
<h1>班次生产汇总</h1>
<p>产线: <span x-text="line"></span></p>
</header>

<section>
<h2>KPI 汇总</h2>
<table>...</table>
</section>

<section class="page-break-before">
<h2>趋势图</h2>
<canvas id="trend"></canvas>
</section>

<footer>生成于 <span x-text="new Date().toLocaleString()"></span></footer>
</div>

<button onclick="window.print()">打印 / 保存 PDF</button>

CSS 打印样式:

@media print {
body > :not(#preview) { display: none; }
.page-break-before { page-break-before: always; }
@page { size: A4; margin: 15mm; }
}

定时导出

HMI 可以定时 (例如每班结束) 自动生成并保存到本地下载目录:

function scheduleDailyExport() {
const now = new Date();
const next = new Date();
next.setHours(23, 50, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);

setTimeout(async () => {
await exportShiftSummary(shiftStart, Date.now());
scheduleDailyExport();
}, next - now);
}

或通过 <darra-scheduler> 控件声明式调度:

<darra-scheduler cron="0 50 23 * * *" @tick="exportShiftSummary()"></darra-scheduler>

邮件分发

HMI 不直接发邮件 (凭据安全), 改用 Service 代理:

async function emailExport(pdfBlob, to) {
const form = new FormData();
form.append('to', to);
form.append('subject', '班次生产汇总');
form.append('attachment', pdfBlob, 'shift-summary.pdf');

// 复用报警推送的邮件通道
await fetch('/api/alarm/push/email', { method: 'POST', body: form });
}

数据量限制

浏览器内生成的实际上限:

类型数据量上限备注
PDF jsPDF100 页超过建议分批或改用服务端
Excel SheetJS5 万行 × 20 列超过用流式 CSV
Canvas 转图2400 × 1600 px超过画质过差

超大数据集 (超过 100MB) 考虑走后端生成, 但本产品默认前端方案满足绝大多数场景。

常见问题

现象原因处理
中文显示方块jsPDF 未嵌字体加载 NotoSansSC base64
打印样式错未加 @media printCSS 加打印媒体查询
图表模糊DPR 未处理Canvas scale(devicePixelRatio)
大数据 Excel 卡主线程阻塞Web Worker 里用 SheetJS
下载失败浏览器阻止多次下载两次下载之间 await setTimeout 500ms

相关文档