feat(production): 新增排产支撑功能并移除手动报工页面
- 在main.go中更新排产物料需求注释说明,明确WMS与MES系统间交互方式 - 从前端help.js中移除手动报工(helpScan)相关帮助文档 - 更新工艺流程、绩效报表等页面帮助文档中的报工流程说明 - 在主布局中添加排产支撑菜单项并移除手动报工菜单项 - 新增ProducibleSupport.vue组件实现排产支撑页面功能 - 移除Scan.vue手动报工页面组件 - 更新相关帮助文档内容以匹配新的业务流程
This commit is contained in:
@@ -24,7 +24,7 @@ export const helpProductType = {
|
||||
export const helpWorkOrder = {
|
||||
title: '工单管理(新建 / 编辑 / 删除工单)',
|
||||
overview:
|
||||
'工单 = 一个"合同/生产批次"的唯一载体,是生产执行和仓库出库挂靠的核心对象。\n\n全流程:先有产品型号(在「物料档案」里建档) → 建工单(选产品型号、选定物料清单、定数量、勾选工位组合) → 进"物料清单"给该产品配料 → 到"日排产"排哪天产多少 → 仓库按排产备料出库 → 产线装配/拧紧/报工 → 完工(成品 SN 系统自动生成,并自动回流 WMS 入库为"成品"品类)。\n工单号会贯穿 备料单、拧紧查询、手动报工、工件追溯、操作日志 全链路。\n\n工程编号:一个合同对应一个工程编号,一个工程编号下对应各个产品的产品序号——即「合同号 → 工程编号 → 产品序号」三级归属,用于按合同/工程项目归集与追溯。\n\n查询:支持按 工单号 / 产品编号 / 产品名称(模糊) / 合同号 / 工程编号 / 状态 组合筛选;删除:仅「已创建 / 待排产 / 已发布」且无排产、备料、生产数据的工单可删除(操作列「删除」),生产中或已完成的工单不可删除。',
|
||||
'工单 = 一个"合同/生产批次"的唯一载体,是生产执行和仓库出库挂靠的核心对象。\n\n全流程:先有产品型号(在「物料档案」里建档) → 建工单(选产品型号、选定物料清单、定数量、勾选工位组合) → 进"物料清单"给该产品配料 → 到"日排产"排哪天产多少 → 仓库按排产备料出库 → 产线装配/拧紧/报工 → 完工(成品 SN 系统自动生成,并自动回流 WMS 入库为"成品"品类)。\n工单号会贯穿 备料单、拧紧查询、工件追溯、操作日志 全链路。\n\n工程编号:一个合同对应一个工程编号,一个工程编号下对应各个产品的产品序号——即「合同号 → 工程编号 → 产品序号」三级归属,用于按合同/工程项目归集与追溯。\n\n查询:支持按 工单号 / 产品编号 / 产品名称(模糊) / 合同号 / 工程编号 / 状态 组合筛选;删除:仅「已创建 / 待排产 / 已发布」且无排产、备料、生产数据的工单可删除(操作列「删除」),生产中或已完成的工单不可删除。',
|
||||
sections: [
|
||||
{
|
||||
title: '工单基本信息',
|
||||
@@ -208,23 +208,10 @@ export const helpTorque = {
|
||||
]
|
||||
}
|
||||
|
||||
export const helpScan = {
|
||||
title: '手动报工(工位工序步骤完成上报)',
|
||||
overview:
|
||||
'页面分两个页签:「报工录入」(默认打开)、「报工记录」(点击才加载,分页)。\n\n录入流程:扫入/输入工件 SN 后,系统自动带出该工件的「应报工位」并载入该工位「启用」工艺流程的工序步骤(扭矩/角度/尺寸等采集项);工位也可手动选择。逐参数录入提交;提交后写报工实绩 + 步骤数据,系统按考核标准自动判定 合格/不合格。\n\n放行校验(严格):必须“在制(已进线/在工位) 且 当前工序(=已完成工序+1) == 所选工位”才允许报工,禁止跳站/漏站/重复报同一站;扫 SN 后页面实时给出校验结果,后端报工时再次强校验。\n\n装机绑定:若该产品在本工序配置了需绑定的装配物料(物料清单「装配工位」),会出现装机绑定区——电气件扫 SN、结构件扫/输批次号,逐项绑齐(已绑/需绑)后才允许提交报工;未配置物料则不校验。\n\n报工记录:「报工记录」页签展示真实报工实绩(本页手动报工与工位终端报工均在此可见),可按 SN / 工单号 / 工位 筛选。\n\n数据链路:工艺流程(工序步骤+考核标准) + 物料清单 装机配置 → 本页扫 SN 自动带出工位并载入 → 绑料+录参数 → 判 合格/不合格 → 报工记录/工件追溯可见。',
|
||||
fields: [
|
||||
{ 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: '可空=全部' }
|
||||
]
|
||||
}
|
||||
|
||||
export const helpStation = {
|
||||
title: '关联工位(工位总览)',
|
||||
overview:
|
||||
'工位数以数据库为准。工位分「内置」与「自定义」两类:内置工位由种子/初始化数据产生(数据库 is_builtin=true),对应实体产线位置,需与 PLC 通讯并必须按工位号顺序流转,不允许修改工位类型、不允许删除;自定义工位是在本页点击「新增工位」创建的,可修改类型、可删除。\n\n本页是工位总览:查看每个工位当前绑定了哪个工艺流程,可修改工位名称、删除自定义工位。\n\n绑定关系由本页单独维护:在下拉中选择「启用」的工艺流程即绑定到该工位;一个工位只能有一个启用的工艺流程(先停用旧流程再绑新流程);流程停用后自动解绑。\n\n「是否有接驳台」对应实体位置(1~10 号工位各 2 个接驳台;0/11/12/13 号无),由内置数据给定,页面不可手动更改。\n\n数据链路:关联工位页(选启用流程绑定) → 手动报工/工位终端按该流程的工序步骤采集 → 报工判定 合格/不合格。',
|
||||
'工位数以数据库为准。工位分「内置」与「自定义」两类:内置工位由种子/初始化数据产生(数据库 is_builtin=true),对应实体产线位置,需与 PLC 通讯并必须按工位号顺序流转,不允许修改工位类型、不允许删除;自定义工位是在本页点击「新增工位」创建的,可修改类型、可删除。\n\n本页是工位总览:查看每个工位当前绑定了哪个工艺流程,可修改工位名称、删除自定义工位。\n\n绑定关系由本页单独维护:在下拉中选择「启用」的工艺流程即绑定到该工位;一个工位只能有一个启用的工艺流程(先停用旧流程再绑新流程);流程停用后自动解绑。\n\n「是否有接驳台」对应实体位置(1~10 号工位各 2 个接驳台;0/11/12/13 号无),由内置数据给定,页面不可手动更改。\n\n数据链路:关联工位页(选启用流程绑定) → 工位终端按该流程的工序步骤采集 → 报工判定 合格/不合格。',
|
||||
fields: [
|
||||
{ name: '工位号', source: '内置工位由系统预置;自定义工位由本页「新增工位」填写。', purpose: '产线物理工位编号', fill: '新增时自定义,可与现有工位不连续(如 14、15…)' },
|
||||
{ name: '工位名称', source: '可编辑。默认如 装配工位1。', purpose: '工位的显示名称', fill: '如 一号装配工位' },
|
||||
@@ -323,7 +310,7 @@ export const helpEventLog = {
|
||||
export const helpProcessFlow = {
|
||||
title: '工艺流程(作业指导,含工序步骤)',
|
||||
overview:
|
||||
'页面分「流程列表」「流程配置」两个页签:列表查看全部流程并做启用/停用/删除;配置页维护流程基本信息与工序步骤(新增与编辑共用,不再弹窗套弹窗)。\n\n工艺流程 = 一份「菜谱」:只描述某个产品该怎么装配(步骤名、采集方式、考核标准),不绑定具体工位。\n\n概念统一说明:\n· 工位:产线上的物理工位(工位数以数据库为准,仅改名称不增删),相当于「灶台」。\n· 工艺流程:一份作业指导,可有多版本(如 总装流程_2026版);其启用状态决定是否参与生产。\n· 工序步骤:流程内包含多个工序步骤(如:装配完成、拧紧扭矩、测试)。\n\n绑定关系在「关联工位」页维护:在该页为某工位选择启用的工艺流程即绑定;一个工位只能有一个启用的流程。手动报工/工位终端按所选工位的绑定流程载入工序步骤。\n\n数据链路:本页维护流程+工序步骤+考核标准 → 关联工位页绑定 → 手动报工/工位终端按工序步骤采集 → 报工按考核标准自动判 合格/不合格 → 工件追溯/绩效报表可见。',
|
||||
'页面分「流程列表」「流程配置」两个页签:列表查看全部流程并做启用/停用/删除;配置页维护流程基本信息与工序步骤(新增与编辑共用,不再弹窗套弹窗)。\n\n工艺流程 = 一份「菜谱」:只描述某个产品该怎么装配(步骤名、采集方式、考核标准),不绑定具体工位。\n\n概念统一说明:\n· 工位:产线上的物理工位(工位数以数据库为准,仅改名称不增删),相当于「灶台」。\n· 工艺流程:一份作业指导,可有多版本(如 总装流程_2026版);其启用状态决定是否参与生产。\n· 工序步骤:流程内包含多个工序步骤(如:装配完成、拧紧扭矩、测试)。\n\n绑定关系在「关联工位」页维护:在该页为某工位选择启用的工艺流程即绑定;一个工位只能有一个启用的流程。工位终端按所选工位的绑定流程载入工序步骤。\n\n数据链路:本页维护流程+工序步骤+考核标准 → 关联工位页绑定 → 工位终端按工序步骤采集 → 报工按考核标准自动判 合格/不合格 → 工件追溯/绩效报表可见。',
|
||||
sections: [
|
||||
{
|
||||
title: '流程基本信息',
|
||||
@@ -332,7 +319,7 @@ export const helpProcessFlow = {
|
||||
|
||||
{ name: '状态', source: '下拉选择 启用/停用。也可在列表操作列点「启用/停用」快捷切换。', purpose: '启用=该流程可被「关联工位」页绑定到工位并参与生产;停用=不可绑定、不参与生产', fill: '启用后可在「关联工位」页绑定到工位;停用后不可绑定' },
|
||||
{ name: '工艺图纸(PDF)', source: '本页上传。文件由 MES 服务器保存。', purpose: '该工位的作业指导图,工位终端打开预览', fill: '选择 PDF 上传,成功后显示文件名可预览' },
|
||||
{ name: '工序步骤', source: '本页维护(步骤名/采集方式/是否拧紧/考核标准)。', purpose: '手动报工/工位终端报工时按此动态渲染参数输入', fill: '一个流程可含多个工序步骤,按序号顺序执行' },
|
||||
{ name: '工序步骤', source: '本页维护(步骤名/采集方式/是否拧紧/考核标准)。', purpose: '工位终端报工时按此动态渲染参数输入', fill: '一个流程可含多个工序步骤,按序号顺序执行' },
|
||||
{ name: '步骤附件/图纸', source: '每个工序步骤单独上传(绑定式)。文件由 MES 服务器保存。', purpose: '把图纸精确绑定到具体步骤,工位终端按当前步骤自动展示对应图纸', fill: '在步骤行上传该步骤图纸/附件(可多个);随工序自动分发到工位终端' }
|
||||
]
|
||||
},
|
||||
@@ -341,13 +328,13 @@ export const helpProcessFlow = {
|
||||
overview: '决定该步骤的数值从哪里来。',
|
||||
fields: [
|
||||
{ name: '拧紧枪自动', source: '拧紧枪设备自动回传。', purpose: '扭矩/角度等由拧紧枪采集,工人无需手输', fill: '选「拧紧枪自动」,并在「是否拧紧」打开开关' },
|
||||
{ name: '手填', source: '工人在工位终端/报工页手动输入数值。', purpose: '无法自动采集的参数(如尺寸、电压)由人工录入', fill: '选「手填」' },
|
||||
{ name: '手填', source: '工人在工位终端手动输入数值。', purpose: '无法自动采集的参数(如尺寸、电压)由人工录入', fill: '选「手填」' },
|
||||
{ name: '仅记录', source: '无数据,只做操作步骤确认(如"装配完成")。', purpose: '记录该步骤已执行,不采集数值、不判定', fill: '选「仅记录」' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '考核标准(每个步骤可设多条)',
|
||||
overview: '用于报工时自动判定该步骤「合格/不合格」。在哪用:工位终端/手动报工提交该步骤数值时,系统按此标准判 合格/不合格,结果进入工件追溯与流程卡。如何收集:数值来自上面的采集方式(拧紧枪自动或手填)。',
|
||||
overview: '用于报工时自动判定该步骤「合格/不合格」。在哪用:工位终端提交该步骤数值时,系统按此标准判 合格/不合格,结果进入工件追溯与流程卡。如何收集:数值来自上面的采集方式(拧紧枪自动或手填)。',
|
||||
fields: [
|
||||
{ name: '指标名', source: '人为定义。', purpose: '标准叫什么(如 扭矩/角度/尺寸)', fill: '如 扭矩' },
|
||||
{ name: '单位', source: '人为填写。', purpose: '数值单位', fill: '如 N·m、°、mm' },
|
||||
@@ -361,7 +348,7 @@ export const helpProcessFlow = {
|
||||
export const helpPerformance = {
|
||||
title: '绩效报表(工人工作量统计)',
|
||||
overview:
|
||||
'按 操作人 × 日期 × 工位 汇总每个工人的完成/合格/NG 数量,用于工作量与绩效统计。\n\n页面提供「按人 / 按工位 / 明细」三个页签:按人看每人汇总,按工位看每工位汇总,明细看逐条记录(含工序名);三个视图各自独立分页,均受上方筛选条件约束。\n\n筛选与导出:支持 操作人 / 工位 / 日期范围 组合筛选(默认近 90 天);顶部汇总卡随查询刷新;「导出 xlsx」一次导出三个 sheet(按人/按工位/明细),导出遵循统一时间硬规则(时间范围为空默认近 3 个月、只传一端报错、跨度超 1 年报错)。\n\n数据来源:全部由工位终端/手动报工的「工位完成上报」自动写入报工实绩(workpiece_process),本页只读统计,不手工录入。',
|
||||
'按 操作人 × 日期 × 工位 汇总每个工人的完成/合格/NG 数量,用于工作量与绩效统计。\n\n页面提供「按人 / 按工位 / 明细」三个页签:按人看每人汇总,按工位看每工位汇总,明细看逐条记录(含工序名);三个视图各自独立分页,均受上方筛选条件约束。\n\n筛选与导出:支持 操作人 / 工位 / 日期范围 组合筛选(默认近 90 天);顶部汇总卡随查询刷新;「导出 xlsx」一次导出三个 sheet(按人/按工位/明细),导出遵循统一时间硬规则(时间范围为空默认近 3 个月、只传一端报错、跨度超 1 年报错)。\n\n数据来源:全部由工位终端的「工位完成上报」自动写入报工实绩(workpiece_process),本页只读统计,不手工录入。',
|
||||
fields: [
|
||||
{ name: '操作人', source: '筛选条件。来自报工实绩的 operator 字段(工位终端登录人)。', purpose: '只看某个工人的工作量', fill: '输入姓名/账号,可空=全部' },
|
||||
{ name: '工位', source: '下拉选择(工位数以数据库为准)。', purpose: '只看某个工位的报工情况', fill: '可空=全部工位' },
|
||||
@@ -528,3 +515,16 @@ export const helpAttachment = {
|
||||
{ name: '归档清理', source: '按钮(权限 sys.attachment:manage;先预览数量再确认执行)。', purpose: '把早于本地保留年数的附件移到外部备份目录并标记已归档,释放磁盘', fill: '点「归档清理」→确认;无权限时该按钮不显示' }
|
||||
]
|
||||
}
|
||||
|
||||
export const helpProducible = {
|
||||
title: '排产支撑',
|
||||
overview:
|
||||
'把「库存还能撑多久」算清楚,给计划员排产/叫料决策用。数据来源:日排产(daily_plan)× 工单(work_order)× BOM(产品物料清单)得到需求量,库存可用量实时来自 WMS(/api/internal/stock/check)。\n\n本页两块:\n· 各成品可生产套数:只统计启用产品(停用产品不再展示产能,列表分页展示);按每个成品的 BOM 单台用量 ÷ 对应物料 WMS 可用量取最小值,即当前库存最多还能装出多少台该成品;短板说明列出限制它的物料(可用量 ÷ 单台用量 = 套数)。\n· 整体支撑:用 WMS 可用量对日排产做逐日消耗模拟,直到某物料不足,得出「还能满产多少天」「库存可支撑到哪天」「最先告罄的短板物料」;排产只排到 N 天时,第 N+1 天起用近3天平均日需求外推(封顶365天防死循环)。\n\n无排产计划时短板物料栏显示「无排产计划」;排产日全部满足且外推无有效需求时显示「充足」。本页只读,不落库。',
|
||||
fields: [
|
||||
{ name: '还能满产(天)', source: '逐日消耗模拟算得(/api/v1/production/producible-support)。', purpose: '一眼看出按当前排产节奏库存能撑几天', fill: '=0 表示今天/近期排产即缺料,需立即补料' },
|
||||
{ name: '库存可支撑到', source: '支撑天数 + 今日日期推算。', purpose: '给出最晚需要到货的日期', fill: '只读;格式 YYYY-MM-DD' },
|
||||
{ name: '最先告罄短板物料', source: '模拟中第一个被耗尽的物料(名称+编码)。', purpose: '指出最该优先补货的物料', fill: '「无排产计划」/「充足」表示暂无瓶颈' },
|
||||
{ name: '可生产套数', source: '逐成品计算(只算启用产品,停用产品不展示):min(各物料可用量 ÷ 单台用量)。', purpose: '该成品当前库存能装出的最多台数', fill: '=0 表示关键物料已缺,无法开线' },
|
||||
{ name: '短板说明', source: '限制该成品可生产套数的物料明细(可用量÷单台用量=套数)。', purpose: '定位缺哪料、缺多少', fill: '只读;多短板时最多列前3个' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
Document, Calendar, Box, Goods, ShoppingCart, Monitor, Tools,
|
||||
Operation, Link, DataLine, EditPen, Search, CircleCheck, Clock, User,
|
||||
Operation, Link, DataLine, Search, CircleCheck, Clock, User,
|
||||
Key, Fold, Expand, Warning, Paperclip, Tickets, Bell, Aim
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElNotification } from 'element-plus'
|
||||
@@ -131,6 +131,7 @@ const menuGroups = [
|
||||
{ code: 'produce.workorder', name: '工单管理', path: '/work-order', icon: Document },
|
||||
{ code: 'produce.dailyplan', name: '日排产', path: '/daily-plan', icon: Calendar },
|
||||
{ code: 'produce.bom', name: '产品物料清单', path: '/bom', icon: Goods },
|
||||
{ code: 'produce.producible', name: '排产支撑', path: '/producible-support', icon: DataLine },
|
||||
{ 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 },
|
||||
@@ -140,7 +141,6 @@ const menuGroups = [
|
||||
{
|
||||
title: '现场作业', icon: Monitor, children: [
|
||||
{ code: 'produce.plc', name: '工位组合下发', path: '/plc-send', icon: Monitor },
|
||||
{ code: 'produce.scan', name: '手动报工', path: '/scan', icon: EditPen },
|
||||
{ code: 'produce.wip', name: '在制品', path: '/wip', icon: Clock },
|
||||
{ code: 'produce.trace', name: '工件追溯', path: '/trace', icon: Search },
|
||||
{ code: 'produce.processcard', name: '生产流程卡', path: '/process-card', icon: Tickets },
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
defineOptions({ name: 'ProducibleSupport' })
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import request from '../utils/request'
|
||||
import { fmtTime } from '../utils/format'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpProducible } from '../help'
|
||||
|
||||
// 排产支撑:逐成品可生产套数 + 整体还能满产多少天/支撑到哪天/最先告罄短板物料
|
||||
const loading = ref(false)
|
||||
const items = ref([])
|
||||
const supportDays = ref(0)
|
||||
const supportUntil = ref('')
|
||||
const shortBoardMaterial = ref('')
|
||||
const computedAt = ref(0)
|
||||
|
||||
// 成品分页(§A2:只算启用成品、按需分页,不再全量无分页)
|
||||
const page = reactive({ current: 1, size: 20 })
|
||||
const pagedItems = computed(() => {
|
||||
const start = (page.current - 1) * page.size
|
||||
return items.value.slice(start, start + page.size)
|
||||
})
|
||||
function onSizeChange(s) {
|
||||
page.size = s
|
||||
page.current = 1
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.get('/production/producible-support')
|
||||
items.value = data?.items || []
|
||||
supportDays.value = data?.supportDays || 0
|
||||
supportUntil.value = data?.supportUntil || ''
|
||||
shortBoardMaterial.value = data?.shortBoardMaterial || ''
|
||||
computedAt.value = data?.computedAt || 0
|
||||
page.current = 1
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="mb12">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<template #title>
|
||||
<div style="font-size:12px;line-height:1.7">
|
||||
本页基于日排产与库存可用量推算:<b>可生产套数</b> = 按各成品 BOM 单台用量 ÷ WMS 可用量取最小;
|
||||
<b>支撑天数</b> = 用可用量对日排产做逐日消耗模拟,直到某物料不足(排产排满后,第 N+1 天起按近3天平均日需求外推,封顶365天)。
|
||||
若无排产计划或排产日全部满足,短板物料栏显示「无排产计划 / 充足」。
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-row :gutter="12" style="margin-top:12px">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never" class="stat">
|
||||
<div class="stat-num" :class="supportDays > 0 ? 'ok' : 'warn'">{{ supportDays }}</div>
|
||||
<div class="stat-label">还能满产(天)</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never" class="stat">
|
||||
<div class="stat-num">{{ supportUntil || '-' }}</div>
|
||||
<div class="stat-label">库存可支撑到</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never" class="stat">
|
||||
<div class="stat-num short" :title="shortBoardMaterial">{{ shortBoardMaterial || '充足' }}</div>
|
||||
<div class="stat-label">最先告罄短板物料</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never" class="stat">
|
||||
<div class="stat-num">{{ computedAt ? fmtTime(computedAt) : '-' }}</div>
|
||||
<div class="stat-label">计算时间</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" header="各成品可生产套数(只算启用成品;按 BOM 单台用量 ÷ 库存可用量取最小,短板物料见说明列)" v-loading="loading">
|
||||
<el-table :data="pagedItems" border size="small" empty-text="暂无成品(未配置 BOM 或物料清单)">
|
||||
<el-table-column prop="productCode" label="成品编码" min-width="150" />
|
||||
<el-table-column prop="productName" label="成品名称" min-width="150">
|
||||
<template #default="{ row }">{{ row.productName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可生产套数" width="120" align="center" sortable prop="producibleQty">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.producibleQty > 0 ? 'ok' : 'warn'" class="qty">{{ row.producibleQty }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="短板说明" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<span class="muted">{{ row.shortText || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pager">
|
||||
<el-pagination background layout="total, sizes, prev, pager, next" :total="items.length"
|
||||
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="(p) => (page.current = p)" @size-change="onSizeChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<PageHelp :data="helpProducible" page="ProducibleSupport" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.stat { text-align: center; }
|
||||
.stat-num { font-size: 26px; font-weight: 700; color: #303133; word-break: break-all; line-height: 1.3; }
|
||||
.stat-num.short { font-size: 18px; color: #e6a23c; }
|
||||
.stat-num.ok { color: #67c23a; }
|
||||
.stat-num.warn { color: #e6a23c; }
|
||||
.stat-label { font-size: 12px; color: #909399; margin-top: 6px; }
|
||||
.qty { font-size: 18px; font-weight: 700; }
|
||||
.muted { color: #909399; }
|
||||
</style>
|
||||
@@ -1,297 +0,0 @@
|
||||
<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>
|
||||
@@ -11,6 +11,7 @@ const routes = [
|
||||
{ path: 'work-order', name: 'WorkOrder', component: () => import('../pages/WorkOrder.vue'), meta: { title: '工单管理' } },
|
||||
{ path: 'daily-plan', name: 'DailyPlan', component: () => import('../pages/DailyPlan.vue'), meta: { title: '日排产' } },
|
||||
{ path: 'bom', name: 'Bom', component: () => import('../pages/Bom.vue'), meta: { title: '产品物料清单' } },
|
||||
{ path: 'producible-support', name: 'ProducibleSupport', component: () => import('../pages/ProducibleSupport.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: '工位组合下发' } },
|
||||
@@ -22,7 +23,6 @@ const routes = [
|
||||
{ path: 'inspect', name: 'Inspect', component: () => import('../pages/Inspect.vue'), meta: { title: '巡检终端' } },
|
||||
{ path: 'quality', name: 'QualityInspection', component: () => import('../pages/QualityInspection.vue'), meta: { title: '质量检验' } },
|
||||
{ path: 'wip', name: 'Wip', component: () => import('../pages/Wip.vue'), meta: { title: '在制品' } },
|
||||
{ path: 'scan', name: 'Scan', component: () => import('../pages/Scan.vue'), meta: { title: '手动报工' } },
|
||||
{ path: 'trace', name: 'Trace', component: () => import('../pages/Trace.vue'), meta: { title: '工件追溯' } },
|
||||
{ path: 'process-card', name: 'ProcessCard', component: () => import('../pages/ProcessCard.vue'), meta: { title: '生产流程卡' } },
|
||||
{ path: 'product-type', name: 'ProductType', component: () => import('../pages/ProductType.vue'), meta: { title: '物料档案' } },
|
||||
|
||||
Reference in New Issue
Block a user