数据大屏
数据大屏 = 把分散的 PLC 实时数据、OEE / SPC 统计、设备状态、报警信息, 用一个 1920x1080 或 4K 全屏页面聚合展示, 悬挂在车间墙面、领导办公室、展厅大堂。Darra HMI 用 <darra-dx-*> + Chart.js + CSS Grid 几分钟即可搭成。
大屏面向设备运行监控, 聚焦 OEE / 产量 / 节拍 / 合格率 / 报警等指标。ERP 工单、销售数据等业务信息由 ERP/MES 系统另建大屏, DarraRT 不做。
典型场景
| 场景 | 展示要点 |
|---|---|
| 车间总览 | OEE / 产量 / 设备在线率 / 当班班次 / 主要工序实时画面 |
| 产线大屏 | 单条产线节拍 / 合格率 / 异常次数 / 前后工序衔接 |
| 工厂总览 | 多车间聚合 / 能耗 / 安全天数 / 产量 vs 计划 |
| 设备状态墙 | 所有工位在线 / 离线 / 故障状态的灯板, 一眼看全厂设备 |
| 展厅大屏 | 品牌故事 + 实时生产画面 + 关键数据 |
核心指标 (设备视角)
| 指标 | 单位 | 说明 | 颜色阈值 |
|---|---|---|---|
| OEE (总效率) | % | 可用率 × 性能率 × 合格率 | <60 红, 60-85 黄, >85 绿 |
| 稼动率 (Availability) | % | 实际运行时间 / 计划时间 | <80 红, >95 绿 |
| 性能率 (Performance) | % | 实际节拍 / 理论节拍 | - |
| 合格率 (Quality) | % | 合格数 / 生产数 | <98 红 |
| 节拍 | ms / s | 当前生产节拍 | 偏差 >10% 黄 |
| 产量 | 件 | 当班/今日累计 | - |
| 故障次数 | 次 | 当班累计 | >5 黄, >10 红 |
| MTBF | h | 平均无故障时间 | - |
| MTTR | min | 平均修复时间 | - |
设计前置
1. 分辨率选择
| 分辨率 | 像素密度 | 适用 |
|---|---|---|
| 1920 × 1080 | 0.52 px/mm @ 55" | 常规, 绝大多数 LED/LCD 屏 |
| 2560 × 1440 | 0.69 px/mm @ 55" | 中端 |
| 3840 × 2160 (4K) | 1.04 px/mm @ 55" | 展厅 / 高档大屏 |
| 拼接屏 (多块) | 参见 多屏分发 | 指挥中心 |
HMIProjectDef.Settings:
{
"DesignWidth": 1920,
"DesignHeight": 1080,
"AspectRatio": "16:9",
"ScaleMode": "FitRatio"
}
4K 需换 DesignWidth: 3840, 字体/图标自动放大。
2. 色彩与可读性
- 暗底:
theme-modern深色系, 大屏投影更显眼 - 对比度: 前景 vs 背景
>= 7:1(AAA) - 大字号: 关键 KPI 至少
72px, 标题96px+ - 不要用纯白:
#EEE/#DDD减轻疲劳 - 配色: 主色 1 + 强调色 2, 避免彩虹
3. 布局栅格
CSS Grid:
.dashboard {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: 80px repeat(6, 1fr) 60px;
gap: 16px;
padding: 24px;
height: 1080px;
background: var(--darra-bg);
}
.header { grid-column: 1 / -1; grid-row: 1; }
.kpi-1 { grid-column: 1 / 4; grid-row: 2 / 4; }
.kpi-2 { grid-column: 4 / 7; grid-row: 2 / 4; }
.chart { grid-column: 1 / 9; grid-row: 4 / 7; }
.grid { grid-column: 9 / -1; grid-row: 2 / 7; }
.footer { grid-column: 1 / -1; grid-row: -2; }
完整示例: 车间总览大屏
HtmlCode
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="/static/darra-hmi.css" />
<link rel="stylesheet" href="/static/devextreme/dx.common.css" />
<link rel="stylesheet" href="/static/devextreme/dx.dark.css" />
<script src="/static/alpine.min.js" defer></script>
<script src="/static/darra-plc.js"></script>
<script src="/static/chart.umd.js"></script>
<script src="/static/devextreme/dx.all.js"></script>
<script src="/static/darra-devextreme.js"></script>
</head>
<body class="theme-modern dashboard-body">
<div class="dashboard">
<!-- 顶部标题栏 -->
<header class="header">
<h1 class="title">1 号车间生产总览</h1>
<div class="meta">
<span>班次: <span x-text="$store.shift"></span></span>
<span x-text="$store.nowText"></span>
</div>
</header>
<!-- KPI 卡片组 -->
<div class="kpi-card kpi-1">
<div class="label">实时 OEE</div>
<div class="value" x-data="{ v: 0 }" x-init="darra.bind('DB100.OEE', x => v = x)">
<span x-text="(v * 100).toFixed(1)"></span>%
</div>
<darra-dx-sparkline var="DB100.OEE_Last60"></darra-dx-sparkline>
</div>
<div class="kpi-card kpi-2">
<div class="label">今日产量</div>
<div class="value" x-data="{ v: 0 }" x-init="darra.bind('DB100.TodayQty', x => v = x)">
<span x-text="v.toLocaleString()"></span>
</div>
<div class="sub">目标 <span x-text="$store.targetQty.toLocaleString()"></span></div>
</div>
<div class="kpi-card kpi-3">
<div class="label">合格率</div>
<div class="value" x-data="{ v: 0 }" x-init="darra.bind('DB100.QualityRate', x => v = x)">
<span x-text="(v * 100).toFixed(2)"></span>%
</div>
</div>
<div class="kpi-card kpi-4">
<div class="label">设备在线</div>
<div class="value">
<span x-data="{ n: 0, t: 30 }" x-init="darra.bind('DB100.OnlineCount', x => n = x)">
<span x-text="n"></span>/<span x-text="t"></span>
</span>
</div>
</div>
<!-- 中央趋势图 -->
<div class="chart">
<h3>产量 & 节拍趋势 (最近 2 小时)</h3>
<canvas id="trend-chart"></canvas>
</div>
<!-- 右侧: 工位状态实时列表 -->
<div class="station-list">
<h3>工位状态</h3>
<darra-dx-grid var="DB100.StationGrid"
columns="id:工位,state:状态,tact:节拍,alarm:报警"
paging="8"></darra-dx-grid>
</div>
<!-- 底部报警条 -->
<footer class="footer">
<darra-alarm-list max-rows="3" level-filter="error"></darra-alarm-list>
</footer>
</div>
<script src="/static/dashboard-page.js"></script>
</body>
</html>
CssCode
.dashboard-body { margin: 0; overflow: hidden; }
.dashboard {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: 100px repeat(10, 1fr) 80px;
gap: 16px;
padding: 24px;
height: 100vh;
background: radial-gradient(ellipse at top, #0f172a, #000);
color: #e5e7eb;
}
.header {
grid-column: 1 / -1;
grid-row: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 2px solid #334155;
}
.header .title {
font-size: 48px;
margin: 0;
background: linear-gradient(90deg, #38bdf8, #818cf8);
-webkit-background-clip: text;
color: transparent;
}
.header .meta { font-size: 22px; display: flex; gap: 24px; }
.kpi-card {
background: rgba(30,41,59,0.7);
backdrop-filter: blur(8px);
border: 1px solid #334155;
border-radius: 12px;
padding: 24px;
display: flex;
flex-direction: column;
justify-content: center;
}
.kpi-card .label { font-size: 22px; color: #94a3b8; }
.kpi-card .value { font-size: 72px; font-weight: 700; color: #38bdf8; }
.kpi-card .sub { font-size: 16px; color: #64748b; }
.kpi-1 { grid-column: 1 / 4; grid-row: 2 / 5; }
.kpi-2 { grid-column: 4 / 7; grid-row: 2 / 5; }
.kpi-3 { grid-column: 7 / 10; grid-row: 2 / 5; }
.kpi-4 { grid-column: 10 / -1; grid-row: 2 / 5; }
.chart { grid-column: 1 / 9; grid-row: 5 / 11; background: rgba(15,23,42,0.7); padding: 16px; border-radius: 12px; }
.chart h3 { margin: 0 0 12px; font-size: 20px; color: #cbd5e1; }
.order-list { grid-column: 9 / -1; grid-row: 5 / 11; background: rgba(15,23,42,0.7); padding: 16px; border-radius: 12px; }
.order-list h3 { margin: 0 0 12px; font-size: 20px; color: #cbd5e1; }
.footer { grid-column: 1 / -1; grid-row: -2; background: rgba(30,41,59,0.7); border-radius: 12px; padding: 8px 16px; }
JsCode (dashboard-page.js)
// ===== 全局 Store =====
document.addEventListener('alpine:init', () => {
Alpine.store('shift', '')
Alpine.store('nowText', '')
Alpine.store('targetQty', 10000)
})
// ===== 时钟 =====
setInterval(() => {
const d = new Date()
Alpine.store('nowText', d.toLocaleString('zh-CN'))
const h = d.getHours()
Alpine.store('shift', h < 8 ? '夜班' : h < 16 ? '白班' : '中班')
}, 1000)
// ===== 趋势图 =====
const ctx = document.getElementById('trend-chart').getContext('2d')
const trend = new Chart(ctx, {
type: 'line',
data: {
datasets: [
{ label: '产量 (件/分)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56,189,248,0.2)', fill: true, tension: 0.3 },
{ label: '节拍 (s)', data: [], borderColor: '#fbbf24', yAxisID: 'y2', tension: 0.3 }
]
},
options: {
animation: false,
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#cbd5e1', font: { size: 16 } } } },
scales: {
x: { type: 'time', time: { unit: 'minute' }, ticks: { color: '#94a3b8' } },
y: { position: 'left', ticks: { color: '#38bdf8' } },
y2: { position: 'right', ticks: { color: '#fbbf24' }, grid: { drawOnChartArea: false } }
}
}
})
darra.bindGroup(['DB100.RateQty', 'DB100.RateCycle'], (vs) => {
const now = new Date()
trend.data.datasets[0].data.push({ x: now, y: vs['DB100.RateQty'] })
trend.data.datasets[1].data.push({ x: now, y: vs['DB100.RateCycle'] })
// 保留最近 2 小时
const cutoff = now.getTime() - 2 * 3600 * 1000
trend.data.datasets.forEach(ds => {
while (ds.data.length && ds.data[0].x.getTime() < cutoff) ds.data.shift()
})
trend.update('none')
})
// ===== 自动轮播 (多页大屏) =====
if (new URLSearchParams(location.search).has('rotate')) {
const pages = ['/local/', '/local/line1', '/local/line2', '/local/quality']
let idx = pages.indexOf(location.pathname)
setTimeout(() => { location.href = pages[(idx + 1) % pages.length] + '?rotate' }, 30000)
}
自动轮播策略
- URL 带
?rotate→ 30 秒后自动跳下一页 - 退出轮播: 鼠标移入某个页面, 或按 ESC
- 生产环境: 用
Darra.PLC.HMI.exe --kiosk --url=/local/?rotate
关键数据源
| 源 | 典型路径 | 刷新 |
|---|---|---|
| 实时 KPI | DB100.* PLC 订阅 (darra.bind) | 推送 (100ms) |
| 工位状态网格 | DB100.StationGrid 结构体 | 推送 |
| 历史 Sparkline | Historian 时序查询 (见 historian) | 启动一次 + 每分钟刷新 |
| 报警 | darra.on('alarm') 推送 | 事件驱动 |
| 班次/目标 | PLC DB 块 (如 DB200.Shift.Target) | 推送 |
性能优化
- Chart.js 关动画:
animation: false, 否则 CPU 飙 - DOM 数量: 保持
<500节点, 用grid-template-areas替代多层嵌套 - 订阅合并: 用
bindGroup而非多次bind - 字体子集: 大标题中文字体按需加载
- GPU 合成:
backdrop-filter: blur小心, 过多会掉帧
4K 适配
Settings.DesignWidth = 3840, 其他 CSS 单位用 em/rem/vw:
.kpi-card .value { font-size: clamp(48px, 4vw, 140px); }
断网容灾
大屏长时间运行难免遇到 PLC 连接中断。切到"冻结模式", 显示"最后更新时间"角标但数据不清零, 运维人员一眼看出是"数据停更"而非"设备停机":
darra.on('offline', () => {
document.body.classList.add('frozen');
document.getElementById('offline-banner').textContent
= '与 PLC 连接中断, 显示 ' + new Date().toLocaleTimeString() + ' 的快照';
});
darra.on('online', () => {
document.body.classList.remove('frozen');
document.getElementById('offline-banner').textContent = '';
});
.frozen { filter: grayscale(0.6) opacity(0.7); }
.frozen::before { content: ''; position: fixed; inset: 0; border: 4px solid #f59e0b; pointer-events: none; }
最佳实践
- 一屏信息不超过 5 个区块: 超过则翻页
- 颜色语义化: 绿=好, 黄=警, 红=坏, 蓝=中性
- 避免闪烁: 大屏长时间观看, 闪烁易疲劳
- 背景低调: 渐变/轻纹理, 不用动图
- 实时优先: 趋势图用最新 N 分钟, 不是昨天
- 断线显示: 右下角 PLC 状态 + Service 状态分别指示