Ver código fonte

手动提交

wangxichen 2 semanas atrás
pai
commit
270397a519

+ 2 - 1
src/com/mes/core/ProjectConfigManager.java

@@ -63,7 +63,8 @@ public class ProjectConfigManager {
             new Project("P1", "P02-1",   "+KA99", "+KB004", "+KB77"),
             new Project("P2", "AY7-610", "+KB24", "+KA94",  "+KB77"),
             new Project("P3", "AY7-520", "+KB23", "+KB004", "+KB77"),
-            new Project("P4", "P04-1",   "+KA93", "+KA94",  "+KB77")
+            new Project("--", "P04-1",   "+KA93", "+KA94",  "+KB77"),
+            new Project("P4", "650",   "+KA64IS", "+KA64IG",  "+KB78")
     ));
 
     // 默认值

+ 105 - 0
src/com/mes/core/UiPrefs.java

@@ -0,0 +1,105 @@
+package com.mes.core;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.JSONWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 本机 UI 偏好 - 管理“始终显示”的菜单白名单
+ *
+ * 存储位置:config/ui_prefs.json
+ * 结构:
+ * {
+ *   "alwaysShow": ["manual_submit"]
+ * }
+ */
+public class UiPrefs {
+    private static final Logger log = LoggerFactory.getLogger(UiPrefs.class);
+    private static final String CONFIG_FILE = "config/ui_prefs.json";
+
+    /** 菜单 key:手动提交子菜单 */
+    public static final String KEY_MANUAL_SUBMIT = "manual_submit";
+
+    private static UiPrefs INSTANCE;
+
+    private final Path configPath;
+    private final Set<String> alwaysShow = new HashSet<>();
+
+    private UiPrefs() {
+        this.configPath = Paths.get(CONFIG_FILE);
+        load();
+    }
+
+    public static synchronized UiPrefs getInstance() {
+        if (INSTANCE == null) {
+            INSTANCE = new UiPrefs();
+        }
+        return INSTANCE;
+    }
+
+    /** 是否勾选了“始终显示” */
+    public boolean isAlwaysShow(String key) {
+        return alwaysShow.contains(key);
+    }
+
+    /** 设置“始终显示”状态,立即落盘 */
+    public void setAlwaysShow(String key, boolean always) {
+        boolean changed;
+        if (always) {
+            changed = alwaysShow.add(key);
+        } else {
+            changed = alwaysShow.remove(key);
+        }
+        if (changed) {
+            save();
+        }
+    }
+
+    private void load() {
+        try {
+            if (!Files.exists(configPath)) {
+                log.info("UI偏好文件不存在,使用默认值: {}", configPath);
+                return;
+            }
+            String content = new String(Files.readAllBytes(configPath), StandardCharsets.UTF_8);
+            JSONObject json = JSON.parseObject(content);
+            if (json == null) return;
+
+            alwaysShow.clear();
+            java.util.List<Object> list = json.getJSONArray("alwaysShow");
+            if (list != null) {
+                for (Object o : list) {
+                    if (o != null) alwaysShow.add(o.toString());
+                }
+            }
+            log.info("UI偏好加载成功: alwaysShow={}", alwaysShow);
+        } catch (Exception e) {
+            log.error("加载UI偏好失败: {}", e.getMessage(), e);
+        }
+    }
+
+    private void save() {
+        try {
+            if (configPath.getParent() != null && !Files.exists(configPath.getParent())) {
+                Files.createDirectories(configPath.getParent());
+            }
+            JSONObject root = new JSONObject();
+            root.put("alwaysShow", new java.util.ArrayList<>(alwaysShow));
+            String content = JSON.toJSONString(root, JSONWriter.Feature.PrettyFormat);
+            Files.write(configPath, content.getBytes(StandardCharsets.UTF_8));
+            log.info("UI偏好已保存: {}", alwaysShow);
+        } catch (IOException e) {
+            log.error("保存UI偏好失败: {}", e.getMessage(), e);
+        }
+    }
+}

+ 39 - 0
src/com/mes/core/WorkflowEngine.java

