导出: 班次 / 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 jsPDF | 100 页 | 超过建议分批或改用服务端 |
| Excel SheetJS | 5 万行 × 20 列 | 超过用流式 CSV |
| Canvas 转图 | 2400 × 1600 px | 超过画质过差 |
超大数据集 (超过 100MB) 考虑走后端生成, 但本产品默认前端方案满足绝大多数场景。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 中文显示方块 | jsPDF 未嵌字体 | 加载 NotoSansSC base64 |
| 打印样式错 | 未加 @media print | CSS 加打印媒体查询 |
| 图表模糊 | DPR 未处理 | Canvas scale(devicePixelRatio) |
| 大数据 Excel 卡 | 主线程阻塞 | Web Worker 里用 SheetJS |
| 下载失败 | 浏览器阻止多次下载 | 两次下载之间 await setTimeout 500ms |