feat: 完成客户第12条需求+多系统改造

1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
This commit is contained in:
SunYF
2026-09-19 17:04:58 +08:00
parent 62e45b7379
commit 05b0b8a909
89 changed files with 7912 additions and 429 deletions
+48 -5
View File
@@ -102,7 +102,7 @@ export const helpBom = {
export const helpMaterial = {
title: '备料单(按日排产自动算料)',
overview:
'备料单 = 仓库备料的依据:某天要生产的所有工单,各需要哪些物料、各要多少。\n\n操作方式:① 先到「日排产」排好某天的计划数量;② 回到本页选该「日期」;③ 点「按日排产生成备料」,系统按各产品物料清单自动计算(计划数量 × 单台用量 × (1+损耗率))并生成备料单明细;④ 仓库据此备料出库,由 AGV 配送到各工位。\n\n数据链路:日排产 → 本页按日期自动算料生成备料单 → 仓库出库 → AGV 配送。',
'备料单 = 仓库备料的依据:某天要生产的所有工单,各需要哪些物料、各要多少。\n\n操作方式:① 先到「日排产」排好某天的计划数量;② 回到本页选该「日期」;③ 点「按日排产生成备料」,系统按各产品物料清单自动计算(计划数量 × 单台用量 × (1+损耗率))并生成备料单明细;④ 生成的同时系统按 FIFO 从 WMS 合格库存自动出库(平时无需人工领料),出库达标的备料单转为「已备料」,由 AGV 配送到各工位。\n\n库存不足怎么办:已出的部分照常记账,差额保持「待备料」并触发「备料未齐套」预警;补货后点「一键自动出库」即可补出(不会超领,重复执行安全)。\n\n超领规则:实际用量超出物料清单需求(损耗/报废/返修)时,系统不硬卡——WMS 出库时填写超领原因即放行并留痕,可统计、可预警;不填原因仍会被拦截。\n\n来源说明:「排产备料」= 按日排产自动生成;「工位补料」= 工位叫料/数量不符补发生成,仓库应优先发补料单。\n\n数据链路:日排产 → 本页按日期自动算料生成备料单 → 自动出库(WMS 扣账)→ AGV 配送 → 工位接料。',
sections: [
{
title: '生成备料',
@@ -111,11 +111,50 @@ export const helpMaterial = {
{ name: '日期', source: '手动选择。', purpose: '按哪个排产日期生成备料', fill: '先选择排产日期,再点「按日排产生成备料」' }
]
},
{
title: '自动出库',
overview: '生成备料单时自动执行;也可在库存补齐后按排产日/工单一键补出。规则:结构件按批次号升序(FIFO)逐批出库,电气件按可用 SN 逐件出库,仅取 WMS「合格」在库库存;超出物料清单需求的领用必须填写超领原因(损耗/报废/返修),自动出库不擅自超领,超领部分会留在「未出齐」清单里等人工处理。',
fields: [
{ name: '一键自动出库', source: '人工在本页操作(生成时已自动执行一次)。', purpose: '把当前排产日/工单下未出齐的备料单补齐', fill: '选好日期或工单号后点「一键自动出库」,结果会列出仍未出齐的明细' }
]
},
{
title: '物料去向对账',
overview:
'点「物料去向对账」查看工单物料的完整去向(只读汇总,不新增账本):需求总量、已发出(仓库已出库)、已退回、已消耗(报工核销)、在途(已发出−已接料)、工位余(已接料−已消耗−已退回)、待发缺口。可展开行看接料明细(每张备料单的实收与确认人),并可导出 CSV。\n\n对账状态只标三类硬异常,其余一律「正常」:① 超领 = 净领用(已发出−已退回)超过需求,超领原因在 WMS「出库管理」留痕可查;② 完结剩料 = 台账已领料完结但工位仍压料(没耗也没退),请催报工或发起退料;③ 在途等待 = 已发出未接料(含已送达未扫码),请看 WMS 出库单是否已送达或催工位扫码。生产中的待发缺口属正常状态,不标色。\n\n消耗明细与出库明细不跨系统重复展示:消耗按工序合计的留痕在 WMS 操作日志按工单号查询;出库明细与超领原因在 WMS「出库管理」按工单号查询。',
fields: [
{ name: '物料去向对账', source: 'WMS 台账 + 本系统已接料量实时合成,前端现算异常。', purpose: '一眼看清料到哪一步了、账对不对', fill: '按当前筛选条件查询,弹窗展示;行可展开看接料明细,右下角可导出 CSV' }
]
},
{
title: '查询字段',
fields: [
{ name: '工单号', source: '手动输入。筛选条件。', purpose: '只查看指定工单的备料', fill: '可空,空=看全部工单' },
{ name: '状态', source: '下拉。待备料/已确认。', purpose: '按备料进度筛选', fill: '可空' }
{ name: '来源', source: '系统标记。排产备料=日排产自动生成;工位补料=工位叫料/差异补发。', purpose: '区分正常备料与补料,补料优先发', fill: '可空=全部' },
{ name: '状态', source: '下拉。待备料 / 已备料(已出库) / 配送中 / 已接料完结。', purpose: '按备料进度筛选', fill: '可空' }
]
}
]
}
export const helpQtyReport = {
title: '数量不符处理(差异闭环)',
overview:
'工位接料时发现「实收 ≠ 应发」,操作工在工位终端上报后,差异记录进入本页等待处理——这是差异闭环的唯一处理入口。\n\n处理方式三选一:\n· 补发:系统生成一张「工位补料」备料单(来源=工位补料),走既有自动出库链路补齐差额,补发量计入工单消耗;\n· 退库:系统生成 WMS 退库单(多发/错发的料退回仓库),仓库确认收货后库存加回、台账缺口自动回补;\n· 调整:双方确认差异、填写说明后直接关闭(不补不退)。\n\n处理完成后差异置为已处理并记录处理单据号,关联预警可在「预警中心」看到同一件事的提醒。补发/退库的单据流转状态请到「备料单」或 WMS「退库单」页查看。',
sections: [
{
title: '查询字段',
fields: [
{ name: '状态', source: '系统维护。待处理/已处理。', purpose: '只看待处理的差异', fill: '默认待处理' },
{ name: '工位 / 工单号 / 图号', source: '工位终端上报时写入。', purpose: '定位差异来源', fill: '可空=全部,不区分大小写' }
]
},
{
title: '处理操作',
fields: [
{ name: '补发', source: '人工在本页操作。', purpose: '少发时补齐差额(生成工位补料单)', fill: '点「补发」→ 确认' },
{ name: '退库', source: '人工在本页操作。', purpose: '多发/错发时退回仓库(生成退库单)', fill: '点「退库」→ 确认' },
{ name: '调整', source: '人工在本页操作(需填处理说明)。', purpose: '少量差异双方确认后关闭', fill: '点「调整」→ 填说明 → 确认' }
]
}
]
@@ -203,7 +242,8 @@ export const helpTrace = {
overview:
'输入工件 SN,查它的完整履历:进线/完工时间、各工位时间线、步骤考核数据(扭矩/角度)、拧紧记录、以及它用了哪些批次/电气件。\n\n数据来源:全部由前端各环节自动写入(扫码报工、拧紧采集、产线设备下发、工位完成),本页只读查询。',
fields: [
{ name: '工件SN', source: '扫码枪/手输。来自工件条码。', purpose: '要追溯的工件唯一序列号', fill: '扫 SN 或手动输入后点查询' }
{ name: '工件SN', source: '扫码枪/手输。来自工件条码。', purpose: '要追溯的工件唯一序列号', fill: '扫 SN 或手动输入后点查询' },
{ name: '工单号', source: '手动输入。筛选条件。', purpose: '按工单号列出该工单下全部工件,再点行内「追溯」查看单个工件履历', fill: '可空;支持模糊匹配,回车即查' }
]
}
@@ -310,6 +350,7 @@ export const helpPerformance = {
fields: [
{ name: '操作人', source: '筛选条件。来自报工实绩的 operator 字段(工位终端登录人)。', purpose: '只看某个工人的工作量', fill: '输入姓名/账号,可空=全部' },
{ name: '工位', source: '下拉选择(工位数以数据库为准)。', purpose: '只看某个工位的报工情况', fill: '可空=全部工位' },
{ name: '工单号', source: '手动输入。筛选条件,取自报工实绩所属工单。', purpose: '只看某个工单的作业量与效率', fill: '可空=全部工单;支持模糊匹配,导出同样生效' },
{ name: '日期范围', source: '筛选条件。来自报工实绩的产生时间,默认近 90 天。', purpose: '按时间段统计', fill: '选择起止日期;导出时同一范围' },
{ name: '完成数', source: '自动汇总。该 操作人×日期×工位 的报工次数。', purpose: '工作量核心指标', fill: '只读' },
{ name: '合格数/不合格数', source: '自动汇总。按报工实绩 result 字段(合格/不合格)分类计数。', purpose: '质量指标,OK+NG=完成数', fill: '只读' },
@@ -428,6 +469,7 @@ export const helpWip = {
overview:
'在制品 = 已进线但尚未完工的工件(状态 已上线/在制/返修),跨天继续停留在此列表中。\n\n数据来源:MES 工件实时状态(GET /workpieces/in-process)。当前工位支持 0=上线位(虚拟)、1~12=物理工位、13=下线位(虚拟),工位数以数据库为准。\n\n与日排产的关系:已进线未完工的工件跨天继续,日排产仅排「未开工数量」,不会重复排产。停留时长自进线起算,便于发现滞留件。',
fields: [
{ name: '工单号', source: '手动输入。筛选条件。', purpose: '只看某个工单的在制品', fill: '可空=全部;支持模糊匹配,回车即查' },
{ name: '当前工位', source: '按工件状态与工位组合推算。', purpose: '工件此刻停在哪一工位', fill: '只读,可筛选' },
{ name: '状态', source: '系统维护。', purpose: '已上线/在制/返修', fill: '只读,可筛选' },
{ name: '进度', source: '已完成工序数 / 工位组合总工序数。', purpose: '看该件走到第几步', fill: '只读' },
@@ -442,6 +484,7 @@ export const helpAlertCenter = {
fields: [
{ name: '状态', source: '系统维护(未读/已读)。', purpose: '区分待处理与已处理预警', fill: '筛选项单选:全部/未读/已读' },
{ name: '预警类型', source: '内置触发源自动写入。', purpose: '只关注某一类异常', fill: '下拉选择八类之一,留空=全部' },
{ name: '工单号', source: '触发预警时自动关联(如拧紧不合格按其 SN 反查所属工单)。', purpose: '按工单号排查该工单相关的所有预警', fill: '可空=全部;支持模糊匹配' },
{ name: '开始时间 / 结束时间', source: '系统记录触发时刻。', purpose: '按时间范围缩小排查范围', fill: '开始时间默认当天;结束时间留空=不限' },
{ name: '类型', source: '内置触发源自动写入。', purpose: '区分八类预警(含应急呼叫)', fill: '只读' },
{ name: '标题 / 内容', source: '触发时系统自动生成。', purpose: '说明预警的具体对象与原因', fill: '只读' },
@@ -466,7 +509,7 @@ export const helpAttachment = {
{ name: '大小', source: '系统统计上传达字节数。', purpose: '评估占用空间', fill: '只读' },
{ name: '状态', source: '系统维护(有效/已归档/已删除)。', purpose: '区分当前可用的与已归档/已删除的附件', fill: '只读' },
{ name: '预览 / 下载', source: '预览走免鉴权直链(PDF/图片浏览器内联打开);下载走鉴权路由并带回中文文件名。', purpose: '查看或另存附件', fill: '点击对应按钮' },
{ name: '删除', source: '人工在本页操作(逻辑删除,需二次确认)。', purpose: '除无效附件(保留可追溯索引)', fill: '点「删除」并确认' },
{ name: '归档清理', source: '人工在本页操作(先预览数量再确认执行)。', purpose: '把早于本地保留年数的附件移到外部备份目录并标记已归档,释放磁盘', fill: '点「归档清理」→确认' }
{ name: '删除', source: '行末按钮(权限 sys.attachment:manage,需二次确认)。', purpose: '逻辑删除无效附件(保留可追溯索引)', fill: '点「删除」并确认;无权限时该按钮不显示' },
{ name: '归档清理', source: '按钮(权限 sys.attachment:manage先预览数量再确认执行)。', purpose: '把早于本地保留年数的附件移到外部备份目录并标记已归档,释放磁盘', fill: '点「归档清理」→确认;无权限时该按钮不显示' }
]
}
@@ -132,6 +132,7 @@ const menuGroups = [
{ code: 'produce.dailyplan', name: '日排产', path: '/daily-plan', icon: Calendar },
{ code: 'produce.bom', name: '产品物料清单', path: '/bom', icon: Goods },
{ code: 'produce.material', name: '备料单', path: '/material-request', icon: ShoppingCart },
{ code: 'produce.qty', name: '数量不符处理', path: '/qty-report', icon: Warning },
{ code: 'produce.performance', name: '绩效报表', path: '/performance', icon: DataLine },
{ code: 'produce.quality', name: '质量检验', path: '/quality', icon: CircleCheck }
]
@@ -14,6 +14,9 @@
<el-option v-for="t in types" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="orderNo" clearable style="width:170px" placeholder="工单号(模糊)" @keyup.enter="search" />
</el-form-item>
<el-form-item label="开始时间">
<el-date-picker v-model="startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始时间" style="width:150px" />
</el-form-item>
@@ -39,6 +42,9 @@
<template #default="{row}">{{ typeLabel(row.type) }}</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="140" />
<el-table-column prop="orderNo" label="工单号" width="130">
<template #default="{row}">{{ row.orderNo || '-' }}</template>
</el-table-column>
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
<el-table-column prop="refId" label="关联ID" width="120" />
<el-table-column prop="createdAt" label="时间" min-width="170">
@@ -70,6 +76,7 @@ const loading = ref(false)
const unread = ref(0)
const statusFilter = ref('all')
const typeFilter = ref('')
const orderNo = ref('')
const startDate = ref(todayStr()) // 开始时间默认当天
const endDate = ref('') // 结束时间默认空
@@ -108,6 +115,7 @@ async function loadAlerts() {
const params = {
status: statusFilter.value === 'all' ? '' : statusFilter.value,
type: typeFilter.value,
orderNo: orderNo.value.trim(),
from: startDate.value,
to: endDate.value,
page: page.value,
@@ -129,6 +137,7 @@ function search() {
function reset() {
statusFilter.value = 'all'
typeFilter.value = ''
orderNo.value = ''
startDate.value = todayStr()
endDate.value = ''
page.value = 1
@@ -8,8 +8,8 @@
<template #default>
<div style="font-size:12px;line-height:1.7">
附件根目录{{ disk.rootDir }}剩余 {{ freePercent }}%剩余 {{ fmtSize(disk.freeBytes) }} / {{ fmtSize(disk.totalBytes) }}
<el-button v-if="disk.level === 'critical'" link type="danger" style="margin-left:8px"
@click="doArchive">立即归档清理</el-button>
<el-button v-if="disk.level === 'critical' && can('sys.attachment:manage')" link type="danger"
style="margin-left:8px" @click="doArchive">立即归档清理</el-button>
</div>
</template>
</el-alert>
@@ -42,7 +42,8 @@
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
<el-button type="warning" plain :loading="archiving" @click="doArchive">归档清理</el-button>
<el-button v-if="can('sys.attachment:manage')" type="warning" plain :loading="archiving"
@click="doArchive">归档清理</el-button>
</el-form-item>
</el-form>
@@ -84,7 +85,8 @@
<template #default="{ row }">
<el-button link type="primary" size="small" @click="preview(row)">预览</el-button>
<el-button link type="success" size="small" @click="download(row)">下载</el-button>
<el-button v-if="!row.deleted" link type="danger" size="small" @click="remove(row)">删除</el-button>
<el-button v-if="!row.deleted && can('sys.attachment:manage')" link type="danger" size="small"
@click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -103,6 +105,7 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { getToken } from '../utils/auth'
import { can } from '../utils/perm'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpAttachment } from '../help'
@@ -12,10 +12,16 @@
<el-form-item label="物料名称"><el-input v-model="query.materialName" clearable style="width:150px" @keyup.enter="load" /></el-form-item>
<el-form-item label="接驳台"><el-input v-model="query.targetDock" clearable style="width:120px" placeholder="如 DOCK05" @keyup.enter="load" /></el-form-item>
<el-form-item label="工位"><el-input v-model="query.stationNo" clearable style="width:90px" placeholder="如 3" @keyup.enter="load" /></el-form-item>
<el-form-item label="来源">
<el-select v-model="query.source" clearable placeholder="全部" style="width:120px">
<el-option label="排产备料" value="PLAN" />
<el-option label="工位补料" value="REFILL" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width:130px">
<el-option label="待备料" value="PENDING" />
<el-option label="已锁定" value="LOCKED" />
<el-option label="已备料(已出库)" value="LOCKED" />
<el-option label="配送中" value="DELIVERING" />
<el-option label="已接料完结" value="DONE" />
</el-select>
@@ -23,6 +29,8 @@
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button v-if="can('produce.material:generate')" type="success" @click="generate">按日排产生成备料</el-button>
<el-button v-if="can('produce.material:outbound')" type="warning" plain :loading="autoing" @click="autoOutbound">一键自动出库</el-button>
<el-button type="info" plain :loading="progressing" @click="showProgress">物料去向对账</el-button>
</el-form-item>
</el-form>
@@ -30,6 +38,12 @@
<el-table-column prop="requestNo" label="备料单号" min-width="180" />
<el-table-column prop="orderNo" label="工单号" min-width="130" />
<el-table-column prop="planDate" label="排产日期" width="110" />
<el-table-column label="来源" width="95" align="center">
<template #default="{row}">
<el-tag v-if="row.source === 'REFILL'" type="danger" size="small">工位补料</el-tag>
<el-tag v-else type="info" size="small">排产备料</el-tag>
</template>
</el-table-column>
<el-table-column prop="materialCode" label="图号" min-width="110" />
<el-table-column prop="materialName" label="物料名称" min-width="110" show-overflow-tooltip />
<el-table-column prop="unit" label="单位" width="70" />
@@ -41,9 +55,12 @@
<el-table-column prop="targetDock" label="接驳台" width="100" align="center" />
<el-table-column prop="batchNo" label="批次" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="reqQty" label="需求数量" width="90" align="center" />
<el-table-column label="已接料" width="90" align="center">
<el-table-column label="已发出" width="90" align="center">
<template #default="{row}">{{ fmtQty(row.sentQty) }}</template>
</el-table-column>
<el-table-column label="已接料" width="90" align="center">
<template #default="{row}">{{ fmtQty(row.receivedQty) }}</template>
</el-table-column>
<el-table-column label="分批进度" min-width="150">
<template #default="{row}">
<el-progress :percentage="progressPct(row)" :status="row.status === 'DONE' ? 'success' : ''" />
@@ -60,6 +77,79 @@
<el-table-column prop="operator" label="操作人" width="90" />
</el-table>
</el-card>
<el-dialog v-model="progressVisible" title="物料去向对账(在库 / 在途 / 线边 / 已消耗)" width="1060px" top="5vh">
<el-table :data="progressRows" border size="small" max-height="560" row-key="rowKey" @expand-change="onExpandRow">
<el-table-column type="expand">
<template #default="{ row }">
<div style="padding:8px 16px">
<div style="font-weight:600;margin-bottom:6px">接料明细按备料单{{ row.orderNo }} / {{ row.materialCode }}</div>
<el-table v-if="expandMap[row.rowKey]?.loaded" :data="expandMap[row.rowKey]?.receives || []" border size="small" max-height="220">
<el-table-column prop="requestNo" label="备料单号" min-width="180" />
<el-table-column label="来源" width="95" align="center">
<template #default="{ row: r }">
<el-tag v-if="r.source === 'REFILL'" type="danger" size="small">工位补料</el-tag>
<el-tag v-else type="info" size="small">排产备料</el-tag>
</template>
</el-table-column>
<el-table-column prop="stationNo" label="工位" width="70" align="center" />
<el-table-column prop="processCode" label="工序" width="70" align="center" />
<el-table-column label="已发出" width="85" align="center">
<template #default="{ row: r }">{{ fmtQty(r.sentQty) }}</template>
</el-table-column>
<el-table-column label="已接料" width="85" align="center">
<template #default="{ row: r }">{{ fmtQty(r.receivedQty) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row: r }">{{ statusText(r.status) }}</template>
</el-table-column>
<el-table-column prop="operator" label="接料确认人" width="100" />
</el-table>
<div v-else style="color:#909399;font-size:12px;padding:6px 0">加载中</div>
<div style="color:#909399;font-size:12px;margin-top:8px">
消耗明细已消耗量由 WMS 报工反冲核销台账已消耗按工序合计的留痕在 WMS 操作日志按工单号查询出库明细与超领原因在 WMS出库管理按工单号查询本页不做跨系统重复建设
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="orderNo" label="工单号" min-width="130" />
<el-table-column prop="materialCode" label="图号" min-width="110" />
<el-table-column prop="materialName" label="物料名称" min-width="110" show-overflow-tooltip />
<el-table-column prop="targetStation" label="目标工位" width="80" align="center" />
<el-table-column prop="totalQty" label="需求" width="70" align="center" />
<el-table-column prop="outQty" label="已发出" width="75" align="center" />
<el-table-column prop="returnedQty" label="已退回" width="75" align="center" />
<el-table-column prop="receivedQty" label="已接料" width="75" align="center" />
<el-table-column prop="consumedQty" label="已消耗" width="75" align="center" />
<el-table-column label="在途" width="70" align="center">
<template #default="{row}"><span :style="row.inTransit > 0 ? 'color:#E6A23C;font-weight:600' : ''">{{ row.inTransit }}</span></template>
</el-table-column>
<el-table-column label="工位余" width="70" align="center">
<template #default="{row}"><span :style="lineSideOf(row) > 0 ? 'color:#E6A23C;font-weight:600' : ''">{{ lineSideOf(row) }}</span></template>
</el-table-column>
<el-table-column prop="gap" label="待发缺口" width="85" align="center">
<template #default="{row}">
<el-tag v-if="row.gap > 0" type="danger" size="small">{{ row.gap }}</el-tag>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column label="对账状态" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-tooltip :content="anomalyTip(row)" placement="top">
<el-tag :type="anomalyOf(row).type" size="small">{{ anomalyOf(row).label }}</el-tag>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div style="color:#909399;font-size:12px;margin-top:8px">
口径净领用 = 已发出 已退回在途 = 已发出 已接料工位余 = 已接料 已消耗 已退回恒等式入库总量 = 在库 + 在途 + 工位余 + 已消耗 + 退库回仓 + 盘点差异
异常判定只标三类硬异常<b>超领</b>净领用 &gt; 需求<b>完结剩料</b>台账领料完结但工位仍压料<b>在途等待</b>已发出未接料含已送达未扫码生产中的待发缺口属正常状态不标色
</div>
<div style="margin-top:10px;text-align:right">
<el-button size="small" @click="exportLedgerCsv">导出 CSV</el-button>
</div>
</el-dialog>
<PageHelp :data="helpMaterial" page="MaterialRequest" />
</template>
@@ -71,22 +161,102 @@ import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpMaterial } from '../help'
const query = reactive({ planDate: '', orderNo: '', status: '', materialCode: '', materialName: '', targetDock: '', stationNo: '' })
const query = reactive({ planDate: '', orderNo: '', status: '', materialCode: '', materialName: '', targetDock: '', stationNo: '', source: '' })
const list = ref([])
const receiving = ref(false)
const receivingNo = ref('')
const autoing = ref(false)
const progressing = ref(false)
const progressVisible = ref(false)
const progressRows = ref([])
const statusText = (s) => (s === 'PENDING' ? '待备料' : s === 'LOCKED' ? '已锁定' : s === 'DELIVERING' ? '配送中' : s === 'DONE' ? '已接料完结' : s)
const statusText = (s) => (s === 'PENDING' ? '待备料' : s === 'LOCKED' ? '已备料(已出库)' : s === 'DELIVERING' ? '配送中' : s === 'DONE' ? '已接料完结' : s)
function fmtQty(v) {
const n = Number(v || 0)
return Number.isInteger(n) ? String(n) : n.toFixed(2)
}
// 进度按「已接料」计算:接料才是工位真正拿到料的节点(已发出只代表仓库发了)
function progressPct(row) {
const req = Number(row.reqQty || 0)
const sent = Number(row.sentQty || 0)
const got = Number(row.receivedQty || 0)
if (req <= 0) return 0
return Math.min(100, Math.round((sent / req) * 100))
return Math.min(100, Math.round((got / req) * 100))
}
// ============ 物料去向对账(只读,后端 progress 接口 + 前端现算,不新增账本) ============
// 口径:净领用 netOut = 已发出 − 已退回;在途 = 已发出 − 已接料;工位余 = 已接料 − 已消耗 − 已退回。
// 异常判定只标三类硬异常(生产中的待发缺口属正常状态,不标色):
// 超领:净领用 > 需求(超领原因在 WMS 出库单留痕,可查)
// 完结剩料:台账「领料完结」但工位仍压料(没耗也没退)
// 在途等待:已发出未接料(含已送达未扫码;是否已送达需 WMS 出库单状态,本页不跨系统拉取)
const expandMap = reactive({})
function lineSideOf(row) {
return Number(row.receivedQty || 0) - Number(row.consumedQty || 0) - Number(row.returnedQty || 0)
}
function anomalyOf(row) {
const netOut = Number(row.netOut ?? (Number(row.outQty || 0) - Number(row.returnedQty || 0)))
if (netOut > Number(row.totalQty || 0)) return { key: 'over', label: '超领', type: 'danger' }
if (row.status === '领料完结' && lineSideOf(row) > 0) return { key: 'left', label: '完结剩料', type: 'warning' }
if (Number(row.inTransit || 0) > 0) return { key: 'transit', label: '在途等待', type: 'warning' }
return { key: 'ok', label: '正常', type: 'success' }
}
function anomalyTip(row) {
const a = anomalyOf(row)
if (a.key === 'over') return '净领用超过工单需求:请到 WMS「出库管理」按工单号查超领原因与明细'
if (a.key === 'left') return '台账已领料完结,但工位仍压料未消耗也未退回:请催报工或发起退料'
if (a.key === 'transit') return '已发出尚未接料(含已送达未扫码):请到 WMS「出库管理」看是否已送达,或催工位扫码接料'
return '在库 / 在途 / 线边 / 已消耗均在正常区间'
}
// 展开行:懒加载该工单+图号的接料明细(备料单行即接料台账,含实收与确认人)
async function onExpandRow(row, expandedRows) {
const key = row.rowKey
if (!expandedRows.some((r) => r.rowKey === key) || expandMap[key]?.loaded) return
expandMap[key] = { loaded: false, receives: [] }
try {
const reqs = (await request.get('/material-requests', { params: { orderNo: row.orderNo } })) || []
const receives = reqs.filter((r) => r.materialCode === row.materialCode)
expandMap[key] = { loaded: true, receives }
} catch {
expandMap[key] = { loaded: true, receives: [] }
}
}
// 导出 CSV(带 BOMExcel 直接打开中文不乱码;MES 前端无 xlsx 库,纯前端导出)
function exportLedgerCsv() {
const header = ['工单号', '图号', '物料名称', '目标工位', '需求', '已发出', '已退回', '已接料', '已消耗', '在途', '工位余', '待发缺口', '对账状态', '台账状态']
const lines = [header.join(',')]
for (const r of progressRows.value) {
const cells = [
r.orderNo, r.materialCode, r.materialName, r.targetStation,
r.totalQty, r.outQty, r.returnedQty, r.receivedQty, r.consumedQty,
r.inTransit, lineSideOf(r), r.gap, anomalyOf(r).label, r.status
]
lines.push(cells.map((c) => '"' + String(c ?? '').replace(/"/g, '""') + '"').join(','))
}
const blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = '物料去向对账.csv'
a.click()
URL.revokeObjectURL(a.href)
}
// 配送进度:WMS 台账(已发出/已退回/已消耗/缺口)+ 本系统已接料 → 在途/线边
async function showProgress() {
progressing.value = true
try {
const data = await request.get('/material-requests/progress', {
params: { orderNo: query.orderNo, materialCode: query.materialCode, page: 1, pageSize: 100 }
})
progressRows.value = (data?.list || []).map((r) => ({ ...r, rowKey: r.orderNo + '|' + r.materialCode }))
progressVisible.value = true
} catch {
// 拦截器已提示
}
progressing.value = false
}
async function load() {
@@ -129,8 +299,38 @@ async function generate() {
ElMessage.success(`已生成 ${data?.count || 0} 条备料`)
load()
}
// 一键自动出库:按排产日(或指定工单)把未出齐的备料单从 WMS 合格库存 FIFO 自动领料。
// 日常「按日排产生成备料」已自动执行本动作,此处用于库存补齐后的补出。
async function autoOutbound() {
if (!query.planDate && !query.orderNo) return ElMessage.warning('请先选择排产日期或填写工单号')
try {
await ElMessageBox.confirm(
`将对 ${query.orderNo ? '工单 ' + query.orderNo : '排产日期 ' + query.planDate} 下未出齐的备料单,按 FIFO 从 WMS 合格库存自动领料出库。确认继续?`,
'一键自动出库', { type: 'warning' })
} catch {
return
}
autoing.value = true
try {
const data = await request.post('/material-requests/auto-outbound', {
planDate: query.planDate, orderNo: query.orderNo
})
const short = data?.short || []
if (short.length) {
await ElMessageBox.alert(
`已出库达标 ${data?.done || 0} 条;以下仍未出齐(库存不足,等补货后可再次执行):\n\n${short.join('\n')}`,
'自动出库结果', { type: 'warning', confirmButtonText: '知道了' })
} else {
ElMessage.success(`自动出库完成:${data?.done || 0} 条已备料`)
}
load()
} catch {
// 拦截器已提示
}
autoing.value = false
}
onMounted(() => {
query.planDate = new Date().toISOString().slice(0, 10)
load()
})
</script>
})</script>
@@ -9,6 +9,9 @@
<el-option v-for="i in stationNos" :key="i" :label="`工位${i}`" :value="String(i)" />
</el-select>
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="query.orderNo" placeholder="工单号(模糊)" clearable style="width:180px" @keyup.enter="load" />
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker
v-model="query.range"
@@ -153,7 +156,7 @@ import request from '../utils/request'
const activeTab = ref('byOperator')
const pageSize = ref(20)
const stationNos = ref([])
const query = reactive({ operator: '', stationNo: '', range: null })
const query = reactive({ operator: '', stationNo: '', orderNo: '', range: null })
const views = reactive({
byOperator: { total: 0, page: 1, list: [] },
byStation: { total: 0, page: 1, list: [] },
@@ -213,6 +216,7 @@ async function load() {
const params = {
operator: query.operator || '',
stationNo: query.stationNo || '',
orderNo: query.orderNo.trim(),
from: from || '',
to: to || '',
opPage: views.byOperator.page, opPageSize: pageSize.value,
@@ -232,6 +236,7 @@ async function exportXlsx() {
const [from, to] = query.range || []
const params = {
operator: query.operator || '', stationNo: query.stationNo || '',
orderNo: query.orderNo.trim(),
startDate: from || '', endDate: to || ''
}
const blob = await request.get('/performance/export', { params, responseType: 'blob' })
@@ -0,0 +1,189 @@
<template>
<el-card>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="数量不符处理:工位终端接料时发现「实收 ≠ 应发」后上报,差异单汇总到本页。仓管/班组长按差异方向处理——少发走「补发」(自动生成补料单并叫 AGV)、多发走「退库」(生成退库单,WMS 确认后库存加回)、少量差异可「调整关闭」(不补不退,必须填说明)。" />
<el-row :gutter="12" style="margin-bottom:12px">
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num warn">{{ stats.pending }}</div><div class="stat-label">待处理</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num ok">{{ stats.replenished }}</div><div class="stat-label">已补发</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num info">{{ stats.returned }}</div><div class="stat-label">已退库</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num">{{ stats.closed }}</div><div class="stat-label">已关闭</div></el-card>
</el-col>
</el-row>
<el-form inline>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width:130px">
<el-option label="待处理" value="PENDING" />
<el-option label="已补发" value="REPLENISHED" />
<el-option label="已退库" value="RETURNED" />
<el-option label="已关闭" value="CLOSED" />
</el-select>
</el-form-item>
<el-form-item label="工位"><el-input v-model="query.stationNo" clearable style="width:90px" placeholder="如 3" @keyup.enter="search" /></el-form-item>
<el-form-item label="工单号"><el-input v-model="query.orderNo" clearable style="width:150px" @keyup.enter="search" /></el-form-item>
<el-form-item label="图号"><el-input v-model="query.materialCode" clearable style="width:150px" placeholder="不区分大小写" @keyup.enter="search" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small" v-loading="loading">
<el-table-column prop="reportNo" label="差异单号" width="100" />
<el-table-column prop="stationNo" label="工位" width="70" align="center" />
<el-table-column prop="orderNo" label="工单号" min-width="130" show-overflow-tooltip />
<el-table-column prop="materialCode" label="图号" min-width="110" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="110" show-overflow-tooltip />
<el-table-column prop="sn" label="序列号" min-width="120" show-overflow-tooltip />
<el-table-column prop="planQty" label="应发" width="70" align="center" />
<el-table-column prop="actualQty" label="实收" width="70" align="center" />
<el-table-column label="差异" width="80" align="center">
<template #default="{row}">
<span :class="row.diffQty > 0 ? 'diff-short' : row.diffQty < 0 ? 'diff-over' : ''">
{{ row.diffQty > 0 ? '少 ' + row.diffQty : row.diffQty < 0 ? '多 ' + (-row.diffQty) : '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="cause" label="上报原因" min-width="130" show-overflow-tooltip />
<el-table-column label="上报时间" width="150">
<template #default="{row}">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="operator" label="上报人" width="90" />
<el-table-column label="状态" width="90">
<template #default="{row}">
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="处理方式" width="90">
<template #default="{row}">{{ handleText(row.handleType) }}</template>
</el-table-column>
<el-table-column prop="handleDocNo" label="处理单据" min-width="170" show-overflow-tooltip />
<el-table-column prop="handleNote" label="处理说明" min-width="150" show-overflow-tooltip />
<el-table-column prop="handledBy" label="处理人" width="90" />
<el-table-column label="处理时间" width="150">
<template #default="{row}">{{ row.handledAt ? fmtTime(row.handledAt) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="210" fixed="right" align="center">
<template #default="{row}">
<template v-if="isPending(row.status) && can('produce.qty:handle')">
<el-button size="small" type="success" :disabled="row.diffQty <= 0" @click="doHandle(row, 'REPLENISH')">补发</el-button>
<el-button size="small" type="warning" :disabled="row.diffQty >= 0" @click="doHandle(row, 'RETURN')">退库</el-button>
<el-button size="small" @click="doHandle(row, 'ADJUST')">调整</el-button>
</template>
<span v-else class="muted">{{ isPending(row.status) ? '无处理权限' : '已处理' }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px; justify-content:flex-end"
background layout="total, sizes, prev, pager, next"
:total="total" :current-page="query.page" :page-size="query.pageSize"
:page-sizes="[20, 50, 100]" @current-change="onPage" @size-change="onSize" />
</el-card>
<PageHelp :data="helpQtyReport" page="QtyReport" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpQtyReport } from '../help'
const query = reactive({ status: '', stationNo: '', orderNo: '', materialCode: '', page: 1, pageSize: 20 })
const list = ref([])
const total = ref(0)
const loading = ref(false)
const stats = reactive({ pending: 0, replenished: 0, returned: 0, closed: 0 })
const statusText = (s) => ({ PENDING: '待处理', PROCESSING: '处理中', REPLENISHED: '已补发', RETURNED: '已退库', ADJUSTED: '已关闭', CLOSED: '已关闭', RESOLVED: '已关闭' }[s] || s)
const statusTag = (s) => (s === 'PENDING' || s === 'PROCESSING' ? 'warning' : s === 'REPLENISHED' ? 'success' : s === 'RETURNED' ? 'primary' : 'info')
const handleText = (t) => ({ REPLENISH: '补发', RETURN: '退库', ADJUST: '调整关闭' }[t] || '-')
const isPending = (s) => s === 'PENDING' || s === 'PROCESSING'
async function load() {
loading.value = true
try {
const data = await request.get('/qty-reports', { params: query })
list.value = data?.list || []
total.value = data?.total || 0
} catch {
// 拦截器已提示
}
loading.value = false
}
async function loadStats() {
try {
const data = await request.get('/qty-reports/stats')
Object.assign(stats, data || {})
} catch {
// 统计失败不影响列表
}
}
function search() { query.page = 1; load() }
function reset() {
Object.assign(query, { status: '', stationNo: '', orderNo: '', materialCode: '', page: 1 })
load()
}
function onPage(p) { query.page = p; load() }
function onSize(s) { query.pageSize = s; query.page = 1; load() }
// 处理差异:补发(少发)/ 退库(多发)/ 调整关闭(不补不退,必须填说明)
async function doHandle(row, type) {
if (type === 'REPLENISH' && row.diffQty <= 0) return ElMessage.warning('本单为「多发」,不能补发;请选择退库或调整')
if (type === 'RETURN' && row.diffQty >= 0) return ElMessage.warning('本单为「少发」,不能退库;请选择补发或调整')
const title = type === 'REPLENISH' ? '补发' : type === 'RETURN' ? '退库' : '调整关闭'
const tip = type === 'REPLENISH'
? `将为「${row.materialCode}」生成补料单(数量 ${row.diffQty}),随后自动出库并叫 AGV 送到工位 ${row.stationNo}`
: type === 'RETURN'
? `将为「${row.materialCode}」生成退库单(数量 ${-row.diffQty}),WMS 确认收货后库存加回。`
: '调整关闭:不补发也不退库,仅登记双方确认结果(必填说明)。'
let note = ''
try {
const { value } = await ElMessageBox.prompt(tip, title, {
inputPlaceholder: type === 'ADJUST' ? '必填:说明双方确认结果' : '选填:处理说明',
inputValidator: (v) => (type !== 'ADJUST' || (v && v.trim().length > 0)) || '调整关闭必须填写处理说明',
confirmButtonText: '确定处理',
type: 'warning'
})
note = (value || '').trim()
} catch {
return
}
try {
const data = await request.post('/qty-reports/handle', { id: row.id, handleType: type, note })
ElMessage.success(`处理完成:${statusText(data?.status)}${data?.handleDocNo ? '' + data.handleDocNo + '' : ''}`)
load()
loadStats()
} catch {
// 拦截器已提示
}
}
onMounted(() => {
load()
loadStats()
})
</script>
<style scoped>
.stat { text-align: center; }
.stat-num { font-size: 22px; font-weight: 600; }
.stat-num.warn { color: #E6A23C; }
.stat-num.ok { color: #67C23A; }
.stat-num.info { color: #409EFF; }
.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
.diff-short { color: #E6A23C; font-weight: 600; }
.diff-over { color: #F56C6C; font-weight: 600; }
.muted { color: #909399; font-size: 12px; }
</style>
+42 -1
View File
@@ -4,12 +4,32 @@
<el-form-item label="工件SN">
<el-input v-model="sn" style="width:220px" placeholder="输入SN查询追溯" @keyup.enter="search" />
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="orderNo" style="width:200px" placeholder="按工单号列出工件(模糊)" @keyup.enter="searchByOrder" />
</el-form-item>
<el-form-item><el-button type="primary" @click="search">追溯查询</el-button></el-form-item>
<el-form-item><el-button @click="searchByOrder">按工单查询</el-button></el-form-item>
<el-form-item>
<el-button type="warning" :disabled="!sn" @click="printCard">打印流程卡</el-button>
</el-form-item>
</el-form>
<el-card v-if="orderRows.length" shadow="never" style="margin-bottom:12px">
<template #header>工单{{ orderNo }}下的工件共 {{ orderRows.length }} 追溯查看单个工件</template>
<el-table :data="orderRows" border size="small" max-height="240">
<el-table-column prop="sn" label="工件SN" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="currentProcess" label="当前工位" width="100" align="center" />
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="pickSn(row.sn)">追溯</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-alert v-if="error" :title="error" type="error" show-icon closable style="margin-bottom:10px" />
<template v-if="data">
@@ -101,16 +121,37 @@
<script setup>
import { ref, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { openPrintableHtml } from '../utils/print'
import PageHelp from '../components/PageHelp.vue'
import { helpTrace } from '../help'
const sn = ref('')
const orderNo = ref('')
const orderRows = ref([])
const data = ref(null)
const error = ref('')
// 按工单号列出该工单下的全部工件,点击行内「追溯」查看单个工件生命周期(客户第12条:记录查看加工单号查询)
async function searchByOrder() {
const no = orderNo.value.trim()
if (!no) {
ElMessage.warning('请输入工单号')
return
}
try {
orderRows.value = (await request.get('/workpieces', { params: { orderNo: no } })) || []
if (!orderRows.value.length) ElMessage.info('该工单下暂无工件')
} catch {
orderRows.value = []
}
}
function pickSn(v) {
sn.value = v
search()
}
async function search() {
if (!sn.value) return
error.value = ''
+5
View File
@@ -8,6 +8,9 @@
<el-option label="下线位(虚拟)" :value="13" />
</el-select>
</el-form-item>
<el-form-item label="工单号">
<el-input v-model="orderNo" clearable placeholder="工单号(模糊)" style="width:200px" @keyup.enter="load" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="statusFilter" clearable placeholder="全部状态" style="width:140px">
<el-option label="已上线" value="ONLINE" />
@@ -74,6 +77,7 @@ import { helpWip } from '../help'
const stations = ref([])
const stationNo = ref('')
const orderNo = ref('')
const statusFilter = ref('')
const list = ref([])
const loading = ref(false)
@@ -106,6 +110,7 @@ async function load() {
try {
const params = {}
if (stationNo.value !== '' && stationNo.value !== null) params.stationNo = stationNo.value
if (orderNo.value.trim()) params.orderNo = orderNo.value.trim()
list.value = (await request.get('/workpieces/in-process', { params })) || []
} finally {
loading.value = false
@@ -12,6 +12,7 @@ const routes = [
{ path: 'daily-plan', name: 'DailyPlan', component: () => import('../pages/DailyPlan.vue'), meta: { title: '日排产' } },
{ path: 'bom', name: 'Bom', component: () => import('../pages/Bom.vue'), meta: { title: '产品物料清单' } },
{ path: 'material-request', name: 'MaterialRequest', component: () => import('../pages/MaterialRequest.vue'), meta: { title: '备料单' } },
{ path: 'qty-report', name: 'QtyReport', component: () => import('../pages/QtyReport.vue'), meta: { title: '数量不符处理' } },
{ path: 'plc-send', name: 'PlcSend', component: () => import('../pages/PlcSend.vue'), meta: { title: '工位组合下发' } },
{ path: 'torque', name: 'Torque', component: () => import('../pages/Torque.vue'), meta: { title: '拧紧查询' } },
{ path: 'process-flow', name: 'ProcessFlow', component: () => import('../pages/ProcessFlow.vue'), meta: { title: '工艺流程' } },