结论
这个 /machine 不是普通“页面切换”,而是一个“后端流程配置 + 前端 WebSocket 事件驱动”的状态机。前端本地只保存少量核心状态,比如 step / flows / record / recordList / points / progress / exception,真正的推进很多时候不是点按钮后直接跳页,而是先发接口,再等后端通过 WebSocket 把“当前步骤”推回来,所以它非常适合写成你那条“基于 WebSocket 的多步骤流程状态机”。核心入口在 index.js、IndexView、model.js。
状态机骨架
- 设备初始化:页面通过
machine_id查询设备配置,拿到mached / flowGroups / flows,默认先加载第一个流程组。IndexView model.js - 状态定义:
step存的其实是“当前 flow 在数组里的索引”,真正的业务步骤码是flows[step].step,这点很关键。IndexView model.js - 流程可配置:自助机管理页里
flowGroups是可配置的,默认步骤来自group.def,说明这个状态机不是纯前端硬编码,而是“设备配置驱动”。Edit - 双流程组:从首页能进入
group.id == '1'的“判级主流程”,也能进入group.id == '2'的“进展查询流程”;切组时还会把mached.id拼上,groupId,回首页再恢复旧 id。GradeTypeView AuthenticationModelView
stateDiagram-v2 [*] --> "0 首页" "0 首页" --> "1 身份/手动录入": "点击判级" "0 首页" --> "6 判级总览": "进展查询 + 口令验证" "1 身份/手动录入" --> "2 卸料点": "chengnan_back 后端推送" "2 卸料点" --> "3 停车确认": "toGrading + WS" "3 停车确认" --> "3 停车确认": "重新校验" "3 停车确认" --> "4 入场预报": "开始/恢复判级" "4 入场预报" --> "5 隐藏确认态": "本地切到确认弹窗" "5 隐藏确认态" --> "6 判级总览": "确认后 toGrading + WS" "6 判级总览" --> "7_0 详情": "选择车次" "7_0 详情" --> "6 判级总览": "返回" "7_0 详情" --> "7_0 详情": "WS 增量刷新" "任意状态" --> "10 异常页": "后端推送其他状况" "任意状态" --> "首页/复位": "fallback=1 或首页"
完整自助机流程
- 首页
step=0:欢迎页只有两个入口,“判级”和“进展查询”,并先校验是否处于工作时间。GradeTypeView - 判级入口进入主流程组:切到
group 1,本地step + 1进入步骤 1。GradeTypeView - 步骤 1 现在不是身份证扫描,而是直接走“手工录入车牌号/毛重/计量单号”。这里还会先调用
getMeasureId自动取计量单号,确认后调用chengnanBack。ManualInputView service.js - 这里有个重要特征:录完并不会本地自己跳到下一页,而是等后端通过 WebSocket 回推
step == "2",再进入“选择卸料点”。IndexView - 步骤 2 选择卸料点:用户点某个有效卸料点,写入
record.pointId / point,然后发toGrading。PointsView - 步骤 3 停车确认:后端推送
record.vehiclePositionConfirmation。如果是1,说明停车位置合格,可以“开始判级/恢复判级”;如果不是,则只能“重新校验”;如果是2,前端会弹出等待校验结果的异常模态。LocalConfirmView ExceptionView - 步骤 4 入场预报:选择料型和预报等级。这里也很像状态机设计,不是立刻调用后端,而是先把
materialType / erGrading / step写进 store,进入一个隐藏确认态。EnterGradeView - 步骤 5 隐藏确认态:
flows[step].display == '1'时不会单独渲染页面,而是覆盖一个InfoConfirmModelView确认弹窗。确认后才真正发toGrading进入下一步。IndexView InfoConfirmModelView - 步骤 6 判级总览:后端推送
recordList后,前端展示车次列表。点某条记录进入详情页7_0。IndexView RecordSelectView - 步骤
7_0详情:这里是实时判级结果页,左侧视频/抓拍图,右侧基本信息、判级详情和操作按钮。状态onStatus决定是否直播、是否允许“结束判级”、是否允许“数据录入”。ResultView ResultView step == "10":统一异常/其他状况页,由后端直接推异常文案。IndexView
WebSocket 如何驱动状态变换
- WebSocket 连接地址按设备号拼成
/websocket/machine/{machineId},页面一加载设备成功后就会建立连接。IndexView status == 1是业务态推进,messageBody里带step / record / recordList / pointList / exception,前端按步骤码更新 store。IndexViewstatus == 2表示同一台设备重复登录,直接把step打成-1,走错误页。IndexViewstatus == 3是服务端心跳响应,前端重启一轮心跳检测;前端每 5 秒发一次"isOnLine",超时没收到回包就主动close()触发重连。IndexView IndexView- 底层
Websocket组件本身还做了断线自动重连,使用指数退避重建连接,所以这里其实是“组件级重连 + 业务级心跳”双保险。Websocket status == 5是详情/总览页增量刷新,不改大步骤,只把最新recordmerge 进当前recordList,非常适合实时判级中的渐进式结果展示。IndexViewstatus == 10会在 10 秒后自动刷新整页,属于最终兜底。IndexView
异常兜底与人工补录
- 前端自己会先做一次“步骤合法性校验”:如果当前 flow 的
nextId和用户点的下一步不匹配,就弹showProgressModal,阻止非法跳转。model.js - 停车未确认、恢复判级中、多次异常消息都会累计进
progress[],由ExceptionView统一做进度展示,这个就是“异常兜底面板”。ExceptionView - 结果页支持人工补录,但只在非“判级中”状态开放;打开前会先
getRecordById拉一次最新记录,避免旧数据编辑。ResultView model.js - 补录内容包括人工级别占比、人工扣杂、毛重、皮重,前端会校验“占比和=100”“人工扣杂必填”“重量非负”。EditModal
- 保存后不会只关弹窗,而是再次
getRecordById,把最新结果回填到record和recordList,这正对应你说的“人工补录刷新”。model.js
一句话概括这个自助机状态机
它本质上是一个“设备配置驱动的流程状态机”:前端根据 flowGroups/flows 决定可执行步骤,以 step(index) 和 flow.step(业务码) 双层状态建模;用户动作只负责触发 start/back/calc/stop/pause 等事件,真正的状态推进由 WebSocket 回推完成,同时叠加了心跳保活、断线重连、重复登录拦截、异常进度弹窗和人工补录后的记录重拉刷新。
如果你要把项目亮点写得更准 可以改成这版:
- 基于设备配置
flowGroups/flows与 WebSocket 事件回推,构建废钢智能判级自助终端多步骤流程状态机,串联设备初始化、身份录入、卸料点选择、停车校验、预报确认、判级总览与结果详情等核心节点,并实现心跳保活、断线重连、重复登录拦截、异常进度兜底及人工补录后记录刷新,支撑现场复杂判级流程闭环。
如果你愿意,我下一步可以继续帮你把这套流程整理成“面试可讲版本”,包括“状态定义、事件、转移条件、为什么用 WebSocket 而不是轮询”三段话。