docs(business): 生成业务契约文档并重构MES客户端代码

- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
SunYF
2026-09-20 18:25:44 +08:00
parent 5f68e77d5f
commit 8e6e8e1d20
46 changed files with 1052 additions and 3954 deletions
+79 -15
View File
@@ -3,15 +3,21 @@
<el-tabs v-model="tab" @tab-change="onTab">
<el-tab-pane label="报工录入" name="entry">
<el-form inline>
<el-form-item label="工件SN">
<el-input v-model="form.sn" placeholder="扫码或手输SN" style="width:200px" clearable @change="onSnChange" />
</el-form-item>
<el-form-item label="工位">
<el-select v-model="form.stationNo" placeholder="请选择工位" style="width:120px" @change="loadSteps">
<el-select v-model="form.stationNo" placeholder="扫SN自动带出,也可手选" style="width:160px" @change="onStationChange">
<el-option v-for="i in stationNos" :key="i" :value="i" :label="`工位${i}`" />
</el-select>
</el-form-item>
<el-form-item label="工件SN"><el-input v-model="form.sn" placeholder="扫码或手输" style="width:190px" @change="loadSteps" /></el-form-item>
<el-form-item><el-tag v-if="steps.length" type="success" size="small">已自动载入当前工位应做步骤</el-tag></el-form-item>
</el-form>
<!-- 放行校验:扫SN带出工件状态与应报工位;必须“在制 且 当前工序(=已完成工序+1) == 所选工位”才允许报工 -->
<el-alert v-if="wpInfo" :type="passGate ? 'success' : 'error'" :closable="false" show-icon style="margin-bottom:12px"
:title="`工件 ${wpInfo.sn}:状态 ${statusText(wpInfo.status)}、已完成工序 ${wpInfo.currentProcess}、应报工位 ${wpInfo.shouldStation}` + (passGate ? '(校验通过,可报工)' : `(当前选择工位${form.stationNo}不符,禁止报工)`)" />
<!-- 参数采集区:按该工位启用工艺流程的工序步骤动态渲染 -->
<el-card v-if="steps.length" shadow="never" style="margin-bottom:12px">
<div slot="header" style="display:flex;justify-content:space-between;align-items:center">
@@ -85,23 +91,33 @@
</el-table-column>
</el-table>
</el-card>
<el-alert v-if="!steps.length && !bindPanel" type="info" :closable="false" title="请选择工位、输入SN后点【载入工艺步骤】;若提示未配置工序步骤,请到「工艺流程」为该工位启用流程并维护工艺步骤" />
<el-alert v-if="!steps.length && !bindPanel" type="info" :closable="false" title="扫码/输入工件SN后自动带出应报工位并载入工艺步骤;若提示未配置工序步骤,请到「工艺流程」为该工位启用流程并维护工艺步骤" />
</el-tab-pane>
<el-tab-pane label="报工记录" name="records" lazy>
<el-alert type="info" :closable="false" show-icon style="margin-bottom:10px"
title="本页展示真实报工记录(手动报工与工位终端报工均在此可见),可按 SN / 工单号 / 工位筛选。" />
<el-form inline>
<el-form-item label="SN"><el-input v-model="query.sn" clearable style="width:160px" /></el-form-item>
<el-form-item label="工单号"><el-input v-model="query.orderNo" clearable style="width:160px" /></el-form-item>
<el-form-item><el-button @click="load">刷新</el-button></el-form-item>
<el-form-item label="SN"><el-input v-model="query.sn" clearable style="width:160px" @keyup.enter="searchRecords" /></el-form-item>
<el-form-item label="工单号"><el-input v-model="query.orderNo" clearable style="width:160px" @keyup.enter="searchRecords" /></el-form-item>
<el-form-item label="工位">
<el-select v-model="query.stationNo" placeholder="全部" clearable style="width:110px" @change="searchRecords">
<el-option v-for="i in stationNos" :key="i" :value="i" :label="`工位${i}`" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="searchRecords">查询</el-button></el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="sn" label="SN" min-width="130" />
<el-table-column prop="orderNo" label="工单号" min-width="130" />
<el-table-column prop="station" label="工位" width="80" />
<el-table-column prop="type" label="类型" width="110" />
<el-table-column prop="processCode" label="工位号" width="80" />
<el-table-column prop="orderNo" label="工单号" min-width="120" />
<el-table-column prop="stationNo" label="工位" width="70" align="center" />
<el-table-column prop="processName" label="工序" width="100" />
<el-table-column label="判定" width="80" align="center">
<template #default="{ row }"><el-tag :type="row.result === 'OK' ? 'success' : 'danger'" size="small">{{ row.result }}</el-tag></template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="90" />
<el-table-column prop="time" label="时间" min-width="170" />
<el-table-column label="时长" width="80" align="center"><template #default="{ row }">{{ row.durationSec }}s</template></el-table-column>
<el-table-column prop="endedAt" label="报工时间" min-width="160" />
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next"
:total="recordsTotal" :page-size="recordsSize" v-model:current-page="recordsPage" @current-change="load" />
@@ -112,7 +128,7 @@
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
@@ -130,9 +146,53 @@ const flowName = ref('')
const saving = ref(false)
const bindPanel = ref(null)
const binding = ref(false)
const query = reactive({ sn: '', orderNo: '' })
const query = reactive({ sn: '', orderNo: '', stationNo: '' })
const list = ref([])
const stationNos = ref([])
// 扫SN带出的工件信息:{ sn, status, currentProcess(已完成工序), shouldStation(应报工位=currentProcess+1), orderNo }
const wpInfo = ref(null)
// 放行门:在制(ONLINE/PROCESSING) 且 当前工序(已完成+1) == 所选工位
const passGate = computed(() => {
const w = wpInfo.value
if (!w) return false
const online = w.status === 'ONLINE' || w.status === 'PROCESSING'
return online && Number(form.stationNo) === Number(w.shouldStation)
})
function statusText(s) {
return { ONLINE: '已进线', PROCESSING: '在工位', DONE: '已完工', REPAIR: '返修', SCRAPPED: '报废' }[s] || s
}
// 扫/输 SN → 查工件,自动带出应报工位(SN 带出工位);非在制则阻断
async function onSnChange() {
const sn = (form.sn || '').trim()
wpInfo.value = null
steps.value = []
bindPanel.value = null
if (!sn) return
const arr = await request.get('/workpieces', { params: { sn } }) || []
const wp = arr.find(x => (x.sn || '').toUpperCase() === sn.toUpperCase())
if (!wp) {
ElMessage.warning('未找到该SN的工件,请先在「工件进线」登记')
return
}
const cur = wp.currentProcess || 0
wpInfo.value = { sn: wp.sn, status: wp.status, currentProcess: cur, shouldStation: cur + 1, orderNo: wp.orderNo }
form.stationNo = cur + 1 // SN 带出工位
if (wp.status !== 'ONLINE' && wp.status !== 'PROCESSING') {
ElMessage.warning(`工件状态为${statusText(wp.status)},非在制,不能报工`)
return
}
await loadSteps()
}
// 手选工位 → 工位限制SN:已扫SN时,所选工位必须==应报工位(前端预校,后端同样严格校验)
async function onStationChange() {
if (wpInfo.value && Number(form.stationNo) !== Number(wpInfo.value.shouldStation)) {
ElMessage.warning(`工件 ${wpInfo.value.sn} 已完成工序${wpInfo.value.currentProcess},只能在工位${wpInfo.value.shouldStation}报工`)
}
if (form.sn) await loadSteps()
}
function critText(c) {
switch (c.logic) {
@@ -196,6 +256,8 @@ async function bindOne(row) {
async function submit() {
if (!form.sn) return ElMessage.warning('请输入SN')
if (!wpInfo.value) return ElMessage.warning('请先扫码/输入工件SN并确认工件状态')
if (!passGate.value) return ElMessage.warning('校验未通过:必须“在制 且 当前工序=该工位”才能报工')
if (!steps.value.length) return ElMessage.warning('该工位未配置工艺步骤,无法报工')
// 报工前强校验:本工序装配物料需绑齐(后端 validateStationBinds)
if (bindPanel.value?.enabled && !bindPanel.value?.complete) {
@@ -219,15 +281,17 @@ async function submit() {
ElMessage.success('报工成功')
steps.value = []
bindPanel.value = null
wpInfo.value = null
Object.assign(form, { sn: '' })
load()
}
async function load() {
const data = await request.get('/scan/records', { params: { ...query, page: recordsPage.value, pageSize: recordsSize.value } })
const data = await request.get('/workpiece/process/records', { params: { ...query, page: recordsPage.value, pageSize: recordsSize.value } })
list.value = data?.list || []
recordsTotal.value = data?.total || 0
}
function searchRecords() { recordsPage.value = 1; load() }
function onTab(name) { if (name === 'records' && !list.value.length) load() }
onMounted(async () => { stationNos.value = await loadStationNos(); if (form.stationNo) loadSteps() })
onMounted(async () => { stationNos.value = await loadStationNos() })
</script>