package com.mes.ui; import com.mes.core.*; import com.mes.device.DeviceDriverFactory; import com.mes.device.IDeviceDriver; import com.mes.prod.ProdDataCollector; import com.mes.prod.ProdDataUploader; import com.mes.step.StartWorkStep; import com.mes.step.StepFactory; import com.mes.step.UploadResultStep; import com.mes.step.WaitCompleteStep; import com.mes.tcp.MesTcpClient; import com.mes.tcp.MessageDispatcher; import com.mes.ui.component.WorkstationPanel; import com.mes.ui.component.WorkflowMonitorPanel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.*; import java.util.*; import java.util.List; import java.util.Timer; /** * 主窗口 - 与OP150版本UI保持一致 */ public class MainFrame extends JFrame { private static final Logger log = LoggerFactory.getLogger(MainFrame.class); // 配置 private StationConfig config; // TCP通信 private MesTcpClient tcpClient; private MessageDispatcher messageDispatcher; // 工位管理 private List contexts; private List engines; private List panels; private Map deviceDrivers; // 步骤工厂 private StepFactory stepFactory; // 过程参数上传器 private ProdDataUploader prodDataUploader; // 当前用户和会话信息 private String currentUser = ""; private String sessionId; private int userAuth; private int loginHour; // UI组件 private JPanel contentPane; private JPanel workPanel; // 工作面板引用,用于编辑模式切换 private JTabbedPane tabbedPane; private JButton heartBeatMenu; private JButton userMenu; private JButton toolbarStatusMenu; // 单工位时的状态显示 private Timer heartBeatDisplayTimer; // WebView组件(参考op150) private com.mes.ui.component.MesWebView shiftCheckWebView; // 开班点检 private com.mes.ui.component.MesWebView workRecordWebView; // 工作记录 // 流程监控面板 private WorkflowMonitorPanel workflowMonitorPanel; private boolean workflowTabVisible = false; // 需要密码解锁的设置菜单项 private JSeparator editModeSeparator; private JCheckBoxMenuItem editModeItem; private JMenuItem saveLayoutItem; private JMenuItem exportLayoutItem; private JMenuItem importLayoutItem; // 项目/设备子菜单(mes123解锁后显示) private JMenu projectDeviceMenu; private JMenuItem switchProjectItem; // 切换产线位置 private JMenuItem switchProductItem; // 切换产品号 private JMenuItem switchStationItem; private JMenuItem deviceIpItem; private JMenuItem serverIpItem; private JMenuItem featuresItem; private JMenuItem deviceInfoUiItem; // 界面布局子菜单(mes123解锁后显示) private JMenu layoutMenu; // 手动提交子菜单(解锁后显示;若勾选"始终显示"则不受解锁控制) private JMenu manualSubmitMenu; // 始终显示子菜单(解锁后显示,用于勾选哪些菜单绕过解锁) private JMenu alwaysShowMenu; private JCheckBoxMenuItem manualSubmitAlwaysItem; // 工具栏项目显示按钮 private JButton projectMenu; // 三击解锁相关 private int userMenuClickCount = 0; private long lastUserMenuClickTime = 0; private static final int TRIPLE_CLICK_INTERVAL = 1500; // 1.5秒内完成3次点击 private static final String WORKFLOW_PASSWORD = "mes123"; // 免密码解锁序列:2次用户 -> 2次项目 -> 2次心跳 private int unlockStage = 0; // 0=等用户, 1=等项目, 2=等心跳 private int unlockStageCount = 0; // 当前阶段已点击次数 private long unlockLastClickTime = 0; private static final int UNLOCK_INTERVAL = 2000; // 每次点击最大间隔 2s public MainFrame() { this(StationConfig.getInstance()); } public MainFrame(StationConfig config) { this.config = config; this.contexts = new ArrayList<>(); this.engines = new ArrayList<>(); this.panels = new ArrayList<>(); this.deviceDrivers = new HashMap<>(); initFrame(); initComponents(); initTcpClient(); initWorkstations(); } /** * 初始化窗口 */ private void initFrame() { String codes = getStationCodesString(); // 标题优先用 OprnoRegistry 按当前产线位置推导的工位名称,其次 yaml 的 name String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId(); String name = ""; if (!config.getStations().isEmpty()) { name = com.mes.core.OprnoRegistry.getGwDes(lineId, config.getStations().get(0).getCode()); } if ((name == null || name.isEmpty()) && config.getStationName() != null) { name = config.getStationName(); } String title = "MES系统客户端:" + codes; if (name != null && !name.isEmpty() && !name.equals(codes)) { title += " - " + name; } setTitle(title); setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/resources/image/bg/logo.png"))); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 根据工位数量设置窗口初始宽度:双工位宽度是单工位的两倍 int stationCount = config.getStations() != null ? config.getStations().size() : 1; int baseWidth = 1024; int windowWidth = stationCount > 1 ? (int)(baseWidth * 1) : baseWidth; setBounds(0, 0, windowWidth, 768); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onWindowClosing(); } }); } /** * 初始化UI组件 - 参照OP150版本 */ private void initComponents() { // 菜单栏 JMenuBar menuBar = new JMenuBar(); menuBar.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 26)); setJMenuBar(menuBar); // 用户菜单 JMenu fileMenu = new JMenu("用户"); fileMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/user.png")))); fileMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); menuBar.add(fileMenu); JMenuItem exitMenuItem = new JMenuItem("退出"); exitMenuItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/logoff.png")))); exitMenuItem.setFont(new Font("微软雅黑", Font.PLAIN, 22)); exitMenuItem.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { logoff(); } }); fileMenu.add(exitMenuItem); // 设置菜单 JMenu settingMenu = new JMenu("设置"); settingMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); settingMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); menuBar.add(settingMenu); // 重连MES JMenuItem resetTcpMenu = new JMenuItem("重连MES"); resetTcpMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png")))); resetTcpMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); resetTcpMenu.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { reconnectMes(); } }); settingMenu.add(resetTcpMenu); // 为每个工位添加刷新菜单 for (int i = 0; i < config.getStationCount(); i++) { final int stationIndex = i; StationConfig.StationInfo station = config.getStation(i); if (station != null) { JMenuItem refreshMenu = new JMenuItem("刷新" + station.getCode()); refreshMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png")))); refreshMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); refreshMenu.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { resetStation(stationIndex); } }); settingMenu.add(refreshMenu); } } editModeSeparator = new JSeparator(); editModeSeparator.setVisible(false); settingMenu.add(editModeSeparator); // ========== 子菜单:项目与设备(默认隐藏,密码解锁后显示) ========== projectDeviceMenu = new JMenu("项目与设备"); projectDeviceMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); projectDeviceMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); projectDeviceMenu.setVisible(false); settingMenu.add(projectDeviceMenu); switchProjectItem = new JMenuItem(); switchProjectItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); switchProjectItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); switchProjectItem.addActionListener(e -> showSwitchLineDialog()); projectDeviceMenu.add(switchProjectItem); switchProductItem = new JMenuItem(); switchProductItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); switchProductItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); switchProductItem.addActionListener(e -> showSwitchProductDialog()); projectDeviceMenu.add(switchProductItem); switchStationItem = new JMenuItem(); switchStationItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); switchStationItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); switchStationItem.addActionListener(e -> showSwitchStationDialog()); projectDeviceMenu.add(switchStationItem); deviceIpItem = new JMenuItem(); deviceIpItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); deviceIpItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); deviceIpItem.addActionListener(e -> showChangeDeviceIpDialog()); projectDeviceMenu.add(deviceIpItem); serverIpItem = new JMenuItem(); serverIpItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); serverIpItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); serverIpItem.addActionListener(e -> showChangeServerIpDialog()); projectDeviceMenu.add(serverIpItem); projectDeviceMenu.addSeparator(); featuresItem = new JMenuItem("工艺流程配置"); featuresItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png")))); featuresItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); featuresItem.addActionListener(e -> showFeaturesDialog()); projectDeviceMenu.add(featuresItem); 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()); projectDeviceMenu.add(deviceInfoUiItem); refreshMenuTexts(); // 首次填充显示当前值 // ========== 子菜单:界面布局(默认隐藏,密码解锁后显示) ========== layoutMenu = new JMenu("界面布局"); layoutMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/bar_setting.png")))); layoutMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); layoutMenu.setVisible(false); settingMenu.add(layoutMenu); editModeItem = new JCheckBoxMenuItem("UI编辑模式", false); editModeItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); editModeItem.addItemListener(e -> toggleEditMode(editModeItem.isSelected())); layoutMenu.add(editModeItem); saveLayoutItem = new JMenuItem("保存UI布局"); saveLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/save_bg.png")))); saveLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); saveLayoutItem.addActionListener(e -> saveUILayout()); layoutMenu.add(saveLayoutItem); exportLayoutItem = new JMenuItem("导出布局配置"); exportLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/download.png")))); exportLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); exportLayoutItem.addActionListener(e -> exportUILayout()); layoutMenu.add(exportLayoutItem); importLayoutItem = new JMenuItem("导入布局配置"); importLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/open_file.png")))); importLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20)); 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)); setContentPane(contentPane); contentPane.setLayout(new BorderLayout(0, 0)); // 工具栏 - 完全参考OP40 JToolBar toolBar = new JToolBar(); contentPane.add(toolBar, BorderLayout.NORTH); // 状态标签 JLabel statusLabel = new JLabel("状态:"); statusLabel.setHorizontalAlignment(SwingConstants.CENTER); statusLabel.setForeground(Color.BLACK); statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); statusLabel.setBackground(Color.LIGHT_GRAY); toolBar.add(statusLabel); toolbarStatusMenu = new JButton("等待加工信号"); toolbarStatusMenu.setForeground(Color.GREEN); toolbarStatusMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); toolbarStatusMenu.setBackground(Color.BLACK); toolBar.add(toolbarStatusMenu); toolBar.add(new JLabel(" ")); // 心跳标签 JLabel heartBeatLabel = new JLabel("心跳:"); heartBeatLabel.setHorizontalAlignment(SwingConstants.CENTER); heartBeatLabel.setForeground(Color.BLACK); heartBeatLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); heartBeatLabel.setBackground(Color.LIGHT_GRAY); toolBar.add(heartBeatLabel); // 心跳显示(带时间)- 完全参考OP40 heartBeatMenu = new JButton(""); heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/green_dot.png")))); heartBeatMenu.setForeground(Color.GREEN); heartBeatMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); heartBeatMenu.setBackground(Color.BLACK); heartBeatMenu.addActionListener(e -> onUnlockClick("heart")); toolBar.add(heartBeatMenu); toolBar.add(new JLabel(" ")); // 用户标签 JLabel userLabel = new JLabel("登录用户:"); userLabel.setHorizontalAlignment(SwingConstants.CENTER); userLabel.setForeground(Color.BLACK); userLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); userLabel.setBackground(Color.LIGHT_GRAY); toolBar.add(userLabel); userMenu = new JButton(""); userMenu.setForeground(Color.GREEN); userMenu.setFont(new Font("微软雅黑", Font.PLAIN, 22)); userMenu.setBackground(Color.BLACK); userMenu.addActionListener(e -> onUserMenuClicked()); toolBar.add(userMenu); toolBar.add(new JLabel(" ")); // 当前项目显示(只读展示,切换在"设置"菜单中) JLabel projectLabel = new JLabel("项目:"); projectLabel.setHorizontalAlignment(SwingConstants.CENTER); projectLabel.setForeground(Color.BLACK); projectLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); projectLabel.setBackground(Color.LIGHT_GRAY); toolBar.add(projectLabel); projectMenu = new JButton(""); projectMenu.setForeground(Color.YELLOW); projectMenu.setFont(new Font("微软雅黑", Font.PLAIN, 22)); projectMenu.setBackground(Color.BLACK); projectMenu.setFocusable(false); projectMenu.addActionListener(e -> onUnlockClick("project")); toolBar.add(projectMenu); updateProjectDisplay(); // 订阅产线位置/产品号切换 → 刷新工具栏显示 com.mes.core.ProjectConfigManager.getInstance().addListener(new com.mes.core.ProjectConfigManager.Listener() { @Override public void onLineChanged(com.mes.core.ProjectConfigManager.Line line) { SwingUtilities.invokeLater(() -> updateProjectDisplay()); } @Override public void onProductChanged(com.mes.core.ProjectConfigManager.Product product) { SwingUtilities.invokeLater(() -> updateProjectDisplay()); } }); // 标签页 tabbedPane = new JTabbedPane(JTabbedPane.TOP); tabbedPane.setMinimumSize(new Dimension(400, 50)); tabbedPane.setFont(new Font("宋体", Font.BOLD, 22)); contentPane.add(tabbedPane); // 工作面板 - 直接添加,支持等比例缩放 JPanel workPanel = createWorkPanel(); tabbedPane.addTab("工作面板", new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/a_side.png"))), workPanel, null); tabbedPane.setEnabledAt(0, true); // 开班点检Tab - 使用WebView(参考op150) // URL格式: http://{server_ip}:8980/js/a/mes/mesProcessCheckRecord/ulist?oprno={oprno}&lineSn={line_sn} String shiftCheckOprno = config.getStation(0) != null ? config.getStation(0).getCode() : ""; String shiftCheckUrl = String.format("http://%s:%d/js/a/mes/mesProcessCheckRecord/ulist?oprno=%s&lineSn=%s", config.getServerIp(), config.getHttpPort(), shiftCheckOprno, config.getLineSn()); shiftCheckWebView = new com.mes.ui.component.MesWebView(shiftCheckUrl); tabbedPane.addTab("开班点检", new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))), shiftCheckWebView, null); // 工作记录Tab - 使用WebView(参考op150) // URL格式: http://{server_ip}:8980/js/a/mes/mesProductRecord/work?oprno={oprno}&lineSn={line_sn} String workRecordOprno = config.getStation(0) != null ? config.getStation(0).getCode() : ""; String workRecordUrl = String.format("http://%s:%d/js/a/mes/mesProductRecord/work?oprno=%s&lineSn=%s", config.getServerIp(), config.getHttpPort(), workRecordOprno, config.getLineSn()); workRecordWebView = new com.mes.ui.component.MesWebView(workRecordUrl); tabbedPane.addTab("工作记录", new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))), workRecordWebView, null); // 流程监控Tab - 默认隐藏,需要三击用户按钮并输入密码解锁 workflowMonitorPanel = new WorkflowMonitorPanel(); // 切换Tab时自动刷新 tabbedPane.addChangeListener(e -> { int selectedIndex = tabbedPane.getSelectedIndex(); if (selectedIndex < 0) return; String tabTitle = tabbedPane.getTitleAt(selectedIndex); if ("开班点检".equals(tabTitle) && shiftCheckWebView != null) { shiftCheckWebView.reload(); } else if ("工作记录".equals(tabTitle) && workRecordWebView != null) { workRecordWebView.reload(); } else if ("流程".equals(tabTitle) && workflowMonitorPanel != null) { workflowMonitorPanel.refresh(); } }); log.info("UI组件初始化完成"); } /** * 创建工作面板 */ private JPanel createWorkPanel() { workPanel = new JPanel(); if (config.isDualMode()) { // 双工位布局 - 使用GridLayout自适应,中间有间距 workPanel.setLayout(new GridLayout(1, 2, 20, 0)); workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); } else { // 单工位布局 - 使用 BorderLayout 让面板填满整个工作区域 // 这样面板大小固定,不会随模式切换而变化 workPanel.setLayout(new BorderLayout()); workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); } // 创建工位面板 for (int i = 0; i < config.getStationCount(); i++) { StationConfig.StationInfo stationInfo = config.getStation(i); if (stationInfo != null) { // 单工位不显示header(工位号和状态栏),双工位显示 boolean showHeader = config.isDualMode(); WorkstationPanel panel = new WorkstationPanel( stationInfo.getCode(), stationInfo.getIndex(), config, showHeader ); // 单工位时,把工具栏状态按钮绑定到面板 if (!showHeader) { panel.setExternalStatusMenu(toolbarStatusMenu); } panels.add(panel); if (config.isDualMode()) { workPanel.add(panel); } else { // 单工位:面板填满整个工作区域 workPanel.add(panel, BorderLayout.CENTER); } } } return workPanel; } /** * 初始化TCP客户端 */ private void initTcpClient() { messageDispatcher = new MessageDispatcher(); tcpClient = new MesTcpClient(config); tcpClient.setDispatcher(messageDispatcher); tcpClient.setConnectionListener(new MesTcpClient.ConnectionListener() { @Override public void onConnected() { SwingUtilities.invokeLater(() -> updateConnectionStatus(true)); for (StationContext ctx : contexts) { tcpClient.sendSync(ctx.getStationCode()); } } @Override public void onDisconnected() { SwingUtilities.invokeLater(() -> updateConnectionStatus(false)); } @Override public void onReconnecting() { SwingUtilities.invokeLater(() -> { heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull( getClass().getResource("/resources/image/bg/grey_dot.png")))); }); } }); stepFactory = new StepFactory(tcpClient); log.info("TCP客户端初始化完成"); } /** * 初始化工位 */ private void initWorkstations() { if (config.isDeviceEnabled()) { deviceDrivers = DeviceDriverFactory.createDrivers(config); } // 初始化过程参数上传器 if (config.hasProdParams()) { int uploadInterval = config.getProdParamsConfig().getUploadInterval(); prodDataUploader = new ProdDataUploader( config.getServerIp(), config.getHttpPort(), uploadInterval); } for (int i = 0; i < config.getStationCount(); i++) { StationConfig.StationInfo stationInfo = config.getStation(i); if (stationInfo == null) continue; // 创建上下文 StationContext context = StationContext.fromConfig(stationInfo, config.getLineSn()); context.setDeviceEnabled(config.isDeviceEnabled()); if (deviceDrivers.containsKey(i)) { context.setDeviceDriver(deviceDrivers.get(i)); } // 设置第二行设备驱动(可能来自不同IP的设备) StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1); if (row2 != null) { int connIdx = row2.getConnectionIndex(); if (deviceDrivers.containsKey(connIdx)) { context.setDeviceDriver2(deviceDrivers.get(connIdx)); } } contexts.add(context); // 创建流程引擎 WorkflowEngine engine = new WorkflowEngine(context); engine.buildFromConfig(config.getWorkflowSteps(), stepFactory); // 注入过程参数采集器到WaitCompleteStep if (config.hasProdParams()) { ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig()); for (com.mes.step.IWorkflowStep step : engine.getSteps()) { if (step instanceof WaitCompleteStep) { ((WaitCompleteStep) step).setProdDataCollector(collector); log.info("[{}] 过程参数采集器已注入", stationInfo.getCode()); } // 注入到 ResetStationStep 供副机模式立即上传使用 stepFactory.injectProdParams(step, collector, prodDataUploader); } prodDataUploader.addCollector(stationInfo.getCode(), collector); } engine.setListener(new WorkflowEngine.WorkflowListener() { @Override public void onStepStarted(WorkflowEngine eng, com.mes.step.IWorkflowStep step) { log.debug("[{}] 步骤开始: {}", context.getStationCode(), step.getStepName()); } @Override public void onStepCompleted(WorkflowEngine eng, com.mes.step.IWorkflowStep step, boolean success) { log.debug("[{}] 步骤完成: {} (成功={})", context.getStationCode(), step.getStepName(), success); } @Override public void onWorkflowCompleted(WorkflowEngine eng) { log.info("[{}] 流程完成", context.getStationCode()); eng.reset(); context.setStatusMessage("结果已提交,请扫下一件", 0); } @Override public void onWorkflowError(WorkflowEngine eng, String error) { log.error("[{}] 流程错误: {}", context.getStationCode(), error); } }); engines.add(engine); // 注册到消息分发器 messageDispatcher.registerStation(stationInfo.getCode(), engine, context); // 绑定UI面板 if (i < panels.size()) { WorkstationPanel panel = panels.get(i); context.setUiPanel(panel); // 注入panel引用到需要弹出扫码框的步骤 for (com.mes.step.IWorkflowStep step : engine.getSteps()) { stepFactory.injectPanel(step, panel); } bindPanelEvents(panel, context, engine); log.info("[{}] UI面板绑定完成", stationInfo.getCode()); } log.info("工位初始化完成: {}", stationInfo.getCode()); } // 设置流程监控面板的工位列表 if (workflowMonitorPanel != null) { workflowMonitorPanel.setWorkstations(engines, contexts); log.info("流程监控面板初始化完成"); } // 启动过程参数上传器 if (prodDataUploader != null) { prodDataUploader.start(); } } /** * 绑定面板事件 */ private void bindPanelEvents(WorkstationPanel panel, StationContext context, WorkflowEngine engine) { panel.setListener(new WorkstationPanel.WorkstationPanelListener() { @Override public void onProductScanned(WorkstationPanel p, String productSn) { context.setProductSn(productSn); context.setUser(currentUser); engine.triggerCurrentStep(); } @Override public void onMaterialScanned(WorkstationPanel p, String materialSn) { context.setMaterialSn(materialSn); engine.triggerCurrentStep(); } @Override public void onOkClicked(WorkstationPanel p) { submitResult(context, engine, "OK"); } @Override public void onNgClicked(WorkstationPanel p) { submitResult(context, engine, "NG"); } @Override public void onUnbindClicked(WorkstationPanel p) { log.info("[{}] 解绑操作", context.getStationCode()); // TODO: 实现解绑逻辑 } }); } /** * 手动启用指定工位的 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 */ private void submitResult(StationContext context, WorkflowEngine engine, String result) { log.info("[{}] 用户提交结果: {}, 当前状态: qualityPassed={}, workStarted={}, waitingForUserAction={}", context.getStationCode(), result, context.isQualityPassed(), context.isWorkStarted(), context.isWaitingForUserAction()); com.mes.step.IWorkflowStep currentStep = engine.getCurrentStep(); log.info("[{}] 当前步骤: {}", context.getStationCode(), currentStep != null ? currentStep.getStepId() : "null"); // 设置结果到 UploadResultStep(不管当前在哪一步) com.mes.step.IWorkflowStep uploadStep = engine.getStep("upload_result"); if (uploadStep instanceof UploadResultStep) { ((UploadResultStep) uploadStep).setResult(result); } else { // 副机模式: 关闭 quality_submit 时 upload_result 不加载, OK/NG 按钮本应隐藏 // 若用户仍触发到这里, 直接跳到 reset_station 完成闭环 log.warn("[{}] quality_submit 已关闭, 无 upload_result 步骤, 直接重置工位", context.getStationCode()); if (engine.jumpToStep("reset_station")) { engine.triggerCurrentStep(); } return; } if (currentStep instanceof UploadResultStep) { // 当前步骤已经是上传结果,直接触发 log.info("[{}] 当前步骤是 UploadResultStep,直接触发", context.getStationCode()); engine.triggerCurrentStep(); } else if (currentStep instanceof StartWorkStep) { // 当前步骤是开始工作(手动模式),标记用户已确认并触发 log.info("[{}] 手动模式:触发 StartWorkStep", context.getStationCode()); context.setWaitingForUserAction(false); engine.triggerCurrentStep(); } else { // 其他情况:直接发送(兼容旧逻辑) log.warn("[{}] 当前步骤非预期: {}, 直接发送结果", context.getStationCode(), currentStep != null ? currentStep.getStepId() : "null"); // 先发送 MKSW,再发送 MQDW tcpClient.sendStartWork( context.getProcessedProductSn(), context.getUser(), context.getStationCode() ); if (uploadStep instanceof UploadResultStep) { tcpClient.sendQuality( context.getProcessedProductSn(), result, context.getUser(), context.getStationCode() ); } } } /** * 更新连接状态 */ private void updateConnectionStatus(boolean connected) { if (connected) { heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull( getClass().getResource("/resources/image/bg/green_dot.png")))); startHeartBeatDisplay(); } else { heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull( getClass().getResource("/resources/image/bg/grey_dot.png")))); stopHeartBeatDisplay(); } } /** * 启动心跳显示 */ private void startHeartBeatDisplay() { stopHeartBeatDisplay(); heartBeatDisplayTimer = new Timer("HeartBeatDisplay", true); heartBeatDisplayTimer.scheduleAtFixedRate(new TimerTask() { private boolean green = true; @Override public void run() { SwingUtilities.invokeLater(() -> { // 更新时间显示 String timeStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()); heartBeatMenu.setText(timeStr); if (green) { heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull( getClass().getResource("/resources/image/bg/green_dot.png")))); } else { heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull( getClass().getResource("/resources/image/bg/grey_dot.png")))); } green = !green; }); } }, 0, 1000); } /** * 停止心跳显示 */ private void stopHeartBeatDisplay() { if (heartBeatDisplayTimer != null) { heartBeatDisplayTimer.cancel(); heartBeatDisplayTimer = null; } } /** * 获取工位号字符串 */ private String getStationCodesString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < config.getStationCount(); i++) { if (i > 0) sb.append("/"); StationConfig.StationInfo station = config.getStation(i); if (station != null) { sb.append(station.getCode()); } } return sb.toString(); } /** * 重连MES */ private void reconnectMes() { log.info("手动重连MES"); tcpClient.disconnect(); tcpClient.connect(); } /** * 重置工位 */ private void resetStation(int stationIndex) { if (stationIndex >= 0 && stationIndex < contexts.size()) { StationContext context = contexts.get(stationIndex); WorkflowEngine engine = engines.get(stationIndex); log.info("[{}] 手动刷新工位", context.getStationCode()); context.setStatusMessage("已手动刷新,请扫码", 0); engine.reset(); if (stationIndex < panels.size()) { panels.get(stationIndex).reset(); } } } /** * 退出登录 */ private void logoff() { int result = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "确认", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { shutdown(); System.exit(0); } } /** * 窗口关闭处理 */ private void onWindowClosing() { int result = JOptionPane.showConfirmDialog(this, "确定要退出程序吗?", "确认退出", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { shutdown(); System.exit(0); } } /** * 关闭资源 */ public void shutdown() { log.info("正在关闭..."); stopHeartBeatDisplay(); // 停止过程参数上传器(会尝试上传剩余数据) if (prodDataUploader != null) { prodDataUploader.stop(); } // 停止流程监控面板的刷新 if (workflowMonitorPanel != null) { workflowMonitorPanel.stopRefresh(); } for (WorkflowEngine engine : engines) { engine.destroy(); } for (IDeviceDriver driver : deviceDrivers.values()) { driver.disconnect(); } if (tcpClient != null) { tcpClient.disconnect(); } log.info("关闭完成"); } /** * 启动 */ public void start() { tcpClient.connect(); for (WorkflowEngine engine : engines) { engine.start(); } log.info("主窗口启动完成"); } // ========== Getters & Setters ========== public void setCurrentUser(String user) { this.currentUser = user; userMenu.setText(user); for (StationContext ctx : contexts) { ctx.setUser(user); } } public void setSessionId(String sessionId) { this.sessionId = sessionId; } public String getSessionId() { return sessionId; } public void setUserAuth(int auth) { this.userAuth = auth; } public int getUserAuth() { return userAuth; } public void setLoginHour(int hour) { this.loginHour = hour; } public int getLoginHour() { return loginHour; } public boolean isAdmin() { return userAuth == 2; } public StationConfig getConfig() { return config; } public MesTcpClient getTcpClient() { return tcpClient; } public MessageDispatcher getMessageDispatcher() { return messageDispatcher; } public List getContexts() { return contexts; } public List getEngines() { return engines; } public List getPanels() { return panels; } // ========== UI编辑模式相关方法 ========== /** * 切换编辑模式 */ private void toggleEditMode(boolean editMode) { log.info("切换UI编辑模式: {}", editMode); // 双工位模式下,编辑模式只显示一个面板(因为布局是同步的) if (config.isDualMode() && panels.size() > 1) { if (editMode) { // 进入编辑模式:隐藏第二个面板,调整布局 panels.get(1).setVisible(false); workPanel.setLayout(new GridLayout(1, 1, 0, 0)); // 只对第一个面板启用编辑模式 panels.get(0).setEditMode(true); } else { // 退出编辑模式:先退出编辑状态 panels.get(0).setEditMode(false); // 同步布局到第二个面板(重新加载配置) panels.get(1).reloadLayoutConfig(); // 恢复双面板布局 workPanel.setLayout(new GridLayout(1, 2, 20, 0)); panels.get(1).setVisible(true); } workPanel.revalidate(); workPanel.repaint(); } else { // 单工位模式:直接切换 for (WorkstationPanel panel : panels) { panel.setEditMode(editMode); } } if (editMode) { String modeHint = config.isDualMode() ? "\n注意:双工位模式下只显示一个编辑面板,布局会同步到两个工位" : ""; JOptionPane.showMessageDialog(this, "已进入UI编辑模式\n\n" + "操作说明:\n" + "• 拖动组件可调整位置\n" + "• 拖动边缘和角落可调整大小\n" + "• 右键组件可访问更多选项\n" + "• 组件会自动吸附到网格\n" + "• 编辑完成后记得保存布局\n\n" + "注意:编辑模式下工位功能将被禁用" + modeHint, "UI编辑模式", JOptionPane.INFORMATION_MESSAGE); } } /** * 切换网格显示 */ private void toggleGrid(boolean show) { for (WorkstationPanel panel : panels) { panel.toggleGrid(show); } } /** * 保存UI布局 */ private void saveUILayout() { if (panels.isEmpty()) { JOptionPane.showMessageDialog(this, "没有可保存的工位面板", "提示", JOptionPane.WARNING_MESSAGE); return; } // 保存第一个面板的布局(单/双工位共享布局) panels.get(0).saveLayoutConfig(); // 保存后自动退出编辑模式(找到菜单项并取消选中) exitEditModeAfterSave(); log.info("UI布局已保存"); } /** * 保存后退出编辑模式 */ private void exitEditModeAfterSave() { // 查找设置菜单中的 UI编辑模式 选项并取消勾选 JMenuBar menuBar = getJMenuBar(); if (menuBar != null) { for (int i = 0; i < menuBar.getMenuCount(); i++) { JMenu menu = menuBar.getMenu(i); if (menu != null && "设置".equals(menu.getText())) { for (int j = 0; j < menu.getItemCount(); j++) { JMenuItem item = menu.getItem(j); if (item instanceof JCheckBoxMenuItem) { JCheckBoxMenuItem checkItem = (JCheckBoxMenuItem) item; if ("UI编辑模式".equals(checkItem.getText()) && checkItem.isSelected()) { checkItem.setSelected(false); // 触发 itemStateChanged 事件 toggleEditMode(false); log.info("保存后自动退出编辑模式"); break; } } } break; } } } } /** * 导出UI布局 */ private void exportUILayout() { if (panels.isEmpty()) { JOptionPane.showMessageDialog(this, "没有可导出的工位面板", "提示", JOptionPane.WARNING_MESSAGE); return; } panels.get(0).exportLayoutConfig(); } /** * 导入UI布局 */ private void importUILayout() { if (panels.isEmpty()) { JOptionPane.showMessageDialog(this, "没有可导入布局的工位面板", "提示", JOptionPane.WARNING_MESSAGE); return; } panels.get(0).importLayoutConfig(); // 询问是否应用到所有工位 if (panels.size() > 1) { int result = JOptionPane.showConfirmDialog(this, "是否将导入的布局应用到所有工位?", "应用布局", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { // TODO: 如果需要,可以同步布局到其他工位面板 log.info("布局将应用到所有工位"); } } } // ========== 流程Tab解锁相关方法 ========== /** * 用户按钮点击处理 * - 同步驱动"2用户→2项目→2心跳"免密码序列 * - 保留原三击密码弹框解锁 */ private void onUserMenuClicked() { // 1) 先处理免密码序列 onUnlockClick("user"); // 2) 保留三击密码解锁 long now = System.currentTimeMillis(); if (now - lastUserMenuClickTime > TRIPLE_CLICK_INTERVAL) { userMenuClickCount = 1; } else { userMenuClickCount++; } lastUserMenuClickTime = now; if (userMenuClickCount >= 3) { userMenuClickCount = 0; if (workflowTabVisible) { hideWorkflowTab(); } else { showWorkflowPasswordDialog(); } } } /** * 免密码解锁序列:用户x2 → 项目x2 → 心跳x2 * 每次点击间隔超过 UNLOCK_INTERVAL 或点错按钮则重置 */ private void onUnlockClick(String source) { long now = System.currentTimeMillis(); if (now - unlockLastClickTime > UNLOCK_INTERVAL) { // 超时重置 unlockStage = 0; unlockStageCount = 0; } unlockLastClickTime = now; String expected; switch (unlockStage) { case 0: expected = "user"; break; case 1: expected = "project"; break; case 2: expected = "heart"; break; default: unlockStage = 0; unlockStageCount = 0; return; } if (!expected.equals(source)) { // 点错按钮,重置(但如果 source==user 说明可能是新一轮开始) unlockStage = 0; unlockStageCount = "user".equals(source) ? 1 : 0; return; } unlockStageCount++; if (unlockStageCount >= 2) { // 进入下一阶段 unlockStage++; unlockStageCount = 0; if (unlockStage >= 3) { // 六步完成,解锁 unlockStage = 0; if (workflowTabVisible) { hideWorkflowTab(); } else { showWorkflowTab(); log.info("免密码解锁成功"); } } } } /** * 显示密码输入对话框 */ private void showWorkflowPasswordDialog() { JPasswordField passwordField = new JPasswordField(); passwordField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); int result = JOptionPane.showConfirmDialog(this, passwordField, "请输入密码", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result == JOptionPane.OK_OPTION) { String input = new String(passwordField.getPassword()); if (WORKFLOW_PASSWORD.equals(input)) { showWorkflowTab(); } else { JOptionPane.showMessageDialog(this, "密码错误", "提示", JOptionPane.ERROR_MESSAGE); } } } /** * 显示流程Tab */ private void showWorkflowTab() { if (!workflowTabVisible) { tabbedPane.addTab("流程", new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))), workflowMonitorPanel, null); workflowTabVisible = true; tabbedPane.setSelectedComponent(workflowMonitorPanel); workflowMonitorPanel.refresh(); log.info("流程监控Tab已解锁显示"); } applyLockVisibility(false); } /** * 隐藏流程Tab */ private void hideWorkflowTab() { if (workflowTabVisible) { tabbedPane.remove(workflowMonitorPanel); workflowTabVisible = false; log.info("流程监控Tab已隐藏"); } 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修改 ========== /** * 刷新工具栏上的产线位置/产品号显示 */ private void updateProjectDisplay() { if (projectMenu == null) return; com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine(); com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct(); if (line != null && product != null) { projectMenu.setText(line.getDisplay() + " / " + product.getDisplay()); projectMenu.setToolTipText(String.format( "产线位置:%s
产品号:%s
工件码前缀:%s
冷板码前缀:%s
底护板码前缀:%s", line.getDisplay(), product.getDisplay(), product.getProductPrefix(), product.getColdPlatePrefix(), product.getBottomPlatePrefix())); } } /** * 弹出切换产线位置对话框(只影响工位号→工序名称的映射) */ private void showSwitchLineDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); java.util.List lines = pc.getLines(); String[] options = new String[lines.size()]; int currentIndex = 0; for (int i = 0; i < lines.size(); i++) { com.mes.core.ProjectConfigManager.Line l = lines.get(i); options[i] = l.getDisplay(); if (l.getId().equalsIgnoreCase(pc.getCurrentLineId())) { currentIndex = i; } } JComboBox combo = new JComboBox<>(options); combo.setSelectedIndex(currentIndex); combo.setFont(new Font("微软雅黑", Font.PLAIN, 16)); JPanel panel = new JPanel(new BorderLayout(5, 5)); panel.add(new JLabel("选择产线位置:"), BorderLayout.NORTH); panel.add(combo, BorderLayout.CENTER); int result = JOptionPane.showConfirmDialog(this, panel, "切换产线位置", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result != JOptionPane.OK_OPTION) return; int idx = combo.getSelectedIndex(); if (idx < 0 || idx >= lines.size()) return; com.mes.core.ProjectConfigManager.Line target = lines.get(idx); if (target.getId().equalsIgnoreCase(pc.getCurrentLineId())) { return; } pc.switchLine(target.getId()); // 切换产线位置后同步工位名(标题 + context 的 stationName) String newLineId = target.getId(); for (StationContext ctx : contexts) { String newName = com.mes.core.OprnoRegistry.getGwDes(newLineId, ctx.getStationCode()); if (newName == null || newName.isEmpty()) newName = ctx.getStationCode(); ctx.updateStation(null, newName, null); } if (!config.getStations().isEmpty()) { String code = config.getStations().get(0).getCode(); String name = com.mes.core.OprnoRegistry.getGwDes(newLineId, code); String title = "MES系统客户端:" + code; if (name != null && !name.isEmpty() && !name.equals(code)) title += " - " + name; setTitle(title); } refreshMenuTexts(); updateProjectDisplay(); JOptionPane.showMessageDialog(this, "已切换到:" + target.getDisplay() + "\n工位名称已刷新", "切换成功", JOptionPane.INFORMATION_MESSAGE); } /** * 弹出切换产品号对话框(只影响工件码/冷板码/底护板码前缀) */ private void showSwitchProductDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); java.util.List products = pc.getProducts(); String[] options = new String[products.size()]; int currentIndex = 0; for (int i = 0; i < products.size(); i++) { com.mes.core.ProjectConfigManager.Product p = products.get(i); options[i] = String.format("%s (工件:%s 冷板:%s 底护板:%s)", p.getDisplay(), p.getProductPrefix(), p.getColdPlatePrefix(), p.getBottomPlatePrefix()); if (p.getId().equalsIgnoreCase(pc.getCurrentProductId())) { currentIndex = i; } } JComboBox combo = new JComboBox<>(options); combo.setSelectedIndex(currentIndex); combo.setFont(new Font("微软雅黑", Font.PLAIN, 16)); JPanel panel = new JPanel(new BorderLayout(5, 5)); panel.add(new JLabel("选择产品号:"), BorderLayout.NORTH); panel.add(combo, BorderLayout.CENTER); int result = JOptionPane.showConfirmDialog(this, panel, "切换产品号", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result != JOptionPane.OK_OPTION) return; int idx = combo.getSelectedIndex(); if (idx < 0 || idx >= products.size()) return; com.mes.core.ProjectConfigManager.Product target = products.get(idx); if (target.getId().equalsIgnoreCase(pc.getCurrentProductId())) { return; } pc.switchProduct(target.getId()); // 重置所有工位,避免沿用旧产品号扫到一半的码 for (int i = 0; i < engines.size(); i++) { engines.get(i).reset(); if (i < panels.size()) panels.get(i).reset(); } refreshMenuTexts(); updateProjectDisplay(); JOptionPane.showMessageDialog(this, "已切换到:" + target.getDisplay() + "\n前缀校验立即生效,所有工位已重置", "切换成功", JOptionPane.INFORMATION_MESSAGE); } /** * 弹出修改拉铆设备IP对话框 */ private void showChangeDeviceIpDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); String currentIp = pc.getDeviceIp(); String input = (String) JOptionPane.showInputDialog(this, "请输入拉铆设备IP:", "修改拉铆设备IP", JOptionPane.PLAIN_MESSAGE, null, null, currentIp); if (input == null) return; input = input.trim(); if (input.isEmpty()) return; if (!isValidIp(input)) { JOptionPane.showMessageDialog(this, "IP格式不正确: " + input, "错误", JOptionPane.ERROR_MESSAGE); return; } if (input.equals(currentIp)) return; // 1. 持久化 pc.setDeviceIp(input); // 2. 覆盖当前 StationConfig 中的连接IP for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) { conn.setIp(input); } // 3. 断开并重连设备驱动 reconnectDeviceDrivers(); refreshMenuTexts(); JOptionPane.showMessageDialog(this, "拉铆设备IP已更新为:" + input + "\n已尝试重新连接设备", "修改成功", JOptionPane.INFORMATION_MESSAGE); } /** * 断开并重新创建设备驱动,重新注入到各工位上下文 */ private void reconnectDeviceDrivers() { // 断开旧驱动 if (deviceDrivers != null) { for (IDeviceDriver d : deviceDrivers.values()) { try { d.disconnect(); } catch (Exception e) { log.warn("断开旧设备驱动异常", e); } } } // 重新创建 Map newDrivers = new HashMap<>(); if (config.isDeviceEnabled()) { newDrivers = DeviceDriverFactory.createDrivers(config); } this.deviceDrivers = newDrivers; // 重新注入到每个工位上下文 for (int i = 0; i < contexts.size(); i++) { StationContext ctx = contexts.get(i); if (deviceDrivers.containsKey(i)) { ctx.setDeviceDriver(deviceDrivers.get(i)); } StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1); if (row2 != null) { int connIdx = row2.getConnectionIndex(); if (deviceDrivers.containsKey(connIdx)) { ctx.setDeviceDriver2(deviceDrivers.get(connIdx)); } } } log.info("设备驱动已重新连接: IP={}", com.mes.core.ProjectConfigManager.getInstance().getDeviceIp()); } /** * 修改MES服务器IP(修改完提示重启生效,避免重建TCP客户端的复杂逻辑) */ private void showChangeServerIpDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); String currentIp = config.getServerIp(); String input = (String) JOptionPane.showInputDialog(this, "请输入MES服务器IP:", "修改MES服务器IP", JOptionPane.PLAIN_MESSAGE, null, null, currentIp); if (input == null) return; input = input.trim(); if (input.isEmpty()) return; if (!isValidIp(input)) { JOptionPane.showMessageDialog(this, "IP格式不正确: " + input, "错误", JOptionPane.ERROR_MESSAGE); return; } if (input.equals(currentIp)) return; pc.setServerIpOverride(input); refreshMenuTexts(); JOptionPane.showMessageDialog(this, "MES服务器IP已保存为:" + input + "\n请重启程序生效", "修改成功", JOptionPane.INFORMATION_MESSAGE); } /** * 简单的IPv4格式校验 */ private boolean isValidIp(String ip) { if (ip == null) return false; String[] parts = ip.split("\\."); if (parts.length != 4) return false; for (String part : parts) { try { int n = Integer.parseInt(part); if (n < 0 || n > 255) return false; } catch (NumberFormatException e) { return false; } } return true; } /** * 刷新"项目与设备"子菜单的显示文本,把当前值显示在菜单项后面 */ private void refreshMenuTexts() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); if (switchProjectItem != null) { com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine(); switchProjectItem.setText("切换产线位置 (当前: " + (line != null ? line.getDisplay() : "?") + ")"); } if (switchProductItem != null) { com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct(); switchProductItem.setText("切换产品号 (当前: " + (product != null ? product.getDisplay() : "?") + ")"); } if (switchStationItem != null && !config.getStations().isEmpty()) { String code = config.getStations().get(0).getCode(); String des = com.mes.core.OprnoRegistry.getGwDes(pc.getCurrentLineId(), code); if (des == null || des.isEmpty()) des = "未知工位"; switchStationItem.setText("修改工位号 (当前: " + code + " - " + des + ")"); } if (deviceIpItem != null) { deviceIpItem.setText("修改拉铆设备IP (当前: " + pc.getDeviceIp() + ")"); } if (serverIpItem != null) { serverIpItem.setText("修改MES服务器IP (当前: " + config.getServerIp() + ")"); } } /** * 弹出修改工位号对话框 * 工位列表按当前产线位置(P1-P4)展示;工位名称由 OprnoRegistry 自动推导 */ private void showSwitchStationDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); String lineId = pc.getCurrentLineId(); String currentCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode(); // 拆分当前code为 "基础码" + "后缀" String currentBase = com.mes.core.OprnoRegistry.formatOprno(currentCode); String currentSuffix = currentCode.length() > currentBase.length() ? currentCode.substring(currentBase.length()).toUpperCase() : ""; // 工位号下拉(固定项,不可编辑) JComboBox codeCombo = new JComboBox<>(); codeCombo.setEditable(false); codeCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16)); for (String c : com.mes.core.OprnoRegistry.listCodes(lineId)) { codeCombo.addItem(c); } if (com.mes.core.OprnoRegistry.listCodes(lineId).contains(currentBase)) { codeCombo.setSelectedItem(currentBase); } // 后缀下拉(空 / A-L) String[] suffixes = new String[]{"(无)", "A","B","C","D","E","F","G","H","I","J","K","L"}; JComboBox suffixCombo = new JComboBox<>(suffixes); suffixCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16)); suffixCombo.setSelectedItem(currentSuffix.isEmpty() ? "(无)" : currentSuffix); // 预览 JLabel previewLabel = new JLabel(" "); previewLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); previewLabel.setForeground(new Color(0x1c, 0x6e, 0xa4)); // 工位名显示 JLabel desLabel = new JLabel(" "); desLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); desLabel.setForeground(new Color(0x3a, 0x7a, 0xc0)); Runnable refresh = () -> { String base = (String) codeCombo.getSelectedItem(); String sfx = (String) suffixCombo.getSelectedItem(); if (base == null) base = ""; String sfxReal = ("(无)".equals(sfx) || sfx == null) ? "" : sfx; String full = base + sfxReal; previewLabel.setText("最终工位号: " + (full.isEmpty() ? "-" : full)); String des = com.mes.core.OprnoRegistry.getGwDes(lineId, full); desLabel.setText("工位名称: " + (des.isEmpty() ? "(未知工位)" : des)); }; refresh.run(); codeCombo.addActionListener(e -> refresh.run()); suffixCombo.addActionListener(e -> refresh.run()); JPanel panel = new JPanel(new java.awt.GridBagLayout()); java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints(); gbc.insets = new Insets(4, 4, 4, 4); gbc.anchor = java.awt.GridBagConstraints.WEST; gbc.gridx = 0; gbc.gridy = 0; panel.add(new JLabel("当前产线位置:"), gbc); gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; JLabel projectLbl = new JLabel(lineId); projectLbl.setFont(new Font("微软雅黑", Font.PLAIN, 16)); panel.add(projectLbl, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.fill = java.awt.GridBagConstraints.NONE; panel.add(new JLabel("工位号:"), gbc); gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; panel.add(codeCombo, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.fill = java.awt.GridBagConstraints.NONE; panel.add(new JLabel("后缀:"), gbc); gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; panel.add(suffixCombo, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; panel.add(previewLabel, gbc); gbc.gridy = 4; panel.add(desLabel, gbc); int result = JOptionPane.showConfirmDialog(this, panel, "修改工位号", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result != JOptionPane.OK_OPTION) return; String base = (String) codeCombo.getSelectedItem(); String sfx = (String) suffixCombo.getSelectedItem(); String sfxReal = ("(无)".equals(sfx) || sfx == null) ? "" : sfx; String newCode = (base == null ? "" : base) + sfxReal; if (newCode.isEmpty()) { JOptionPane.showMessageDialog(this, "工位号不能为空", "错误", JOptionPane.ERROR_MESSAGE); return; } if (newCode.equalsIgnoreCase(currentCode)) return; pc.setStationOverride(newCode, null); applyStationChange(newCode); String des = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode); JOptionPane.showMessageDialog(this, "工位已更新为:" + newCode + (des.isEmpty() ? "" : (" - " + des)) + "\n已刷新界面并向MES重新同步", "修改成功", JOptionPane.INFORMATION_MESSAGE); } /** * 热更新工位号:更新 config/context/dispatcher/面板显示/窗口标题,并重新向MES同步 */ private void applyStationChange(String newCode) { String oldCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode(); String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId(); // 1. 更新 config if (!config.getStations().isEmpty()) { config.getStations().get(0).setCode(newCode); } // 2. 更新 context(推导工位名) String name = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode); if (name == null || name.isEmpty()) name = newCode; for (StationContext ctx : contexts) { if (ctx.getStationIndex() == 0) { ctx.updateStation(newCode, name, null); } } // 3. dispatcher 注销旧的、注册新的 if (messageDispatcher != null && !oldCode.isEmpty() && !oldCode.equalsIgnoreCase(newCode)) { messageDispatcher.unregisterStation(oldCode); if (!engines.isEmpty() && !contexts.isEmpty()) { messageDispatcher.registerStation(newCode, engines.get(0), contexts.get(0)); } } // 4. 刷新 WorkstationPanel 工位标签 for (WorkstationPanel p : panels) { p.updateStationCode(newCode); } // 5. 重置工位并向 MES 重新同步 for (int i = 0; i < engines.size(); i++) { engines.get(i).reset(); if (i < panels.size()) panels.get(i).reset(); contexts.get(i).setStatusMessage("工位已切换,请扫下一件", 0); } if (tcpClient != null && !contexts.isEmpty()) { tcpClient.sendSync(contexts.get(0).getStationCode()); } // 5b. 重建过程参数采集器(collector 以工位号为 key) if (prodDataUploader != null && config.hasProdParams()) { prodDataUploader.clearCollectors(); for (int i = 0; i < contexts.size(); i++) { StationContext c = contexts.get(i); ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig()); WorkflowEngine eng = engines.get(i); for (com.mes.step.IWorkflowStep step : eng.getSteps()) { if (step instanceof WaitCompleteStep) { ((WaitCompleteStep) step).setProdDataCollector(collector); } // 注入到 ResetStationStep 供副机模式立即上传使用 stepFactory.injectProdParams(step, collector, prodDataUploader); } prodDataUploader.addCollector(c.getStationCode(), collector); } } // 6. 窗口标题 String title = "MES系统客户端:" + newCode; if (!name.isEmpty() && !name.equals(newCode)) title += " - " + name; setTitle(title); // 7. 刷新菜单文字 refreshMenuTexts(); log.info("工位已切换: {} -> {} ({})", oldCode, newCode, name); } /** * 弹出工艺流程配置对话框 * 用户勾选启用的功能,保存后软重启所有工位流程 */ private void showFeaturesDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); // yaml 定义的功能集合 + 当前有效值 Map yamlFeatures = config.getYamlFeatures(); if (yamlFeatures.isEmpty()) { JOptionPane.showMessageDialog(this, "station.yaml 中未定义 workflow.features", "提示", JOptionPane.WARNING_MESSAGE); return; } // 功能显示名映射 Map labels = new LinkedHashMap<>(); labels.put("check_quality", "质量查询(发AQDW问工件能否加工)"); labels.put("cold_plate", "扫+校验冷板码"); labels.put("bottom_plate", "扫+校验底护板码"); labels.put("riveting", "拉铆流程(心跳+计数+等待+停心跳)"); labels.put("prod_params", "拉铆过程参数采集"); labels.put("quality_submit", "提交工位结果(MBDW+MQDW;关闭=副机模式)"); // 构建复选框(按 yaml 声明顺序) JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15)); Map checkBoxes = new LinkedHashMap<>(); for (String key : yamlFeatures.keySet()) { String label = labels.getOrDefault(key, key); JCheckBox cb = new JCheckBox(label, config.isFeatureEnabled(key)); cb.setFont(new Font("微软雅黑", Font.PLAIN, 18)); cb.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(cb); panel.add(Box.createVerticalStrut(6)); checkBoxes.put(key, cb); } // cold_plate 与 bottom_plate 互斥(勾一个自动取消另一个) final JCheckBox cold = checkBoxes.get("cold_plate"); final JCheckBox bottom = checkBoxes.get("bottom_plate"); if (cold != null && bottom != null) { cold.addActionListener(ev -> { if (cold.isSelected()) bottom.setSelected(false); }); bottom.addActionListener(ev -> { if (bottom.isSelected()) cold.setSelected(false); }); } JLabel hint = new JLabel("冷板/底护板二选一;保存后将重建所有工位流程并重置"); hint.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(hint); // 设备连接开关 panel.add(Box.createVerticalStrut(10)); JSeparator sep = new JSeparator(); sep.setMaximumSize(new java.awt.Dimension(Integer.MAX_VALUE, 1)); sep.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(sep); panel.add(Box.createVerticalStrut(6)); Boolean deviceOverride = pc.getDeviceEnabledOverride(); boolean deviceCurrently = config.isDeviceEnabled(); JCheckBox deviceEnabledCb = new JCheckBox("设备连接(PLC/Modbus)", deviceCurrently); deviceEnabledCb.setFont(new Font("微软雅黑", Font.PLAIN, 18)); deviceEnabledCb.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(deviceEnabledCb); panel.add(Box.createVerticalStrut(4)); JLabel deviceHint = new JLabel("禁用后不连接PLC,上传结果不需要等待设备信号"); deviceHint.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(deviceHint); // 单向联动:取消"拉铆流程"时自动取消"设备连接"(用户可再手动勾回) final JCheckBox rivetingCb = checkBoxes.get("riveting"); if (rivetingCb != null) { rivetingCb.addActionListener(ev -> { if (!rivetingCb.isSelected()) { deviceEnabledCb.setSelected(false); } }); } // 记录旧的"设备连接"状态,用于保存后判断是否需要提示重启 boolean oldDeviceEnabled = deviceCurrently; int result = JOptionPane.showConfirmDialog(this, panel, "工艺流程配置", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result != JOptionPane.OK_OPTION) return; // 收集勾选结果 Map override = new LinkedHashMap<>(); for (Map.Entry e : checkBoxes.entrySet()) { override.put(e.getKey(), e.getValue().isSelected()); } // 二次兜底:如果用户绕过监听同时勾了两个,强制只保留冷板 if (Boolean.TRUE.equals(override.get("cold_plate")) && Boolean.TRUE.equals(override.get("bottom_plate"))) { override.put("bottom_plate", false); } pc.setFeaturesOverride(override); // 保存设备连接覆盖 pc.setDeviceEnabledOverride(deviceEnabledCb.isSelected()); // 物料码输入框联动:勾了冷板或底护板 → 显示;都没勾 → 隐藏 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( row1Enabled, pc.getDeviceRow1LabelOverride(), row2Enabled, pc.getDeviceRow2LabelOverride(), coldOn || bottomOn); // 重新加载 yaml 配置(会按新 override 展开 steps) StationConfig.reload(); this.config = StationConfig.getInstance(); // 重新应用当前设备IP覆盖 String ip = pc.getDeviceIp(); for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) { conn.setIp(ip); } rebuildWorkflows(); // 按功能开关同步面板显示(物料码输入框显隐、label 文字) for (WorkstationPanel p : panels) { p.refreshDisplayFromConfig(config); } // 若行数变化涉及组件增删,强制重载布局(与"设备信息显示"对话框保持一致) for (WorkstationPanel p : panels) { try { p.reloadLayoutConfig(); } catch (Exception ex) { /* 忽略 */ } } JOptionPane.showMessageDialog(this, "工艺流程已更新,所有工位已重建", "配置成功", JOptionPane.INFORMATION_MESSAGE); // 设备连接状态变更需要重启程序生效(PLC 连接在启动时初始化,软重建工位不会重连) if (deviceEnabledCb.isSelected() != oldDeviceEnabled) { JOptionPane.showMessageDialog(this, "设备连接配置已变更,请重启程序使其生效", "需要重启", JOptionPane.WARNING_MESSAGE); } } /** * 软重启工作流:重新构建所有工位的流程引擎、重新注入 panel 和过程参数采集器 * 不断开 TCP,不销毁设备驱动(IP变化由 showChangeDeviceIpDialog 另行处理) */ private void rebuildWorkflows() { log.info("开始重建工作流..."); // 先停止过程参数上传器并清空采集器 if (prodDataUploader != null) { prodDataUploader.stop(); prodDataUploader.clearCollectors(); prodDataUploader = null; } // 若 features 启用了 prod_params 且 yaml 配置存在,重新创建 uploader if (config.hasProdParams()) { int uploadInterval = config.getProdParamsConfig().getUploadInterval(); prodDataUploader = new ProdDataUploader( config.getServerIp(), config.getHttpPort(), uploadInterval); } // 逐个工位重建 for (int i = 0; i < engines.size(); i++) { WorkflowEngine engine = engines.get(i); StationContext ctx = contexts.get(i); // 停止旧流程 engine.stop(); // 用新配置重建步骤 engine.buildFromConfig(config.getWorkflowSteps(), stepFactory); // 重新注入 panel(扫码框弹出) if (i < panels.size()) { WorkstationPanel p = panels.get(i); for (com.mes.step.IWorkflowStep step : engine.getSteps()) { stepFactory.injectPanel(step, p); } } // 重新注入过程参数采集器 if (prodDataUploader != null && config.hasProdParams()) { ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig()); for (com.mes.step.IWorkflowStep step : engine.getSteps()) { if (step instanceof WaitCompleteStep) { ((WaitCompleteStep) step).setProdDataCollector(collector); } // 注入到 ResetStationStep 供副机模式立即上传使用 stepFactory.injectProdParams(step, collector, prodDataUploader); } prodDataUploader.addCollector(ctx.getStationCode(), collector); } // 重置并重新启动 engine.reset(); engine.start(); if (i < panels.size()) panels.get(i).reset(); ctx.setStatusMessage("工艺已更新,请扫下一件", 0); } // 刷新监控面板 if (workflowMonitorPanel != null) { workflowMonitorPanel.setWorkstations(engines, contexts); } // 启动新的过程参数上传器 if (prodDataUploader != null) { prodDataUploader.start(); } log.info("工作流重建完成, 步骤数={}", config.getWorkflowSteps().size()); } /** * 弹出"设备信息显示"对话框 * 只允许勾选启用/禁用 + 改 label,寄存器地址不在UI改(留yaml) */ private void showDeviceInfoUiDialog() { com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance(); java.util.List rows = config.getDeviceInfoRows(); // 第1行 StationConfig.DeviceInfoRow r1 = rows.size() > 0 ? rows.get(0) : null; JCheckBox enableA = new JCheckBox("启用第1行", r1 != null && r1.isEnabled()); enableA.setFont(new Font("微软雅黑", Font.PLAIN, 16)); JTextField labelA = new JTextField(r1 != null && r1.getLabel() != null ? r1.getLabel() : ""); labelA.setFont(new Font("微软雅黑", Font.PLAIN, 16)); // 第2行 StationConfig.DeviceInfoRow r2 = rows.size() > 1 ? rows.get(1) : null; JCheckBox enableB = new JCheckBox("启用第2行", r2 != null && r2.isEnabled()); enableB.setFont(new Font("微软雅黑", Font.PLAIN, 16)); JTextField labelB = new JTextField(r2 != null && r2.getLabel() != null ? r2.getLabel() : ""); labelB.setFont(new Font("微软雅黑", Font.PLAIN, 16)); // 物料码输入框显隐 JCheckBox showMaterial = new JCheckBox("显示物料码输入框", config.isShowMaterialInput()); showMaterial.setFont(new Font("微软雅黑", Font.PLAIN, 16)); JPanel panel = new JPanel(new java.awt.GridBagLayout()); java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints(); gbc.insets = new Insets(4, 4, 4, 4); gbc.anchor = java.awt.GridBagConstraints.WEST; // 第1行 gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; panel.add(enableA, gbc); gbc.gridy = 1; gbc.gridwidth = 1; panel.add(new JLabel("标签:"), gbc); gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; gbc.weightx = 1; labelA.setPreferredSize(new Dimension(160, 26)); panel.add(labelA, gbc); // 第2行 gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = java.awt.GridBagConstraints.NONE; gbc.weightx = 0; panel.add(enableB, gbc); gbc.gridy = 3; gbc.gridwidth = 1; panel.add(new JLabel("标签:"), gbc); gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; gbc.weightx = 1; labelB.setPreferredSize(new Dimension(160, 26)); panel.add(labelB, gbc); // 提示 gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.fill = java.awt.GridBagConstraints.NONE; panel.add(showMaterial, gbc); gbc.gridy = 5; JLabel tip = new JLabel("寄存器地址需改 station.yaml"); panel.add(tip, gbc); int result = JOptionPane.showConfirmDialog(this, panel, "设备信息显示", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (result != JOptionPane.OK_OPTION) return; pc.setDeviceInfoRowsOverride( enableA.isSelected(), labelA.getText(), enableB.isSelected(), labelB.getText(), showMaterial.isSelected()); // 重新加载 config 让 UI 覆盖生效 StationConfig.reload(); this.config = StationConfig.getInstance(); // 重新应用运行时IP for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) { conn.setIp(pc.getDeviceIp()); } // 刷新工位面板显示(启用/禁用行、label) for (WorkstationPanel p : panels) { p.refreshDisplayFromConfig(config); } // 若显示开关改变涉及组件增删,强制重载布局 for (WorkstationPanel p : panels) { try { p.reloadLayoutConfig(); } catch (Exception ex) { /* 忽略 */ } } JOptionPane.showMessageDialog(this, "设备信息显示配置已更新\n如行数变化布局未立即生效,请重启程序", "配置成功", JOptionPane.INFORMATION_MESSAGE); } }