ch12 Vue3 看板与简易 APS:Andon 大屏 + OEE 实时图 + 化成瓶颈排产

事故现场:厂长上班要看 8 个系统,APS 用 Excel 排产漏掉化成瓶颈
事故复盘要把 MES 前端的工程坑还原。ch00-ch11 全部功能落地后,MES 后端能力齐全(BOM/Routing/Dispatch/采集/SPC/OEE/Andon/化成预测/IoTDB),但前端没整合:
问题 1:厂长上班看 8 个系统。厂长早上 8 点上班,要打开 SCADA 看板(设备实时状态)、MES 派工单页面、SPC 控制图、OEE 报表(Excel 导出)、Andon 历史记录、化成预测结果(独立看板)、ERP 工单、SCADA 报警中心——8 个 tab 切来切去,厂长说"我看个产线状态要切 8 个 tab,这 MES 是给我增加工作量还是减少?"。
问题 2:APS 排产用 Excel。APS(Advanced Planning and Scheduling)是排产系统,电芯厂 A 当时的 APS 是计划员用 Excel 排——周一早上排本周的 7 段工艺派工单,按工单优先级 + 设备可用性人工排。漏掉化成瓶颈——化成柜 96 台但每柜 12 小时一柜电芯,是 7 段工艺的瓶颈(其他工序都是分钟级节拍,化成是小时级),计划员排产时没把化成柜能力作为约束,导致前工序产出堆积在 WIP(在制品)区,化成柜排不上——周一排的 1000 颗 X-280 工单,涂布周二就产完,化成柜周三才能上,WIP 区堆了 1000 颗裸芯 24 小时。
问题 3:Andon 看板不在车间大屏显示。ch09 的 Andon 系统做了,但只在 MES 后台页面里,车间没大屏——操作员看不到 Andon 呼叫,响应慢。事故 1 的客诉 Andon L3 呼叫发出后,化成车间没人看到,质量工程师 2 小时后才在后台看到。
排查:前端整合与 APS 瓶颈识别
复盘把前端问题拆成三层:
| 层 | 缺口 | 后果 |
|---|---|---|
| 数据层 | 8 个系统 8 套 API | 厂长切 tab 切到崩溃 |
| 视图层 | 车间没 Andon 大屏 | 呼叫响应慢 |
| 排产层 | APS 用 Excel + 无瓶颈约束 | WIP 堆积、化成瓶颈漏识别 |
MES 前端的整合策略
业内 MES 前端整合有三种思路:
思路 1:单页面整合——所有功能在 MES 单页面内 tab 切换,用户登录 MES 后看到所有信息。简单但单页面信息密度高,厂长看不懂。
思路 2:仪表板整合——按角色做仪表板(厂长仪表板、车间主任仪表板、操作员工位终端),每个仪表板聚合角色关心的所有数据。业内主流。
思路 3:iframe 整合——把 SCADA 看板、ERP 工单页面 iframe 嵌入 MES 仪表板。最快但用户体验差(iframe 切换慢、跨域 cookie 问题)。
本期选思路 2——按角色 3 个仪表板:
- 厂长仪表板:8 个核心指标卡(OEE 全厂、客诉 Andon 数、SPC 异常数、化成预测异常、WIP 库存、当日产出、当周排产、瓶颈告警)
- 车间主任仪表板:本车间设备实时状态 + Andon 呼叫 + 派工单进度 + SPC 控制图
- 操作员工位终端:当前派工单 + 设备状态 + Andon 呼叫按钮 + SOP 文档
APS 瓶颈识别:化成是 7 段工艺瓶颈

