Files
bj_power/bj_power_mes/frontend/src/pages/Scan.vue
T
SunYF 8e6e8e1d20 docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
2026-09-20 18:25:44 +08:00

298 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-card>
<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="扫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><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">
<b>工位 {{ form.stationNo }} 参数采集{{ flowName ? '' + flowName + '' : '' }}</b>
<el-button v-if="can('produce.scan')" type="primary" size="small" :loading="saving" @click="submit">提交报工</el-button>
</div>
<el-table :data="steps" border size="small">
<el-table-column prop="seq" label="步骤" width="60" align="center" />
<el-table-column prop="name" label="步骤名称" min-width="130" />
<el-table-column label="采集方式" width="130" align="center">
<template #default="{ row }">
<el-tag v-if="row.collectType === 'AUTO'" type="warning" size="small">拧紧枪自动</el-tag>
<el-tag v-else-if="row.collectType === 'MANUAL'" type="primary" size="small">手填</el-tag>
<el-tag v-else type="info" size="small">仅记录</el-tag>
</template>
</el-table-column>
<el-table-column label="采集值" min-width="180">
<template #default="{ row }">
<el-input v-if="row.collectType !== 'NONE'" v-model="row.value" placeholder="采集值(可空)" style="width:130px" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="判定标准" min-width="200">
<template #default="{ row }">
<template v-if="row.criteria.length">
<el-tag v-for="c in row.criteria" :key="c.id" size="small" style="margin-right:4px">
{{ c.name }}{{ critText(c) }}
</el-tag>
</template>
<span v-else style="color:#909399">未设标准</span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 装机绑定区该工件在本工序应装什么料物料清单的装配工位配置)→ 扫码/手录绑定齐套后才允许报工 -->
<el-card v-if="bindPanel" shadow="never" style="margin-bottom:12px;border-color:var(--el-color-primary)">
<div slot="header" style="display:flex;justify-content:space-between;align-items:center">
<b>装配工位 {{ form.stationNo }} 装机绑定装配物料</b>
<el-alert v-if="bindPanel.enabled && !bindPanel.complete" type="warning" :closable="false" show-icon style="flex:1;margin-left:16px"
:title="'尚未绑齐:' + bindPanel.missing" />
<el-alert v-else-if="bindPanel.enabled && bindPanel.complete" type="success" :closable="false" show-icon style="flex:1;margin-left:16px"
title="装配物料已绑齐,可提交报工" />
<el-alert v-else type="info" :closable="false" style="flex:1;margin-left:16px"
title="该产品未在当前装配工位配置需绑定物料(物料清单「装配工位」为空),不校验" />
</div>
<el-table v-if="bindPanel.required && bindPanel.required.length" :data="bindPanel.required" border size="small">
<el-table-column prop="materialCode" label="图号" min-width="110" />
<el-table-column prop="materialName" label="物料名称" min-width="120" />
<el-table-column prop="manageMode" label="管理方式" width="120">
<template #default="{row}">{{ row.manageMode === '2' ? '电气件(SN)' : '结构件(批次)' }}</template>
</el-table-column>
<el-table-column label="已绑/需绑" width="100" align="center">
<template #default="{row}">
<b :style="{color: row.complete ? 'var(--el-color-success)' : 'var(--el-color-danger)'}">{{ row.boundCount }}/{{ row.need }}</b>
</template>
</el-table-column>
<el-table-column label="已绑定值" min-width="170">
<template #default="{row}">
<el-tag v-for="b in row.bound" :key="b" size="small" style="margin-right:4px">{{ b }}</el-tag>
<span v-if="!row.bound.length" style="color:#909399">未绑定</span>
</template>
</el-table-column>
<el-table-column label="绑定操作" width="230">
<template #default="{row}">
<div style="display:flex;gap:4px">
<el-input v-model="row._input" :placeholder="row.manageMode === '2' ? '扫码SN' : '扫码/输入批次号'" size="small" @keyup.enter="bindOne(row)" />
<el-button size="small" type="primary" :loading="binding" @click="bindOne(row)">绑定</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<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" @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="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 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" />
</el-tab-pane>
</el-tabs>
</el-card>
<PageHelp :data="helpScan" page="Scan" />
</template>
<script setup>
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpScan } from '../help'
import { loadStationNos } from '../utils/stations'
const tab = ref('entry')
const recordsPage = ref(1)
const recordsSize = ref(20)
const recordsTotal = ref(0)
const form = reactive({ stationNo: 1, sn: '' })
const steps = ref([])
const flowName = ref('')
const saving = ref(false)
const bindPanel = ref(null)
const binding = ref(false)
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) {
case 'GE': return ` ≥${c.target}${c.unit}`
case 'LE': return ` ≤${c.target}${c.unit}`
case 'GT': return ` >${c.target}${c.unit}`
case 'LT': return ` <${c.target}${c.unit}`
case 'RANGE': return ` [${c.min},${c.max}]${c.unit}`
case 'EQUAL': return ` =${c.target}${c.unit}`
default: return c.name || ''
}
}
// 从后端加载该工位启用工艺流程的工序步骤(value 挂在 row 上,提交时收集)
async function loadSteps() {
if (!form.sn) return ElMessage.warning('请先输入工件SN')
bindPanel.value = null
try {
const data = await request.get('/process-steps', { params: { stationNo: form.stationNo } }) || []
steps.value = data.map(s => ({ ...s, value: '', text: '' }))
flowName.value = ''
if (!steps.value.length) {
ElMessage.warning(`工位${form.stationNo} 未配置启用的工艺流程,请到「工艺流程」维护`)
}
loadBindPanel()
} catch { steps.value = [] }
}
// 拉取本工件本工序的装配物料绑定面板(应绑清单 + 已绑情况)
async function loadBindPanel() {
if (!form.sn) return
try {
const p = await request.get('/bind-panel', { params: { sn: form.sn, processCode: form.stationNo } }) || null
if (p?.required) {
p.required.forEach((r) => { r._input = '' })
}
bindPanel.value = p
} catch {
bindPanel.value = null
}
}
// 扫/录单个物料 → 立即绑定落库并刷新齐套状态(防错即时反馈)
async function bindOne(row) {
const v = (row._input || '').trim()
if (!v) return ElMessage.warning('请先输入绑定值(电气件扫码SN / 结构件批次号)')
binding.value = true
try {
await request.post('/binds', {
sn: form.sn,
orderNo: '',
processCode: form.stationNo,
stationNo: form.stationNo,
items: [{ materialCode: row.materialCode, bindValue: v }]
})
ElMessage.success('绑定成功')
loadBindPanel()
} catch { /* 错误已在拦截器提示 */ }
binding.value = false
}
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) {
return ElMessage.warning('装配物料未绑齐,请先完成装机绑定:' + (bindPanel.value.missing || ''))
}
const stepReq = steps.value
.filter(s => s.collectType !== 'NONE' && (s.text !== '' || s.value !== ''))
.map(s => ({
stepId: s.id,
name: s.name,
value: Number(s.value) || 0,
text: s.text || String(s.value || '')
}))
// 手动报工:processCode = 工位号(工位 1~12 固定)
await request.post('/workpiece/process/report', {
sn: form.sn,
processCode: form.stationNo,
stationNo: form.stationNo,
steps: stepReq
})
ElMessage.success('报工成功')
steps.value = []
bindPanel.value = null
wpInfo.value = null
Object.assign(form, { sn: '' })
load()
}
async function load() {
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() })
</script>