跳到主要内容

数据大屏

数据大屏 = 把分散的 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
MTBFh平均无故障时间-
MTTRmin平均修复时间-

设计前置

1. 分辨率选择

分辨率像素密度适用
1920 × 10800.52 px/mm @ 55"常规, 绝大多数 LED/LCD 屏
2560 × 14400.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

关键数据源

典型路径刷新
实时 KPIDB100.* PLC 订阅 (darra.bind)推送 (100ms)
工位状态网格DB100.StationGrid 结构体推送
历史 SparklineHistorian 时序查询 (见 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 状态分别指示

相关文档