7 段工艺的节拍对比:
| 工序 | 设备数 | 单设备节拍 | 单设备日产能 | 工序日产能 |
|---|---|---|---|---|
| 涂布 | 2 | 0.5 片/min | 720 片 | 1440 片 |
| 辊压 | 1 | 1 片/min | 1440 片 | 1440 片 |
| 分切 | 1 | 2 片/min | 2880 片 | 2880 片 |
| 卷绕 | 8 | 3 PPM | 4320 颗 | 34560 颗 |
| 注液 | 1 | 2 PPM | 2880 颗 | 2880 颗 |
| 化成 | 96 | 12 h/柜 × 7 颗 | 14 颗/柜 × 96 = 1344 颗 | 1344 颗 |
| 分容 | 96 | 12 h/柜 × 7 颗 | 14 颗/柜 × 96 = 1344 颗 | 1344 颗 |
化成是瓶颈——日产能 1344 颗远低于卷绕 34560 颗和注液 2880 颗,瓶颈比 1:25.7。APS 必须把化成柜能力作为主约束,按化成柜可用能力倒推前工序排产,让前工序产出节奏匹配化成柜吞吐——涂布不能一天产 1440 片,要按化成柜当天能消化多少产多少,避免 WIP 堆积。
底层原理:Vue3 仪表板 + 简易 APS 算法
Vue3 仪表板技术栈
技术栈:
框架: Vue 3 + Vite
UI库: Element Plus + ECharts 5
状态管理: Pinia
路由: Vue Router 4
HTTP: Axios
实时推送: WebSocket + Server-Sent Events
大屏适配: postcss-px-to-viewport (1920x1080 → 自适应)
厂长仪表板组件设计
<!-- DashboardDirector.vue -->
<template>
<div class="dashboard-director">
<h1>厂长仪表板 - {{ factoryName }} - {{ currentTime }}</h1>
<!-- 8 个核心指标卡 -->
<el-row :gutter="16" class="metrics-cards">
<el-col :span="6" v-for="m in metrics" :key="m.key">
<MetricCard :metric="m" @click="showDetail(m)" />
</el-col>
</el-row>
<!-- 实时 Andon 呼叫滚动条 -->
<AndonScroll :calls="openAndonCalls" />
<!-- 全厂 OEE 矩阵 -->
<OeeMatrix :devices="devices" :oee-data="oeeData" />
<!-- 化成瓶颈告警 -->
<BottleneckAlert v-if="bottleneckWarning" :warning="bottleneckWarning" />
<!-- 周排产甘特图 -->
<ApsGantt :schedule="weeklySchedule" />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useWebSocket } from '@vueuse/core'
const metrics = ref([])
const openAndonCalls = ref([])
const oeeData = ref({})
const weeklySchedule = ref([])
const bottleneckWarning = ref(null)
// WebSocket 实时推送
const { status, data } = useWebSocket('wss://api.mashangan.com/ws/dashboard')
watch(data, (msg) => {
const evt = JSON.parse(msg)
if (evt.type === 'METRIC_UPDATE') {
const idx = metrics.value.findIndex(m => m.key === evt.key)
if (idx >= 0) metrics.value[idx] = evt
} else if (evt.type === 'ANDON_CALL') {
openAndonCalls.value.unshift(evt.call)
} else if (evt.type === 'BOTTLENECK_WARNING') {
bottleneckWarning.value = evt.warning
}
})
onMounted(async () => {
// 初次加载
const [m, a, o, s] = await Promise.all([
api.getMetrics(), api.getOpenAndon(),
api.getOeeMatrix(), api.getWeeklySchedule()
])
metrics.value = m
openAndonCalls.value = a
oeeData.value = o
weeklySchedule.value = s
})
</script>
Andon 车间大屏组件
<!-- AndonBigScreen.vue - 投影到车间大屏(55寸电视) -->
<template>
<div class="andon-big-screen">
<div class="header">
<h1>车间 Andon 看板</h1>
<div class="time">{{ currentTime }}</div>
</div>
<div class="calls-grid">
<div v-for="call in sortedCalls" :key="call.id"
:class="['call-card', `level-${call.level.toLowerCase()}`,
isOverdue(call) ? 'overdue-blink' : '']">
<div class="level">{{ call.level }}</div>
<div class="title">{{ call.title }}</div>
<div class="meta">
<span>呼叫 {{ timeAgo(call.callTime) }}</span>
<span>剩余 {{ remaining(call.slaResponseDeadline) }}</span>
<span>{{ call.assigneeName || '待响应' }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.andon-big-screen {
font-size: 24px; /* 大屏字号要大 */
background: #1a1a2e;
color: #fff;
min-height: 100vh;
}
.level-l1 { background: #e74c3c; } /* 红 */
.level-l2 { background: #e67e22; } /* 橙 */
.level-l3 { background: #f1c40f; color: #000; } /* 黄 */
.level-l4 { background: #3498db; } /* 蓝 */
.overdue-blink { animation: blink 0.5s infinite; }
@keyframes blink {
50% { opacity: 0.5; }
}
</style>
简易 APS 算法:化成瓶颈驱动排产
APS 主流算法有 3 种:① TOC(Theory of Constraints)瓶颈驱动;② 遗传算法;③ LP(线性规划)启发式。本期选 TOC 简化版——化成柜是瓶颈,按化成柜能力倒推前工序。
@Service
public class SimpleApsService {
@Autowired private WorkOrderService woService;
@Autowired private DeviceService deviceService;
@Autowired private ShiftCalendarService shiftService;
/**
* 排产算法(化成瓶颈驱动):
* 1. 找出化成柜的可用时段
* 2. 按工单优先级 + 化成曲线分配化成柜
* 3. 倒推前工序(注液/卷绕/分切/辊压/涂布)的开始时间
*/
public ScheduleResult schedule(LocalDate fromDate, int days) {
// 1. 拉取所有待排产工单,按优先级排序
List<WorkOrder> wos = woService.findPending(fromDate, days);
wos.sort(Comparator.comparing(WorkOrder::getPriority).reversed());
// 2. 找出化成柜可用时段
List<DeviceAvailability> formationSlots =
deviceService.findAvailability("化成", fromDate, days);
// 3. 按化成柜能力分配工单
List<ScheduledOperation> scheduled = new ArrayList<>();
for (WorkOrder wo : wos) {
// 化成需要的柜时 = 数量 / 单柜容量(7) × 单柜时长(12h)
int formationCells = (int) Math.ceil(wo.getQuantity() / 7.0);
int formationHours = formationCells * 12;
// 找最早可用的化成柜时段
DeviceAvailability slot = findEarliestSlot(
formationSlots, formationHours, wo.getFormationCurve());
if (slot == null) {
log.warn("化成柜能力不足,工单 " + wo.getWoNo() + " 排不下");
continue;
}
// 分配化成工序
scheduled.add(scheduleFormation(wo, slot, formationHours));
// 占用化成柜时段
slot.consume(formationHours);
// 倒推注液/卷绕/分切/辊压/涂布的开始时间
LocalDate formationStart = slot.getStartTime();
scheduled.addAll(scheduleUpstreamOps(wo, formationStart));
}
return new ScheduleResult(scheduled, detectBottleneck(scheduled));
}
private List<ScheduledOperation> scheduleUpstreamOps(WorkOrder wo,
LocalDate formationStart) {
List<ScheduledOperation> ops = new ArrayList<>();
// 注液在化成前 4 小时(注液 2 PPM × 1000 颗 = 500 分钟 ≈ 8 小时,重叠排)
ops.add(scheduleOp(wo, 5, formationStart.minusHours(4)));
// 卷绕在注液前 2 小时
ops.add(scheduleOp(wo, 4, formationStart.minusHours(6)));
// 分切在卷绕前 1 小时
ops.add(scheduleOp(wo, 3, formationStart.minusHours(7)));
// 辊压在分切前 1 小时
ops.add(scheduleOp(wo, 2, formationStart.minusHours(8)));
// 涂布在辊压前 4 小时(涂布 0.5 片/min,1000 片 = 2000 分钟 = 33 小时)
ops.add(scheduleOp(wo, 1, formationStart.minusHours(37)));
return ops;
}
private BottleneckWarning detectBottleneck(List<ScheduledOperation> scheduled) {
// 检测 WIP 堆积:若某工序产出与下工序消化能力差 > 200 颗,告警
// ...
}
}
排产甘特图组件
<!-- ApsGantt.vue -->
<template>
<div class="aps-gantt">
<h2>周排产甘特图</h2>
<div class="gantt-container" ref="ganttRef"></div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps(['schedule'])
const ganttRef = ref(null)
let chart = null
onMounted(() => {
chart = echarts.init(ganttRef.value)
renderGantt()
})
watch(() => props.schedule, renderGantt)
function renderGantt() {
// 把 schedule 转换为 ECharts 自定义系列甘特图
const categories = ['涂布', '辊压', '分切', '卷绕', '注液', '化成', '分容']
const data = props.schedule.map(op => ({
name: op.woNo,
value: [categories.indexOf(op.opName),
op.startTs, op.endTs],
itemStyle: { color: levelToColor(op.priority) }
}))
chart.setOption({
tooltip: { formatter: p => `${p.data.name}<br/>${p.data.value[1]} - ${p.data.value[2]}` },
xAxis: { type: 'time' },
yAxis: { type: 'category', data: categories },
series: [{ type: 'custom', renderItem: renderGanttBar, data }]
})
}
</script>
数据说话:前端整合 + APS 上线后对比
上线 3 个月,对比事故前后:
| 维度 | 事故前 | 上线后 | 改进 |
|---|---|---|---|
| 厂长上班看指标耗时 | 8 tab × 5 min = 40 min | 1 仪表板 5 min | -87% |
| Andon 车间响应时间 | 后台看到 2h | 大屏看到 5min | -96% |
| WIP 库存堆积 | 月均 1500 颗 × 24h | 月均 200 颗 × 4h | -87% |
| 化成柜利用率 | 71% | 89% | +18% |
| 排产准确率 | Excel 排,化成漏 30% | APS 算法化成 0 漏 | +30% |
| 厂长对 MES 满意度 | "增加工作量" | "5 分钟看完状态" | 显著提升 |
关键发现:
- WIP 堆积减少 87%——化成柜倒推排产后,前工序按化成能力产出,不再过量生产。
- 化成柜利用率从 71% → 89%——APS 把工单合理分配到不同化成柜,避免某柜空闲某柜堵塞。
- 排产准确率 +30%——APS 考虑了化成曲线与产品料号匹配(ch05 的核心校验),不再排错曲线。
面试怎么答:MES 前端与 APS
问:MES 前端为什么按角色做仪表板,不做单页面?
答:3 个原因:① 信息密度——单页面聚合所有功能信息密度高,厂长看不懂(厂长关心 OEE 全厂/客诉/瓶颈,不关心单设备 SPC 控制图);② 角色关注点差异——厂长看 8 个核心指标卡(5 秒判断异常),车间主任看本车间设备实时状态(5 分钟决策改派),操作员看当前派工单(实时操作),三个角色三个仪表板;③ 大屏适配——车间大屏 55 寸要求字号大、信息简洁,操作员工位终端 21 寸要求操作按钮大,单页面无法同时适配。按角色仪表板是工业软件标准实践。
问:APS 为什么选 TOC 瓶颈驱动,不用遗传算法或 LP?
答:3 个原因:① 算法透明度——TOC 是"找瓶颈→按瓶颈倒推"的确定性算法,结果可解释(厂长问"为什么这么排",工程师能解释化成柜能力约束);遗传算法是元启发式,结果不可解释;② 计算复杂度——TOC 是 O(N×M) 线性复杂度,96 设备 × 30 工单 30 秒出结果;LP 需建模为整数规划,96 设备变 96 决策变量 × 30 工时窗 = 2880 变量,求解需商用求解器 Gurobi/CPLEX;③ 场景匹配——电芯厂 7 段工艺瓶颈明确是化成柜,TOC 适用"单瓶颈"场景;多瓶颈场景才需要遗传/LP。本期 TOC 够用,未来多车间多瓶颈可升级到 LP。
问:化成柜为什么是瓶颈?APS 怎么利用这个信息?
答:化成柜是瓶颈因为单柜 12 小时一柜 × 7 颗电芯,96 柜日产能 1344 颗,远低于卷绕 34560 颗、注液 2880 颗——瓶颈比 1:25.7。APS 利用这个信息的核心算法:① 找化成柜可用时段——按班次日历 + 设备维护计划算出每个化成柜的可用时段;② 按工单优先级分配化成柜——优先级高的工单先排,按化成曲线匹配可用柜(ch05 校验);③ 倒推前工序开始时间——化成开始前 4 小时注液、前 6 小时卷绕、前 7 小时分切、前 8 小时辊压、前 37 小时涂布。倒推让前工序在化成柜需要时刚好产出,避免 WIP 堆积。
问:车间 Andon 大屏与 MES 后台有什么不同?
答:3 点差异:① 字号——后台 14px,大屏 24px+(55 寸电视观看距离 5-10 米,14px 看不清);② 配色——后台浅色主题(白底黑字),大屏深色主题(黑底彩字,黑暗车间更醒目,超时闪烁红蓝);③ 信息密度——后台显示所有 Andon 呼叫(含历史),大屏只显示当前 OPEN 状态 + SLA 倒计时,按级别排序,超时高亮。大屏是车间操作员的第一信息源,事故 1 客诉响应慢 2 小时的根因就是没大屏。
落地清单:Vue3 仪表板 + APS 工程化动作
- 3 个角色仪表板:厂长(8 指标卡+Andon 滚动+OEE 矩阵+瓶颈告警+甘特图)、车间主任(设备实时+Andon+派工进度+SPC)、操作员(当前派工+设备状态+Andon 按钮+SOP),禁用单页面整合。
- 车间 Andon 大屏:55 寸电视深色主题,字号 24px+,超时闪烁,只显示 OPEN 呼叫。
- WebSocket 实时推送:禁用轮询,WS 推送指标更新/Andon 呼叫/瓶颈告警。
- APS TOC 瓶颈驱动:化成柜能力作主约束,倒推前工序开始时间,禁用 Excel 排产。
- 化成曲线与产品料号匹配校验:APS 分配化成柜时校验曲线 capacity_spec 与产品 capacity_spec 一致(ch05 防线)。
- 排产甘特图 ECharts:自定义系列渲染甘特图,按工序分行,按优先级配色。
- WIP 堆积检测:APS 输出后自动检测各工序在制品堆积,超 200 颗告警。
- 黄金回归:① 厂长仪表板 5 秒内加载完 8 指标卡;② Andon 大屏超时必须闪烁;③ APS 排产必须把化成柜作主约束;④ 化成曲线与产品料号不匹配的工单不能排产;⑤ WIP 超 200 颗必须告警。
最后一章我们离开工程实现,把全 13 章知识结构化成面试总纲——30 道高频 MES 面试题,覆盖九大模块的答题框架。
