feat:多项功能优化

This commit is contained in:
zhoujianjin
2026-09-18 18:54:54 +08:00
parent 672cd23329
commit 543655d441
48 changed files with 1289 additions and 447 deletions
+19 -30
View File
@@ -3,16 +3,16 @@
// 说明:这里的"数据来源"特指该字段的数据由谁产生、从哪个系统/页面来,帮助跑通真实流程。
export const helpProductType = {
title: '产品型号(档案维护)',
title: '物料档案(档案维护)',
overview:
'产品型号 = 企业要生产哪些产品的分类档案,是整条产线的源头基础数据。\n\n主数据链条:产品型号 → 工单(选产品型号+物料清单) → 物料清单(给该产品编号配料) → 日排产 → 备料单 → 库房出库 → 产线生产 → 完工成品自动回流 WMS 入库。\n\n关系说明(几对几):\n· 产品型号 ↔ 工单:1 对 N。一个产品型号可以建多个工单(同一型号今天做一批、明天做一批,工单号不同、产品编号相同)。\n· 产品型号 ↔ 物料清单:1 对多(选用制)。同一型号可并存多份物料清单(不同厂家供货/含半成品结构),用「物料清单名称」区分,工单建单时选定一份,备料/装机都按工单锁定的那份算。\n· 产品型号 ↔ 备料单:间接。备料单由「日排产 × 工单锁定的物料清单」算出,与产品型号无直接约束。\n· 强约束:工单/物料清单都挂在产品编号下,产品型号编码变了,下游关联会断,建议建好后不要改。\n· 与 WMS 的关系:两套系统解耦。产品/图号主数据以 WMS 为单一来源,MES 实时拉取(WMS 不可达时允许手填、仅提示不强校验);成品完工后由系统自动生成 SN 回流 WMS 入库,无需人工对账。',
'物料档案 = 本页是 WMS「基础数据(物料主数据)」中"成品"品类在本系统的视图,与 WMS 指向同一份主数据(以 WMS 为准,实时拉取、不双录)。本页承载的是"要生产哪些成品",是整条产线的源头基础数据。\n\n品类来源(与 WMS 一致):原材料=进货来的(外购);半成品、成品=自己生产的;其他=辅料/工装/试验设备等。成品还可以作为零部件,放进另一个成品的物料清单(BOM)里被再次加工成新的成品。\n\n主数据链条:物料档案(成品) → 工单(选成品+物料清单) → 物料清单(给该产品编号配料) → 日排产 → 备料单 → 库房出库 → 产线生产 → 完工成品自动回流 WMS 入库。\n\n关系说明(几对几):\n· 成品 ↔ 工单:1 对 N。一个成品可以建多个工单(同一型号今天做一批、明天做一批,工单号不同、产品编号相同)。\n· 成品 ↔ 物料清单:1 对多(选用制)。同一成品可并存多份物料清单(不同厂家供货/含半成品结构),用「物料清单名称」区分,工单建单时选定一份,备料/装机都按工单锁定的那份算。\n· 成品 ↔ 备料单:间接。备料单由「日排产 × 工单锁定的物料清单」算出,与成品无直接约束。\n· 强约束:工单/物料清单都挂在产品编号下,成品编码变了,下游关联会断,建议建好后不要改。\n· 与 WMS 的关系:两套系统解耦。物料/图号主数据以 WMS 为单一来源,MES 实时拉取(WMS 不可达时降级为本地只读缓存);成品完工后由系统自动生成 SN 回流 WMS 入库,无需人工对账。',
fields: [
{ name: '名称', source: '手工填写。用户自定义,无上游来源。', purpose: '产品型号的中文名,用于识别与下拉展示', fill: '如:智能电表、断路器、配电柜' },
{ name: '名称', source: '手工填写。用户自定义,无上游来源。', purpose: '成品的中文名,用于识别与下拉展示', fill: '如:智能电表、断路器、配电柜' },
{
name: '编码',
source: '系统自动生成 + 可手动修改。新建时会自动带出(格式 PROD-YYYYMMDD-序号),你可直接使用或改为自定义编码;编辑时保留已存值。一个产品编号可对应多个工单(同一型号今天做一批、明天做一批,工单号不同,产品编号相同)。',
purpose: '产品唯一编码。它是 MES 内部识别该产品的标识,决定该产品的物料清单内容,也是工单、备料、拧紧、追溯的挂靠键。',
fill: '建议用有意义的编码,如 METER-01。要点:①编码主数据以 WMS(库房) 为单一来源,本系统实时从 WMS 拉取物料/品档案;WMS 不可达时允许手填、仅提示不强校验;②成品完工后系统自动生成 SN 并回流 WMS 入库,无需人工对账;③如产品编号就是"产品型号",则直接沿用该类型编码即可;④建好后不要随意改,否则下游工单/追溯的关联会断。'
fill: '建议用有意义的编码,如 METER-01。要点:①编码主数据以 WMS(库房) 为单一来源,本系统实时从 WMS 拉取物料/品档案;WMS 不可达时降级为本地只读缓存;②成品完工后系统自动生成 SN 并回流 WMS 入库,无需人工对账;③如产品编号就是成品编码,则直接沿用该编码即可;④建好后不要随意改,否则下游工单/追溯的关联会断。'
},
{ name: '类别', source: '手工填写。可选,无上游来源。', purpose: '产品所属大类,便于分类统计', fill: '如:计量表 / 开关柜;可空' },
{ name: '启用', source: '开关选择。默认开启。', purpose: '启用后才允许在新建工单/备料的下拉里被选到', fill: '默认开启;暂时停产的可关闭(不删除,保留档案)' },
@@ -23,13 +23,13 @@ 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: '工单基本信息',
fields: [
{ name: '工单号', source: '系统自动生成(WO+时间戳),可手工改。新建时自动带出。', purpose: '唯一标识一个工单,备料/出库/报工/追溯都挂在此号下', fill: '一般不用改;如改动须保证唯一' },
{ name: '产品型号', source: '下拉选择。数据来自"产品型号"页面已启用的记录。', purpose: '说明这批生产的是什么产品,并决定物料清单内容', fill: '先到"产品型号"页面建好记录再选择' },
{ name: '产品型号', source: '下拉选择。数据来自「物料档案」页面已启用的记录。', purpose: '说明这批生产的是什么产品,并决定物料清单内容', fill: '先到「物料档案」页面建好记录再选择' },
{ name: '合同号', source: '手工填写。', purpose: '标记本工单所属的合同(一个合同对应一个工程编号)', fill: '如 HT-2026-001(可空)' },
{ name: '工程编号', source: '手工填写。', purpose: '标记本工单所属的工程编号(一个工程编号下挂多个产品序号)', fill: '如 GC-2026-001(可空)' },
{ name: '产品序号', source: '手工填写。', purpose: '本产品在该工程编号下的序号,用于区分同工程下的不同产品', fill: '如 01、02(可空)' },
@@ -81,10 +81,10 @@ export const helpDailyPlan = {
export const helpBom = {
title: '产品物料清单',
overview:
'物料清单 = 生产 1 台该产品需要哪些物料、各用多少。\n按「产品编号 + 物料清单名称」维护:同一产品编号可并存多份物料清单(选用制)——不同厂家供货、含半成品的结构差异,都用不同的物料清单名称区分(缺省叫「默认」,不用 V1/V2 版本号)。\n工单建单时选定其中一份,之后该工单的备料算料、装机绑定校验都锁定这份物料清单,不同厂家的料单互不串。\n\n与 WMS 的关系:本页(MES)只维护「产品由哪几种料、各用几个」;每种物料叫什么、规格、是电气件(SN)还是结构件(批次)、库存有多少,统一在 WMS(库房)的「物料档案」里维护,两套系统互相解耦、通过图号对齐。\n\n编码对应规则:本页的「图号」必须与 WMS 物料档案里的「图号」完全一致;「管理方式」也要对应(电气件=SN 逐件,结构件=批次)。编码对不上,WMS 将无法为对应工单备料/出库。\n\n保护规则:某份物料清单一旦被工单选用过,就不允许再从中移除物料(防止历史工单追溯断链);新建/改名的物料清单名称在本页「物料清单名称」下拉直接输入即可创建。\n\n数据链路:先建「产品型号」→ 到本页选产品编号 + 命名物料清单,逐条添加物料 → 建工单时选定物料清单 → 仓库才能为对应工单领料。',
'物料清单 = 生产 1 台该产品需要哪些物料、各用多少。\n按「产品编号 + 物料清单名称」维护:同一产品编号可并存多份物料清单(选用制)——不同厂家供货、含半成品的结构差异,都用不同的物料清单名称区分(缺省叫「默认」,不用 V1/V2 版本号)。\n工单建单时选定其中一份,之后该工单的备料算料、装机绑定校验都锁定这份物料清单,不同厂家的料单互不串。\n\n与 WMS 的关系:本页(MES)只维护「产品由哪几种料、各用几个」;每种物料叫什么、规格、是电气件(SN)还是结构件(批次)、库存有多少,统一在 WMS(库房)的「物料档案」里维护,两套系统互相解耦、通过图号对齐。\n\n编码对应规则:本页的「图号」必须与 WMS 物料档案里的「图号」完全一致;「管理方式」也要对应(电气件=SN 逐件,结构件=批次)。编码对不上,WMS 将无法为对应工单备料/出库。\n\n保护规则:某份物料清单一旦被工单选用过,就不允许再从中移除物料(防止历史工单追溯断链);新建/改名的物料清单名称在本页「物料清单名称」下拉直接输入即可创建。\n\n数据链路:先建「物料档案」→ 到本页选产品编号 + 命名物料清单,逐条添加物料 → 建工单时选定物料清单 → 仓库才能为对应工单领料。',
fields: [
{ name: '物料清单名称', source: '下拉选择/输入(可搜索,输入新名即创建)。缺省「默认」。', purpose: '同一产品编号下多份物料清单的区分名,工单建单时按此选定', fill: '如「默认」「A厂料单」「含半成品」;同一型号下名称唯一' },
{ name: '产品编号', source: '下拉选择。来自「产品型号」里建的编码。', purpose: '要为哪个产品配置物料清单', fill: '在查询区或「添加物料」弹窗里选择产品编号' },
{ name: '产品编号', source: '下拉选择。来自「物料档案」里建的编码。', purpose: '要为哪个产品配置物料清单', fill: '在查询区或「添加物料」弹窗里选择产品编号' },
{ name: '图号', source: '手工填写。必须与 WMS(库房) 物料档案的图号一致。', purpose: '该物料在库房的全库唯一编码,两系统靠它关联', fill: '如 螺丝=GJ-LUOSI-M16;图号对不上 WMS 将无法备料' },
{ name: '物料名称', source: '手工填写。', purpose: '物料中文名,便于识别', fill: '如 十字螺丝、主控芯片' },
{ name: '规格', source: '手工填写。', purpose: '物料规格型号', fill: '如 M16×80 / SMT-48脚' },
@@ -184,11 +184,15 @@ export const helpScan = {
export const helpStation = {
title: '关联工位(工位总览)',
overview:
'工位数以数据库为准(仅可修改名称,不增删工位)。本页是工位总览:查看每个工位当前绑定了哪个工艺流程,可修改工位名称。\n\n绑定关系由本页单独维护:在下拉中选择「启用」的工艺流程即绑定到该工位;一个工位只能有一个启用的工艺流程(先停用旧流程再绑新流程);流程停用后自动解绑。\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: '无需填写' },
{ name: '工位号', source: '内置工位由系统预置;自定义工位由本页「新增工位」填写。', purpose: '产线物理工位编号', fill: '新增时自定义,可与现有工位不连续(如 14、15…)' },
{ name: '工位名称', source: '可编辑。默认如 装配工位1。', purpose: '工位的显示名称', fill: '如 一号装配工位' },
{ name: '类型', source: '内置工位由系统给定且不可修改;自定义工位可改。', purpose: '线上位(连 PLC)/线下位(虚拟,不连 PLC)', fill: '内置工位只读;自定义工位可在改名弹窗切换' },
{ name: '来源', source: '系统标识(数据库 is_builtin)。', purpose: '区分内置工位与自定义工位', fill: '只读' },
{ name: '接驳台', source: '内置数据给定(对应实体位置),不可手动更改。', purpose: '该工位位置是否有接驳台', fill: '只读' },
{ name: '绑定工艺流程', source: '自动来自本页:为工位选择「启用」的工艺流程即绑定。', purpose: '该工位执行的工艺流程(决定工序步骤/考核标准/PDF)', fill: '到本页为该工位选择启用流程;本页只读展示' },
{ name: '删除', source: '本页操作。', purpose: '删除自定义工位', fill: '仅自定义工位可删除;内置工位不显示删除按钮' },
{ name: '虚拟工位 0 / 13', source: '系统预置。工位号 0=上线位、13=下线位。', purpose: '产线进线口建档上线、出料口完工入库,由 PAD 操作,不连 PLC/拧紧枪', fill: '只读;上线位/下线位在工位终端(PAD)用「上线建档」「下线完工」面板操作' }
]
}
@@ -391,7 +395,7 @@ export const helpProcessCard = {
overview:
'这个菜单做什么用:按**产品型号**全方位展示该型号的完整制造定义,用于工艺交底、审核与客户查阅;与「工件追溯」页里按单个 SN 打印的流程卡互补——后者记的是“这一件实际怎么做的”,本页讲的是“这个型号应该怎么做”。\n\n内容构成(页面按页签展示,打印时全部输出):①工位流程总览——工位组合、每工位绑定的工艺流程、流程状态、步骤数、检验点数、拧紧步骤数、工艺图纸、该型号的历史作业时间;②工艺流程与步骤——逐工位列出每条步骤的采集方式(手工录入/自动采集/无需采集)、是否需检测确认、是否拧紧采集、考核标准(含上下限);③物料清单——该型号的物料清单明细,按物料清单名称分组,含图号/名称/规格/单位/管理方式/装配工位/单台用量/损耗率/需求总量;④检验卡项——把所有「需要检测确认」的步骤汇总成检验项清单,含判定标准。\n\n工位组合从哪来:优先取该产品编号最近一张工单的「工位组合」;若该型号还没有工单,则按全产线工位顺序(升序)展示,页面顶部会标注来源。\n\n打印/导出:点「打印 / 导出 PDF」,系统会在新窗口打开排版好的 A4 卡片版式,用浏览器打印即可(选择“另存为 PDF”即导出电子档)。',
fields: [
{ name: '产品型号', source: '下拉选择,来自「产品型号」页。', purpose: '要查看哪个型号的全景流程', fill: '选定后自动带出产品编号' },
{ name: '产品型号', source: '下拉选择,来自「物料档案」页。', purpose: '要查看哪个型号的全景流程', fill: '选定后自动带出产品编号' },
{ name: '产品编号', source: '自动带出(也可手工填写,直接按编号生成)。', purpose: '匹配工单、物料清单的关键字', fill: '一般无需手填' },
{ name: '工位组合', source: '只读,自动带出。', purpose: '该型号要走哪些工位、按什么顺序', fill: '-' },
{ name: '工位流程总览-工艺图纸', source: '来自「工艺流程」页上传的图纸文件名。', purpose: '该工位是否有可查阅的工艺图', fill: '-' },
@@ -401,23 +405,6 @@ export const helpProcessCard = {
}
export const helpQtyReport = {
title: '数量不符上报',
overview:
'这个菜单做什么用:记录「工位收料时实收数量 ≠ 备料单应发数量」的差异,形成闭环——上报→落预警→生产/仓库核实处理。\n\n上报入口有两处:①工位终端「巡检/异常」面板——操作工现场发现数量不符时直接上报;②本页(MES 电脑端)——由生产调度/线长代录或复核。\n\n差异怎么算:系统自动按「实收 − 应发」计算差异(正数=多收、负数=少收);上报后同步落一条预警中心消息(type=qty_diff),未读状态可提醒相关人员。\n\n处理流程:仓库/生产核实后在预警中心标记已读,本页记录保持存档,供后续对账与追溯。',
fields: [
{ name: '工位', source: '工位终端上报自动带入;本页手录时手动选择(工位数以数据库为准)。', purpose: '差异发生在哪个工位', fill: '选工位号' },
{ name: '工单号', source: '工位终端上报自动带入;来自【工单管理】。', purpose: '定位是哪张工单的物料', fill: '搜索/输入工单号' },
{ name: '产品序列号', source: '工位终端上报自动带入(当前作业件)。', purpose: '追溯到具体那一件', fill: '扫描/输入 SN' },
{ name: '图号/名称', source: '工位终端上报自动带入(当前收料物料)。', purpose: '是哪一种物料数量不符', fill: '选择/输入物料' },
{ name: '应发数量', source: '手动/自动带入备料单应发数。', purpose: '作为差异计算基准', fill: '填应发数' },
{ name: '实收数量', source: '手动填写(现场实际清点结果)。', purpose: '作为差异计算基准', fill: '填实收数' },
{ name: '差异', source: '系统自动计算(实收−应发)。', purpose: '一眼看出多收/少收', fill: '只读' },
{ name: '原因', source: '手动填写。', purpose: '说明数量不符的原因(如供应商短装/破损/漏发)', fill: '描述原因' },
{ name: '状态', source: '系统维护(待处理/已处理)。', purpose: '跟踪闭环进度', fill: '只读,筛选用' }
]
}
export const helpQuality = {
title: '质量检验(过程检 / 完工检)',
overview:
@@ -450,9 +437,11 @@ export const helpWip = {
export const helpAlertCenter = {
title: '预警中心(系统自动触发)',
overview:
'预警中心汇集全系统自动产生的异常/提醒消息,人工无需配置规则——全部由内置触发源自动落库并推送。顶部铃铛角标显示未读数,点击进入本页处理。\n\n八类内置触发源:\n· 扭紧不合格(torque_ng):拧紧扭矩/角度超考核标准时自动告警;\n· 检验不合格(inspection_ng):质量检验判定 NG 时告警;\n· 备料未齐套(material_shortage):工单装机绑定物料未齐套时告警;\n· 库存偏低(inventory_low):可用量低于安全库存时告警;\n· 工单超时(task_overdue):工单超过完成日期未完工时告警;\n· 缓存位满(cache_full):产线缓存/接驳台占满时告警;\n· 数量不符(qty_diff):工位收料实收≠应发时告警(见「数量不符上报」);\n· 应急呼叫(emergency_call):工位终端现场发起的紧急呼叫。\n\n应急呼叫闭环:操作工在工位终端点「应急呼叫」(设备故障/质量异常/缺料/安全等紧急情形)→ 系统即时落一条 type=emergency_call 的未读预警并通过 SSE 推到大屏/看板 → 线长、维修、仓库等相关人员在本页看到未读告警并到场处置 → 处置完点「标记已读」闭环。\n\n处理流程:按状态(全部/未读/已读)筛选,核实后点行末「标记已读」;已读记录留存备查。',
'预警中心汇集全系统自动产生的异常/提醒消息,人工无需配置规则——全部由内置触发源自动落库并推送。顶部铃铛角标显示未读数,点击进入本页处理。\n\n八类内置触发源:\n· 扭紧不合格(torque_ng):拧紧扭矩/角度超考核标准时自动告警;\n· 检验不合格(inspection_ng):质量检验判定 NG 时告警;\n· 备料未齐套(material_shortage):工单装机绑定物料未齐套时告警;\n· 库存偏低(inventory_low):可用量低于安全库存时告警;\n· 工单超时(task_overdue):工单超过完成日期未完工时告警;\n· 缓存位满(cache_full):产线缓存/接驳台占满时告警;\n· 数量不符(qty_diff):工位收料实收≠应发时告警(MES 仅预警,明细由 WMS 处理);\n· 应急呼叫(emergency_call):工位终端现场发起的紧急呼叫。\n\n应急呼叫闭环:操作工在工位终端点「应急呼叫」(设备故障/质量异常/缺料/安全等紧急情形)→ 系统即时落一条 type=emergency_call 的未读预警并通过 SSE 推到大屏/看板 → 线长、维修、仓库等相关人员在本页看到未读告警并到场处置 → 处置完点「标记已读」闭环。\n\n处理流程:按 状态(全部/未读/已读)、预警类型、开始时间(默认当天)、结束时间(默认不限)筛选并分页浏览,核实后点行末「标记已读」;已读记录留存备查。',
fields: [
{ name: '状态', source: '系统维护(未读/已读)。', purpose: '区分待处理与已处理预警', fill: '顶部单选筛选:全部/未读/已读' },
{ name: '状态', source: '系统维护(未读/已读)。', purpose: '区分待处理与已处理预警', fill: '筛选项单选:全部/未读/已读' },
{ name: '预警类型', source: '内置触发源自动写入。', purpose: '只关注某一类异常', fill: '下拉选择八类之一,留空=全部' },
{ name: '开始时间 / 结束时间', source: '系统记录触发时刻。', purpose: '按时间范围缩小排查范围', fill: '开始时间默认当天;结束时间留空=不限' },
{ name: '类型', source: '内置触发源自动写入。', purpose: '区分八类预警(含应急呼叫)', fill: '只读' },
{ name: '标题 / 内容', source: '触发时系统自动生成。', purpose: '说明预警的具体对象与原因', fill: '只读' },
{ name: '关联ID', source: '系统写入(工单号/SN/工位等业务键)。', purpose: '定位预警来源的具体业务对象', fill: '只读' },
@@ -151,8 +151,7 @@ const menuGroups = [
title: '基础配置', icon: Box, children: [
{ code: 'produce.processflow', name: '工艺流程', path: '/process-flow', icon: Operation },
{ code: 'produce.station', name: '关联工位', path: '/station', icon: Link },
{ code: 'produce.product', name: '产品型号', path: '/product-type', icon: Box },
{ code: 'produce.qtyreport', name: '数量不符上报', path: '/qty-report', icon: Warning }
{ code: 'produce.product', name: '物料档案', path: '/product-type', icon: Box }
]
},
{
@@ -1,18 +1,37 @@
<template>
<el-card>
<!-- 预警中心不设规则设置Tab预警全部由系统内置触发源自动产生人工无需配置规则 -->
<el-form inline>
<el-form-item label="状态">
<el-radio-group v-model="statusFilter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="UNREAD">未读</el-radio-button>
<el-radio-button label="READ">已读</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="预警类型">
<el-select v-model="typeFilter" clearable style="width:150px" placeholder="全部">
<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-date-picker v-model="startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始时间" style="width:150px" />
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker v-model="endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束时间" style="width:150px" />
</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>
<div style="margin-bottom:12px;display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<el-radio-group v-model="statusFilter" @change="loadAlerts">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="UNREAD">未读</el-radio-button>
<el-radio-button label="READ">已读</el-radio-button>
</el-radio-group>
<span style="color:#888">未读{{ unread }}</span>
<span style="color:#aaa;font-size:12px">
系统内置触发源自动产生预警扭紧不合格 / 检验不合格 / 备料未齐套 / 库存偏低 / 工单超时 / 缓存位满 / 数量不符 / 应急呼叫无需人工配置
</span>
</div>
<el-table :data="alerts" border size="small">
<el-table :data="alerts" border size="small" v-loading="loading">
<el-table-column label="状态" width="90">
<template #default="{row}"><el-tag :type="row.status==='UNREAD' ? 'danger' : 'info'">{{ row.status==='UNREAD' ? '未读' : '已读' }}</el-tag></template>
</el-table-column>
@@ -22,13 +41,18 @@
<el-table-column prop="title" label="标题" min-width="140" />
<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" />
<el-table-column prop="createdAt" label="时间" min-width="170">
<template #default="{row}">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{row}">
<el-button size="small" :disabled="row.status==='READ'" @click="markRead(row)">标记已读</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next, sizes"
:total="total" v-model:current-page="page" v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]" @current-change="loadAlerts" @size-change="search" />
</el-card>
<PageHelp :data="helpAlertCenter" page="AlertCenter" />
</template>
@@ -39,8 +63,28 @@ import PageHelp from '../components/PageHelp.vue'
import { helpAlertCenter } from '../help'
const alerts = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const unread = ref(0)
const statusFilter = ref('all')
const typeFilter = ref('')
const startDate = ref(todayStr()) // 开始时间默认当天
const endDate = ref('') // 结束时间默认空
function todayStr() {
const d = new Date()
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
function fmtTime(v) {
if (!v) return '-'
const d = new Date(v)
if (Number.isNaN(d.getTime())) return String(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
// 内置触发源类型(仅用于展示文案,不在前端配置)
const types = [
@@ -59,10 +103,37 @@ function typeLabel(v) {
}
async function loadAlerts() {
const status = statusFilter.value === 'all' ? '' : statusFilter.value
alerts.value = (await request.get('/alerts', { params: { status } })) || []
loading.value = true
try {
const params = {
status: statusFilter.value === 'all' ? '' : statusFilter.value,
type: typeFilter.value,
from: startDate.value,
to: endDate.value,
page: page.value,
pageSize: pageSize.value
}
const data = await request.get('/alerts', { params })
alerts.value = data?.list || []
total.value = data?.total || 0
} finally {
loading.value = false
}
// 未读数不带时间/类型筛选,始终反映全局未读,与右上角铃铛一致
unread.value = (await request.get('/alerts', { params: { status: 'UNREAD' } }))?.length || 0
}
function search() {
page.value = 1
loadAlerts()
}
function reset() {
statusFilter.value = 'all'
typeFilter.value = ''
startDate.value = todayStr()
endDate.value = ''
page.value = 1
loadAlerts()
}
async function markRead(row) {
await request.post('/alert/read', { id: row.id })
loadAlerts()
+16 -40
View File
@@ -1,26 +1,31 @@
<template>
<el-card>
<!-- 附件中心仅作集中查阅只读新增/删除在各业务页面内完成如检验页工序步骤行
本页展示文件路径点击路径可直接下载查看 -->
<el-alert type="info" :closable="false" show-icon style="margin-bottom:12px"
title="附件中心为只读查阅页:新增/删除附件请在各自业务页面(如质量检验、工序步骤)内操作" />
<el-form inline style="margin-bottom:12px">
<el-form-item label="业务类型">
<el-select v-model="form.bizType" style="width:160px">
<el-select v-model="form.bizType" clearable style="width:160px" placeholder="全部">
<el-option label="工单" value="work_order" />
<el-option label="物料" value="material" />
<el-option label="工艺流程" value="process_flow" />
<el-option label="检验" value="inspection" />
<el-option label="质量" value="quality" />
</el-select>
</el-form-item>
<el-form-item label="业务ID">
<el-input v-model="form.bizId" placeholder="如 工单号 / 图号" style="width:220px" />
<el-input v-model="form.bizId" placeholder="如 工单号 / 图号" style="width:220px" @keyup.enter="load" />
</el-form-item>
<el-form-item>
<el-upload action="#" :show-file-list="false" :http-request="doUpload">
<el-button type="primary">选择文件上传</el-button>
</el-upload>
</el-form-item>
<el-form-item><el-button @click="load">查询</el-button></el-form-item>
<el-form-item><el-button type="primary" @click="load">查询</el-button></el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="fileName" label="文件名" min-width="180" />
<el-table-column label="文件路径" min-width="260">
<template #default="{row}">
<el-link type="primary" :underline="false" @click="download(row)">{{ row.filePath || '-' }}</el-link>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="大小" width="110">
<template #default="{row}">{{ fmtSize(row.fileSize) }}</template>
</el-table-column>
@@ -28,22 +33,16 @@
<el-table-column prop="bizId" label="业务ID" width="140" />
<el-table-column prop="uploader" label="上传人" width="100" />
<el-table-column prop="createdAt" label="上传时间" min-width="170" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{row}">
<el-button size="small" type="primary" @click="download(row)">下载</el-button>
<el-button size="small" type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { getToken } from '../utils/auth'
const form = reactive({ bizType: 'work_order', bizId: '' })
const form = reactive({ bizType: '', bizId: '' })
const list = ref([])
async function load() {
@@ -55,25 +54,8 @@ function fmtSize(n) {
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
return (n / 1024 / 1024).toFixed(1) + ' MB'
}
async function doUpload(option) {
if (!form.bizId) {
ElMessage.warning('请先填写业务ID再上传')
return
}
const fd = new FormData()
fd.append('file', option.file)
fd.append('bizType', form.bizType)
fd.append('bizId', form.bizId)
try {
await request.post('/attachment/upload', fd)
ElMessage.success('上传成功')
if (option.onSuccess) option.onSuccess()
load()
} catch (e) {
if (option.onError) option.onError(e)
}
}
async function download(row) {
if (!row.filePath) return
const token = getToken()
const resp = await fetch('/api/v1/attachment/download?name=' + encodeURIComponent(row.filePath), { headers: { Authorization: 'Bearer ' + token } })
if (!resp.ok) { ElMessage.error('下载失败'); return }
@@ -85,11 +67,5 @@ async function download(row) {
a.click()
URL.revokeObjectURL(url)
}
async function remove(row) {
await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
await request.post('/attachment/delete', { id: row.id })
ElMessage.success('已删除')
load()
}
onMounted(load)
</script>
@@ -30,7 +30,6 @@
<el-table-column label="事件类型" min-width="170">
<template #default="{ row }">
<el-tag :type="familyTag(row.eventType)" size="small">{{ familyName(row.eventType) }}</el-tag>
<span class="evt-code">{{ row.eventType }}</span>
</template>
</el-table-column>
<el-table-column prop="workOrderNo" label="工单号" min-width="140">
@@ -161,11 +160,3 @@ function onReset() {
onMounted(load)
</script>
<style scoped>
.evt-code {
margin-left: 6px;
color: #909399;
font-size: 12px;
}
</style>
@@ -16,7 +16,7 @@
<el-form-item label="结束时间"><el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" style="width:140px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button v-if="can('produce.product:add')" type="primary" plain @click="openAdd">新增产品型号</el-button>
<el-button v-if="can('produce.product:add')" type="primary" plain @click="openAdd">新增物料档案</el-button>
<el-button @click="exportXlsx">导出</el-button>
</el-form-item>
</el-form>
@@ -47,7 +47,7 @@
:total="total" v-model:current-page="page" v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]" @current-change="load" @size-change="search" />
<el-dialog v-model="dialog.show" :title="dialog.id?'编辑产品型号':'新增产品型号'" width="460px">
<el-dialog v-model="dialog.show" :title="dialog.id?'编辑物料档案':'新增物料档案'" width="460px">
<el-form :model="dialog" label-width="80px">
<el-form-item label="名称"><el-input v-model="dialog.name" /></el-form-item>
<el-form-item label="编码">
@@ -75,7 +75,7 @@ import PageHelp from '../components/PageHelp.vue'
import { helpProductType } from '../help'
import { fmtTime } from '../utils/format'
// 数据源说明:产品型号档案已并入 WMS 物料主数据(item_type=3,品类=成品),
// 数据源说明:物料档案已并入 WMS 物料主数据(item_type=3,品类=成品),
// 本页经 MES 后端实时代理 WMS;WMS 不可达时降级为本地只读缓存(wmsOnline=false)。
const list = ref([])
const total = ref(0)
@@ -148,7 +148,7 @@ async function exportXlsx() {
const blob = await request.get('/product-types/export', { params, responseType: 'blob' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = '产品型号.xlsx'
a.download = '物料档案.xlsx'
a.click()
URL.revokeObjectURL(a.href)
}
@@ -1,73 +0,0 @@
<template>
<el-card>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="数量不符上报:工位收料时发现「实收数量」与备料单「应发数量」不一致,由工位终端或本页上报。系统自动算出差异并落预警,供生产/仓库核实处理。" />
<el-form inline>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width:130px" @change="load">
<el-option label="待处理" value="PENDING" />
<el-option label="已处理" value="RESOLVED" />
</el-select>
</el-form-item>
<el-form-item label="工单号"><el-input v-model="query.orderNo" clearable style="width:160px" @keyup.enter="load" /></el-form-item>
<el-form-item label="图号"><el-input v-model="query.materialCode" clearable style="width:150px" placeholder="不区分大小写" @keyup.enter="load" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="stationNo" label="工位" width="70" align="center" />
<el-table-column prop="orderNo" label="工单号" min-width="130" />
<el-table-column prop="sn" label="产品序列号" min-width="140" />
<el-table-column prop="materialCode" label="图号" min-width="110" />
<el-table-column prop="materialName" label="物料名称" min-width="110" />
<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 :style="{ color: row.diffQty === 0 ? '#909399' : (row.diffQty > 0 ? '#67c23a' : '#f56c6c'), fontWeight: 600 }">
{{ row.diffQty > 0 ? '+' + row.diffQty : row.diffQty }}
</span>
</template>
</el-table-column>
<el-table-column prop="cause" label="原因" min-width="140" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.status === 'RESOLVED' ? 'success' : 'warning'" size="small">
{{ row.status === 'RESOLVED' ? '已处理' : '待处理' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="上报人" width="90" />
<el-table-column label="上报时间" width="160">
<template #default="{row}">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</el-card>
<PageHelp :data="helpQtyReport" page="QtyReport" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpQtyReport } from '../help'
const query = reactive({ status: '', orderNo: '', materialCode: '' })
const list = ref([])
function fmtTime(t) {
if (!t) return '-'
const d = new Date(t)
if (isNaN(d.getTime())) return t
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
async function load() {
list.value = (await request.get('/qty-reports', { params: query })) || []
}
onMounted(() => { load() })
</script>
+40 -8
View File
@@ -1,7 +1,7 @@
<template>
<el-card>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="工位数量以数据库为准。本页可新增工位(工位号自定义,可扩展到 14、15…)与改名;每个工位的工艺流程在「关联工位」页维护:在下拉选择启用的工艺流程即绑定到该工位;一个工位只能有一个启用的工艺流程。" />
title="工位数量以数据库为准。本页可新增工位(工位号自定义,可扩展到 14、15…)与改名;每个工位的工艺流程在「关联工位」页维护:在下拉选择启用的工艺流程即绑定到该工位;一个工位只能有一个启用的工艺流程。内置工位由种子/初始化数据产生,需与 PLC 通讯、必须按工位号顺序流转,不允许修改工位类型、不允许删除;「是否有接驳台」对应实体位置,由内置数据给定,不可手动更改。页面点击「新增工位」创建的是非内置工位,可改类型、可删除。" />
<div style="margin-bottom:12px">
<el-button v-if="can('produce.station:add')" type="primary" @click="openAdd">新增工位</el-button>
@@ -18,6 +18,18 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="来源" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isBuiltin ? 'warning' : 'info'">
{{ row.isBuiltin ? '内置' : '自定义' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="接驳台" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.hasDock ? 'success' : 'info'">{{ row.hasDock ? '有' : '无' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定工艺流程" min-width="240">
<template #default="{ row }">
<el-select :model-value="row.flowId" size="small" placeholder="未绑定" @change="(v) => bindFlow(row, v)" style="width:100%">
@@ -26,9 +38,10 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<el-table-column label="操作" width="170" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="can('produce.station:edit')" size="small" @click="openName(row)">改名</el-button>
<el-button v-if="can('produce.station:delete') && !row.isBuiltin" size="small" type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -45,10 +58,13 @@
<el-input v-model="dlg.name" placeholder="如 一号装配工位" />
</el-form-item>
<el-form-item label="工位类型">
<el-radio-group v-model="dlg.stationType">
<el-radio-group v-model="dlg.stationType" :disabled="dlg.isBuiltin">
<el-radio label="LINE">线上位 PLC</el-radio>
<el-radio label="OFFLINE">线下位虚拟不连 PLC</el-radio>
</el-radio-group>
<div v-if="dlg.isBuiltin" style="color:#909399;font-size:12px;line-height:1.5;margin-top:4px">
内置工位需与 PLC 通讯并按顺序流转工位类型不可修改
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -62,7 +78,7 @@
<script setup>
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
@@ -74,10 +90,10 @@ const flows = ref([])
const rows = computed(() => {
return [...stations.value]
.sort((a, b) => (a.stationNo || 0) - (b.stationNo || 0))
.map((s) => ({ id: s.id, stationNo: s.stationNo, name: s.name, flowId: s.flowId, flowName: s.flowName, stationType: s.stationType }))
.map((s) => ({ id: s.id, stationNo: s.stationNo, name: s.name, flowId: s.flowId, flowName: s.flowName, stationType: s.stationType, isBuiltin: !!s.isBuiltin, hasDock: !!s.hasDock }))
})
const dlg = reactive({ show: false, isAdd: false, title: '', id: 0, stationNo: 1, name: '', stationType: 'LINE' })
const dlg = reactive({ show: false, isAdd: false, title: '', id: 0, stationNo: 1, name: '', stationType: 'LINE', isBuiltin: false })
async function load() {
stations.value = (await request.get('/stations')) || []
@@ -87,11 +103,11 @@ function openAdd() {
const used = new Set(stations.value.map((s) => s.stationNo))
let next = 14
while (used.has(next)) next++
Object.assign(dlg, { show: true, isAdd: true, title: '新增工位', id: 0, stationNo: next, name: '', stationType: 'LINE' })
Object.assign(dlg, { show: true, isAdd: true, title: '新增工位', id: 0, stationNo: next, name: '', stationType: 'LINE', isBuiltin: false })
}
function openName(row) {
Object.assign(dlg, { show: true, isAdd: false, title: `工位${row.stationNo} - 修改`, id: row.id, stationNo: row.stationNo, name: row.name, stationType: row.stationType || 'LINE' })
Object.assign(dlg, { show: true, isAdd: false, title: `工位${row.stationNo} - 修改`, id: row.id, stationNo: row.stationNo, name: row.name, stationType: row.stationType || 'LINE', isBuiltin: !!row.isBuiltin })
}
async function save() {
@@ -118,6 +134,22 @@ async function bindFlow(row, fid) {
}
}
async function remove(row) {
try {
await ElMessageBox.confirm(`确认删除工位${row.stationNo}${row.name}」?`, '删除工位', { type: 'warning' })
} catch {
return
}
try {
await request.delete(`/stations/${row.id}`)
ElMessage.success('工位已删除')
} catch {
// 拦截器已提示(如内置工位不允许删除)
} finally {
await load()
}
}
onMounted(async () => {
await load()
flows.value = (await request.get('/process-flows', { params: { pageSize: 1000, page: 1 } }))?.list || []
+1 -2
View File
@@ -23,8 +23,7 @@ const routes = [
{ 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: '产品型号' } },
{ path: 'qty-report', name: 'QtyReport', component: () => import('../pages/QtyReport.vue'), meta: { title: '数量不符上报' } },
{ path: 'product-type', name: 'ProductType', component: () => import('../pages/ProductType.vue'), meta: { title: '物料档案' } },
{ path: 'event-log', name: 'EventLog', component: () => import('../pages/EventLog.vue'), meta: { title: '操作日志' } },
{ path: 'change-password', name: 'ChangePassword', component: () => import('../pages/ChangePassword.vue'), meta: { title: '修改密码' } },
{ path: 'account', name: 'AccountManage', component: () => import('../pages/AccountManage.vue'), meta: { title: '账号管理' } },