@@ -85,23 +91,33 @@
-
+
+
-
-
- 刷新
+
+
+
+
+
+
+
+ 查询
-
-
-
-
+
+
+
+
+ {{ row.result }}
+
-
+ {{ row.durationSec }}s
+
@@ -112,7 +128,7 @@
diff --git a/bj_power_mes/frontend/src/utils/confirm.js b/bj_power_mes/frontend/src/utils/confirm.js
new file mode 100644
index 0000000..1144680
--- /dev/null
+++ b/bj_power_mes/frontend/src/utils/confirm.js
@@ -0,0 +1,26 @@
+import { ElMessageBox } from 'element-plus'
+
+// 导出前统一确认:告知用户即将导出的数据条数。
+// count 为 null/undefined(无法预知条数)时提示“全部”;否则回显具体条数。
+export async function confirmExport(count, name = '数据') {
+ const msg = (count === null || count === undefined)
+ ? `即将导出全部${name},是否继续?`
+ : `即将导出 ${count} 条${name},是否继续?`
+ return confirmExportMessage(msg)
+}
+
+// 自定义文案的导出确认:用于多 Sheet 报表等无法用单一条数概括的场景。
+export async function confirmExportMessage(msg, title = '导出确认') {
+ try {
+ await ElMessageBox.confirm(msg, title, {
+ confirmButtonText: '确认导出',
+ cancelButtonText: '取消',
+ type: 'warning'
+ })
+ return true
+ } catch {
+ return false
+ }
+}
+
+export default { confirmExport, confirmExportMessage }
diff --git a/bj_power_mes/internal/handler/production/plc_torque_scan.go b/bj_power_mes/internal/handler/production/plc_torque_scan.go
index 83201f7..45861f3 100644
--- a/bj_power_mes/internal/handler/production/plc_torque_scan.go
+++ b/bj_power_mes/internal/handler/production/plc_torque_scan.go
@@ -126,28 +126,15 @@ func TorqueStatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
-// ---------- 扫码报工 ----------
+// ---------- 报工记录 ----------
-func ScanReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- var req logic.ScanReq
- if err := httpx.ParseJSON(r, &req); err != nil {
- httpx.BadRequest(w, "请求体解析失败")
- return
- }
- if err := logic.New(svcCtx).ReportScan(r.Context(), req, operator(r, "")); err != nil {
- httpx.Fail(w, 3205, err.Error())
- return
- }
- httpx.OkMessage(w, "扫码成功", nil)
- }
-}
-
-func ScanRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+// ReportRecordsHandler GET /workpiece/process/records?sn=&orderNo=&stationNo=&page=&pageSize=
+// 报工记录列表:直查真实报工表 workpiece_process,手动报工与工位终端报工均在此可见(报工闭环)。
+func ReportRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
- data, total, err := logic.New(svcCtx).ListScanRecords(r.Context(), q.Get("sn"), q.Get("orderNo"),
- atoiDefault(q.Get("page"), 1), atoiDefault(q.Get("pageSize"), 20))
+ data, total, err := logic.New(svcCtx).ListProcessRecords(r.Context(), q.Get("sn"), q.Get("orderNo"),
+ atoiDefault(q.Get("stationNo"), 0), atoiDefault(q.Get("page"), 1), atoiDefault(q.Get("pageSize"), 20))
if err != nil {
httpx.Fail(w, 3206, err.Error())
return
diff --git a/bj_power_mes/internal/handler/routes_production.go b/bj_power_mes/internal/handler/routes_production.go
index c4cf229..7f119d8 100644
--- a/bj_power_mes/internal/handler/routes_production.go
+++ b/bj_power_mes/internal/handler/routes_production.go
@@ -192,8 +192,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
{Method: http.MethodPost, Path: "/torque/audit", Handler: production.TorqueAuditHandler(serverCtx)},
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
- {Method: http.MethodPost, Path: "/scan/report", Handler: production.ScanReportHandler(serverCtx)},
- {Method: http.MethodGet, Path: "/scan/records", Handler: production.ScanRecordsHandler(serverCtx)},
+ {Method: http.MethodGet, Path: "/workpiece/process/records", Handler: production.ReportRecordsHandler(serverCtx)},
{Method: http.MethodGet, Path: "/process-steps", Handler: production.ProcessStepsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/workpiece/online", Handler: production.OnlineWorkpieceHandler(serverCtx)},
diff --git a/bj_power_mes/internal/logic/scan.go b/bj_power_mes/internal/logic/scan.go
index a9144ed..cfc27c4 100644
--- a/bj_power_mes/internal/logic/scan.go
+++ b/bj_power_mes/internal/logic/scan.go
@@ -2,56 +2,13 @@ package logic
import (
"context"
- "errors"
"time"
- "bj_power_mes/ent"
"bj_power_mes/ent/dailyplan"
- "bj_power_mes/ent/scanrecord"
"bj_power_mes/ent/workorder"
- "bj_power_mes/ent/workpiece"
)
-type ScanReq struct {
- Station string `json:"station"`
- Sn string `json:"sn"`
- OrderNo string `json:"orderNo"`
- Type string `json:"type"` // ONLINE / PROCESS / DONE / TEMP_STORE
- ProcessCode int `json:"processCode"`
- Operator string `json:"operator"`
-}
-
-// ReportScan 扫码报工:记录 scan_record,并更新工单进度
-func (s *Service) ReportScan(ctx context.Context, req ScanReq, operator string) error {
- if req.Sn == "" {
- return errors.New("sn 不能为空")
- }
- scanType := req.Type
- if scanType == "" {
- scanType = "PROCESS"
- }
- if req.Operator == "" {
- req.Operator = operator
- }
- _, err := s.ctx.EntClient.ScanRecord.Create().
- SetStation(req.Station).SetSn(req.Sn).SetOrderNo(req.OrderNo).
- SetType(scanType).SetProcessCode(req.ProcessCode).SetOperator(req.Operator).
- SetTime(time.Now()).Save(ctx)
- if err != nil {
- return err
- }
- s.ctx.EventLog.Write(ctx, "scan.report", req.OrderNo, operator, "scan_record", req.Sn, "扫码报工", map[string]any{"station": req.Station, "type": scanType, "processCode": req.ProcessCode})
-
- // 更新工单进度:调高 finished 进度(去重按 sn+工序)
- wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
- if err == nil && wp != nil && wp.WorkOrderId > 0 {
- s.bumpWorkOrderProgress(ctx, wp.OrderNo, wp.Sn)
- }
- s.notifyDashboard()
- return nil
-}
-
-// bumpWorkOrderProgress 增加工单已完成数量(由完工动作驱动;扫码报工仅流转状态不重复计数)
+// bumpWorkOrderProgress 增加工单已完成数量(由完工动作驱动)
func (s *Service) bumpWorkOrderProgress(ctx context.Context, orderNo, sn string) {
wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).First(ctx)
if err != nil || wo == nil {
@@ -90,24 +47,3 @@ func (s *Service) bumpDailyPlanCompleted(ctx context.Context, orderNo string, t
SetCompletedQty(newQty).SetStatus(st).Save(ctx)
}
}
-
-// ListScanRecords 查询扫码报工记录
-func (s *Service) ListScanRecords(ctx context.Context, sn, orderNo string, page, pageSize int) ([]*ent.ScanRecord, int, error) {
- q := s.ctx.EntClient.ScanRecord.Query()
- if sn != "" {
- q = q.Where(scanrecord.Sn(sn))
- }
- if orderNo != "" {
- q = q.Where(scanrecord.OrderNoEqualFold(orderNo))
- }
- if sn != "" {
- q = q.Where(scanrecord.SnEqualFold(sn))
- }
- total, err := q.Count(ctx)
- if err != nil {
- return nil, 0, err
- }
- rows, err := q.Order(ent.Desc(scanrecord.FieldID)).
- Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
- return rows, total, err
-}
diff --git a/bj_power_mes/internal/logic/seed.go b/bj_power_mes/internal/logic/seed.go
index e441e01..9e351aa 100644
--- a/bj_power_mes/internal/logic/seed.go
+++ b/bj_power_mes/internal/logic/seed.go
@@ -12,7 +12,7 @@ import (
"golang.org/x/crypto/bcrypt"
)
-// Seed 初始化:超级管理员、默认角色、默认菜单权限、12 道工序与工位派工
+// Seed 初始化:超级管理员、默认角色、默认菜单权限、产线工位(内置只读主数据;工艺流程由用户自建不预置)
func (s *Service) Seed(ctx context.Context) error {
// 1. 默认角色(已存在则合并权限码,保证新按钮权限能落到已有角色上)
roles := []struct{ name, code string }{
@@ -168,37 +168,23 @@ func (s *Service) Seed(ctx context.Context) error {
// 清掉存量角色里的旧码与旧菜单定义行(幂等);新菜单 produce.qty 在 1. 的 seedMenus 中定义。
migrateLegacyQtyReport(ctx, s)
- // 4. 初始化工位与默认工艺流程(工位绑定流程,流程承载步骤)
- seedFlowsAndStations(ctx, s)
+ // 4. 初始化产线工位(内置只读主数据);工艺流程/工序步骤由用户在「工艺流程」页自建,不预置
+ seedStations(ctx, s)
return nil
}
-func seedFlowsAndStations(ctx context.Context, s *Service) {
- // 初始数据:12 个工位(工位数量以数据库为准,这里只做首次初始化;要 13 个就往 station 表插一行)
- // 初始化的工位一律为「内置工位」:需连 PLC、必须按工位号顺序流转、不可改类型、不可删除;
- // 其中 1~10 号工位对应实体接驳台,11/12 为产线末端无接驳台。
+func seedStations(ctx context.Context, s *Service) {
+ // 工位是产线固定物理工位,一律「内置工位」:需连 PLC、按工位号顺序流转、不可改类型、不可删除、仅可改名,
+ // 属系统内置只读主数据(同 WMS 接驳台),故由种子初始化。12 个装配工位(1~10 带接驳台,11/12 产线末端无接驳台);
+ // 工位数量以数据库为准,要 13 个就往 station 表插一行。
+ // 工艺流程/工序步骤不在此预置:流程可在「工艺流程」页新建/编辑/删除/启停,是用户可维护的业务数据,
+ // 由实施按产品工艺自建、并在「关联工位」页绑定到工位(station.flow_id),不硬编码进种子。
for i := 1; i <= 12; i++ {
- st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
- if err != nil {
- st, _ = s.ctx.EntClient.Station.Create().
+ if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx); err != nil {
+ _, _ = s.ctx.EntClient.Station.Create().
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").
SetIsBuiltin(true).SetHasDock(i <= 10).Save(ctx)
}
- if st == nil || st.FlowId > 0 {
- continue
- }
- flow, err := s.ctx.EntClient.ProcessFlow.Create().
- SetName("工位" + strconv.Itoa(i) + "_默认").
- SetStatus("ACTIVE").SetRemark("默认工艺流程").Save(ctx)
- if err != nil {
- continue
- }
- if _, err := s.ctx.EntClient.ProcessStep.Create().
- SetFlowId(flow.ID).SetSeq(1).SetName("装配完成").SetCollectType("NONE").Save(ctx); err == nil && i == 3 {
- _, _ = s.ctx.EntClient.ProcessStep.Create().
- SetFlowId(flow.ID).SetSeq(2).SetName("拧紧扭矩").SetCollectType("AUTO").SetIsTorque(true).Save(ctx)
- }
- _, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flow.ID).Save(ctx)
}
// 0 号(上线位) / 13 号(下线位) 虚拟工位:OFFLINE 类型,用 PAD 操作,不连 PLC、
// 不叫料、不进入产线组合排序;仅做上下线登记,不参与物理工位计数。
diff --git a/bj_power_mes/internal/logic/workpiece.go b/bj_power_mes/internal/logic/workpiece.go
index ef0e1aa..5157104 100644
--- a/bj_power_mes/internal/logic/workpiece.go
+++ b/bj_power_mes/internal/logic/workpiece.go
@@ -132,6 +132,16 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
if err != nil {
return errors.New("工件未进线登记")
}
+ // 严格校验(业务口径已确认):仅“在制”工件可报工,且必须按工序顺序流转,禁止跳站/漏站/重复报同一站。
+ // 在制 = ONLINE(已进线) / PROCESSING(在工位);DONE/REPAIR/SCRAPPED 一律拒绝。
+ if wp.Status != "ONLINE" && wp.Status != "PROCESSING" {
+ return errors.New("工件当前状态为「" + wp.Status + "」,非在制,不允许报工")
+ }
+ // 顺序流转:currentProcess 语义为“已报工完成的工序”(进线时=0),本工位必须是其下一道,即 processCode == currentProcess+1。
+ if req.ProcessCode != wp.CurrentProcess+1 {
+ return errors.New("工序顺序不符:工件已完成工序" + itoa(wp.CurrentProcess) +
+ ",应在工位" + itoa(wp.CurrentProcess+1) + "报工,当前请求工位" + itoa(req.ProcessCode))
+ }
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 U3):
// 任一步失败全部回滚,杜绝"实绩已建但工件状态没推进/结果没落"的半成品脏数据;
// 校验(齐套/needCheck)也走同一 tx client,才能读到本事务内刚写入的绑定。
@@ -429,6 +439,56 @@ func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error)
}, nil
}
+// ListProcessRecords 报工记录列表(分页):直接查真实报工表 workpiece_process,
+// 手动报工与工位终端报工都在此可见,形成报工闭环(取代已废弃的 scan_record 查询)。
+func (s *Service) ListProcessRecords(ctx context.Context, sn, orderNo string, stationNo, page, pageSize int) ([]map[string]any, int, error) {
+ q := s.ctx.EntClient.WorkpieceProcess.Query()
+ if sn != "" {
+ q = q.Where(workpieceprocess.SnEqualFold(sn))
+ }
+ if orderNo != "" {
+ q = q.Where(workpieceprocess.OrderNoEqualFold(orderNo))
+ }
+ if stationNo > 0 {
+ q = q.Where(workpieceprocess.StationNo(itoa(stationNo)))
+ }
+ total, err := q.Count(ctx)
+ if err != nil {
+ return nil, 0, err
+ }
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 {
+ pageSize = 20
+ }
+ rows, err := q.Order(ent.Desc(workpieceprocess.FieldID)).
+ Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
+ if err != nil {
+ return nil, 0, err
+ }
+ list := make([]map[string]any, 0, len(rows))
+ for _, r := range rows {
+ ended := ""
+ if r.EndedAt != nil {
+ ended = r.EndedAt.Format("2006-01-02 15:04:05")
+ }
+ list = append(list, map[string]any{
+ "sn": r.Sn,
+ "orderNo": r.OrderNo,
+ "stationNo": r.StationNo,
+ "processCode": r.ProcessCode,
+ "processName": r.ProcessName,
+ "status": r.Status,
+ "result": r.Result,
+ "operator": r.Operator,
+ "durationSec": r.DurationSec,
+ "endedAt": ended,
+ })
+ }
+ return list, total, nil
+}
+
func (s *Service) ListWorkpieces(ctx context.Context, sn, orderNo string) ([]*ent.Workpiece, error) {
q := s.ctx.EntClient.Workpiece.Query()
if sn != "" {
diff --git a/bj_power_mes/public/index.html b/bj_power_mes/public/index.html
index c1bb051..77229d5 100644
--- a/bj_power_mes/public/index.html
+++ b/bj_power_mes/public/index.html
@@ -6,7 +6,7 @@
MES 产线控制
-
+
diff --git a/bj_power_mes/schema/scan_record.go b/bj_power_mes/schema/scan_record.go
deleted file mode 100644
index 1d84df4..0000000
--- a/bj_power_mes/schema/scan_record.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package schema
-
-import (
- "time"
-
- "entgo.io/ent"
- "entgo.io/ent/dialect/entsql"
- "entgo.io/ent/schema"
- "entgo.io/ent/schema/field"
- "entgo.io/ent/schema/index"
-)
-
-// ScanRecord 扫码报工记录(station,sn,orderNo,type,operator,time)
-type ScanRecord struct {
- ent.Schema
-}
-
-func (ScanRecord) Annotations() []schema.Annotation {
- return []schema.Annotation{entsql.Annotation{Table: "scan_record"}}
-}
-
-func (ScanRecord) Fields() []ent.Field {
- return []ent.Field{
- field.Int("id").Positive(),
- field.String("station").Default("").MaxLen(32).Comment("工位/扫码点"),
- field.String("sn").MaxLen(64).Comment("工件SN"),
- field.String("orderNo").Default("").MaxLen(64).Comment("工单号"),
- // ONLINE(进线) / PROCESS(工序报工) / DONE(完工) / TEMP_STORE(暂存)
- field.String("type").Default("PROCESS").MaxLen(20).Comment("报工类型"),
- field.Int("processCode").Optional().Comment("工序号 1~12"),
- field.String("operator").Default("").MaxLen(64).Comment("操作人"),
- field.Time("time").Default(time.Now).Comment("报工时间"),
- field.Time("createdAt").Default(time.Now).Immutable(),
- }
-}
-
-func (ScanRecord) Indexes() []ent.Index {
- return []ent.Index{
- index.Fields("station"),
- index.Fields("sn"),
- index.Fields("orderNo"),
- index.Fields("createdAt"),
- }
-}
diff --git a/bj_power_mes/tools/generate.go b/bj_power_mes/tools/generate.go
new file mode 100644
index 0000000..a733f9c
--- /dev/null
+++ b/bj_power_mes/tools/generate.go
@@ -0,0 +1,22 @@
+// ent 代码生成器:读取 ./schema 下的实体定义,生成 ./ent 目录下的 ORM 代码。
+// 用法(在 bj_power_mes 根目录):go run ./tools/generate.go
+// 注意:Features 必须与既有生成代码保持一致(sql/modifier 提供 Modify、sql/lock 提供 ForUpdate/ForShare),
+// 否则重生会丢失这些方法。
+package main
+
+import (
+ "log"
+
+ "entgo.io/ent/entc"
+ "entgo.io/ent/entc/gen"
+)
+
+func main() {
+ if err := entc.Generate("./schema", &gen.Config{
+ Target: "./ent",
+ Package: "bj_power_mes/ent",
+ Features: []gen.Feature{gen.FeatureModifier, gen.FeatureLock},
+ }); err != nil {
+ log.Fatalf("running ent codegen: %v", err)
+ }
+}
diff --git a/bj_power_wms/frontend/src/help.js b/bj_power_wms/frontend/src/help.js
index d2b1f5b..4e0972f 100644
--- a/bj_power_wms/frontend/src/help.js
+++ b/bj_power_wms/frontend/src/help.js
@@ -62,9 +62,9 @@ export const helpInbound = {
export const helpOutbound = {
title: '出库管理(统一主表)',
overview:
- '所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n三个页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";未启用 MES 或工单未同步时,工单下拉可能为空,可直接手输工单号(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/退货/样品/报废/发货/其他),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息),含工单号与目标工位。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n通用出库操作顺序(客户诉求 问题17/18/20):① 先选物料(下拉统一显示 编码/图号/类型/品类)→ ② 系统自动判定出库类型(电气件/结构件,无需手选)→ ③ 选出库类别(纯标签,不联动任何地方;选「其他」时备注必填原因)→ ④ 填归属(工单编号/科技项目号/其他,非其他时编号必填)→ ⑤ 结构件从可用批次下拉选批次(默认 FIFO 首个)并填数量 / 电气件逐个扫 SN → ⑥ 选目标工位(下拉,来自 MES 工位列表,不可手输)。\n\n说明:出库单号由系统自动生成、不可修改,规则与入库一致(入库 IB+ / 出库 OB+ 同一编号规则);出库无法与入库批次/合同对应,故出库类别仅是标签记录,列表与出库单都显示。质量门禁:发货/样品/其他类出库仅允许「合格」品;退货/报废/退料类豁免(本身即处置不合格品)。',
+ '所有出库都落在同一张「出库主表」(OutboundOrder),用"类型"区分,装箱信息(箱号/SN清单/合同号)也直接记在主表上,不再单独建装箱表。\n三个页签:\n· 工单备料出库(MES):对接 MES 工单台账领料,强约束"单物料累计出库 ≤ 工单需求总量";工单号下拉选择(来自 MES 实时工单,未完成优先,无输入默认展示前 20 条),支持输入模糊搜索;MES 不可达或工单未同步时下拉可能为空,可手输兜底(需台账已同步)。\n· 通用出库:不依赖工单的手动出库(退料/退货/样品/报废/发货/其他),支持结构件批次与电气件 SN,填箱号即一并记录装箱;MES 不可用或无需挂靠工单时请用本页。\n· 出库记录:统一主表的全量查询与导出(一键导出 CSV,含装箱信息),含工单号与目标工位。\n选用原则:有 MES 工单要按 物料清单 领料 → 工单备料出库;无工单的自由出库/装箱发货 → 通用出库。\n\n通用出库操作顺序(客户诉求 问题17/18/20):① 先选物料(下拉统一显示 编码/图号/类型/品类)→ ② 系统自动判定出库类型(电气件/结构件,无需手选)→ ③ 选出库类别(纯标签,不联动任何地方;选「其他」时备注必填原因)→ ④ 填归属(工单编号/科技项目号/其他,非其他时编号必填)→ ⑤ 结构件从可用批次下拉选批次(默认 FIFO 首个)并填数量 / 电气件逐个扫 SN → ⑥ 选目标工位(下拉,来自 MES 工位列表,不可手输)。\n\n说明:出库单号由系统自动生成、不可修改,规则与入库一致(入库 IB+ / 出库 OB+ 同一编号规则);出库无法与入库批次/合同对应,故出库类别仅是标签记录,列表与出库单都显示。质量门禁:发货/样品/其他类出库仅允许「合格」品;退货/报废/退料类豁免(本身即处置不合格品)。',
fields: [
- { name: '备料出库-工单号', source: '手动输入。数据来自 MES 已建的工单及导入的物料台账。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '输入工单号回车,查询该工单的物料台账' },
+ { name: '备料出库-工单号', source: '下拉选择 + 模糊搜索。数据来自 MES 实时工单(未完成优先,无输入默认前 20 条)。', purpose: '出库挂靠的工单,决定可领料总量与需求缺口', fill: '下拉选工单号(或输入关键字模糊搜索;MES 不可达时可手输兜底),选中即查询该工单的物料台账' },
{ name: '备料出库-目标工位', source: '下拉选择 DOCK01~DOCK20(选填,可过滤搜索)。', purpose: 'AGV 配送送达的接驳台(toDock 参数)', fill: '选择如 DOCK05;不选默认运到产线入口 DOCK01' },
{ name: '备料出库-一键分配出库', source: '系统自动按 FIFO 分配。数据来自合格区Z02的批次库存。', purpose: '从合格区按先进先出凑够缺口并出库', fill: '点按钮,系统自动算批次并二次确认' },
{ name: '通用出库-物料', source: '下拉选择(全项目统一组件)。', purpose: '选定要出库的物料', fill: '下拉显示 编码/图号/类型(电气件/结构件/其他)/品类(原材料/半成品/成品/其他),支持远程搜索' },
@@ -255,14 +255,14 @@ export const helpZone = {
export const helpDock = {
title: '接驳台维护',
overview:
- '维护全厂接驳台(AGV 取送货的物理停靠位)主数据。接驳台是独立主数据,不依附工位:除了产线各工位旁,库房收货口、线边缓存区等其他位置也有接驳台位。\n\n三种类型:\n· 产线:位于产线工位旁,必须绑定一个工位号,且与工位一一对应(一个工位只能有一个接驳台、一个接驳台只能给一个工位);\n· 库房:库房收货/发货口(如 DOCK21),不绑定工位;\n· 其他:缓存区、暂存位等,不绑定工位。\n\n接驳台编码是产线与 AGV 任务的引用锚点,创建后不可修改;MES「关联工位」页绑定接驳台时从本页数据实时拉取。有托盘占用的接驳台不允许删除,需先处理托盘。',
+ '查看全厂接驳台(AGV 取送货的物理停靠位)。接驳台是系统预置的只读主数据,本页仅供查看,不提供新增/修改/删除。\n\n三种类型:\n· 产线:位于产线工位旁,与工位一一对应(DOCK01~20 对应工位 1~20,一个工位一个接驳台);\n· 库房:库房收货/发货口(如 DOCK21),不绑定工位;\n· 其他:缓存区、暂存位等,不绑定工位。\n\n接驳台编码是产线与 AGV 任务的引用锚点;MES「关联工位」页绑定接驳台时从本页数据实时拉取。',
fields: [
- { name: '接驳台编码', source: '手工填写,创建后不可改。', purpose: '接驳台唯一编号,产线/AGV 任务据此定位', fill: '如 DOCK21、DOCK22;建议 DOCK+两位数字' },
- { name: '接驳台名称', source: '手工填写。', purpose: '接驳台中文名,便于识别', fill: '如 库房收货接驳台、三号缓存接驳台' },
- { name: '类型', source: '选择:产线 / 库房 / 其他。', purpose: '区分接驳台所处位置与用途', fill: '产线接驳台需填工位号;库房/其他不填' },
- { name: '绑定工位号', source: '仅产线类型填写。', purpose: '该接驳台服务哪个工位,与工位 1:1', fill: '如 3 表示 3 号工位;该工位已绑定其他接驳台时会提示' },
- { name: '托盘状态', source: '系统自动维护(PLC 托盘传感器 / AGV 到位卸货)。', purpose: '看该接驳台当前是否被占用', fill: '不可手改;有托盘的接驳台不允许删除' },
- { name: '状态', source: '选择:启用 / 停用。', purpose: '停用的接驳台不再参与配送与下拉选择', fill: '默认启用' }
+ { name: '接驳台编码', source: '系统预置,只读。', purpose: '接驳台唯一编号,产线/AGV 任务据此定位', fill: '只读查看,如 DOCK01~DOCK21' },
+ { name: '接驳台名称', source: '系统预置,只读。', purpose: '接驳台中文名,便于识别', fill: '只读查看' },
+ { name: '类型', source: '系统预置:产线 / 库房 / 其他。', purpose: '区分接驳台所处位置与用途', fill: '只读;产线接驳台绑定工位号,库房/其他不绑定' },
+ { name: '绑定工位号', source: '仅产线类型有,与工位 1:1。', purpose: '该接驳台服务哪个工位', fill: '只读,如 3 表示 3 号工位' },
+ { name: '托盘状态', source: '系统自动维护(PLC 托盘传感器 / AGV 到位卸货)。', purpose: '看该接驳台当前是否被占用', fill: '只读,实时反映是否有托盘占用' },
+ { name: '状态', source: '系统预置:启用 / 停用。', purpose: '停用的接驳台不再参与配送与下拉选择', fill: '只读查看' }
]
}
diff --git a/bj_power_wms/frontend/src/pages/BaseData.vue b/bj_power_wms/frontend/src/pages/BaseData.vue
index be995dc..f59192e 100644
--- a/bj_power_wms/frontend/src/pages/BaseData.vue
+++ b/bj_power_wms/frontend/src/pages/BaseData.vue
@@ -55,6 +55,7 @@ const f2Shelf = ref([]); const f3Shelf = ref([]); const f3Layer = ref([])
const f4Shelf = ref([]); const f4Layer = ref([])
const cShelf = ref([]); const cLayer = ref([])
const bShelf = ref([])
+const lbShelf = ref([])
async function picker(level, params) {
const data = await request.get('/zone/picker', { params: { level, ...params } })
@@ -239,6 +240,47 @@ async function submitBatch() {
}
}
+/* ---------- 区域维护:层批量生成 ---------- */
+const layerBatchDialog = ref(false)
+const layerBatchForm = reactive({ zoneCode: '', shelfNo: '', layerStart: 1, layerEnd: 1 })
+const layerBatchPreview = ref([])
+const layerBatchPreviewing = ref(false)
+const layerBatchSubmitting = ref(false)
+
+function openLayerBatchGen() {
+ Object.assign(layerBatchForm, { zoneCode: '', shelfNo: '', layerStart: 1, layerEnd: 1 })
+ layerBatchPreview.value = []
+ layerBatchDialog.value = true
+}
+async function onLayerBatchRegionChange() {
+ layerBatchForm.shelfNo = ''
+ await loadShelfOpts(lbShelf, layerBatchForm.zoneCode)
+}
+async function previewLayerBatch() {
+ if (!layerBatchForm.zoneCode || !layerBatchForm.shelfNo) { ElMessage.warning('请选择区域与货架'); return }
+ layerBatchPreviewing.value = true
+ try {
+ const data = await request.post('/zone/batch-generate', { ...layerBatchForm, level: 3, preview: true })
+ layerBatchPreview.value = data?.list || []
+ } finally {
+ layerBatchPreviewing.value = false
+ }
+}
+async function submitLayerBatch() {
+ const go = await confirmAction(`已预检 ${layerBatchPreview.value.length} 个层,确认批量生成到库位表?(已存在的自动跳过)`, '批量生成确认')
+ if (!go) return
+ layerBatchSubmitting.value = true
+ try {
+ const data = await request.post('/zone/batch-generate', { ...layerBatchForm, level: 3, preview: false })
+ ElMessage.success(`已生成 ${data?.created || 0} 个层,跳过已存在 ${data?.skipped || 0} 个`)
+ layerBatchDialog.value = false
+ zoneTabLoaded[3] = false
+ if (activeZoneTab.value === 3) await loadNodes(3)
+ } finally {
+ layerBatchSubmitting.value = false
+ }
+}
+
/* ---------- 物料档案维护 ---------- */
const materials = ref([])
const materialTotal = ref(0)
@@ -411,8 +453,11 @@ async function exportMaterials() {
name: matFilter.name.trim(),
spec: matFilter.spec.trim(),
manageMode: matFilter.manageMode,
- itemType: matFilter.itemType
- }, '物料档案.xlsx')
+ itemType: matFilter.itemType,
+ startDate: matFilter.startDate,
+ endDate: matFilter.endDate
+ }, '物料档案.xlsx', { countUrl: '/material/query', name: '物料档案' })
+ if (!name) return
ElMessage.success('已导出:' + name)
} catch (e) {
ElMessage.error('导出失败:' + (e?.message || e))
@@ -603,6 +648,7 @@ async function onMaterialFile(e) {
查询
新增层
+ 批量生成层
@@ -763,6 +809,41 @@ async function onMaterialFile(e) {
共 {{ batchPreview.length }} 个位置,其中已存在 {{ batchPreview.filter(x=>x.exists).length }} 个(生成时自动跳过)。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 至
+
+
+
+ 预览
+ 确认生成
+
+
+
+
+
+
+ {{ row.exists ? '已存在' : '新增' }}
+
+
+
+ 共 {{ layerBatchPreview.length }} 个层,其中已存在 {{ layerBatchPreview.filter(x=>x.exists).length }} 个(生成时自动跳过)。
+
+
diff --git a/bj_power_wms/frontend/src/pages/Dock.vue b/bj_power_wms/frontend/src/pages/Dock.vue
index 04570ca..1571057 100644
--- a/bj_power_wms/frontend/src/pages/Dock.vue
+++ b/bj_power_wms/frontend/src/pages/Dock.vue
@@ -2,7 +2,7 @@
+ title="接驳台是产线与库房之间供 AGV 取送货的物理停靠位,为系统预置的只读主数据,本页仅供查看。类型分 产线 / 库房 / 其他:产线接驳台与工位一一对应(DOCK01~20 对应工位 1~20),库房接驳台为收货/发货口(DOCK21),其他为缓存/暂存位;只有产线接驳台绑定工位号。" />
diff --git a/bj_power_wms/frontend/src/pages/Inbound.vue b/bj_power_wms/frontend/src/pages/Inbound.vue
index ebddb00..82c7a4e 100644
--- a/bj_power_wms/frontend/src/pages/Inbound.vue
+++ b/bj_power_wms/frontend/src/pages/Inbound.vue
@@ -440,7 +440,7 @@ async function exportCsv() {
voided: filters.voided,
startDate: dateRange.value?.[0] || '',
endDate: dateRange.value?.[1] || ''
- }, '入库管理.xlsx')
+ }, '入库管理.xlsx', { countUrl: '/inbound/query', name: '入库记录' })
} catch (e) {
/* 错误提示由请求层统一处理 */
}
diff --git a/bj_power_wms/frontend/src/pages/Inspection.vue b/bj_power_wms/frontend/src/pages/Inspection.vue
index a4691a1..65274cf 100644
--- a/bj_power_wms/frontend/src/pages/Inspection.vue
+++ b/bj_power_wms/frontend/src/pages/Inspection.vue
@@ -333,7 +333,7 @@ async function exportCsv() {
inspector: filters.inspector.trim(),
startDate: filters.startDate,
endDate: filters.endDate
- }, '检验记录.xlsx')
+ }, '检验记录.xlsx', { countUrl: '/inspection/query', name: '检验记录' })
} catch (e) {
/* 错误提示由请求层统一处理 */
}
diff --git a/bj_power_wms/frontend/src/pages/Inventory.vue b/bj_power_wms/frontend/src/pages/Inventory.vue
index 49f528c..1908d2c 100644
--- a/bj_power_wms/frontend/src/pages/Inventory.vue
+++ b/bj_power_wms/frontend/src/pages/Inventory.vue
@@ -233,11 +233,11 @@ async function exportCsv() {
materialCode: filters.materialCode.trim(),
materialName: filters.materialName.trim(),
manageMode: filters.manageMode
- }, '物料汇总.xlsx')
+ }, '物料汇总.xlsx', { count: matTotal.value, name: '物料汇总' })
return
}
if (viewTab.value === 'summary') {
- await exportXlsxFetch('/api/stock/export', { view: 'zone' }, '区域汇总.xlsx')
+ await exportXlsxFetch('/api/stock/export', { view: 'zone' }, '区域汇总.xlsx', { count: summaryRows.value.length, name: '区域汇总' })
return
}
await exportXlsxFetch('/api/stock/export', {
@@ -248,7 +248,7 @@ async function exportCsv() {
manageMode: filters.manageMode,
startDate: filters.startDate,
endDate: filters.endDate
- }, '库存汇总.xlsx')
+ }, '库存汇总.xlsx', { countUrl: '/stock/query', name: '库存明细' })
} catch (e) {
// 错误信息已由 request 层处理
}
diff --git a/bj_power_wms/frontend/src/pages/Outbound.vue b/bj_power_wms/frontend/src/pages/Outbound.vue
index 15feff5..a2e652f 100644
--- a/bj_power_wms/frontend/src/pages/Outbound.vue
+++ b/bj_power_wms/frontend/src/pages/Outbound.vue
@@ -24,14 +24,22 @@ const loadingPrep = ref(false)
const queried = ref(false)
const rows = ref([])
-onMounted(loadOrders)
-async function loadOrders() {
+const orders = ref([])
+const ordersLoading = ref(false)
+// 工单号来自 MES 实时查询:无输入默认拉「未完成优先」的前 20 条;输入时按工单号模糊远程搜索。
+async function searchOrders(query) {
+ ordersLoading.value = true
try {
- const data = await request.get('/orders', { params: { page: 1, pageSize: 200 } })
+ const data = await request.get('/orders', { params: { orderNo: query || '', limit: 20 } })
orders.value = Array.isArray(data) ? data : data?.list || []
} catch { orders.value = [] }
+ finally { ordersLoading.value = false }
}
-const orders = ref([])
+async function loadOrders() { await searchOrders('') }
+function orderLabel(o) { return o.productName ? `${o.workOrderNo} ${o.productName}` : o.workOrderNo }
+// 选中下拉项或手输兜底(allow-create)后直接查该工单台账
+function onOrderPick(v) { if (v && String(v).trim()) queryPrep() }
+onMounted(loadOrders)
const pendingRows = computed(() => rows.value.filter(r => r.status !== '领料完结'))
function gapOf(row) { return Math.max((row.totalQty || 0) - (row.outQty || 0), 0) }
@@ -326,7 +334,14 @@ async function submitGen() {
}
/* ===================== 出库记录(统一主表,全部 + 导出) ===================== */
-const recFilters = reactive({ outboundType: '', status: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: [] })
+// 出库记录默认展示近 3 个月(与后端导出时间硬规则一致,保证「列表条数 = 导出条数」)
+function recDefaultRange() {
+ const p = (n) => String(n).padStart(2, '0')
+ const fmt = (d) => `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
+ const end = new Date(); const start = new Date(); start.setMonth(start.getMonth() - 3)
+ return [fmt(start), fmt(end)]
+}
+const recFilters = reactive({ outboundType: '', status: '', materialCode: '', boxNo: '', ownershipType: '', dateRange: recDefaultRange() })
const recRows = ref([])
const recTotal = ref(0)
const loadingRec = ref(false)
@@ -346,8 +361,8 @@ async function loadRecords() {
page: recPage.current, pageSize: recPage.size
}
if (Array.isArray(recFilters.dateRange) && recFilters.dateRange.length === 2) {
- params.startAt = new Date(recFilters.dateRange[0]).getTime()
- params.endAt = new Date(recFilters.dateRange[1]).getTime() + 86399999
+ params.startDate = recFilters.dateRange[0]
+ params.endDate = recFilters.dateRange[1]
}
const data = await request.get('/outbound/query', { params })
recRows.value = (data?.list || []).map(r => ({ ...r, snCodes: snOf(r) }))
@@ -355,16 +370,20 @@ async function loadRecords() {
} finally { loadingRec.value = false }
}
function searchRec() { recPage.current = 1; loadRecords() }
-function resetRec() { Object.assign(recFilters, { outboundType: '', status: '', materialCode: '', boxNo: '', dateRange: [] }); searchRec() }
+function resetRec() { Object.assign(recFilters, { outboundType: '', status: '', materialCode: '', boxNo: '', dateRange: recDefaultRange() }); searchRec() }
function downloadCSV() {
+ // 时间窗口与列表保持一致:未选/已清空时回落近 3 个月,保证确认框条数 = 实际导出条数
+ const dr = (Array.isArray(recFilters.dateRange) && recFilters.dateRange.length === 2) ? recFilters.dateRange : recDefaultRange()
exportXlsxFetch('/api/outbound/export', {
outboundType: recFilters.outboundType,
status: recFilters.status,
materialCode: recFilters.materialCode.trim(),
- boxNo: recFilters.boxNo.trim()
- }, '出库管理.xlsx')
- .then(() => ElMessage.success('导出成功'))
+ boxNo: recFilters.boxNo.trim(),
+ startDate: dr[0],
+ endDate: dr[1]
+ }, '出库管理.xlsx', { countUrl: '/outbound/query', name: '出库记录' })
+ .then((name) => { if (name) ElMessage.success('已导出:' + name) })
.catch(() => ElMessage.error('导出失败'))
}
@@ -387,8 +406,15 @@ onMounted(loadDicts)
-
+
+
+ {{ o.workOrderNo }}
+ {{ o.productName }} {{ o.status }}
+
+
diff --git a/bj_power_wms/frontend/src/pages/Semi.vue b/bj_power_wms/frontend/src/pages/Semi.vue
index 6ea321d..d82985c 100644
--- a/bj_power_wms/frontend/src/pages/Semi.vue
+++ b/bj_power_wms/frontend/src/pages/Semi.vue
@@ -11,15 +11,14 @@ import MaterialSelect from '../components/MaterialSelect.vue'
import AttachmentPanel from '../components/AttachmentPanel.vue'
import { helpSemi } from '../help'
-// 半成品/成品附件(bizType=semi,bizId=SN)
+// 半成品附件(bizType=semi,bizId=SN)
const attachDrawer = reactive({ visible: false, sn: '' })
function openAttach(row) {
Object.assign(attachDrawer, { visible: true, sn: row.sn })
}
-/* ---------- 半成品/成品入库 ----------
- * 客户诉求 问题记录 L222:半成品与成品入库要区分(category 2/3);
- * L218 成品入库 9 字段:SN/图号/归属/产品状态/相关标准/试验记录单号/记录员/记录日期/工单号。 */
+/* ---------- 半成品入库(category=2) ----------
+ * 成品由 MES 完工自动回流入库、经 WMS 完工检验,不在本页手工入库。 */
const operator = getRealName()
const todayStr = new Date().toLocaleDateString('sv-SE')
@@ -33,11 +32,11 @@ function newInForm() {
recordedBy: operator, recordedAt: todayStr
}
}
-// inForms[2]=半成品入库表单,inForms[3]=成品入库表单
-const inForms = reactive({ 2: newInForm(), 3: newInForm() })
-const snInputs = reactive({ 2: '', 3: '' })
-const snLists = reactive({ 2: [], 3: [] })
-const inbounding = reactive({ 2: false, 3: false })
+// inForms[2]=半成品入库表单(category=2)
+const inForms = reactive({ 2: newInForm() })
+const snInputs = reactive({ 2: '' })
+const snLists = reactive({ 2: [] })
+const inbounding = reactive({ 2: false })
function addSnLines(cat) {
for (let raw of String(snInputs[cat]).split(/\s+/)) {
@@ -53,7 +52,7 @@ function removeSn(cat, i) {
async function submitInbound(cat) {
const f = inForms[cat]
- const label = cat === 3 ? '成品' : '半成品'
+ const label = '半成品'
if (!f.materialCode) {
ElMessage.warning('请选择物料')
return
@@ -62,10 +61,6 @@ async function submitInbound(cat) {
ElMessage.warning('请扫码录入 SN(回车追加)')
return
}
- if (cat === 3 && !f.productStatus) {
- ElMessage.warning('成品入库请选择产品状态(已通过/部分通过/返修)')
- return
- }
// 归属必选(与入库管理同口径,不再有「无」);选其他可不填编号,工单号/科技项目号必须填编号
if (!f.ownershipType) {
ElMessage.warning('请选择归属(工单编号 / 科技项目号 / 其他)')
@@ -109,11 +104,11 @@ async function submitInbound(cat) {
}
}
-/* ---------- 半成品/成品出库(扫SN带出 + 出库原因按钮,客户诉求 L224) ---------- */
+/* ---------- 半成品出库(扫SN带出 + 出库原因按钮);成品发货走「出库管理」通用出库 ---------- */
const outReasons = ['包装', '继续生产', '检验试验', '其他']
-// outForms[2]=半成品出库,outForms[3]=成品出库
-const outForms = reactive({ 2: { sn: '', info: null, reason: '' }, 3: { sn: '', info: null, reason: '' } })
-const outbounding = reactive({ 2: false, 3: false })
+// outForms[2]=半成品出库(category=2)
+const outForms = reactive({ 2: { sn: '', info: null, reason: '' } })
+const outbounding = reactive({ 2: false })
// 扫 SN 回车:带出该件信息(物料/已完成工序/工单/归属),再点原因按钮出库
async function scanSn(cat) {
@@ -126,7 +121,7 @@ async function scanSn(cat) {
const data = await request.get('/semi/query', { params: { sn, category: cat, page: 1, pageSize: 5 } })
const hit = (data?.list || []).find((r) => r.sn === sn)
if (!hit) {
- ElMessage.warning(`未找到该 SN 的${cat === 3 ? '成品' : '半成品'}记录`)
+ ElMessage.warning(`未找到该 SN 的半成品记录`)
return
}
if (hit.status !== '在库') {
@@ -139,7 +134,7 @@ async function scanSn(cat) {
async function submitOutbound(cat) {
const f = outForms[cat]
- const label = cat === 3 ? '成品' : '半成品'
+ const label = '半成品'
const sn = f.sn.trim()
if (!sn) {
ElMessage.warning('请扫描或输入 SN')
@@ -167,8 +162,8 @@ async function submitOutbound(cat) {
}
}
-/* ---------- 半成品/成品查询(L230 加归属;L222 品类筛选) ---------- */
-const qForm = reactive({ materialCode: '', status: '', sn: '', ownershipNo: '', category: '' })
+/* ---------- 半成品查询(L230 加归属) ---------- */
+const qForm = reactive({ materialCode: '', status: '', sn: '', ownershipNo: '' })
const list = ref([])
const total = ref(0)
const loading = ref(false)
@@ -183,7 +178,6 @@ async function loadList() {
status: qForm.status,
sn: qForm.sn.trim(),
ownershipNo: qForm.ownershipNo.trim(),
- category: qForm.category || undefined,
page: page.current,
pageSize: page.size
}
@@ -271,73 +265,6 @@ onMounted(() => {