docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
@@ -211,13 +211,13 @@ export const helpTorque = {
|
||||
export const helpScan = {
|
||||
title: '手动报工(工位工序步骤完成上报)',
|
||||
overview:
|
||||
'页面分两个页签:「报工录入」(默认打开)、「报工记录」(点击才加载,分页)。\n\n录入流程:选择工位、扫入/输入工件 SN 后,系统自动载入该工位「启用」工艺流程的工序步骤(扭矩/角度/尺寸等采集项),逐参数录入提交;提交后写报工实绩 + 步骤数据,系统按考核标准自动判定 合格/不合格。\n\n装机绑定:若该产品在本工序配置了需绑定的装配物料(物料清单「装配工位」),会出现装机绑定区——电气件扫 SN、结构件扫/输批次号,逐项绑齐(已绑/需绑)后才允许提交报工;未配置物料则不校验。\n\n数据链路:工艺流程(工序步骤+考核标准) + 物料清单 装机配置 → 本页选工位扫 SN 自动载入 → 绑料+录参数 → 判 合格/不合格 → 工件追溯可见。',
|
||||
'页面分两个页签:「报工录入」(默认打开)、「报工记录」(点击才加载,分页)。\n\n录入流程:扫入/输入工件 SN 后,系统自动带出该工件的「应报工位」并载入该工位「启用」工艺流程的工序步骤(扭矩/角度/尺寸等采集项);工位也可手动选择。逐参数录入提交;提交后写报工实绩 + 步骤数据,系统按考核标准自动判定 合格/不合格。\n\n放行校验(严格):必须“在制(已进线/在工位) 且 当前工序(=已完成工序+1) == 所选工位”才允许报工,禁止跳站/漏站/重复报同一站;扫 SN 后页面实时给出校验结果,后端报工时再次强校验。\n\n装机绑定:若该产品在本工序配置了需绑定的装配物料(物料清单「装配工位」),会出现装机绑定区——电气件扫 SN、结构件扫/输批次号,逐项绑齐(已绑/需绑)后才允许提交报工;未配置物料则不校验。\n\n报工记录:「报工记录」页签展示真实报工实绩(本页手动报工与工位终端报工均在此可见),可按 SN / 工单号 / 工位 筛选。\n\n数据链路:工艺流程(工序步骤+考核标准) + 物料清单 装机配置 → 本页扫 SN 自动带出工位并载入 → 绑料+录参数 → 判 合格/不合格 → 报工记录/工件追溯可见。',
|
||||
fields: [
|
||||
{ name: '工位', source: '下拉选择(工位数以数据库为准)。', purpose: '哪个工位在报工,选择后自动载入该工位的工序步骤', fill: '选择工位 1~12' },
|
||||
{ name: '工件SN', source: '扫码枪扫描或手输。来自工件条码。', purpose: '工件的唯一序列号,报工对象', fill: '扫码枪扫入,与工位一起触发自动载入' },
|
||||
{ name: '工件SN', source: '扫码枪扫描或手输。来自工件条码。', purpose: '工件的唯一序列号,报工对象;扫入后自动带出工件状态与应报工位', fill: '扫码枪扫入或手输,触发自动带出工位与工艺步骤' },
|
||||
{ name: '工位', source: '扫 SN 自动带出「应报工位」;也可下拉手动选择(工位数以数据库为准)。', purpose: '哪个工位在报工;手选时受工件应报工位约束', fill: '自动带出或手选工位 1~12,须等于应报工位方可报工' },
|
||||
{ name: '各步骤采集值', source: '手工填写 或 拧紧枪自动带出(视采集方式)。字段由【工艺流程】的工序步骤人为定义。', purpose: '收集该工位的扭矩/角度/尺寸等参数,按标准判定合格与否', fill: '逐条填采集值,未维护标准的步骤仅记录' },
|
||||
{ name: '装机绑定(物料/已绑/需绑)', source: '来自物料清单的「装配工位」配置(按工单锁定的物料清单过滤)。', purpose: '该工件本工序应装哪些料;电气件按 SN、结构件按批次绑定,齐套后才允许报工', fill: '在绑定输入框扫码或录入后点「绑定」,绑齐后提交报工' },
|
||||
{ name: '报工记录查询(SN / 工单号)', source: '「报工记录」页签筛选条件,分页查看。', purpose: '查历史报工实绩', fill: '可空=全部' }
|
||||
{ name: '报工记录查询(SN / 工单号 / 工位)', source: '「报工记录」页签筛选条件,分页查看真实报工实绩(含手动与工位终端报工)。', purpose: '查历史报工实绩', fill: '可空=全部' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpInspect } from '../help'
|
||||
import { loadStationNos } from '../utils/stations'
|
||||
import request from '../utils/request'
|
||||
import { confirmExport } from '../utils/confirm'
|
||||
import { fmtTime } from '../utils/format'
|
||||
|
||||
// ---------- 当前用户与权限 ----------
|
||||
@@ -504,6 +505,7 @@ async function exportCsv() {
|
||||
})
|
||||
const rows = data?.list || []
|
||||
if (!rows.length) return ElMessage.warning('当前筛选条件下没有记录')
|
||||
if (!(await confirmExport(rows.length, '巡检记录'))) return
|
||||
const esc = (v) => { const s = v == null ? '' : String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s }
|
||||
const header = ['ID', '时间', '类型', '工位', '班次', '工单号', 'SN', '结果', '点检项/异常类型', '备注/签字', '操作人', '照片']
|
||||
const lines = [header.join(',')]
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { confirmExport } from '../utils/confirm'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpMaterial } from '../help'
|
||||
@@ -225,7 +226,8 @@ async function onExpandRow(row, expandedRows) {
|
||||
}
|
||||
|
||||
// 导出 CSV(带 BOM,Excel 直接打开中文不乱码;MES 前端无 xlsx 库,纯前端导出)
|
||||
function exportLedgerCsv() {
|
||||
async function exportLedgerCsv() {
|
||||
if (!(await confirmExport(progressRows.value.length, '物料去向对账记录'))) return
|
||||
const header = ['工单号', '图号', '物料名称', '目标工位', '需求', '已发出', '已退回', '已接料', '已消耗', '在途', '工位余', '待发缺口', '对账状态', '台账状态']
|
||||
const lines = [header.join(',')]
|
||||
for (const r of progressRows.value) {
|
||||
|
||||
@@ -151,6 +151,7 @@ import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpPerformance } from '../help'
|
||||
import { loadStationNos } from '../utils/stations'
|
||||
import request from '../utils/request'
|
||||
import { confirmExportMessage } from '../utils/confirm'
|
||||
|
||||
// 后端三视图各自真分页(默认20):byOperator/byStation/detail 各带 {total, list}
|
||||
const activeTab = ref('byOperator')
|
||||
@@ -234,6 +235,19 @@ async function load() {
|
||||
// 导出:后端一个 xlsx 三个 sheet(按人/按工位/明细),执行统一导出时间硬规则
|
||||
async function exportXlsx() {
|
||||
const [from, to] = query.range || []
|
||||
const base = {
|
||||
operator: query.operator || '', stationNo: query.stationNo || '',
|
||||
orderNo: query.orderNo.trim(), from: from || '', to: to || ''
|
||||
}
|
||||
// 按与导出相同口径取三张表的条数,用于确认告知
|
||||
const cnt = (await request.get('/performance', {
|
||||
params: { ...base, opPage: 1, opPageSize: 1, stPage: 1, stPageSize: 1, detPage: 1, detPageSize: 1 }
|
||||
})) || {}
|
||||
const op = cnt.byOperator?.total || 0
|
||||
const st = cnt.byStation?.total || 0
|
||||
const det = cnt.detail?.total || 0
|
||||
const msg = `即将导出绩效报表(时间 ${from || '不限'} 至 ${to || '不限'}):\n明细 ${det} 条、按人汇总 ${op} 人、按工位汇总 ${st} 个工位,是否继续?`
|
||||
if (!(await confirmExportMessage(msg))) return
|
||||
const params = {
|
||||
operator: query.operator || '', stationNo: query.stationNo || '',
|
||||
orderNo: query.orderNo.trim(),
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { confirmExport } from '../utils/confirm'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpProductType } from '../help'
|
||||
@@ -153,6 +154,9 @@ async function exportXlsx() {
|
||||
isActive: filters.isActive,
|
||||
startDate: filters.startDate, endDate: filters.endDate
|
||||
}
|
||||
// 按与导出完全相同的筛选取精确条数,确认后才下载
|
||||
const cnt = await request.get('/product-types/page', { params: { ...params, page: 1, pageSize: 1 } })
|
||||
if (!(await confirmExport(cnt?.total ?? null, '物料档案'))) return
|
||||
const blob = await request.get('/product-types/export', { params, responseType: 'blob' })
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
// 导出前统一确认:告知用户即将导出的数据条数。
|
||||
// count 为 null/undefined(无法预知条数)时提示“全部”;否则回显具体条数。
|
||||
export async function confirmExport(count, name = '数据') {
|
||||
const msg = (count === null || count === undefined)
|
||||
? `即将导出全部${name},是否继续?`
|
||||
: `即将导出 ${count} 条${name},是否继续?`
|
||||
return confirmExportMessage(msg)
|
||||
}
|
||||
|
||||
// 自定义文案的导出确认:用于多 Sheet 报表等无法用单一条数概括的场景。
|
||||
export async function confirmExportMessage(msg, title = '导出确认') {
|
||||
try {
|
||||
await ElMessageBox.confirm(msg, title, {
|
||||
confirmButtonText: '确认导出',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default { confirmExport, confirmExportMessage }
|
||||
Reference in New Issue
Block a user