feat&refactor: 完成多模块功能迭代与配置优化

本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
This commit is contained in:
SunYF
2026-09-10 16:59:15 +08:00
parent 6d8bb75696
commit 0ab21b1234
202 changed files with 21397 additions and 3172 deletions
@@ -8,7 +8,7 @@ import request from '../utils/request'
const props = defineProps({
modelValue: { type: [String, Array], default: '' },
multiple: { type: Boolean, default: false },
manageMode: { type: Number, default: 0 }, // 0=全部 1=结构件 2=精密
manageMode: { type: Number, default: 0 }, // 0=全部 1=结构件 2=电气
placeholder: { type: String, default: '选择物料' },
clearable: { type: Boolean, default: true },
width: { type: String, default: '100%' }
+17 -17
View File
@@ -5,7 +5,7 @@
export const helpInbound = {
title: '入库(来料入账)',
overview:
'把到货的来料记入 WMS 库存。按管理类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 精密件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n管理类型由 WMS 的"物料档案"决定(精密件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。\n\n本页三个页签(录入在前,记录在后点击才见),参照出库管理:\n· 结构件入库:按批次入库 + Excel 批量导入;\n· 精密件 SN 入库:逐件扫码 + Excel 批量导入;\n· 入库记录:分页列表(一单一行),点「明细」在右侧抽屉查看该单的行(懒加载+分页,不内嵌子表)。',
'把到货的来料记入 WMS 库存。按管理类型分两种入库方式:\n· 结构件(螺丝/机加工件) → 按【批次】入库,一批多件;\n· 电气件(芯片/器件) → 按【SN 序列号】逐件扫码,可单件追溯。\n\n管理类型由 WMS 的"物料档案"决定(电气件/结构件),入库方式随之自动给出。\n入库是整个库存链路的第一环:入库(Held) → 质量检验(合格Z02) → 之后才能被出库锁定。\n\n本页三个页签(录入在前,记录在后点击才见),参照出库管理:\n· 结构件入库:按批次入库 + Excel 批量导入;\n· 电气件 SN 入库:逐件扫码 + Excel 批量导入;\n· 入库记录:分页列表(一单一行),点「明细」在右侧抽屉查看该单的行(懒加载+分页,不内嵌子表)。',
sections: [
{
title: '结构件入库(页签1',
@@ -21,10 +21,10 @@ export const helpInbound = {
]
},
{
title: '精密件 SN 入库(页签2',
title: '电气件 SN 入库(页签2',
overview: '适用于芯片、器件等需要单件追溯的来料,一件一个 SN。录入与 Excel 批量导入都在本页签。',
fields: [
{ name: '物料', source: '下拉选择(必填)。来自"物料档案"类型为"精密件"的物料。', purpose: '入库的精密件物料', fill: '选择物料编码' },
{ name: '物料', source: '下拉选择(必填)。来自"物料档案"类型为"电气件"的物料。', purpose: '入库的电气件物料', fill: '选择物料编码' },
{ name: '区域', source: '下拉选择(必填)。来自"区域维护"。', purpose: '放到哪个库区', fill: '必填;新到货一般先放"待检"区' },
{ name: '扫码录入', source: '扫码枪连续扫码。来自每颗器件自带的 SN 条码。', purpose: '逐件登记 SN,实现单件追溯', fill: '扫入/粘贴 SN 后点【录入到列表】加入清单,可一次连续多条' },
{ name: '批量导入', source: '本页签下方。', purpose: '整表校验后一次性入库', fill: '模板(首行忽略表头):物料编码 | SN | 区域 | 生产日期 | 供应商。任一行错误则整体回退' }
@@ -32,9 +32,9 @@ export const helpInbound = {
},
{
title: '入库记录(页签3',
overview: '分页展示全部入库单,结构件与精密件同表(用"类型"列区分)。支持按入库单号、物料编码、类型、物料名称、入库时间筛选(各字段独立,无关键字混搜);可按当前条件导出 xlsx。点行「明细」在右侧抽屉查看该单的批次号/SN 行(懒加载+分页,避免精密件上万条 SN 撑爆前端)。',
overview: '分页展示全部入库单,结构件与电气件同表(用"类型"列区分)。支持按入库单号、物料编码、类型、物料名称、入库时间筛选(各字段独立,无关键字混搜);可按当前条件导出 xlsx。点行「明细」在右侧抽屉查看该单的批次号/SN 行(懒加载+分页,避免电气件上万条 SN 撑爆前端)。',
fields: [
{ name: '查询条件', source: '本页签顶部筛选区。', purpose: '缩小列表范围', fill: '可组合:入库单号(模糊)/物料编码(模糊)/类型(全部·结构件·精密件)/物料名称(模糊)/入库时间区间' },
{ name: '查询条件', source: '本页签顶部筛选区。', purpose: '缩小列表范围', fill: '可组合:入库单号(模糊)/物料编码(模糊)/类型(全部·结构件·电气件)/物料名称(模糊)/入库时间区间' },
{ name: '明细', source: '行末「明细」按钮 → 右侧抽屉。', purpose: '查看该入库单的批次号/SN/数量明细', fill: '抽屉内分页展示,不回列表里嵌套子表' },
{ name: '导出', source: '按钮。后端 /inbound/export 按当前条件返回全部(不分页)。', purpose: '把筛选结果一次性导出为 xlsx', fill: '点【导出】下载"入库管理.xlsx"' }
]
@@ -45,12 +45,12 @@ export const helpInbound = {
export const helpOutbound = {
title: '出库管理(统一主表)',
overview:
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n两个出库页签 + 一个记录页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/样品/报废/发货),支持结构件批次与精密件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息)。\n选用原则:有 MES 工单要按 BOM 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。',
'所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n两个出库页签 + 一个记录页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/样品/报废/发货),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息)。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。',
fields: [
{ name: '备料出库-工单号', source: '手动输入。数据来自 MES 已建的工单及导入的物料台账。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '输入工单号回车,查询该工单的物料台账' },
{ name: '备料出库-目标工位', source: '下拉选择 DOCK01~DOCK20(选填,可过滤搜索)。', purpose: 'AGV 配送送达的接驳台(toDock 参数)', fill: '选择如 DOCK05;不选默认运到产线入口 DOCK01' },
{ name: '备料出库-一键分配出库', source: '系统自动按 FIFO 分配。数据来自合格区Z02的批次库存。', purpose: '从合格区按先进先出凑够缺口并出库', fill: '点按钮,系统自动算批次并二次确认' },
{ name: '通用出库-物料/类型', source: '下拉选择物料;类型选"结构件(批次)"或"精密件(SN)"。', purpose: '决定走批次扣减还是 SN 扣减', fill: '选物料后选类型;批次填批次号+数量,SN 扫入 SN 列表' },
{ name: '通用出库-物料/类型', source: '下拉选择物料;类型选"结构件(批次)"或"电气件(SN)"。', purpose: '决定走批次扣减还是 SN 扣减', fill: '选物料后选类型;批次填批次号+数量,SN 扫入 SN 列表' },
{ name: '通用出库-箱号', source: '手动填写(选填)。', purpose: '整箱发货时填写,装箱信息随出库单记录(无需再单独录入装箱)', fill: '填箱号后,本出库单即带装箱属性,可在"出库记录"按箱号追溯' },
{ name: '通用出库-合同号', source: '手动填写(选填)。', purpose: '关联合同,便于按合同追溯', fill: '可空' },
{ name: '出库记录-导出 CSV', source: '按钮操作。', purpose: '把统一主表当前筛选结果一次导出为 CSV(含箱号/装箱SN)', fill: '点【导出 CSV】下载全部出库记录' }
@@ -60,10 +60,10 @@ export const helpOutbound = {
export const helpInventory = {
title: '库存查询',
overview:
'查看当前库存:全品类统一一张库存主表——原材料(结构件按批次/精密件按SN)、半成品(MES 暂存退库)、成品(MES 完工自动回流,系统生成 SN)。数据由入库、出库、检验、盘点及 MES 回流等业务实时维护,本页只读。\n\n本页三个视图页签(首屏默认「物料汇总」):\n· 物料汇总:一物料一行,直接回答"这个物料总共有多少"——总量/锁定/可用 + 质量分布(合格/未检/不合格) + 分布在哪些区域;ID 与创建时间取自物料主表,另有最近入库单/最近入库时间;点行下钻该物料的批次/SN 明细。\n· 库存明细:按 物料 × 区域 × 质量 聚合,可组合 区域/质量状态/入库时间/品类 筛选(物料汇总不受区域/质量筛选影响)。\n· 区域汇总:按 区域 × 质量状态 × 类型(批次/精密件/半成品/成品) 看各库区的货量;ID 与创建时间取自区域主表,另有组内最近入库时间;支持全量导出。\n\n层级概念:入库单 → 批次/SN → 库存行。区域/质量状态/批次/最近入库单都是某物料的一对多属性,放在下钻明细中查看,不在物料行上并排平铺。',
'查看当前库存:全品类统一一张库存主表——原材料(结构件按批次/电气件按SN)、半成品(MES 暂存退库)、成品(MES 完工自动回流,系统生成 SN)。数据由入库、出库、检验、盘点及 MES 回流等业务实时维护,本页只读。\n\n本页三个视图页签(首屏默认「物料汇总」):\n· 物料汇总:一物料一行,直接回答"这个物料总共有多少"——总量/锁定/可用 + 质量分布(合格/未检/不合格) + 分布在哪些区域;ID 与创建时间取自物料主表,另有最近入库单/最近入库时间;点行下钻该物料的批次/SN 明细。\n· 库存明细:按 物料 × 区域 × 质量 聚合,可组合 区域/质量状态/入库时间/品类 筛选(物料汇总不受区域/质量筛选影响)。\n· 区域汇总:按 区域 × 质量状态 × 类型(批次/电气件/半成品/成品) 看各库区的货量;ID 与创建时间取自区域主表,另有组内最近入库时间;支持全量导出。\n\n层级概念:入库单 → 批次/SN → 库存行。区域/质量状态/批次/最近入库单都是某物料的一对多属性,放在下钻明细中查看,不在物料行上并排平铺。',
fields: [
{ name: '物料编码 / 物料名称', source: '筛选条件。', purpose: '只看某个物料', fill: '输入编码或名称,可空=全部' },
{ name: '库存类型', source: '单选(全部/结构件/精密件)。', purpose: '按管理粒度过滤', fill: '全部 / 结构件(批次) / 精密件(SN)' },
{ name: '库存类型', source: '单选(全部/结构件/电气件)。', purpose: '按管理粒度过滤', fill: '全部 / 结构件(批次) / 电气件(SN)' },
{ name: '品类', source: '单选(全部/原材料/半成品/成品)。', purpose: '按品类过滤:原材料=采购到货入库;半成品=MES 工位暂存退库;成品=MES 完工自动回流(成品 SN 系统生成,入库单号形如 MES:工单号)', fill: '全部 / 原材料 / 半成品 / 成品;三个视图与导出均生效' },
{ name: '物料汇总-质量分布', source: '系统按当前在库行统计。', purpose: '一个物料合格/未检/不合格各多少', fill: '合格/未检/不合格三列,自动统计' },
{ name: '物料汇总-分布区域', source: '系统统计该物料当前占用了哪些区域。', purpose: '一个物料可跨多个区域存放(批次可拆分),物料行不写死单一区域', fill: '如 Z02、Z05;点行下钻可看每个批次/SN 具体在哪个区' },
@@ -75,11 +75,11 @@ export const helpInventory = {
}
export const helpInspection = {
title: '质量检验(来料',
title: '检验(来料/过程/完工',
overview:
'对入库的未检物料/SN 做检验:把"未检"翻转为【合格】或【不合格】。\n检验只修改"质量状态"字段,与物料/SN 的存放区域无关。\n合格品才能参与出库与锁库(库存校验只取质量状态=合格的批次)。\n\n本页分两个页签(录入在前默认打开,记录在后点击才见),参照出库管理:\n· 质量检验录入(常操作,默认打开):单条与批量已合并为同一处——扫码枪连续扫、或从"库存"下拉直接选,都先进入下方"已选清单",点一次"提交检验"统一翻转。\n· 检验记录(点击才加载):分页查询历史检验记录,支持按 批次/SN、物料、检验类型、结论、检验员、时间 筛选与导出。\n编号是否真实存在会即时校验:找不到对应库存记录的编号会直接报错,不会"提示成功但库存仍是未检"。',
'对入库的未检物料/SN 做检验:把"未检"翻转为【合格】或【不合格】。\n检验只修改"质量状态"字段,与物料/SN 的存放区域无关。\n合格品才能参与出库与锁库(库存校验只取质量状态=合格的批次)。\n\n本页分两个页签(录入在前默认打开,记录在后点击才见),参照出库管理:\n· 质量检验录入(常操作,默认打开):单条与批量已合并为同一处——扫码枪连续扫、或从"库存"下拉直接选,都先进入下方"已选清单",点一次"提交检验"统一翻转。\n· 检验记录(点击才加载):分页查询历史检验记录,支持按 批次/SN、物料、检验类型、结论、检验员、时间 筛选与导出。\n菜单按业务分为「来料检验 / 过程检验 / 完工检验」三个入口,同一张表、同一接口,仅按检验类别默认过滤(页面内不再切换类别)。记录支持「报检单号」字段,用于与生产报检流程关联;按需求已移除“不合格原因”字段。\n编号是否真实存在会即时校验:找不到对应库存记录的编号会直接报错,不会"提示成功但库存仍是未检"。',
fields: [
{ name: '目标类型', source: '单选,决定录入框标题与可选库存范围。', purpose: '检验对象是批次(结构件)还是 SN(精密件)', fill: '批次 BATCH / 序列号 SN;切换时会自动刷新"从库存选"下拉' },
{ name: '目标类型', source: '单选,决定录入框标题与可选库存范围。', purpose: '检验对象是批次(结构件)还是 SN(电气件)', fill: '批次 BATCH / 序列号 SN;切换时会自动刷新"从库存选"下拉' },
{ name: '检验结论', source: '单选。', purpose: '判定结果', fill: '合格 / 不合格' },
{ name: '检验员', source: '手动填写(默认当前用户)。', purpose: '谁检验的,追溯用', fill: '检验员姓名' },
{ name: '从库存选批次号/SN', source: '下拉选择,数据来自当前库存(按目标类型过滤)。', purpose: '避免手输编号、防止输错', fill: '在库存里挑待检的批次号/SN,自动加入清单' },
@@ -98,7 +98,7 @@ export const helpStocktake = {
{ name: '操作人', source: '登录账号自动带出。', purpose: '发起盘点人', fill: '自动' },
{ name: '发起盘点', source: '按钮操作。', purpose: '按所选范围生成盘点单号并冻结库存快照', fill: '点按钮选择范围后发起' },
{ name: '差异写回库存', source: '开关。', purpose: '完成后是否用实盘差异修正账面库存', fill: '开=写回;关=仅记录差异' },
{ name: '批次号 / SN 序列号', source: '扫码枪。来自批次号/SN条码。', purpose: '要盘点的批次号(结构件)或 SN(精密件)', fill: '扫批次号/SN,回车提交;同一目标重复扫=覆盖实盘数' },
{ name: '批次号 / SN 序列号', source: '扫码枪。来自批次号/SN条码。', purpose: '要盘点的批次号(结构件)或 SN(电气件)', fill: '扫批次号/SN,回车提交;同一目标重复扫=覆盖实盘数' },
{ name: '实盘数量', source: '扫码/手输。', purpose: '实际数到数量', fill: '非负整数;0=盘亏为零' }
]
}
@@ -137,14 +137,14 @@ export const helpSemi = {
export const helpLedger = {
title: '工单物料台账',
overview:
'实时展示每个工单下每个物料:需求总量、累计已出库、剩余缺口。\n系统强约束"累计出库 ≤ 需求总量",达到即标记"领料完结",禁止再出库。\n数据链路:MES 工单 + 物料清单(BOM) → 同步导入本台账 → 出库时自动扣减累计,形成缺口 / 完结。\n\n说明:本页只读查询,数据来自 MES 同步的工单台账。若列表为空,表示当前没有已同步的台账数据(请先在 MES 建工单并同步物料清单),不会展示任何模拟数据。',
'实时展示每个工单下每个物料:需求总量、累计已出库、剩余缺口。\n系统强约束"累计出库 ≤ 需求总量",达到即标记"领料完结",禁止再出库。\n数据链路:MES 工单 + 物料清单(物料清单) → 同步导入本台账 → 出库时自动扣减累计,形成缺口 / 完结。\n\n说明:本页只读查询,数据来自 MES 同步的工单台账。若列表为空,表示当前没有已同步的台账数据(请先在 MES 建工单并同步物料清单),不会展示任何模拟数据。',
fields: [
{ name: '工单号', source: '筛选条件。数据来自 MES 工单。', purpose: '只看某个工单的领料台账', fill: '输入工单号(可空=查全部工单)' },
{ name: '物料编码', source: '筛选条件。', purpose: '按物料编码精确过滤', fill: '输入物料编码(可空)' },
{ name: '物料名称', source: '筛选条件。', purpose: '按物料名称模糊过滤', fill: '输入物料名称(可空)' },
{ name: '状态', source: '筛选条件。可选项:待领料 / 部分领料 / 领料完结。', purpose: '按领料进度过滤', fill: '待领料=还没出过库;部分领料=已出库但没领完;领料完结=缺口为 0(前两种均由"进行中"按已出库数实时派生)' },
{ name: 'ID', source: '台账记录主键(系统生成,只读)。', purpose: '唯一标识一行台账,便于定位与对接', fill: '系统自动生成,无需填写' },
{ name: '需求总量 / 已出库 / 缺口', source: '系统计算。需求总量来自 BOM;已出库随出库自动累加;缺口 = 需求总量 − 已出库。', purpose: '一眼看清某物料还差多少未领', fill: '自动;缺口为 0 即领料完结' },
{ name: '需求总量 / 已出库 / 缺口', source: '系统计算。需求总量来自 物料清单;已出库随出库自动累加;缺口 = 需求总量 − 已出库。', purpose: '一眼看清某物料还差多少未领', fill: '自动;缺口为 0 即领料完结' },
{ name: '状态标签', source: '系统实时派生。', purpose: '待领料(未出过库) / 部分领料(出过但仍有缺口) / 领料完结(已足额出库)', fill: '自动' }
]
}
@@ -175,7 +175,7 @@ export const helpZone = {
export const helpMaterial = {
title: '物料档案',
overview:
'定义每一种物料的字典:叫什么、什么规格、什么品类(原材料/半成品/成品)、是【结构件(批次)】还是【精密件(SN)】。\n此处维护的物料会作为下拉选项出现在入库/出库/装箱等页面;其中"类型"直接决定后续入库方式——结构件走批次(一批多件)、精密件走 SN 逐件扫码(单件追溯)。\n\n品类说明:1=原材料(默认) / 2=半成品 / 3=成品。编码主数据单一来源在本表:产品型号档案即本表中"成品"品类的行(MES 实时拉取,不双录);库存主表 inventories 的品类与其呼应。列表支持按品类筛选,导出含品类列。\n\n物料档案是库房的另一道地基:先建好物料,入库时才能选到;类型选错会导致入库方式与追溯粒度不符。',
'定义每一种物料的字典:叫什么、什么规格、什么品类(原材料/半成品/成品)、是【结构件(批次)】还是【电气件(SN)】。\n此处维护的物料会作为下拉选项出现在入库/出库/装箱等页面;其中"类型"直接决定后续入库方式——结构件走批次(一批多件)、电气件走 SN 逐件扫码(单件追溯)。\n\n品类说明:1=原材料(默认) / 2=半成品 / 3=成品。编码主数据单一来源在本表:产品型号档案即本表中"成品"品类的行(MES 实时拉取,不双录);库存主表 inventories 的品类与其呼应。列表支持按品类筛选,导出含品类列。\n\n物料档案是库房的另一道地基:先建好物料,入库时才能选到;类型选错会导致入库方式与追溯粒度不符。',
fields: [
{ name: '物料编码', source: '手工填写,仓库自定义(新增后不可改)。', purpose: '物料唯一编号', fill: '如 SCREW-M3;新增后不可改编码' },
{ name: '名称', source: '手工填写。', purpose: '物料名称', fill: '如 螺丝 M3' },
@@ -183,7 +183,7 @@ export const helpMaterial = {
{ name: '规格', source: '手工填写(可空)。', purpose: '规格型号', fill: '可空' },
{ name: '单位', source: '手工填写。', purpose: '计量单位', fill: '如 个/套' },
{ name: '品类', source: '创建时默认"原材料";成品品类由「产品型号」页维护(自动生成),本页查询/导出覆盖全部品类。', purpose: '区分原材料/半成品/成品,与统一库存主表品类对应', fill: '查询区可选 原材料/半成品/成品' },
{ name: '类型', source: '选择:1=结构件(批次管理) / 2=精密件(SN 管理)。语义由库房定:结构件按批次管理,精密件按 SN 逐件管理。', purpose: '决定入库方式:结构件走批次、精密件走 SN 逐件扫码', fill: '结构件(批次) / 精密件(SN)' },
{ name: '类型', source: '选择:1=结构件(批次管理) / 2=电气件(SN 管理)。语义由库房定:结构件按批次管理,电气件按 SN 逐件管理。', purpose: '决定入库方式:结构件走批次、电气件走 SN 逐件扫码', fill: '结构件(批次) / 电气件(SN)' },
{ name: '说明', source: '手工填写(可空)。', purpose: '补充描述', fill: '可空' }
]
}
@@ -84,7 +84,9 @@ const menuGroups = [
title: '仓储作业', icon: Operation, children: [
{ path: '/inbound', title: '入库管理', icon: Download, code: 'inbound:view' },
{ path: '/outbound', title: '出库管理', icon: Upload, code: 'outbound:view' },
{ path: '/inspection', title: '质量检验', icon: CircleCheck, code: 'inspection:view' }
{ path: '/inspection-incoming', title: '来料检验', icon: CircleCheck, code: 'inspection:view' },
{ path: '/inspection-process', title: '过程检验', icon: CircleCheck, code: 'inspection:view' },
{ path: '/inspection-final', title: '完工检验', icon: CircleCheck, code: 'inspection:view' }
]
},
{
@@ -153,10 +155,17 @@ function onUserCmd(cmd) {
}
function onLogout() {
// 尽力通知后端记录"退出登录"操作日志,失败不阻塞本地退出
request.post('/user/logout').catch(() => {})
logout()
ElMessage.success('已退出登录')
router.push('/login')
}
// 免登录大屏:新标签页打开,不覆盖当前窗口
function openDisplay() {
window.open('/display', '_blank')
}
</script>
<template>
@@ -199,7 +208,7 @@ function onLogout() {
<span class="title">{{ pageTitle }}</span>
</div>
<div class="spacer"></div>
<el-button text type="primary" @click="router.push('/display')">
<el-button text type="primary" @click="openDisplay">
<el-icon style="margin-right:4px"><Monitor /></el-icon>免登录大屏
</el-button>
<el-button class="help-btn" text @click="openHelp">帮助</el-button>
+18 -10
View File
@@ -115,7 +115,7 @@ const matDialog = ref(false)
const mEditingId = ref(0)
const mForm = reactive({
code: '', name: '', shortName: '', spec: '', unit: '',
manageMode: 1, templateCode: '', description: ''
manageMode: 1, templateCode: '', description: '', drawingNo: ''
})
const mSaving = ref(false)
@@ -151,7 +151,7 @@ function resetMForm() {
mEditingId.value = 0
Object.assign(mForm, {
code: '', name: '', shortName: '', spec: '', unit: '',
manageMode: 1, templateCode: '', description: ''
manageMode: 1, templateCode: '', description: '', drawingNo: ''
})
}
@@ -166,6 +166,7 @@ function openMatEdit(row) {
code: row.code, name: row.name, shortName: row.shortName || '',
spec: row.spec || '', unit: row.unit || '', manageMode: row.manageMode,
templateCode: row.templateCode || '',
drawingNo: row.drawingNo || '',
description: row.description || ''
})
matDialog.value = true
@@ -182,6 +183,7 @@ async function saveMaterial() {
await request.post('/material/update', {
id: mEditingId.value, name: mForm.name, shortName: mForm.shortName,
spec: mForm.spec, unit: mForm.unit, description: mForm.description,
drawingNo: mForm.drawingNo,
manageMode: mForm.manageMode
})
ElMessage.success('物料修改成功')
@@ -361,7 +363,7 @@ async function onMaterialFile(e) {
<el-form-item label="类型">
<el-select v-model="matFilter.manageMode" placeholder="全部" clearable style="width:120px" @change="searchMaterials">
<el-option label="结构件(批次)" :value="1" />
<el-option label="精密(SN)" :value="2" />
<el-option label="电气(SN)" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
@@ -376,7 +378,7 @@ async function onMaterialFile(e) {
<el-button type="success" @click="openMatCreate">新增物料</el-button>
<el-button v-if="can('material:import')" type="warning" :loading="matImporting" @click="pickMaterialFile">导入</el-button>
<el-button v-if="can('material:export')" :loading="matExporting" @click="exportMaterials">导出</el-button>
<el-tooltip v-if="can('material:import')" content="Excel 模板首行表头物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(结构件/精密) | 说明">
<el-tooltip v-if="can('material:import')" content="Excel 模板首行表头物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(结构件/电气) | 说明 | 图号(可选)">
<el-link type="info" :underline="false" style="margin-left:4px">导入说明</el-link>
</el-tooltip>
<input ref="matFileRef" type="file" accept=".xlsx,.xls" style="display:none" @change="onMaterialFile" />
@@ -391,8 +393,11 @@ async function onMaterialFile(e) {
<template #default="{ row }">{{ row.spec || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="70" align="center">
<template #default="{ row }">{{ row.unit || '-' }}</template>
</el-table-column>
<template #default="{ row }">{{ row.unit || '-' }}</template>
</el-table-column>
<el-table-column prop="drawingNo" label="图号" min-width="90" align="center">
<template #default="{ row }">{{ row.drawingNo || '-' }}</template>
</el-table-column>
<el-table-column label="品类" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.itemType === 3 ? 'success' : (row.itemType === 2 ? 'warning' : 'info')" size="small">
@@ -403,7 +408,7 @@ async function onMaterialFile(e) {
<el-table-column label="类型" width="140" align="center">
<template #default="{ row }">
<el-tag :type="row.manageMode === 2 ? 'warning' : 'info'" size="small">
{{ row.manageMode === 2 ? '精密件(SN)' : '结构件(批次)' }}
{{ row.manageMode === 2 ? '电气件(SN)' : '结构件(批次)' }}
</el-tag>
</template>
</el-table-column>
@@ -439,12 +444,15 @@ async function onMaterialFile(e) {
<el-input v-model="mForm.spec" placeholder="规格可空" clearable />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="mForm.unit" placeholder=" /" clearable />
</el-form-item>
<el-input v-model="mForm.unit" placeholder=" /" clearable />
</el-form-item>
<el-form-item label="图号">
<el-input v-model="mForm.drawingNo" placeholder="图纸编号可空" clearable />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="mForm.manageMode" placeholder="请选择" style="width:100%">
<el-option :value="1" label="结构件批次管理" />
<el-option :value="2" label="精密SN 管理" />
<el-option :value="2" label="电气SN 管理" />
</el-select>
</el-form-item>
<el-form-item label="说明">
+11 -5
View File
@@ -7,7 +7,7 @@
<el-option v-for="f in EVENT_FAMILIES" :key="f.value" :label="f.label" :value="f.value" />
</el-select>
</el-form-item>
<el-form-item label="操作人">
<el-form-item v-if="isAdmin" label="操作人">
<el-input v-model="filters.operator" clearable style="width:130px" placeholder="用户名"
@keyup.enter="onSearch" />
</el-form-item>
@@ -36,7 +36,9 @@
<template #default="{ row }">{{ entityName(row.entityType) }}</template>
</el-table-column>
<el-table-column prop="entityId" label="对象标识" width="140" show-overflow-tooltip />
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column label="操作人" width="110">
<template #default="{ row }">{{ row.operatorName || row.operator || '-' }}</template>
</el-table-column>
<el-table-column label="时间" width="165">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
@@ -52,14 +54,16 @@
<script setup>
defineOptions({ name: 'EventLog' })
import { onMounted, reactive, ref } from 'vue'
import { onMounted, reactive, ref, computed } from 'vue'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { getUser } from '../utils/auth'
import PageHelp from '../components/PageHelp.vue'
import { helpEventLog } from '../help'
// 事件类型按"族前缀"归类(写点处 eventType 形如 user.create / inbound.create …)
const EVENT_FAMILIES = [
{ label: '登录/退出', value: 'auth.' },
{ label: '账号管理', value: 'user.' },
{ label: '角色管理', value: 'role.' },
{ label: '权限初始化', value: 'rbac.' },
@@ -73,12 +77,12 @@ const EVENT_FAMILIES = [
]
const FAMILY_LABEL = Object.fromEntries(EVENT_FAMILIES.map((f) => [f.value, f.label]))
const FAMILY_TAG = {
'user.': 'primary', 'role.': 'warning', 'rbac.': 'danger',
'auth.': 'danger', 'user.': 'primary', 'role.': 'warning', 'rbac.': 'danger',
'material.': 'success', 'zone.': 'success', 'inbound.': 'success',
'outbound.': 'warning', 'inspection.': 'info', 'stocktake.': 'info', 'semi.': 'info'
}
const ENTITY_LABEL = {
user: '账号', role: '角色', rbac: '权限', material: '物料', zone: '区域',
auth: '登录', user: '账号', role: '角色', rbac: '权限', material: '物料', zone: '区域',
inbound: '入库', outbound: '出库', inspection: '检验', stocktake: '盘点', semi: '半成品'
}
@@ -95,6 +99,8 @@ function entityName(t) {
}
const filters = reactive({ eventType: '', operator: '' })
// 仅 admin 可按操作人筛选/查看全部;普通用户后端强制只返回自己的日志
const isAdmin = computed(() => getUser()?.role === 'admin')
const range = ref(null) // [开始,结束] YYYY-MM-DD
const list = ref([])
const total = ref(0)
+51 -22
View File
@@ -2,7 +2,7 @@
defineOptions({ name: 'Inbound' })
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { getRealName } from '../utils/auth'
import { fmtTime } from '../utils/format'
@@ -15,8 +15,8 @@ import { helpInbound } from '../help'
const route = useRoute()
const operator = getRealName()
// ===== 多 Tab:结构件入库 / 精密件 SN 入库 在前(常操作),入库记录在后 =====
const activeTab = ref('batch') // batch=结构件入库 / sn=精密件SN入库 / records=入库记录
// ===== 多 Tab:结构件入库 / 电气件 SN 入库 在前(常操作),入库记录在后 =====
const activeTab = ref('batch') // batch=结构件入库 / sn=电气件SN入库 / records=入库记录
// 公共字典:区域(物料下拉由 MaterialSelect 组件按需远程搜索,最近 100 条)
const zones = ref([])
@@ -32,7 +32,7 @@ const submitting1 = ref(false)
const lastInboundNo = ref('')
const form1 = reactive({
materialCode: '', batchNo: '', quantity: 1,
zoneCode: '', productionDate: '', supplier: '', remark: ''
zoneCode: '', productionDate: '', supplier: '', remark: '', inspectionNo: ''
})
const rules1 = {
materialCode: [{ required: true, message: '请选择物料', trigger: 'change' }],
@@ -46,6 +46,15 @@ async function submitBatch() {
ElMessage.warning('数量必须大于 0')
return
}
if (!form1.inspectionNo.trim()) {
ElMessage.warning('请填写检测单号')
return
}
try {
await ElMessageBox.confirm(
'确认入库:物料 ' + form1.materialCode + '|批次 ' + form1.batchNo + '|数量 ' + form1.quantity + '|区域 ' + form1.zoneCode + '',
'入库提交确认', { type: 'warning' })
} catch { return }
submitting1.value = true
try {
const data = await request.post('/inbound/create', {
@@ -57,6 +66,7 @@ async function submitBatch() {
productionDate: form1.productionDate,
supplier: form1.supplier.trim(),
remark: form1.remark.trim(),
inspectionNo: form1.inspectionNo.trim(),
operator
})
lastInboundNo.value = data?.inboundNo || ''
@@ -70,11 +80,11 @@ async function submitBatch() {
}
}
/* ===================== Tab2 精密件 SN 入库(连续扫码、实时已扫数量) ===================== */
/* ===================== Tab2 电气件 SN 入库(连续扫码、实时已扫数量) ===================== */
const snInput = ref('')
const submitting2 = ref(false)
const snList = ref([])
const form2 = reactive({ materialCode: '', zoneCode: '', remark: '' })
const form2 = reactive({ materialCode: '', zoneCode: '', remark: '', inspectionNo: '' })
// 支持多种分隔符:中英文逗号、中英文句号、分号、顿号、空格、回车、制表符等
const SN_SEP = /[,.。;;、\s]+/
@@ -103,10 +113,19 @@ async function submitSn() {
ElMessage.warning('请选择入库区域')
return
}
if (!form2.inspectionNo.trim()) {
ElMessage.warning('请填写检测单号')
return
}
if (!snList.value.length) {
ElMessage.warning('请先扫码录入 SN(输入后点击【录入到列表】)')
return
}
try {
await ElMessageBox.confirm(
'确认 SN 入库:物料 ' + form2.materialCode + '|数量 ' + snList.value.length + ' 件|区域 ' + form2.zoneCode + '',
'入库提交确认', { type: 'warning' })
} catch { return }
submitting2.value = true
try {
const data = await request.post('/inbound/create', {
@@ -115,6 +134,7 @@ async function submitSn() {
zoneCode: form2.zoneCode,
snList: [...snList.value],
remark: form2.remark.trim(),
inspectionNo: form2.inspectionNo.trim(),
operator
})
ElMessage.success(`入库成功:入库单号 ${data?.inboundNo || ''}SN 共 ${snList.value.length}`)
@@ -127,7 +147,7 @@ async function submitSn() {
}
}
/* ===================== 各 Tab 批量导入(结构件 batch / 精密件 sn ===================== */
/* ===================== 各 Tab 批量导入(结构件 batch / 电气件 sn ===================== */
const importing = ref(false)
const excelResult = ref(null)
const batchUploadRef = ref()
@@ -175,7 +195,7 @@ const list = ref([])
const total = ref(0)
const page = reactive({ current: 1, size: 10 })
// 列表数据:结构件(manageMode=1)与精密件(manageMode=2)同主表(inbound_order)
// 列表数据:结构件(manageMode=1)与电气件(manageMode=2)同主表(inbound_order)
async function loadRecords() {
loadingRec.value = true
try {
@@ -256,7 +276,7 @@ function changeDetailPage(p) {
}
function typeLabel(m) {
return m === 1 ? '结构件' : m === 2 ? '精密件' : '-'
return m === 1 ? '结构件' : m === 2 ? '电气件' : '-'
}
function onTabChange(tab) {
@@ -306,8 +326,11 @@ onMounted(() => {
<el-input v-model="form1.supplier" placeholder="供应商名称" clearable />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form1.remark" placeholder="备注(可空)" clearable />
</el-form-item>
<el-input v-model="form1.remark" placeholder="备注(可空)" clearable />
</el-form-item>
<el-form-item label="检测单号" required>
<el-input v-model="form1.inspectionNo" placeholder="入库检报告单号(必填)" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting1" @click="submitBatch">提交入库</el-button>
</el-form-item>
@@ -332,12 +355,12 @@ onMounted(() => {
</el-card>
</el-tab-pane>
<!-- ============ Tab2 精密件 SN 入库 ============ -->
<el-tab-pane :label="`精密件 SN 入库${snList.length ? `(已扫 ${snList.length}` : ''}`" name="sn">
<!-- ============ Tab2 电气件 SN 入库 ============ -->
<el-tab-pane :label="`电气件 SN 入库${snList.length ? `(已扫 ${snList.length}` : ''}`" name="sn">
<el-card shadow="never">
<el-form label-width="110px" style="max-width:680px">
<el-form-item label="物料" required>
<MaterialSelect v-model="form2.materialCode" :manage-mode="2" placeholder="请选择精密件物料必填" />
<MaterialSelect v-model="form2.materialCode" :manage-mode="2" placeholder="请选择电气件物料必填" />
</el-form-item>
<el-form-item label="区域" required>
<el-select v-model="form2.zoneCode" placeholder="请选择入库区域必填" clearable style="width:100%">
@@ -355,8 +378,11 @@ onMounted(() => {
</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form2.remark" type="textarea" :rows="2" placeholder="本批 SN 的统一备注可空" />
</el-form-item>
<el-input v-model="form2.remark" type="textarea" :rows="2" placeholder="本批 SN 的统一备注可空" />
</el-form-item>
<el-form-item label="检测单号" required>
<el-input v-model="form2.inspectionNo" placeholder="入库检报告单号必填" clearable />
</el-form-item>
</el-form>
<div style="margin-bottom:8px">
@@ -385,7 +411,7 @@ onMounted(() => {
</div>
<el-upload ref="snUploadRef" :auto-upload="false" accept=".xlsx" :limit="1" drag style="max-width:680px"
:on-exceed="(f) => onExceed(f, 'sn')">
<div class="dropzone">将精密件 .xlsx 拖到此处,或点击选择</div>
<div class="dropzone">将电气件 .xlsx 拖到此处,或点击选择</div>
</el-upload>
<el-button type="primary" :loading="importing" style="margin-top:8px"
@click="importExcel('sn', 'sn')">开始批量导入</el-button>
@@ -407,7 +433,7 @@ onMounted(() => {
<el-select v-model="filters.manageMode" placeholder="全部" clearable style="width:120px" @change="search">
<el-option label="全部" value="" />
<el-option label="结构件" value="1" />
<el-option label="精密" value="2" />
<el-option label="电气" value="2" />
</el-select>
</el-form-item>
<el-form-item label="物料名称">
@@ -427,7 +453,7 @@ onMounted(() => {
</el-form>
</el-card>
<!-- 入库单列表(一单一行,结构件/精密件同表) -->
<!-- 入库单列表(一单一行,结构件/电气件同表) -->
<el-card shadow="never" header="入库单每行一笔入库明细在右侧抽屉查看该单的行">
<el-table :data="list" border size="small" v-loading="loadingRec" empty-text="暂无入库数据">
<el-table-column prop="id" label="ID" width="70" align="center" />
@@ -453,8 +479,11 @@ onMounted(() => {
<template #default="{ row }">{{ row.zoneCode || '-' }}</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100">
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<el-table-column prop="inspectionNo" label="检测单号" min-width="130" align="center">
<template #default="{ row }">{{ row.inspectionNo || '-' }}</template>
</el-table-column>
<el-table-column label="入库时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
@@ -477,7 +506,7 @@ onMounted(() => {
</el-tab-pane>
</el-tabs>
<!-- 批量导入结果(结构件/精密件共用) -->
<!-- 批量导入结果(结构件/电气件共用) -->
<el-card v-if="excelResult" shadow="never" class="mb12"
:header="excelResult.failed > 0 ? '导入结果:被拒绝(整体回退,未发生写入)' : '导入结果:成功'">
<el-alert v-if="excelResult.failed > 0" type="error" :closable="false"
+63 -22
View File
@@ -1,6 +1,7 @@
<script setup>
defineOptions({ name: 'Inspection' })
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { getRealName } from '../utils/auth'
@@ -13,6 +14,10 @@ import { helpInspection } from '../help'
// ===== 多 Tab:录入在前默认打开,记录在后点击才见(参照出库管理) =====
const activeTab = ref('entry') // entry=质量检验(录入) / records=检验记录
// 菜单按业务分开(来料/过程/完工检验):从路由 meta 锁定检验类型,同表同接口按类别过滤
const route = useRoute()
const pageType = String(route.meta.inspectionType || '')
/* ===================== Tab1 质量检验(录入,默认打开) ===================== */
const targetType = ref('BATCH')
const status = ref('合格')
@@ -23,7 +28,8 @@ const submitting = ref(false)
const inspectQty = ref(0)
const passQty = ref(0)
const checkDesc = ref('')
const failReason = ref('')
const inspectionNo = ref('')
const reportNo = ref('')
// 待检编号列表(合并单/批量,统一一次提交)
const targetList = ref([])
@@ -102,8 +108,9 @@ async function submitInspection() {
inspectQty: Number(inspectQty.value) || 0,
passQty: Number(passQty.value) || 0,
checkDesc: checkDesc.value.trim(),
failReason: failReason.value.trim(),
inspector: inspector.value.trim()
reportNo: reportNo.value.trim(),
inspector: inspector.value.trim(),
inspectionNo: inspectionNo.value.trim(),
})
const n = data?.flipped ?? targetList.value.length
ElMessage.success(`检验已记录:共 ${n} 条 → ${status.value}`)
@@ -111,7 +118,7 @@ async function submitInspection() {
inspectQty.value = 0
passQty.value = 0
checkDesc.value = ''
failReason.value = ''
reportNo.value = ''
// 提交成功后主动刷新记录 Tab(用户切过去即可见最新)
await loadRecords()
} finally {
@@ -132,10 +139,23 @@ function defaultStart() {
// 筛选独立条件(不再笼统关键字);默认近3个月
const filters = reactive({
targetId: '', materialCode: '', inspectionType: '', status: '', inspector: '',
targetId: '', materialCode: '', inspectionType: pageType, status: '', inspector: '',
startDate: defaultStart(), endDate: ''
})
const loading = ref(false)
const statsList = ref([])
const statsLoading = ref(false)
async function loadStats() {
statsLoading.value = true
try {
const data = await request.get('/inspection/stats', {
params: { inspectionType: filters.inspectionType, startDate: filters.startDate, endDate: filters.endDate }
})
statsList.value = data?.list || []
} finally {
statsLoading.value = false
}
}
const list = ref([])
const total = ref(0)
const page = reactive({ current: 1, size: 20 })
@@ -215,7 +235,7 @@ onMounted(() => {
<el-form-item label="目标类型">
<el-radio-group v-model="targetType" @change="() => loadPicker()">
<el-radio-button value="BATCH">批次 (结构件)</el-radio-button>
<el-radio-button value="SN">序列号 SN (精密)</el-radio-button>
<el-radio-button value="SN">序列号 SN (电气)</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="检验结论">
@@ -225,8 +245,14 @@ onMounted(() => {
</el-radio-group>
</el-form-item>
<el-form-item label="检验员">
<el-input v-model="inspector" placeholder="检验员姓名" clearable style="width:260px" />
</el-form-item>
<el-input v-model="inspector" placeholder="检验员姓名" clearable style="width:260px" />
</el-form-item>
<el-form-item label="检测单号">
<el-input v-model="inspectionNo" placeholder="检验报告单号(可空,与入库单关联)" clearable style="width:420px" />
</el-form-item>
<el-form-item label="报检单号">
<el-input v-model="reportNo" placeholder="生产报检单号(可空,与报检流程关联)" clearable style="width:420px" />
</el-form-item>
<el-divider content-position="left">检验量化可空</el-divider>
<el-form-item label="检验数量">
@@ -238,10 +264,6 @@ onMounted(() => {
<el-form-item label="检测说明">
<el-input v-model="checkDesc" placeholder="检验项目/方法/标准(可空)" clearable style="width:420px" />
</el-form-item>
<el-form-item v-if="status === '不合格'" label="不合格原因">
<el-input v-model="failReason" type="textarea" :rows="2" placeholder="不合格原因(可空)" style="width:420px" />
</el-form-item>
<el-divider content-position="left">待检编号可扫/选多条统一提交</el-divider>
<!-- 从库存直接拾取避免手输批次号远程模糊搜索最近 100 -->
@@ -303,7 +325,7 @@ onMounted(() => {
<el-input v-model="filters.materialCode" placeholder="物料编码" clearable style="width:150px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="检验类型">
<el-form-item v-if="!pageType" label="检验类型">
<el-select v-model="filters.inspectionType" placeholder="全部" clearable style="width:120px" @change="search">
<el-option label="来料检" value="incoming" />
<el-option label="过程检" value="process" />
@@ -366,17 +388,15 @@ onMounted(() => {
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="不合格原因" min-width="140">
<template #default="{ row }">
<el-tooltip v-if="row.failReason" :content="row.failReason" placement="top">
<span style="color:#f56c6c">{{ row.failReason }}</span>
</el-tooltip>
<span v-else>-</span>
</template>
<el-table-column prop="reportNo" label="报检单号" min-width="130" align="center">
<template #default="{ row }">{{ row.reportNo || '-' }}</template>
</el-table-column>
<el-table-column prop="inspector" label="检验员" width="90" align="center">
<template #default="{ row }">{{ row.inspector || '-' }}</template>
</el-table-column>
<template #default="{ row }">{{ row.inspector || '-' }}</template>
</el-table-column>
<el-table-column prop="inspectionNo" label="检测单号" min-width="130" align="center">
<template #default="{ row }">{{ row.inspectionNo || '-' }}</template>
</el-table-column>
<el-table-column label="检验时间" width="155" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
@@ -390,6 +410,27 @@ onMounted(() => {
@size-change="(s) => { page.size = s; page.current = 1; loadRecords() }" />
</div>
</el-tab-pane>
<!-- ============ 按物料统计(点击才加载) ============ -->
<el-tab-pane label="按物料统计" name="stats">
<el-card shadow="never" header="检验次数 / 合格率按物料">
<div style="margin-bottom:10px">
<el-button type="primary" :loading="statsLoading" @click="loadStats">统计</el-button>
<span style="margin-left:8px;color:#909399;font-size:12px">按当前时间区间与检验类别统计;每次检验独立计数,不合并记录</span>
</div>
<el-table :data="statsList" border size="small" v-loading="statsLoading" empty-text="统计查看结果">
<el-table-column prop="materialCode" label="物料编码" min-width="160" />
<el-table-column prop="total" label="检验次数" width="110" align="center" />
<el-table-column prop="pass" label="合格次数" width="110" align="center" />
<el-table-column prop="fail" label="不合格次数" width="120" align="center" />
<el-table-column label="合格率" width="120" align="center">
<template #default="{ row }">
<span :style="row.passRate >= 100 ? 'color:#67c23a' : 'color:#e6a23c'">{{ row.passRate }}%</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
<PageHelp :data="helpInspection" page="Inspection" />
@@ -25,7 +25,7 @@ function defaultStart() {
return `${y}-${m}-${day}`
}
// manageMode: ''=全部 1=结构件 2=精密件;category: ''=全部 1=原材料 2=半成品 3=成品
// manageMode: ''=全部 1=结构件 2=电气件;category: ''=全部 1=原材料 2=半成品 3=成品
const filters = reactive({
materialCode: '',
materialName: '',
@@ -144,7 +144,7 @@ const detailPage = reactive({ current: 1, size: 50 })
const detailCond = reactive({ materialCode: '', zoneCode: '', qualityStatus: '', manageMode: '' })
function typeLabel(m) {
return m === 1 ? '结构件' : m === 2 ? '精密件' : '-'
return m === 1 ? '结构件' : m === 2 ? '电气件' : '-'
}
// 兼容两类行:物料汇总行(无区域/质量) 与 库存明细行(带区域/质量)
@@ -252,7 +252,7 @@ onMounted(() => {
<el-radio-group v-model="filters.manageMode" @change="search">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="1">结构件</el-radio-button>
<el-radio-button value="2">精密</el-radio-button>
<el-radio-button value="2">电气</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="viewTab === 'detail'">
+5 -2
View File
@@ -21,7 +21,10 @@ async function onSubmit() {
loading.value = true
try {
// /api 前缀由网关代理到 WMS 的 /api/auth/login
const data = await request.post('/auth/login', { username: form.username, password: form.password })
const data = await request.post('/auth/login', {
username: form.username,
password: form.password
})
if (!data?.token) throw new Error('登录响应缺少 token')
setSession(data.token, data.user || {})
ElMessage.success('登录成功')
@@ -71,4 +74,4 @@ async function onSubmit() {
.head h2 { margin: 0 0 6px; }
.head p { margin: 0; color: #909399; font-size: 13px; }
.btn { width: 100%; margin-top: 4px; }
</style>
</style>
+1 -1
View File
@@ -284,7 +284,7 @@ onMounted(loadDicts)
<el-form-item label="出库类型">
<el-radio-group v-model="genForm.mode">
<el-radio :value="1">结构件批次</el-radio>
<el-radio :value="2">精密SN</el-radio>
<el-radio :value="2">电气SN</el-radio>
</el-radio-group>
</el-form-item>
+11 -6
View File
@@ -1,7 +1,7 @@
<script setup>
defineOptions({ name: 'Stocktake' })
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { fmtTime } from '../utils/format'
import { getRealName } from '../utils/auth'
@@ -18,7 +18,7 @@ const scope = reactive({
type: 'all', // all / zone / material
zones: [], // 区域多选
materialCodes: [], // 物料多选
manageMode: 0, // 0全部 / 1结构件 / 2精密
manageMode: 0, // 0全部 / 1结构件 / 2电气
qualityStatus: '' // 空=全部
})
@@ -64,7 +64,7 @@ async function startStocktake() {
function scopeLabel() {
if (scope.type === 'zone') return `按区域【${scope.zones.join('、')}`
if (scope.type === 'material') return `按物料【${scope.materialCodes.join('、')}${scope.manageMode ? (scope.manageMode === 1 ? '(结构件)' : '(精密件)') : ''}`
if (scope.type === 'material') return `按物料【${scope.materialCodes.join('、')}${scope.manageMode ? (scope.manageMode === 1 ? '(结构件)' : '(电气件)') : ''}`
return '全库'
}
@@ -81,7 +81,7 @@ async function submitRecord() {
}
const targetId = recordForm.targetId.trim()
if (!targetId) {
ElMessage.warning('请扫入批次号(结构件)或 SN 序列号(精密件)')
ElMessage.warning('请扫入批次号(结构件)或 SN 序列号(电气件)')
return
}
const qty = Number(recordForm.scanQty)
@@ -125,6 +125,11 @@ async function finishStocktake() {
ElMessage.warning('请先发起盘点')
return
}
try {
await ElMessageBox.confirm(
adjust.value ? '确认过账?差异将写回库存,此操作不可逆。' : '确认过账?本次仅出结论,不调整库存。',
'盘点过账确认', { type: 'warning' })
} catch { return }
finishing.value = true
try {
const data = await request.post('/stocktake/finish', { stocktakeNo: active.no, adjust: adjust.value })
@@ -224,7 +229,7 @@ const currentStep = computed(() => {
<el-form-item label="类型">
<el-select v-model="scope.manageMode" clearable placeholder="全部类型" style="width:100%">
<el-option :value="1" label="结构件" />
<el-option :value="2" label="精密件" />
<el-option :value="2" label="电气件" />
</el-select>
</el-form-item>
</template>
@@ -257,7 +262,7 @@ const currentStep = computed(() => {
</template>
<el-form inline @submit.prevent="submitRecord">
<el-form-item label="批次号/SN">
<el-input ref="idRef" v-model="recordForm.targetId" clearable autofocus placeholder="批次号(结构件) / SN精密件)"
<el-input ref="idRef" v-model="recordForm.targetId" clearable autofocus placeholder="批次号(结构件) / SN电气件)"
style="width:220px" @keyup.enter="submitRecord" />
</el-form-item>
<el-form-item label="实盘数量">
+3 -1
View File
@@ -13,7 +13,9 @@ const routes = [
{ path: 'inbound', name: 'Inbound', component: () => import('../pages/Inbound.vue'), meta: { title: '入库管理', keepAlive: true, componentName: 'Inbound' } },
{ path: 'outbound', name: 'Outbound', component: () => import('../pages/Outbound.vue'), meta: { title: '出库管理', keepAlive: true, componentName: 'Outbound' } },
{ path: 'inventory', name: 'Inventory', component: () => import('../pages/Inventory.vue'), meta: { title: '库存查询', keepAlive: true, componentName: 'Inventory' } },
{ path: 'inspection', name: 'Inspection', component: () => import('../pages/Inspection.vue'), meta: { title: '质量检验', keepAlive: true, componentName: 'Inspection' } },
{ path: 'inspection-incoming', name: 'InspectionIncoming', component: () => import('../pages/Inspection.vue'), meta: { title: '来料检验', keepAlive: true, componentName: 'InspectionIncoming', inspectionType: 'incoming' } },
{ path: 'inspection-process', name: 'InspectionProcess', component: () => import('../pages/Inspection.vue'), meta: { title: '过程检验', keepAlive: true, componentName: 'InspectionProcess', inspectionType: 'process' } },
{ path: 'inspection-final', name: 'InspectionFinal', component: () => import('../pages/Inspection.vue'), meta: { title: '完工检验', keepAlive: true, componentName: 'InspectionFinal', inspectionType: 'finished' } },
{ path: 'stocktake', name: 'Stocktake', component: () => import('../pages/Stocktake.vue'), meta: { title: '库存盘点', keepAlive: true, componentName: 'Stocktake' } },
{ path: 'semi', name: 'Semi', component: () => import('../pages/Semi.vue'), meta: { title: '半成品管理', keepAlive: true, componentName: 'Semi' } },
{ path: 'ledger', name: 'Ledger', component: () => import('../pages/Ledger.vue'), meta: { title: '工单备料台账', keepAlive: true, componentName: 'Ledger' } },