@@ -490,6 +490,45 @@ public class WorkflowEngine {
     }
 
     /**
+     * 跳转到指定步骤(手动提交时强制跳到 upload_result)
+     * - 取消当前的超时任务
+     * - 清除 async 回调(避免旧步骤完成时再触发)
+     * - 更新 currentStepIndex
+     * 注意:不会自动执行新步骤,调用方需自行 triggerCurrentStep 或靠 UI 触发
+     *
+     * @return true=跳转成功,false=未找到 stepId 或流程未运行
+     */
+    public boolean jumpToStep(String stepId) {
+        if (!running) {
+            log.warn("[{}] 流程未运行,无法跳转步骤", context.getStationCode());
+            return false;
+        }
+        if (!stepMap.containsKey(stepId)) {
+            log.warn("[{}] 未找到步骤: {}", context.getStationCode(), stepId);
+            return false;
+        }
+        int targetIndex = -1;
+        for (int i = 0; i < steps.size(); i++) {
+            if (steps.get(i).getStepId().equals(stepId)) {
+                targetIndex = i;
+                break;
+            }
+        }
+        if (targetIndex < 0) return false;
+
+        // 取消当前步骤的超时和 async 回调(避免旧步骤干扰)
+        cancelTimeout();
+        IWorkflowStep oldStep = getCurrentStep();
+        if (oldStep != null) {
+            oldStep.setAsyncCompleteCallback(null);
+        }
+        context.setCurrentStepIndex(targetIndex);
+        log.warn("[{}] 强制跳转到步骤: {} (index={})",
+                context.getStationCode(), stepId, targetIndex);
+        return true;
+    }
+
+    /**
      * 获取当前步骤索引
      */
     public int getCurrentStepIndex() {

+ 128 - 13
src/com/mes/ui/MainFrame.java

@@ -88,6 +88,11 @@ public class MainFrame extends JFrame {
     private JMenuItem deviceInfoUiItem;
     // 界面布局子菜单(mes123解锁后显示)
     private JMenu layoutMenu;
+    // 手动提交子菜单(解锁后显示;若勾选"始终显示"则不受解锁控制)
+    private JMenu manualSubmitMenu;
+    // 始终显示子菜单(解锁后显示,用于勾选哪些菜单绕过解锁)
+    private JMenu alwaysShowMenu;
+    private JCheckBoxMenuItem manualSubmitAlwaysItem;
     // 工具栏项目显示按钮
     private JButton projectMenu;
     
@@ -262,7 +267,7 @@ public class MainFrame extends JFrame {
         featuresItem.addActionListener(e -> showFeaturesDialog());
         projectDeviceMenu.add(featuresItem);
 
-        deviceInfoUiItem = new JMenuItem("设备信息显示...");
+        deviceInfoUiItem = new JMenuItem("设备信息显示");
         deviceInfoUiItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
         deviceInfoUiItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
         deviceInfoUiItem.addActionListener(e -> showDeviceInfoUiDialog());
@@ -300,6 +305,48 @@ public class MainFrame extends JFrame {
         importLayoutItem.addActionListener(e -> importUILayout());
         layoutMenu.add(importLayoutItem);
 
+        // ========== 子菜单:手动提交(每个工位一个菜单项) ==========
+        // 默认隐藏,解锁后显示;若在"始终显示"中勾选则无视解锁状态
+        manualSubmitMenu = new JMenu("手动提交");
+        manualSubmitMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
+        manualSubmitMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        manualSubmitMenu.setVisible(false);
+        settingMenu.add(manualSubmitMenu);
+
+        int stationCount = config.getStationCount();
+        for (int i = 0; i < stationCount; i++) {
+            final int idx = i;
+            StationConfig.StationInfo st = config.getStation(i);
+            String label = stationCount > 1
+                    ? "手动提交 工位" + (i + 1) + (st != null ? " (" + st.getCode() + ")" : "")
+                    : "手动提交" + (st != null ? " (" + st.getCode() + ")" : "");
+            JMenuItem item = new JMenuItem(label);
+            item.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
+            item.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+            item.addActionListener(e -> manualEnableSubmit(idx));
+            manualSubmitMenu.add(item);
+        }
+
+        // ========== 子菜单:始终显示(管理绕过解锁的白名单) ==========
+        alwaysShowMenu = new JMenu("始终显示");
+        alwaysShowMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/bar_setting.png"))));
+        alwaysShowMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        alwaysShowMenu.setVisible(false);
+        settingMenu.add(alwaysShowMenu);
+
+        manualSubmitAlwaysItem = new JCheckBoxMenuItem("手动提交",
+                com.mes.core.UiPrefs.getInstance().isAlwaysShow(com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT));
+        manualSubmitAlwaysItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        manualSubmitAlwaysItem.addItemListener(e -> {
+            com.mes.core.UiPrefs.getInstance().setAlwaysShow(
+                    com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT, manualSubmitAlwaysItem.isSelected());
+            applyLockVisibility(!workflowTabVisible);
+        });
+        alwaysShowMenu.add(manualSubmitAlwaysItem);
+
+        // 初始按偏好设置一次显隐(默认锁定)
+        applyLockVisibility(true);
+
         // 内容面板
         contentPane = new JPanel();
         contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
@@ -669,6 +716,46 @@ public class MainFrame extends JFrame {
     }
 
     /**
+     * 手动启用指定工位的 OK/NG 按钮(救急用:设备完成信号缺失或拉铆数量未达预设时)
+     * 参考 OP220A:只解锁按钮,用户自行点 OK 走正常提交路径。
+     */
+    private void manualEnableSubmit(int stationIndex) {
+        if (stationIndex < 0 || stationIndex >= contexts.size()) {
+            log.warn("手动提交: 无效工位索引 {}", stationIndex);
+            return;
+        }
+        StationContext context = contexts.get(stationIndex);
+        WorkflowEngine engine = engines.get(stationIndex);
+
+        // 必须先扫码
+        String sn = context.getProductSn();
+        if (sn == null || sn.trim().isEmpty()) {
+            JOptionPane.showMessageDialog(this,
+                    "请先扫描工件码再执行手动提交",
+                    "手动提交", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
+        log.warn("[{}] 手动提交:强制启用 OK/NG 按钮 (sn={})", context.getStationCode(), sn);
+
+        // 1) 置位 UploadResultStep.canExecute 所需的全部条件
+        context.setQualityPassed(true);
+        context.setWorkStarted(true);          // auto 模式下必须
+        context.setMaterialValidated(true);    // 有物料配置时必须
+        context.setMaterialBound(true);        // 跳过 MBDW 阶段
+        context.setResultUploaded(false);
+        context.setWaitingForUserAction(true);
+
+        // 2) 跳到 upload_result 步骤,保证 submitResult 走 "currentStep instanceof UploadResultStep" 分支
+        engine.jumpToStep("upload_result");
+
+        // 3) 标记按钮强制启用(面板 refresh 时绕过 auto 模式禁用逻辑;reset() 会自动清除)
+        context.setExtra("forceSubmitEnabled", Boolean.TRUE);
+        context.setStatusMessage("手动提交已启用,请点击OK完成", 0);
+        context.updateUI();
+    }
+
+    /**
      * 提交结果
      * 手动模式下:用户点击OK/NG后,按顺序执行 StartWorkStep -> UploadResultStep
      */
@@ -1196,11 +1283,7 @@ public class MainFrame extends JFrame {
             workflowMonitorPanel.refresh();
             log.info("流程监控Tab已解锁显示");
         }
-        
-        // 同时显示隐藏的设置菜单项
-        editModeSeparator.setVisible(true);
-        projectDeviceMenu.setVisible(true);
-        layoutMenu.setVisible(true);
+        applyLockVisibility(false);
     }
     
     /**
@@ -1212,11 +1295,27 @@ public class MainFrame extends JFrame {
             workflowTabVisible = false;
             log.info("流程监控Tab已隐藏");
         }
-        
-        // 同时隐藏设置菜单项
-        editModeSeparator.setVisible(false);
-        projectDeviceMenu.setVisible(false);
-        layoutMenu.setVisible(false);
+        applyLockVisibility(true);
+    }
+
+    /**
+     * 根据锁定状态统一应用所有"需解锁"菜单的显隐
+     * - locked=true  所有需解锁项默认隐藏;但"始终显示"白名单中的保留可见
+     * - locked=false 所有需解锁项全部可见
+     */
+    private void applyLockVisibility(boolean locked) {
+        boolean unlocked = !locked;
+        if (editModeSeparator != null) editModeSeparator.setVisible(unlocked);
+        if (projectDeviceMenu != null) projectDeviceMenu.setVisible(unlocked);
+        if (layoutMenu != null) layoutMenu.setVisible(unlocked);
+        if (alwaysShowMenu != null) alwaysShowMenu.setVisible(unlocked);
+
+        // 手动提交:尊重"始终显示"勾选
+        if (manualSubmitMenu != null) {
+            boolean always = com.mes.core.UiPrefs.getInstance()
+                    .isAlwaysShow(com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT);
+            manualSubmitMenu.setVisible(unlocked || always);
+        }
     }
 
     // ========== 项目切换/设备IP修改 ==========
@@ -1687,9 +1786,21 @@ public class MainFrame extends JFrame {
         // 物料码输入框联动:勾了冷板或底护板 → 显示;都没勾 → 隐藏
         boolean coldOn = Boolean.TRUE.equals(override.get("cold_plate"));
         boolean bottomOn = Boolean.TRUE.equals(override.get("bottom_plate"));
+        // 拉铆流程联动:
+        //  - 关闭 riveting: 两行(拉铆数量)都隐藏
+        //  - 开启 riveting: 自动开启第1行;第2行保持用户设置
+        boolean rivetingOn = Boolean.TRUE.equals(override.get("riveting"));
+        Boolean row1Enabled = pc.getDeviceRow1EnabledOverride();
+        Boolean row2Enabled = pc.getDeviceRow2EnabledOverride();
+        if (!rivetingOn) {
+            row1Enabled = Boolean.FALSE;
+            row2Enabled = Boolean.FALSE;
+        } else {
+            row1Enabled = Boolean.TRUE;
+        }
         pc.setDeviceInfoRowsOverride(
-                pc.getDeviceRow1EnabledOverride(), pc.getDeviceRow1LabelOverride(),
-                pc.getDeviceRow2EnabledOverride(), pc.getDeviceRow2LabelOverride(),
+                row1Enabled, pc.getDeviceRow1LabelOverride(),
+                row2Enabled, pc.getDeviceRow2LabelOverride(),
                 coldOn || bottomOn);
 
         // 重新加载 yaml 配置(会按新 override 展开 steps)
@@ -1707,6 +1818,10 @@ public class MainFrame extends JFrame {
         for (WorkstationPanel p : panels) {
             p.refreshDisplayFromConfig(config);
         }
+        // 若行数变化涉及组件增删,强制重载布局(与"设备信息显示"对话框保持一致)
+        for (WorkstationPanel p : panels) {
+            try { p.reloadLayoutConfig(); } catch (Exception ex) { /* 忽略 */ }
+        }
 
         JOptionPane.showMessageDialog(this,
                 "工艺流程已更新,所有工位已重建",

+ 43 - 0
src/com/mes/ui/component/WorkstationPanel.java

@@ -859,6 +859,11 @@ public class WorkstationPanel extends JPanel {
         } else {
             canSubmit = false;
         }
+        // 手动提交菜单启用标志:无视模式强制亮起 OK/NG
+        Boolean forceSubmit = context.getExtra("forceSubmitEnabled", Boolean.FALSE);
+        if (Boolean.TRUE.equals(forceSubmit) && !context.isResultUploaded()) {
+            canSubmit = true;
+        }
         okButton.setEnabled(canSubmit);
         ngButton.setEnabled(canSubmit);
 
@@ -2147,9 +2152,14 @@ public class WorkstationPanel extends JPanel {
     /**
      * 重新加载布局配置并刷新界面
      * 用于双工位模式下,编辑完成后同步布局到另一个工位
+     * 同时支持 show_device_info / show_material_input 切换后的动态组件创建与添加
      */
     public void reloadLayoutConfig() {
         log.info("[{}] 重新加载布局配置", stationCode);
+        // 1) 按当前 show_* 开关创建缺失的组件
+        ensureComponentsCreated();
+        // 2) 把尚未添加到面板的可选组件补加进去(幂等:已添加的不重复加)
+        ensureComponentsAdded();
         loadLayoutConfig();
         if (!editMode && getWidth() > 0 && getHeight() > 0) {
             applyRuntimeLayout();
@@ -2157,6 +2167,39 @@ public class WorkstationPanel extends JPanel {
             repaint();
         }
     }
+
+    /**
+     * 把当前 show_* 开关需要的可选组件补加到面板
+     * 幂等:若组件已在本面板中则跳过
+     */
+    private void ensureComponentsAdded() {
+        if (showDeviceInfo) {
+            addIfAbsent(deviceInfoLabel);
+            addIfAbsent(presetCountLabel);
+            addIfAbsent(presetCountField);
+            addIfAbsent(finishedCountLabel);
+            addIfAbsent(finishedCountField);
+        }
+        if (showDeviceInfo2) {
+            addIfAbsent(deviceInfoLabel2);
+            addIfAbsent(presetCountLabel2);
+            addIfAbsent(presetCountField2);
+            addIfAbsent(finishedCountLabel2);
+            addIfAbsent(finishedCountField2);
+        }
+        if (showMaterialInput) {
+            addIfAbsent(materialLabelUI);
+            addIfAbsent(materialSnInput);
+            addIfAbsent(scanMaterialBtn);
+            addIfAbsent(unbindMaterialBtn);
+        }
+    }
+
+    private void addIfAbsent(java.awt.Component c) {
+        if (c == null) return;
+        if (c.getParent() == this) return;
+        add(c);
+    }
     
     /**
      * 创建默认布局配置

+ 1 - 1
src/resources/config/station.yaml

@@ -107,7 +107,7 @@ device:
     address: 4160          # 心跳值寄存器(MES 写入 1/2/3 循环)
     interval: 500          # 心跳周期(ms)
     timeout_address: 4170  # 心跳超时阈值寄存器
-    timeout_value: 10      # 超时阈值(单位100ms,10=1秒)
+    timeout_value: 20      # 超时阈值(单位100ms,10=1秒)
 
   complete_condition:
     type: count