| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104 |
- 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<StationContext> contexts;
- private List<WorkflowEngine> engines;
- private List<WorkstationPanel> panels;
- private Map<Integer, IDeviceDriver> 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<StationContext> getContexts() {
- return contexts;
- }
- public List<WorkflowEngine> getEngines() {
- return engines;
- }
- public List<WorkstationPanel> 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(
- "<html>产线位置:%s<br>产品号:%s<br>工件码前缀:%s<br>冷板码前缀:%s<br>底护板码前缀:%s</html>",
- 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<com.mes.core.ProjectConfigManager.Line> 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<String> 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<com.mes.core.ProjectConfigManager.Product> 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<String> 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<Integer, IDeviceDriver> 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<String> 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<String> 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<String, Boolean> yamlFeatures = config.getYamlFeatures();
- if (yamlFeatures.isEmpty()) {
- JOptionPane.showMessageDialog(this, "station.yaml 中未定义 workflow.features",
- "提示", JOptionPane.WARNING_MESSAGE);
- return;
- }
- // 功能显示名映射
- Map<String, String> 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<String, JCheckBox> 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("<html><font color='gray' size='3'>冷板/底护板二选一;保存后将重建所有工位流程并重置</font></html>");
- 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("<html><font color='gray' size='3'>禁用后不连接PLC,上传结果不需要等待设备信号</font></html>");
- 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<String, Boolean> override = new LinkedHashMap<>();
- for (Map.Entry<String, JCheckBox> 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<StationConfig.DeviceInfoRow> 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("<html><font color='gray' size='3'>寄存器地址需改 station.yaml</font></html>");
- 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);
- }
- }
|