MainFrame.java 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104
  1. package com.mes.ui;
  2. import com.mes.core.*;
  3. import com.mes.device.DeviceDriverFactory;
  4. import com.mes.device.IDeviceDriver;
  5. import com.mes.prod.ProdDataCollector;
  6. import com.mes.prod.ProdDataUploader;
  7. import com.mes.step.StartWorkStep;
  8. import com.mes.step.StepFactory;
  9. import com.mes.step.UploadResultStep;
  10. import com.mes.step.WaitCompleteStep;
  11. import com.mes.tcp.MesTcpClient;
  12. import com.mes.tcp.MessageDispatcher;
  13. import com.mes.ui.component.WorkstationPanel;
  14. import com.mes.ui.component.WorkflowMonitorPanel;
  15. import org.slf4j.Logger;
  16. import org.slf4j.LoggerFactory;
  17. import javax.swing.*;
  18. import javax.swing.border.EmptyBorder;
  19. import java.awt.*;
  20. import java.awt.event.*;
  21. import java.util.*;
  22. import java.util.List;
  23. import java.util.Timer;
  24. /**
  25. * 主窗口 - 与OP150版本UI保持一致
  26. */
  27. public class MainFrame extends JFrame {
  28. private static final Logger log = LoggerFactory.getLogger(MainFrame.class);
  29. // 配置
  30. private StationConfig config;
  31. // TCP通信
  32. private MesTcpClient tcpClient;
  33. private MessageDispatcher messageDispatcher;
  34. // 工位管理
  35. private List<StationContext> contexts;
  36. private List<WorkflowEngine> engines;
  37. private List<WorkstationPanel> panels;
  38. private Map<Integer, IDeviceDriver> deviceDrivers;
  39. // 步骤工厂
  40. private StepFactory stepFactory;
  41. // 过程参数上传器
  42. private ProdDataUploader prodDataUploader;
  43. // 当前用户和会话信息
  44. private String currentUser = "";
  45. private String sessionId;
  46. private int userAuth;
  47. private int loginHour;
  48. // UI组件
  49. private JPanel contentPane;
  50. private JPanel workPanel; // 工作面板引用,用于编辑模式切换
  51. private JTabbedPane tabbedPane;
  52. private JButton heartBeatMenu;
  53. private JButton userMenu;
  54. private JButton toolbarStatusMenu; // 单工位时的状态显示
  55. private Timer heartBeatDisplayTimer;
  56. // WebView组件(参考op150)
  57. private com.mes.ui.component.MesWebView shiftCheckWebView; // 开班点检
  58. private com.mes.ui.component.MesWebView workRecordWebView; // 工作记录
  59. // 流程监控面板
  60. private WorkflowMonitorPanel workflowMonitorPanel;
  61. private boolean workflowTabVisible = false;
  62. // 需要密码解锁的设置菜单项
  63. private JSeparator editModeSeparator;
  64. private JCheckBoxMenuItem editModeItem;
  65. private JMenuItem saveLayoutItem;
  66. private JMenuItem exportLayoutItem;
  67. private JMenuItem importLayoutItem;
  68. // 项目/设备子菜单(mes123解锁后显示)
  69. private JMenu projectDeviceMenu;
  70. private JMenuItem switchProjectItem; // 切换产线位置
  71. private JMenuItem switchProductItem; // 切换产品号
  72. private JMenuItem switchStationItem;
  73. private JMenuItem deviceIpItem;
  74. private JMenuItem serverIpItem;
  75. private JMenuItem featuresItem;
  76. private JMenuItem deviceInfoUiItem;
  77. // 界面布局子菜单(mes123解锁后显示)
  78. private JMenu layoutMenu;
  79. // 手动提交子菜单(解锁后显示;若勾选"始终显示"则不受解锁控制)
  80. private JMenu manualSubmitMenu;
  81. // 始终显示子菜单(解锁后显示,用于勾选哪些菜单绕过解锁)
  82. private JMenu alwaysShowMenu;
  83. private JCheckBoxMenuItem manualSubmitAlwaysItem;
  84. // 工具栏项目显示按钮
  85. private JButton projectMenu;
  86. // 三击解锁相关
  87. private int userMenuClickCount = 0;
  88. private long lastUserMenuClickTime = 0;
  89. private static final int TRIPLE_CLICK_INTERVAL = 1500; // 1.5秒内完成3次点击
  90. private static final String WORKFLOW_PASSWORD = "mes123";
  91. // 免密码解锁序列:2次用户 -> 2次项目 -> 2次心跳
  92. private int unlockStage = 0; // 0=等用户, 1=等项目, 2=等心跳
  93. private int unlockStageCount = 0; // 当前阶段已点击次数
  94. private long unlockLastClickTime = 0;
  95. private static final int UNLOCK_INTERVAL = 2000; // 每次点击最大间隔 2s
  96. public MainFrame() {
  97. this(StationConfig.getInstance());
  98. }
  99. public MainFrame(StationConfig config) {
  100. this.config = config;
  101. this.contexts = new ArrayList<>();
  102. this.engines = new ArrayList<>();
  103. this.panels = new ArrayList<>();
  104. this.deviceDrivers = new HashMap<>();
  105. initFrame();
  106. initComponents();
  107. initTcpClient();
  108. initWorkstations();
  109. }
  110. /**
  111. * 初始化窗口
  112. */
  113. private void initFrame() {
  114. String codes = getStationCodesString();
  115. // 标题优先用 OprnoRegistry 按当前产线位置推导的工位名称,其次 yaml 的 name
  116. String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId();
  117. String name = "";
  118. if (!config.getStations().isEmpty()) {
  119. name = com.mes.core.OprnoRegistry.getGwDes(lineId, config.getStations().get(0).getCode());
  120. }
  121. if ((name == null || name.isEmpty()) && config.getStationName() != null) {
  122. name = config.getStationName();
  123. }
  124. String title = "MES系统客户端:" + codes;
  125. if (name != null && !name.isEmpty() && !name.equals(codes)) {
  126. title += " - " + name;
  127. }
  128. setTitle(title);
  129. setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/resources/image/bg/logo.png")));
  130. setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
  131. // 根据工位数量设置窗口初始宽度:双工位宽度是单工位的两倍
  132. int stationCount = config.getStations() != null ? config.getStations().size() : 1;
  133. int baseWidth = 1024;
  134. int windowWidth = stationCount > 1 ? (int)(baseWidth * 1) : baseWidth;
  135. setBounds(0, 0, windowWidth, 768);
  136. addWindowListener(new WindowAdapter() {
  137. @Override
  138. public void windowClosing(WindowEvent e) {
  139. onWindowClosing();
  140. }
  141. });
  142. }
  143. /**
  144. * 初始化UI组件 - 参照OP150版本
  145. */
  146. private void initComponents() {
  147. // 菜单栏
  148. JMenuBar menuBar = new JMenuBar();
  149. menuBar.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 26));
  150. setJMenuBar(menuBar);
  151. // 用户菜单
  152. JMenu fileMenu = new JMenu("用户");
  153. fileMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/user.png"))));
  154. fileMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  155. menuBar.add(fileMenu);
  156. JMenuItem exitMenuItem = new JMenuItem("退出");
  157. exitMenuItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/logoff.png"))));
  158. exitMenuItem.setFont(new Font("微软雅黑", Font.PLAIN, 22));
  159. exitMenuItem.addMouseListener(new MouseAdapter() {
  160. @Override
  161. public void mousePressed(MouseEvent e) {
  162. logoff();
  163. }
  164. });
  165. fileMenu.add(exitMenuItem);
  166. // 设置菜单
  167. JMenu settingMenu = new JMenu("设置");
  168. settingMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  169. settingMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  170. menuBar.add(settingMenu);
  171. // 重连MES
  172. JMenuItem resetTcpMenu = new JMenuItem("重连MES");
  173. resetTcpMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  174. resetTcpMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  175. resetTcpMenu.addMouseListener(new MouseAdapter() {
  176. @Override
  177. public void mousePressed(MouseEvent e) {
  178. reconnectMes();
  179. }
  180. });
  181. settingMenu.add(resetTcpMenu);
  182. // 为每个工位添加刷新菜单
  183. for (int i = 0; i < config.getStationCount(); i++) {
  184. final int stationIndex = i;
  185. StationConfig.StationInfo station = config.getStation(i);
  186. if (station != null) {
  187. JMenuItem refreshMenu = new JMenuItem("刷新" + station.getCode());
  188. refreshMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  189. refreshMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  190. refreshMenu.addMouseListener(new MouseAdapter() {
  191. @Override
  192. public void mousePressed(MouseEvent e) {
  193. resetStation(stationIndex);
  194. }
  195. });
  196. settingMenu.add(refreshMenu);
  197. }
  198. }
  199. editModeSeparator = new JSeparator();
  200. editModeSeparator.setVisible(false);
  201. settingMenu.add(editModeSeparator);
  202. // ========== 子菜单:项目与设备(默认隐藏,密码解锁后显示) ==========
  203. projectDeviceMenu = new JMenu("项目与设备");
  204. projectDeviceMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  205. projectDeviceMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  206. projectDeviceMenu.setVisible(false);
  207. settingMenu.add(projectDeviceMenu);
  208. switchProjectItem = new JMenuItem();
  209. switchProjectItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  210. switchProjectItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  211. switchProjectItem.addActionListener(e -> showSwitchLineDialog());
  212. projectDeviceMenu.add(switchProjectItem);
  213. switchProductItem = new JMenuItem();
  214. switchProductItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  215. switchProductItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  216. switchProductItem.addActionListener(e -> showSwitchProductDialog());
  217. projectDeviceMenu.add(switchProductItem);
  218. switchStationItem = new JMenuItem();
  219. switchStationItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  220. switchStationItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  221. switchStationItem.addActionListener(e -> showSwitchStationDialog());
  222. projectDeviceMenu.add(switchStationItem);
  223. deviceIpItem = new JMenuItem();
  224. deviceIpItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  225. deviceIpItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  226. deviceIpItem.addActionListener(e -> showChangeDeviceIpDialog());
  227. projectDeviceMenu.add(deviceIpItem);
  228. serverIpItem = new JMenuItem();
  229. serverIpItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  230. serverIpItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  231. serverIpItem.addActionListener(e -> showChangeServerIpDialog());
  232. projectDeviceMenu.add(serverIpItem);
  233. projectDeviceMenu.addSeparator();
  234. featuresItem = new JMenuItem("工艺流程配置");
  235. featuresItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  236. featuresItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  237. featuresItem.addActionListener(e -> showFeaturesDialog());
  238. projectDeviceMenu.add(featuresItem);
  239. deviceInfoUiItem = new JMenuItem("设备信息显示");
  240. deviceInfoUiItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  241. deviceInfoUiItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  242. deviceInfoUiItem.addActionListener(e -> showDeviceInfoUiDialog());
  243. projectDeviceMenu.add(deviceInfoUiItem);
  244. refreshMenuTexts(); // 首次填充显示当前值
  245. // ========== 子菜单:界面布局(默认隐藏,密码解锁后显示) ==========
  246. layoutMenu = new JMenu("界面布局");
  247. layoutMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/bar_setting.png"))));
  248. layoutMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  249. layoutMenu.setVisible(false);
  250. settingMenu.add(layoutMenu);
  251. editModeItem = new JCheckBoxMenuItem("UI编辑模式", false);
  252. editModeItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  253. editModeItem.addItemListener(e -> toggleEditMode(editModeItem.isSelected()));
  254. layoutMenu.add(editModeItem);
  255. saveLayoutItem = new JMenuItem("保存UI布局");
  256. saveLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/save_bg.png"))));
  257. saveLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  258. saveLayoutItem.addActionListener(e -> saveUILayout());
  259. layoutMenu.add(saveLayoutItem);
  260. exportLayoutItem = new JMenuItem("导出布局配置");
  261. exportLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/download.png"))));
  262. exportLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  263. exportLayoutItem.addActionListener(e -> exportUILayout());
  264. layoutMenu.add(exportLayoutItem);
  265. importLayoutItem = new JMenuItem("导入布局配置");
  266. importLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/open_file.png"))));
  267. importLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  268. importLayoutItem.addActionListener(e -> importUILayout());
  269. layoutMenu.add(importLayoutItem);
  270. // ========== 子菜单:手动提交(每个工位一个菜单项) ==========
  271. // 默认隐藏,解锁后显示;若在"始终显示"中勾选则无视解锁状态
  272. manualSubmitMenu = new JMenu("手动提交");
  273. manualSubmitMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  274. manualSubmitMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  275. manualSubmitMenu.setVisible(false);
  276. settingMenu.add(manualSubmitMenu);
  277. int stationCount = config.getStationCount();
  278. for (int i = 0; i < stationCount; i++) {
  279. final int idx = i;
  280. StationConfig.StationInfo st = config.getStation(i);
  281. String label = stationCount > 1
  282. ? "手动提交 工位" + (i + 1) + (st != null ? " (" + st.getCode() + ")" : "")
  283. : "手动提交" + (st != null ? " (" + st.getCode() + ")" : "");
  284. JMenuItem item = new JMenuItem(label);
  285. item.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  286. item.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  287. item.addActionListener(e -> manualEnableSubmit(idx));
  288. manualSubmitMenu.add(item);
  289. }
  290. // ========== 子菜单:始终显示(管理绕过解锁的白名单) ==========
  291. alwaysShowMenu = new JMenu("始终显示");
  292. alwaysShowMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/bar_setting.png"))));
  293. alwaysShowMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  294. alwaysShowMenu.setVisible(false);
  295. settingMenu.add(alwaysShowMenu);
  296. manualSubmitAlwaysItem = new JCheckBoxMenuItem("手动提交",
  297. com.mes.core.UiPrefs.getInstance().isAlwaysShow(com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT));
  298. manualSubmitAlwaysItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  299. manualSubmitAlwaysItem.addItemListener(e -> {
  300. com.mes.core.UiPrefs.getInstance().setAlwaysShow(
  301. com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT, manualSubmitAlwaysItem.isSelected());
  302. applyLockVisibility(!workflowTabVisible);
  303. });
  304. alwaysShowMenu.add(manualSubmitAlwaysItem);
  305. // 初始按偏好设置一次显隐(默认锁定)
  306. applyLockVisibility(true);
  307. // 内容面板
  308. contentPane = new JPanel();
  309. contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
  310. setContentPane(contentPane);
  311. contentPane.setLayout(new BorderLayout(0, 0));
  312. // 工具栏 - 完全参考OP40
  313. JToolBar toolBar = new JToolBar();
  314. contentPane.add(toolBar, BorderLayout.NORTH);
  315. // 状态标签
  316. JLabel statusLabel = new JLabel("状态:");
  317. statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
  318. statusLabel.setForeground(Color.BLACK);
  319. statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  320. statusLabel.setBackground(Color.LIGHT_GRAY);
  321. toolBar.add(statusLabel);
  322. toolbarStatusMenu = new JButton("等待加工信号");
  323. toolbarStatusMenu.setForeground(Color.GREEN);
  324. toolbarStatusMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  325. toolbarStatusMenu.setBackground(Color.BLACK);
  326. toolBar.add(toolbarStatusMenu);
  327. toolBar.add(new JLabel(" "));
  328. // 心跳标签
  329. JLabel heartBeatLabel = new JLabel("心跳:");
  330. heartBeatLabel.setHorizontalAlignment(SwingConstants.CENTER);
  331. heartBeatLabel.setForeground(Color.BLACK);
  332. heartBeatLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  333. heartBeatLabel.setBackground(Color.LIGHT_GRAY);
  334. toolBar.add(heartBeatLabel);
  335. // 心跳显示(带时间)- 完全参考OP40
  336. heartBeatMenu = new JButton("");
  337. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/green_dot.png"))));
  338. heartBeatMenu.setForeground(Color.GREEN);
  339. heartBeatMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  340. heartBeatMenu.setBackground(Color.BLACK);
  341. heartBeatMenu.addActionListener(e -> onUnlockClick("heart"));
  342. toolBar.add(heartBeatMenu);
  343. toolBar.add(new JLabel(" "));
  344. // 用户标签
  345. JLabel userLabel = new JLabel("登录用户:");
  346. userLabel.setHorizontalAlignment(SwingConstants.CENTER);
  347. userLabel.setForeground(Color.BLACK);
  348. userLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  349. userLabel.setBackground(Color.LIGHT_GRAY);
  350. toolBar.add(userLabel);
  351. userMenu = new JButton("");
  352. userMenu.setForeground(Color.GREEN);
  353. userMenu.setFont(new Font("微软雅黑", Font.PLAIN, 22));
  354. userMenu.setBackground(Color.BLACK);
  355. userMenu.addActionListener(e -> onUserMenuClicked());
  356. toolBar.add(userMenu);
  357. toolBar.add(new JLabel(" "));
  358. // 当前项目显示(只读展示,切换在"设置"菜单中)
  359. JLabel projectLabel = new JLabel("项目:");
  360. projectLabel.setHorizontalAlignment(SwingConstants.CENTER);
  361. projectLabel.setForeground(Color.BLACK);
  362. projectLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  363. projectLabel.setBackground(Color.LIGHT_GRAY);
  364. toolBar.add(projectLabel);
  365. projectMenu = new JButton("");
  366. projectMenu.setForeground(Color.YELLOW);
  367. projectMenu.setFont(new Font("微软雅黑", Font.PLAIN, 22));
  368. projectMenu.setBackground(Color.BLACK);
  369. projectMenu.setFocusable(false);
  370. projectMenu.addActionListener(e -> onUnlockClick("project"));
  371. toolBar.add(projectMenu);
  372. updateProjectDisplay();
  373. // 订阅产线位置/产品号切换 → 刷新工具栏显示
  374. com.mes.core.ProjectConfigManager.getInstance().addListener(new com.mes.core.ProjectConfigManager.Listener() {
  375. @Override
  376. public void onLineChanged(com.mes.core.ProjectConfigManager.Line line) {
  377. SwingUtilities.invokeLater(() -> updateProjectDisplay());
  378. }
  379. @Override
  380. public void onProductChanged(com.mes.core.ProjectConfigManager.Product product) {
  381. SwingUtilities.invokeLater(() -> updateProjectDisplay());
  382. }
  383. });
  384. // 标签页
  385. tabbedPane = new JTabbedPane(JTabbedPane.TOP);
  386. tabbedPane.setMinimumSize(new Dimension(400, 50));
  387. tabbedPane.setFont(new Font("宋体", Font.BOLD, 22));
  388. contentPane.add(tabbedPane);
  389. // 工作面板 - 直接添加,支持等比例缩放
  390. JPanel workPanel = createWorkPanel();
  391. tabbedPane.addTab("工作面板",
  392. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/a_side.png"))),
  393. workPanel, null);
  394. tabbedPane.setEnabledAt(0, true);
  395. // 开班点检Tab - 使用WebView(参考op150)
  396. // URL格式: http://{server_ip}:8980/js/a/mes/mesProcessCheckRecord/ulist?oprno={oprno}&lineSn={line_sn}
  397. String shiftCheckOprno = config.getStation(0) != null ? config.getStation(0).getCode() : "";
  398. String shiftCheckUrl = String.format("http://%s:%d/js/a/mes/mesProcessCheckRecord/ulist?oprno=%s&lineSn=%s",
  399. config.getServerIp(), config.getHttpPort(), shiftCheckOprno, config.getLineSn());
  400. shiftCheckWebView = new com.mes.ui.component.MesWebView(shiftCheckUrl);
  401. tabbedPane.addTab("开班点检",
  402. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))),
  403. shiftCheckWebView, null);
  404. // 工作记录Tab - 使用WebView(参考op150)
  405. // URL格式: http://{server_ip}:8980/js/a/mes/mesProductRecord/work?oprno={oprno}&lineSn={line_sn}
  406. String workRecordOprno = config.getStation(0) != null ? config.getStation(0).getCode() : "";
  407. String workRecordUrl = String.format("http://%s:%d/js/a/mes/mesProductRecord/work?oprno=%s&lineSn=%s",
  408. config.getServerIp(), config.getHttpPort(), workRecordOprno, config.getLineSn());
  409. workRecordWebView = new com.mes.ui.component.MesWebView(workRecordUrl);
  410. tabbedPane.addTab("工作记录",
  411. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))),
  412. workRecordWebView, null);
  413. // 流程监控Tab - 默认隐藏,需要三击用户按钮并输入密码解锁
  414. workflowMonitorPanel = new WorkflowMonitorPanel();
  415. // 切换Tab时自动刷新
  416. tabbedPane.addChangeListener(e -> {
  417. int selectedIndex = tabbedPane.getSelectedIndex();
  418. if (selectedIndex < 0) return;
  419. String tabTitle = tabbedPane.getTitleAt(selectedIndex);
  420. if ("开班点检".equals(tabTitle) && shiftCheckWebView != null) {
  421. shiftCheckWebView.reload();
  422. } else if ("工作记录".equals(tabTitle) && workRecordWebView != null) {
  423. workRecordWebView.reload();
  424. } else if ("流程".equals(tabTitle) && workflowMonitorPanel != null) {
  425. workflowMonitorPanel.refresh();
  426. }
  427. });
  428. log.info("UI组件初始化完成");
  429. }
  430. /**
  431. * 创建工作面板
  432. */
  433. private JPanel createWorkPanel() {
  434. workPanel = new JPanel();
  435. if (config.isDualMode()) {
  436. // 双工位布局 - 使用GridLayout自适应,中间有间距
  437. workPanel.setLayout(new GridLayout(1, 2, 20, 0));
  438. workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
  439. } else {
  440. // 单工位布局 - 使用 BorderLayout 让面板填满整个工作区域
  441. // 这样面板大小固定,不会随模式切换而变化
  442. workPanel.setLayout(new BorderLayout());
  443. workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
  444. }
  445. // 创建工位面板
  446. for (int i = 0; i < config.getStationCount(); i++) {
  447. StationConfig.StationInfo stationInfo = config.getStation(i);
  448. if (stationInfo != null) {
  449. // 单工位不显示header(工位号和状态栏),双工位显示
  450. boolean showHeader = config.isDualMode();
  451. WorkstationPanel panel = new WorkstationPanel(
  452. stationInfo.getCode(),
  453. stationInfo.getIndex(),
  454. config,
  455. showHeader
  456. );
  457. // 单工位时,把工具栏状态按钮绑定到面板
  458. if (!showHeader) {
  459. panel.setExternalStatusMenu(toolbarStatusMenu);
  460. }
  461. panels.add(panel);
  462. if (config.isDualMode()) {
  463. workPanel.add(panel);
  464. } else {
  465. // 单工位:面板填满整个工作区域
  466. workPanel.add(panel, BorderLayout.CENTER);
  467. }
  468. }
  469. }
  470. return workPanel;
  471. }
  472. /**
  473. * 初始化TCP客户端
  474. */
  475. private void initTcpClient() {
  476. messageDispatcher = new MessageDispatcher();
  477. tcpClient = new MesTcpClient(config);
  478. tcpClient.setDispatcher(messageDispatcher);
  479. tcpClient.setConnectionListener(new MesTcpClient.ConnectionListener() {
  480. @Override
  481. public void onConnected() {
  482. SwingUtilities.invokeLater(() -> updateConnectionStatus(true));
  483. for (StationContext ctx : contexts) {
  484. tcpClient.sendSync(ctx.getStationCode());
  485. }
  486. }
  487. @Override
  488. public void onDisconnected() {
  489. SwingUtilities.invokeLater(() -> updateConnectionStatus(false));
  490. }
  491. @Override
  492. public void onReconnecting() {
  493. SwingUtilities.invokeLater(() -> {
  494. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  495. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  496. });
  497. }
  498. });
  499. stepFactory = new StepFactory(tcpClient);
  500. log.info("TCP客户端初始化完成");
  501. }
  502. /**
  503. * 初始化工位
  504. */
  505. private void initWorkstations() {
  506. if (config.isDeviceEnabled()) {
  507. deviceDrivers = DeviceDriverFactory.createDrivers(config);
  508. }
  509. // 初始化过程参数上传器
  510. if (config.hasProdParams()) {
  511. int uploadInterval = config.getProdParamsConfig().getUploadInterval();
  512. prodDataUploader = new ProdDataUploader(
  513. config.getServerIp(), config.getHttpPort(), uploadInterval);
  514. }
  515. for (int i = 0; i < config.getStationCount(); i++) {
  516. StationConfig.StationInfo stationInfo = config.getStation(i);
  517. if (stationInfo == null) continue;
  518. // 创建上下文
  519. StationContext context = StationContext.fromConfig(stationInfo, config.getLineSn());
  520. context.setDeviceEnabled(config.isDeviceEnabled());
  521. if (deviceDrivers.containsKey(i)) {
  522. context.setDeviceDriver(deviceDrivers.get(i));
  523. }
  524. // 设置第二行设备驱动(可能来自不同IP的设备)
  525. StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
  526. if (row2 != null) {
  527. int connIdx = row2.getConnectionIndex();
  528. if (deviceDrivers.containsKey(connIdx)) {
  529. context.setDeviceDriver2(deviceDrivers.get(connIdx));
  530. }
  531. }
  532. contexts.add(context);
  533. // 创建流程引擎
  534. WorkflowEngine engine = new WorkflowEngine(context);
  535. engine.buildFromConfig(config.getWorkflowSteps(), stepFactory);
  536. // 注入过程参数采集器到WaitCompleteStep
  537. if (config.hasProdParams()) {
  538. ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig());
  539. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  540. if (step instanceof WaitCompleteStep) {
  541. ((WaitCompleteStep) step).setProdDataCollector(collector);
  542. log.info("[{}] 过程参数采集器已注入", stationInfo.getCode());
  543. }
  544. // 注入到 ResetStationStep 供副机模式立即上传使用
  545. stepFactory.injectProdParams(step, collector, prodDataUploader);
  546. }
  547. prodDataUploader.addCollector(stationInfo.getCode(), collector);
  548. }
  549. engine.setListener(new WorkflowEngine.WorkflowListener() {
  550. @Override
  551. public void onStepStarted(WorkflowEngine eng, com.mes.step.IWorkflowStep step) {
  552. log.debug("[{}] 步骤开始: {}", context.getStationCode(), step.getStepName());
  553. }
  554. @Override
  555. public void onStepCompleted(WorkflowEngine eng, com.mes.step.IWorkflowStep step, boolean success) {
  556. log.debug("[{}] 步骤完成: {} (成功={})", context.getStationCode(), step.getStepName(), success);
  557. }
  558. @Override
  559. public void onWorkflowCompleted(WorkflowEngine eng) {
  560. log.info("[{}] 流程完成", context.getStationCode());
  561. eng.reset();
  562. context.setStatusMessage("结果已提交,请扫下一件", 0);
  563. }
  564. @Override
  565. public void onWorkflowError(WorkflowEngine eng, String error) {
  566. log.error("[{}] 流程错误: {}", context.getStationCode(), error);
  567. }
  568. });
  569. engines.add(engine);
  570. // 注册到消息分发器
  571. messageDispatcher.registerStation(stationInfo.getCode(), engine, context);
  572. // 绑定UI面板
  573. if (i < panels.size()) {
  574. WorkstationPanel panel = panels.get(i);
  575. context.setUiPanel(panel);
  576. // 注入panel引用到需要弹出扫码框的步骤
  577. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  578. stepFactory.injectPanel(step, panel);
  579. }
  580. bindPanelEvents(panel, context, engine);
  581. log.info("[{}] UI面板绑定完成", stationInfo.getCode());
  582. }
  583. log.info("工位初始化完成: {}", stationInfo.getCode());
  584. }
  585. // 设置流程监控面板的工位列表
  586. if (workflowMonitorPanel != null) {
  587. workflowMonitorPanel.setWorkstations(engines, contexts);
  588. log.info("流程监控面板初始化完成");
  589. }
  590. // 启动过程参数上传器
  591. if (prodDataUploader != null) {
  592. prodDataUploader.start();
  593. }
  594. }
  595. /**
  596. * 绑定面板事件
  597. */
  598. private void bindPanelEvents(WorkstationPanel panel, StationContext context, WorkflowEngine engine) {
  599. panel.setListener(new WorkstationPanel.WorkstationPanelListener() {
  600. @Override
  601. public void onProductScanned(WorkstationPanel p, String productSn) {
  602. context.setProductSn(productSn);
  603. context.setUser(currentUser);
  604. engine.triggerCurrentStep();
  605. }
  606. @Override
  607. public void onMaterialScanned(WorkstationPanel p, String materialSn) {
  608. context.setMaterialSn(materialSn);
  609. engine.triggerCurrentStep();
  610. }
  611. @Override
  612. public void onOkClicked(WorkstationPanel p) {
  613. submitResult(context, engine, "OK");
  614. }
  615. @Override
  616. public void onNgClicked(WorkstationPanel p) {
  617. submitResult(context, engine, "NG");
  618. }
  619. @Override
  620. public void onUnbindClicked(WorkstationPanel p) {
  621. log.info("[{}] 解绑操作", context.getStationCode());
  622. // TODO: 实现解绑逻辑
  623. }
  624. });
  625. }
  626. /**
  627. * 手动启用指定工位的 OK/NG 按钮(救急用:设备完成信号缺失或拉铆数量未达预设时)
  628. * 参考 OP220A:只解锁按钮,用户自行点 OK 走正常提交路径。
  629. */
  630. private void manualEnableSubmit(int stationIndex) {
  631. if (stationIndex < 0 || stationIndex >= contexts.size()) {
  632. log.warn("手动提交: 无效工位索引 {}", stationIndex);
  633. return;
  634. }
  635. StationContext context = contexts.get(stationIndex);
  636. WorkflowEngine engine = engines.get(stationIndex);
  637. // 必须先扫码
  638. String sn = context.getProductSn();
  639. if (sn == null || sn.trim().isEmpty()) {
  640. JOptionPane.showMessageDialog(this,
  641. "请先扫描工件码再执行手动提交",
  642. "手动提交", JOptionPane.WARNING_MESSAGE);
  643. return;
  644. }
  645. log.warn("[{}] 手动提交:强制启用 OK/NG 按钮 (sn={})", context.getStationCode(), sn);
  646. // 1) 置位 UploadResultStep.canExecute 所需的全部条件
  647. context.setQualityPassed(true);
  648. context.setWorkStarted(true); // auto 模式下必须
  649. context.setMaterialValidated(true); // 有物料配置时必须
  650. context.setMaterialBound(true); // 跳过 MBDW 阶段
  651. context.setResultUploaded(false);
  652. context.setWaitingForUserAction(true);
  653. // 2) 跳到 upload_result 步骤,保证 submitResult 走 "currentStep instanceof UploadResultStep" 分支
  654. engine.jumpToStep("upload_result");
  655. // 3) 标记按钮强制启用(面板 refresh 时绕过 auto 模式禁用逻辑;reset() 会自动清除)
  656. context.setExtra("forceSubmitEnabled", Boolean.TRUE);
  657. context.setStatusMessage("手动提交已启用,请点击OK完成", 0);
  658. context.updateUI();
  659. }
  660. /**
  661. * 提交结果
  662. * 手动模式下:用户点击OK/NG后,按顺序执行 StartWorkStep -> UploadResultStep
  663. */
  664. private void submitResult(StationContext context, WorkflowEngine engine, String result) {
  665. log.info("[{}] 用户提交结果: {}, 当前状态: qualityPassed={}, workStarted={}, waitingForUserAction={}",
  666. context.getStationCode(), result,
  667. context.isQualityPassed(), context.isWorkStarted(), context.isWaitingForUserAction());
  668. com.mes.step.IWorkflowStep currentStep = engine.getCurrentStep();
  669. log.info("[{}] 当前步骤: {}", context.getStationCode(),
  670. currentStep != null ? currentStep.getStepId() : "null");
  671. // 设置结果到 UploadResultStep(不管当前在哪一步)
  672. com.mes.step.IWorkflowStep uploadStep = engine.getStep("upload_result");
  673. if (uploadStep instanceof UploadResultStep) {
  674. ((UploadResultStep) uploadStep).setResult(result);
  675. } else {
  676. // 副机模式: 关闭 quality_submit 时 upload_result 不加载, OK/NG 按钮本应隐藏
  677. // 若用户仍触发到这里, 直接跳到 reset_station 完成闭环
  678. log.warn("[{}] quality_submit 已关闭, 无 upload_result 步骤, 直接重置工位",
  679. context.getStationCode());
  680. if (engine.jumpToStep("reset_station")) {
  681. engine.triggerCurrentStep();
  682. }
  683. return;
  684. }
  685. if (currentStep instanceof UploadResultStep) {
  686. // 当前步骤已经是上传结果,直接触发
  687. log.info("[{}] 当前步骤是 UploadResultStep,直接触发", context.getStationCode());
  688. engine.triggerCurrentStep();
  689. } else if (currentStep instanceof StartWorkStep) {
  690. // 当前步骤是开始工作(手动模式),标记用户已确认并触发
  691. log.info("[{}] 手动模式:触发 StartWorkStep", context.getStationCode());
  692. context.setWaitingForUserAction(false);
  693. engine.triggerCurrentStep();
  694. } else {
  695. // 其他情况:直接发送(兼容旧逻辑)
  696. log.warn("[{}] 当前步骤非预期: {}, 直接发送结果",
  697. context.getStationCode(),
  698. currentStep != null ? currentStep.getStepId() : "null");
  699. // 先发送 MKSW,再发送 MQDW
  700. tcpClient.sendStartWork(
  701. context.getProcessedProductSn(),
  702. context.getUser(),
  703. context.getStationCode()
  704. );
  705. if (uploadStep instanceof UploadResultStep) {
  706. tcpClient.sendQuality(
  707. context.getProcessedProductSn(),
  708. result,
  709. context.getUser(),
  710. context.getStationCode()
  711. );
  712. }
  713. }
  714. }
  715. /**
  716. * 更新连接状态
  717. */
  718. private void updateConnectionStatus(boolean connected) {
  719. if (connected) {
  720. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  721. getClass().getResource("/resources/image/bg/green_dot.png"))));
  722. startHeartBeatDisplay();
  723. } else {
  724. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  725. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  726. stopHeartBeatDisplay();
  727. }
  728. }
  729. /**
  730. * 启动心跳显示
  731. */
  732. private void startHeartBeatDisplay() {
  733. stopHeartBeatDisplay();
  734. heartBeatDisplayTimer = new Timer("HeartBeatDisplay", true);
  735. heartBeatDisplayTimer.scheduleAtFixedRate(new TimerTask() {
  736. private boolean green = true;
  737. @Override
  738. public void run() {
  739. SwingUtilities.invokeLater(() -> {
  740. // 更新时间显示
  741. String timeStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
  742. heartBeatMenu.setText(timeStr);
  743. if (green) {
  744. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  745. getClass().getResource("/resources/image/bg/green_dot.png"))));
  746. } else {
  747. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  748. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  749. }
  750. green = !green;
  751. });
  752. }
  753. }, 0, 1000);
  754. }
  755. /**
  756. * 停止心跳显示
  757. */
  758. private void stopHeartBeatDisplay() {
  759. if (heartBeatDisplayTimer != null) {
  760. heartBeatDisplayTimer.cancel();
  761. heartBeatDisplayTimer = null;
  762. }
  763. }
  764. /**
  765. * 获取工位号字符串
  766. */
  767. private String getStationCodesString() {
  768. StringBuilder sb = new StringBuilder();
  769. for (int i = 0; i < config.getStationCount(); i++) {
  770. if (i > 0) sb.append("/");
  771. StationConfig.StationInfo station = config.getStation(i);
  772. if (station != null) {
  773. sb.append(station.getCode());
  774. }
  775. }
  776. return sb.toString();
  777. }
  778. /**
  779. * 重连MES
  780. */
  781. private void reconnectMes() {
  782. log.info("手动重连MES");
  783. tcpClient.disconnect();
  784. tcpClient.connect();
  785. }
  786. /**
  787. * 重置工位
  788. */
  789. private void resetStation(int stationIndex) {
  790. if (stationIndex >= 0 && stationIndex < contexts.size()) {
  791. StationContext context = contexts.get(stationIndex);
  792. WorkflowEngine engine = engines.get(stationIndex);
  793. log.info("[{}] 手动刷新工位", context.getStationCode());
  794. context.setStatusMessage("已手动刷新,请扫码", 0);
  795. engine.reset();
  796. if (stationIndex < panels.size()) {
  797. panels.get(stationIndex).reset();
  798. }
  799. }
  800. }
  801. /**
  802. * 退出登录
  803. */
  804. private void logoff() {
  805. int result = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "确认", JOptionPane.YES_NO_OPTION);
  806. if (result == JOptionPane.YES_OPTION) {
  807. shutdown();
  808. System.exit(0);
  809. }
  810. }
  811. /**
  812. * 窗口关闭处理
  813. */
  814. private void onWindowClosing() {
  815. int result = JOptionPane.showConfirmDialog(this, "确定要退出程序吗?", "确认退出", JOptionPane.YES_NO_OPTION);
  816. if (result == JOptionPane.YES_OPTION) {
  817. shutdown();
  818. System.exit(0);
  819. }
  820. }
  821. /**
  822. * 关闭资源
  823. */
  824. public void shutdown() {
  825. log.info("正在关闭...");
  826. stopHeartBeatDisplay();
  827. // 停止过程参数上传器(会尝试上传剩余数据)
  828. if (prodDataUploader != null) {
  829. prodDataUploader.stop();
  830. }
  831. // 停止流程监控面板的刷新
  832. if (workflowMonitorPanel != null) {
  833. workflowMonitorPanel.stopRefresh();
  834. }
  835. for (WorkflowEngine engine : engines) {
  836. engine.destroy();
  837. }
  838. for (IDeviceDriver driver : deviceDrivers.values()) {
  839. driver.disconnect();
  840. }
  841. if (tcpClient != null) {
  842. tcpClient.disconnect();
  843. }
  844. log.info("关闭完成");
  845. }
  846. /**
  847. * 启动
  848. */
  849. public void start() {
  850. tcpClient.connect();
  851. for (WorkflowEngine engine : engines) {
  852. engine.start();
  853. }
  854. log.info("主窗口启动完成");
  855. }
  856. // ========== Getters & Setters ==========
  857. public void setCurrentUser(String user) {
  858. this.currentUser = user;
  859. userMenu.setText(user);
  860. for (StationContext ctx : contexts) {
  861. ctx.setUser(user);
  862. }
  863. }
  864. public void setSessionId(String sessionId) {
  865. this.sessionId = sessionId;
  866. }
  867. public String getSessionId() {
  868. return sessionId;
  869. }
  870. public void setUserAuth(int auth) {
  871. this.userAuth = auth;
  872. }
  873. public int getUserAuth() {
  874. return userAuth;
  875. }
  876. public void setLoginHour(int hour) {
  877. this.loginHour = hour;
  878. }
  879. public int getLoginHour() {
  880. return loginHour;
  881. }
  882. public boolean isAdmin() {
  883. return userAuth == 2;
  884. }
  885. public StationConfig getConfig() {
  886. return config;
  887. }
  888. public MesTcpClient getTcpClient() {
  889. return tcpClient;
  890. }
  891. public MessageDispatcher getMessageDispatcher() {
  892. return messageDispatcher;
  893. }
  894. public List<StationContext> getContexts() {
  895. return contexts;
  896. }
  897. public List<WorkflowEngine> getEngines() {
  898. return engines;
  899. }
  900. public List<WorkstationPanel> getPanels() {
  901. return panels;
  902. }
  903. // ========== UI编辑模式相关方法 ==========
  904. /**
  905. * 切换编辑模式
  906. */
  907. private void toggleEditMode(boolean editMode) {
  908. log.info("切换UI编辑模式: {}", editMode);
  909. // 双工位模式下,编辑模式只显示一个面板(因为布局是同步的)
  910. if (config.isDualMode() && panels.size() > 1) {
  911. if (editMode) {
  912. // 进入编辑模式:隐藏第二个面板,调整布局
  913. panels.get(1).setVisible(false);
  914. workPanel.setLayout(new GridLayout(1, 1, 0, 0));
  915. // 只对第一个面板启用编辑模式
  916. panels.get(0).setEditMode(true);
  917. } else {
  918. // 退出编辑模式:先退出编辑状态
  919. panels.get(0).setEditMode(false);
  920. // 同步布局到第二个面板(重新加载配置)
  921. panels.get(1).reloadLayoutConfig();
  922. // 恢复双面板布局
  923. workPanel.setLayout(new GridLayout(1, 2, 20, 0));
  924. panels.get(1).setVisible(true);
  925. }
  926. workPanel.revalidate();
  927. workPanel.repaint();
  928. } else {
  929. // 单工位模式:直接切换
  930. for (WorkstationPanel panel : panels) {
  931. panel.setEditMode(editMode);
  932. }
  933. }
  934. if (editMode) {
  935. String modeHint = config.isDualMode() ?
  936. "\n注意:双工位模式下只显示一个编辑面板,布局会同步到两个工位" : "";
  937. JOptionPane.showMessageDialog(this,
  938. "已进入UI编辑模式\n\n" +
  939. "操作说明:\n" +
  940. "• 拖动组件可调整位置\n" +
  941. "• 拖动边缘和角落可调整大小\n" +
  942. "• 右键组件可访问更多选项\n" +
  943. "• 组件会自动吸附到网格\n" +
  944. "• 编辑完成后记得保存布局\n\n" +
  945. "注意:编辑模式下工位功能将被禁用" + modeHint,
  946. "UI编辑模式",
  947. JOptionPane.INFORMATION_MESSAGE);
  948. }
  949. }
  950. /**
  951. * 切换网格显示
  952. */
  953. private void toggleGrid(boolean show) {
  954. for (WorkstationPanel panel : panels) {
  955. panel.toggleGrid(show);
  956. }
  957. }
  958. /**
  959. * 保存UI布局
  960. */
  961. private void saveUILayout() {
  962. if (panels.isEmpty()) {
  963. JOptionPane.showMessageDialog(this, "没有可保存的工位面板", "提示",
  964. JOptionPane.WARNING_MESSAGE);
  965. return;
  966. }
  967. // 保存第一个面板的布局(单/双工位共享布局)
  968. panels.get(0).saveLayoutConfig();
  969. // 保存后自动退出编辑模式(找到菜单项并取消选中)
  970. exitEditModeAfterSave();
  971. log.info("UI布局已保存");
  972. }
  973. /**
  974. * 保存后退出编辑模式
  975. */
  976. private void exitEditModeAfterSave() {
  977. // 查找设置菜单中的 UI编辑模式 选项并取消勾选
  978. JMenuBar menuBar = getJMenuBar();
  979. if (menuBar != null) {
  980. for (int i = 0; i < menuBar.getMenuCount(); i++) {
  981. JMenu menu = menuBar.getMenu(i);
  982. if (menu != null && "设置".equals(menu.getText())) {
  983. for (int j = 0; j < menu.getItemCount(); j++) {
  984. JMenuItem item = menu.getItem(j);
  985. if (item instanceof JCheckBoxMenuItem) {
  986. JCheckBoxMenuItem checkItem = (JCheckBoxMenuItem) item;
  987. if ("UI编辑模式".equals(checkItem.getText()) && checkItem.isSelected()) {
  988. checkItem.setSelected(false);
  989. // 触发 itemStateChanged 事件
  990. toggleEditMode(false);
  991. log.info("保存后自动退出编辑模式");
  992. break;
  993. }
  994. }
  995. }
  996. break;
  997. }
  998. }
  999. }
  1000. }
  1001. /**
  1002. * 导出UI布局
  1003. */
  1004. private void exportUILayout() {
  1005. if (panels.isEmpty()) {
  1006. JOptionPane.showMessageDialog(this, "没有可导出的工位面板", "提示",
  1007. JOptionPane.WARNING_MESSAGE);
  1008. return;
  1009. }
  1010. panels.get(0).exportLayoutConfig();
  1011. }
  1012. /**
  1013. * 导入UI布局
  1014. */
  1015. private void importUILayout() {
  1016. if (panels.isEmpty()) {
  1017. JOptionPane.showMessageDialog(this, "没有可导入布局的工位面板", "提示",
  1018. JOptionPane.WARNING_MESSAGE);
  1019. return;
  1020. }
  1021. panels.get(0).importLayoutConfig();
  1022. // 询问是否应用到所有工位
  1023. if (panels.size() > 1) {
  1024. int result = JOptionPane.showConfirmDialog(this,
  1025. "是否将导入的布局应用到所有工位?",
  1026. "应用布局",
  1027. JOptionPane.YES_NO_OPTION);
  1028. if (result == JOptionPane.YES_OPTION) {
  1029. // TODO: 如果需要,可以同步布局到其他工位面板
  1030. log.info("布局将应用到所有工位");
  1031. }
  1032. }
  1033. }
  1034. // ========== 流程Tab解锁相关方法 ==========
  1035. /**
  1036. * 用户按钮点击处理
  1037. * - 同步驱动"2用户→2项目→2心跳"免密码序列
  1038. * - 保留原三击密码弹框解锁
  1039. */
  1040. private void onUserMenuClicked() {
  1041. // 1) 先处理免密码序列
  1042. onUnlockClick("user");
  1043. // 2) 保留三击密码解锁
  1044. long now = System.currentTimeMillis();
  1045. if (now - lastUserMenuClickTime > TRIPLE_CLICK_INTERVAL) {
  1046. userMenuClickCount = 1;
  1047. } else {
  1048. userMenuClickCount++;
  1049. }
  1050. lastUserMenuClickTime = now;
  1051. if (userMenuClickCount >= 3) {
  1052. userMenuClickCount = 0;
  1053. if (workflowTabVisible) {
  1054. hideWorkflowTab();
  1055. } else {
  1056. showWorkflowPasswordDialog();
  1057. }
  1058. }
  1059. }
  1060. /**
  1061. * 免密码解锁序列:用户x2 → 项目x2 → 心跳x2
  1062. * 每次点击间隔超过 UNLOCK_INTERVAL 或点错按钮则重置
  1063. */
  1064. private void onUnlockClick(String source) {
  1065. long now = System.currentTimeMillis();
  1066. if (now - unlockLastClickTime > UNLOCK_INTERVAL) {
  1067. // 超时重置
  1068. unlockStage = 0;
  1069. unlockStageCount = 0;
  1070. }
  1071. unlockLastClickTime = now;
  1072. String expected;
  1073. switch (unlockStage) {
  1074. case 0: expected = "user"; break;
  1075. case 1: expected = "project"; break;
  1076. case 2: expected = "heart"; break;
  1077. default: unlockStage = 0; unlockStageCount = 0; return;
  1078. }
  1079. if (!expected.equals(source)) {
  1080. // 点错按钮,重置(但如果 source==user 说明可能是新一轮开始)
  1081. unlockStage = 0;
  1082. unlockStageCount = "user".equals(source) ? 1 : 0;
  1083. return;
  1084. }
  1085. unlockStageCount++;
  1086. if (unlockStageCount >= 2) {
  1087. // 进入下一阶段
  1088. unlockStage++;
  1089. unlockStageCount = 0;
  1090. if (unlockStage >= 3) {
  1091. // 六步完成,解锁
  1092. unlockStage = 0;
  1093. if (workflowTabVisible) {
  1094. hideWorkflowTab();
  1095. } else {
  1096. showWorkflowTab();
  1097. log.info("免密码解锁成功");
  1098. }
  1099. }
  1100. }
  1101. }
  1102. /**
  1103. * 显示密码输入对话框
  1104. */
  1105. private void showWorkflowPasswordDialog() {
  1106. JPasswordField passwordField = new JPasswordField();
  1107. passwordField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1108. int result = JOptionPane.showConfirmDialog(this,
  1109. passwordField, "请输入密码",
  1110. JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1111. if (result == JOptionPane.OK_OPTION) {
  1112. String input = new String(passwordField.getPassword());
  1113. if (WORKFLOW_PASSWORD.equals(input)) {
  1114. showWorkflowTab();
  1115. } else {
  1116. JOptionPane.showMessageDialog(this, "密码错误", "提示", JOptionPane.ERROR_MESSAGE);
  1117. }
  1118. }
  1119. }
  1120. /**
  1121. * 显示流程Tab
  1122. */
  1123. private void showWorkflowTab() {
  1124. if (!workflowTabVisible) {
  1125. tabbedPane.addTab("流程",
  1126. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))),
  1127. workflowMonitorPanel, null);
  1128. workflowTabVisible = true;
  1129. tabbedPane.setSelectedComponent(workflowMonitorPanel);
  1130. workflowMonitorPanel.refresh();
  1131. log.info("流程监控Tab已解锁显示");
  1132. }
  1133. applyLockVisibility(false);
  1134. }
  1135. /**
  1136. * 隐藏流程Tab
  1137. */
  1138. private void hideWorkflowTab() {
  1139. if (workflowTabVisible) {
  1140. tabbedPane.remove(workflowMonitorPanel);
  1141. workflowTabVisible = false;
  1142. log.info("流程监控Tab已隐藏");
  1143. }
  1144. applyLockVisibility(true);
  1145. }
  1146. /**
  1147. * 根据锁定状态统一应用所有"需解锁"菜单的显隐
  1148. * - locked=true 所有需解锁项默认隐藏;但"始终显示"白名单中的保留可见
  1149. * - locked=false 所有需解锁项全部可见
  1150. */
  1151. private void applyLockVisibility(boolean locked) {
  1152. boolean unlocked = !locked;
  1153. if (editModeSeparator != null) editModeSeparator.setVisible(unlocked);
  1154. if (projectDeviceMenu != null) projectDeviceMenu.setVisible(unlocked);
  1155. if (layoutMenu != null) layoutMenu.setVisible(unlocked);
  1156. if (alwaysShowMenu != null) alwaysShowMenu.setVisible(unlocked);
  1157. // 手动提交:尊重"始终显示"勾选
  1158. if (manualSubmitMenu != null) {
  1159. boolean always = com.mes.core.UiPrefs.getInstance()
  1160. .isAlwaysShow(com.mes.core.UiPrefs.KEY_MANUAL_SUBMIT);
  1161. manualSubmitMenu.setVisible(unlocked || always);
  1162. }
  1163. }
  1164. // ========== 产线位置/产品号切换/设备IP修改 ==========
  1165. /**
  1166. * 刷新工具栏上的产线位置/产品号显示
  1167. */
  1168. private void updateProjectDisplay() {
  1169. if (projectMenu == null) return;
  1170. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1171. com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine();
  1172. com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct();
  1173. if (line != null && product != null) {
  1174. projectMenu.setText(line.getDisplay() + " / " + product.getDisplay());
  1175. projectMenu.setToolTipText(String.format(
  1176. "<html>产线位置:%s<br>产品号:%s<br>工件码前缀:%s<br>冷板码前缀:%s<br>底护板码前缀:%s</html>",
  1177. line.getDisplay(), product.getDisplay(),
  1178. product.getProductPrefix(), product.getColdPlatePrefix(), product.getBottomPlatePrefix()));
  1179. }
  1180. }
  1181. /**
  1182. * 弹出切换产线位置对话框(只影响工位号→工序名称的映射)
  1183. */
  1184. private void showSwitchLineDialog() {
  1185. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1186. java.util.List<com.mes.core.ProjectConfigManager.Line> lines = pc.getLines();
  1187. String[] options = new String[lines.size()];
  1188. int currentIndex = 0;
  1189. for (int i = 0; i < lines.size(); i++) {
  1190. com.mes.core.ProjectConfigManager.Line l = lines.get(i);
  1191. options[i] = l.getDisplay();
  1192. if (l.getId().equalsIgnoreCase(pc.getCurrentLineId())) {
  1193. currentIndex = i;
  1194. }
  1195. }
  1196. JComboBox<String> combo = new JComboBox<>(options);
  1197. combo.setSelectedIndex(currentIndex);
  1198. combo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1199. JPanel panel = new JPanel(new BorderLayout(5, 5));
  1200. panel.add(new JLabel("选择产线位置:"), BorderLayout.NORTH);
  1201. panel.add(combo, BorderLayout.CENTER);
  1202. int result = JOptionPane.showConfirmDialog(this, panel,
  1203. "切换产线位置", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1204. if (result != JOptionPane.OK_OPTION) return;
  1205. int idx = combo.getSelectedIndex();
  1206. if (idx < 0 || idx >= lines.size()) return;
  1207. com.mes.core.ProjectConfigManager.Line target = lines.get(idx);
  1208. if (target.getId().equalsIgnoreCase(pc.getCurrentLineId())) {
  1209. return;
  1210. }
  1211. pc.switchLine(target.getId());
  1212. // 切换产线位置后同步工位名(标题 + context 的 stationName)
  1213. String newLineId = target.getId();
  1214. for (StationContext ctx : contexts) {
  1215. String newName = com.mes.core.OprnoRegistry.getGwDes(newLineId, ctx.getStationCode());
  1216. if (newName == null || newName.isEmpty()) newName = ctx.getStationCode();
  1217. ctx.updateStation(null, newName, null);
  1218. }
  1219. if (!config.getStations().isEmpty()) {
  1220. String code = config.getStations().get(0).getCode();
  1221. String name = com.mes.core.OprnoRegistry.getGwDes(newLineId, code);
  1222. String title = "MES系统客户端:" + code;
  1223. if (name != null && !name.isEmpty() && !name.equals(code)) title += " - " + name;
  1224. setTitle(title);
  1225. }
  1226. refreshMenuTexts();
  1227. updateProjectDisplay();
  1228. JOptionPane.showMessageDialog(this,
  1229. "已切换到:" + target.getDisplay() + "\n工位名称已刷新",
  1230. "切换成功", JOptionPane.INFORMATION_MESSAGE);
  1231. }
  1232. /**
  1233. * 弹出切换产品号对话框(只影响工件码/冷板码/底护板码前缀)
  1234. */
  1235. private void showSwitchProductDialog() {
  1236. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1237. java.util.List<com.mes.core.ProjectConfigManager.Product> products = pc.getProducts();
  1238. String[] options = new String[products.size()];
  1239. int currentIndex = 0;
  1240. for (int i = 0; i < products.size(); i++) {
  1241. com.mes.core.ProjectConfigManager.Product p = products.get(i);
  1242. options[i] = String.format("%s (工件:%s 冷板:%s 底护板:%s)",
  1243. p.getDisplay(), p.getProductPrefix(), p.getColdPlatePrefix(), p.getBottomPlatePrefix());
  1244. if (p.getId().equalsIgnoreCase(pc.getCurrentProductId())) {
  1245. currentIndex = i;
  1246. }
  1247. }
  1248. JComboBox<String> combo = new JComboBox<>(options);
  1249. combo.setSelectedIndex(currentIndex);
  1250. combo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1251. JPanel panel = new JPanel(new BorderLayout(5, 5));
  1252. panel.add(new JLabel("选择产品号:"), BorderLayout.NORTH);
  1253. panel.add(combo, BorderLayout.CENTER);
  1254. int result = JOptionPane.showConfirmDialog(this, panel,
  1255. "切换产品号", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1256. if (result != JOptionPane.OK_OPTION) return;
  1257. int idx = combo.getSelectedIndex();
  1258. if (idx < 0 || idx >= products.size()) return;
  1259. com.mes.core.ProjectConfigManager.Product target = products.get(idx);
  1260. if (target.getId().equalsIgnoreCase(pc.getCurrentProductId())) {
  1261. return;
  1262. }
  1263. pc.switchProduct(target.getId());
  1264. // 重置所有工位,避免沿用旧产品号扫到一半的码
  1265. for (int i = 0; i < engines.size(); i++) {
  1266. engines.get(i).reset();
  1267. if (i < panels.size()) panels.get(i).reset();
  1268. }
  1269. refreshMenuTexts();
  1270. updateProjectDisplay();
  1271. JOptionPane.showMessageDialog(this,
  1272. "已切换到:" + target.getDisplay() + "\n前缀校验立即生效,所有工位已重置",
  1273. "切换成功", JOptionPane.INFORMATION_MESSAGE);
  1274. }
  1275. /**
  1276. * 弹出修改拉铆设备IP对话框
  1277. */
  1278. private void showChangeDeviceIpDialog() {
  1279. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1280. String currentIp = pc.getDeviceIp();
  1281. String input = (String) JOptionPane.showInputDialog(this,
  1282. "请输入拉铆设备IP:",
  1283. "修改拉铆设备IP",
  1284. JOptionPane.PLAIN_MESSAGE,
  1285. null, null, currentIp);
  1286. if (input == null) return;
  1287. input = input.trim();
  1288. if (input.isEmpty()) return;
  1289. if (!isValidIp(input)) {
  1290. JOptionPane.showMessageDialog(this, "IP格式不正确: " + input,
  1291. "错误", JOptionPane.ERROR_MESSAGE);
  1292. return;
  1293. }
  1294. if (input.equals(currentIp)) return;
  1295. // 1. 持久化
  1296. pc.setDeviceIp(input);
  1297. // 2. 覆盖当前 StationConfig 中的连接IP
  1298. for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) {
  1299. conn.setIp(input);
  1300. }
  1301. // 3. 断开并重连设备驱动
  1302. reconnectDeviceDrivers();
  1303. refreshMenuTexts();
  1304. JOptionPane.showMessageDialog(this,
  1305. "拉铆设备IP已更新为:" + input + "\n已尝试重新连接设备",
  1306. "修改成功", JOptionPane.INFORMATION_MESSAGE);
  1307. }
  1308. /**
  1309. * 断开并重新创建设备驱动,重新注入到各工位上下文
  1310. */
  1311. private void reconnectDeviceDrivers() {
  1312. // 断开旧驱动
  1313. if (deviceDrivers != null) {
  1314. for (IDeviceDriver d : deviceDrivers.values()) {
  1315. try { d.disconnect(); } catch (Exception e) { log.warn("断开旧设备驱动异常", e); }
  1316. }
  1317. }
  1318. // 重新创建
  1319. Map<Integer, IDeviceDriver> newDrivers = new HashMap<>();
  1320. if (config.isDeviceEnabled()) {
  1321. newDrivers = DeviceDriverFactory.createDrivers(config);
  1322. }
  1323. this.deviceDrivers = newDrivers;
  1324. // 重新注入到每个工位上下文
  1325. for (int i = 0; i < contexts.size(); i++) {
  1326. StationContext ctx = contexts.get(i);
  1327. if (deviceDrivers.containsKey(i)) {
  1328. ctx.setDeviceDriver(deviceDrivers.get(i));
  1329. }
  1330. StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
  1331. if (row2 != null) {
  1332. int connIdx = row2.getConnectionIndex();
  1333. if (deviceDrivers.containsKey(connIdx)) {
  1334. ctx.setDeviceDriver2(deviceDrivers.get(connIdx));
  1335. }
  1336. }
  1337. }
  1338. log.info("设备驱动已重新连接: IP={}", com.mes.core.ProjectConfigManager.getInstance().getDeviceIp());
  1339. }
  1340. /**
  1341. * 修改MES服务器IP(修改完提示重启生效,避免重建TCP客户端的复杂逻辑)
  1342. */
  1343. private void showChangeServerIpDialog() {
  1344. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1345. String currentIp = config.getServerIp();
  1346. String input = (String) JOptionPane.showInputDialog(this,
  1347. "请输入MES服务器IP:",
  1348. "修改MES服务器IP",
  1349. JOptionPane.PLAIN_MESSAGE,
  1350. null, null, currentIp);
  1351. if (input == null) return;
  1352. input = input.trim();
  1353. if (input.isEmpty()) return;
  1354. if (!isValidIp(input)) {
  1355. JOptionPane.showMessageDialog(this, "IP格式不正确: " + input,
  1356. "错误", JOptionPane.ERROR_MESSAGE);
  1357. return;
  1358. }
  1359. if (input.equals(currentIp)) return;
  1360. pc.setServerIpOverride(input);
  1361. refreshMenuTexts();
  1362. JOptionPane.showMessageDialog(this,
  1363. "MES服务器IP已保存为:" + input + "\n请重启程序生效",
  1364. "修改成功", JOptionPane.INFORMATION_MESSAGE);
  1365. }
  1366. /**
  1367. * 简单的IPv4格式校验
  1368. */
  1369. private boolean isValidIp(String ip) {
  1370. if (ip == null) return false;
  1371. String[] parts = ip.split("\\.");
  1372. if (parts.length != 4) return false;
  1373. for (String part : parts) {
  1374. try {
  1375. int n = Integer.parseInt(part);
  1376. if (n < 0 || n > 255) return false;
  1377. } catch (NumberFormatException e) {
  1378. return false;
  1379. }
  1380. }
  1381. return true;
  1382. }
  1383. /**
  1384. * 刷新"项目与设备"子菜单的显示文本,把当前值显示在菜单项后面
  1385. */
  1386. private void refreshMenuTexts() {
  1387. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1388. if (switchProjectItem != null) {
  1389. com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine();
  1390. switchProjectItem.setText("切换产线位置 (当前: " + (line != null ? line.getDisplay() : "?") + ")");
  1391. }
  1392. if (switchProductItem != null) {
  1393. com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct();
  1394. switchProductItem.setText("切换产品号 (当前: " + (product != null ? product.getDisplay() : "?") + ")");
  1395. }
  1396. if (switchStationItem != null && !config.getStations().isEmpty()) {
  1397. String code = config.getStations().get(0).getCode();
  1398. String des = com.mes.core.OprnoRegistry.getGwDes(pc.getCurrentLineId(), code);
  1399. if (des == null || des.isEmpty()) des = "未知工位";
  1400. switchStationItem.setText("修改工位号 (当前: " + code + " - " + des + ")");
  1401. }
  1402. if (deviceIpItem != null) {
  1403. deviceIpItem.setText("修改拉铆设备IP (当前: " + pc.getDeviceIp() + ")");
  1404. }
  1405. if (serverIpItem != null) {
  1406. serverIpItem.setText("修改MES服务器IP (当前: " + config.getServerIp() + ")");
  1407. }
  1408. }
  1409. /**
  1410. * 弹出修改工位号对话框
  1411. * 工位列表按当前产线位置(P1-P4)展示;工位名称由 OprnoRegistry 自动推导
  1412. */
  1413. private void showSwitchStationDialog() {
  1414. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1415. String lineId = pc.getCurrentLineId();
  1416. String currentCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode();
  1417. // 拆分当前code为 "基础码" + "后缀"
  1418. String currentBase = com.mes.core.OprnoRegistry.formatOprno(currentCode);
  1419. String currentSuffix = currentCode.length() > currentBase.length()
  1420. ? currentCode.substring(currentBase.length()).toUpperCase() : "";
  1421. // 工位号下拉(固定项,不可编辑)
  1422. JComboBox<String> codeCombo = new JComboBox<>();
  1423. codeCombo.setEditable(false);
  1424. codeCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1425. for (String c : com.mes.core.OprnoRegistry.listCodes(lineId)) {
  1426. codeCombo.addItem(c);
  1427. }
  1428. if (com.mes.core.OprnoRegistry.listCodes(lineId).contains(currentBase)) {
  1429. codeCombo.setSelectedItem(currentBase);
  1430. }
  1431. // 后缀下拉(空 / A-L)
  1432. String[] suffixes = new String[]{"(无)", "A","B","C","D","E","F","G","H","I","J","K","L"};
  1433. JComboBox<String> suffixCombo = new JComboBox<>(suffixes);
  1434. suffixCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1435. suffixCombo.setSelectedItem(currentSuffix.isEmpty() ? "(无)" : currentSuffix);
  1436. // 预览
  1437. JLabel previewLabel = new JLabel(" ");
  1438. previewLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
  1439. previewLabel.setForeground(new Color(0x1c, 0x6e, 0xa4));
  1440. // 工位名显示
  1441. JLabel desLabel = new JLabel(" ");
  1442. desLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
  1443. desLabel.setForeground(new Color(0x3a, 0x7a, 0xc0));
  1444. Runnable refresh = () -> {
  1445. String base = (String) codeCombo.getSelectedItem();
  1446. String sfx = (String) suffixCombo.getSelectedItem();
  1447. if (base == null) base = "";
  1448. String sfxReal = ("(无)".equals(sfx) || sfx == null) ? "" : sfx;
  1449. String full = base + sfxReal;
  1450. previewLabel.setText("最终工位号: " + (full.isEmpty() ? "-" : full));
  1451. String des = com.mes.core.OprnoRegistry.getGwDes(lineId, full);
  1452. desLabel.setText("工位名称: " + (des.isEmpty() ? "(未知工位)" : des));
  1453. };
  1454. refresh.run();
  1455. codeCombo.addActionListener(e -> refresh.run());
  1456. suffixCombo.addActionListener(e -> refresh.run());
  1457. JPanel panel = new JPanel(new java.awt.GridBagLayout());
  1458. java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints();
  1459. gbc.insets = new Insets(4, 4, 4, 4);
  1460. gbc.anchor = java.awt.GridBagConstraints.WEST;
  1461. gbc.gridx = 0; gbc.gridy = 0;
  1462. panel.add(new JLabel("当前产线位置:"), gbc);
  1463. gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
  1464. JLabel projectLbl = new JLabel(lineId);
  1465. projectLbl.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1466. panel.add(projectLbl, gbc);
  1467. gbc.gridx = 0; gbc.gridy = 1; gbc.fill = java.awt.GridBagConstraints.NONE;
  1468. panel.add(new JLabel("工位号:"), gbc);
  1469. gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
  1470. panel.add(codeCombo, gbc);
  1471. gbc.gridx = 0; gbc.gridy = 2; gbc.fill = java.awt.GridBagConstraints.NONE;
  1472. panel.add(new JLabel("后缀:"), gbc);
  1473. gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
  1474. panel.add(suffixCombo, gbc);
  1475. gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2;
  1476. panel.add(previewLabel, gbc);
  1477. gbc.gridy = 4;
  1478. panel.add(desLabel, gbc);
  1479. int result = JOptionPane.showConfirmDialog(this, panel, "修改工位号",
  1480. JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1481. if (result != JOptionPane.OK_OPTION) return;
  1482. String base = (String) codeCombo.getSelectedItem();
  1483. String sfx = (String) suffixCombo.getSelectedItem();
  1484. String sfxReal = ("(无)".equals(sfx) || sfx == null) ? "" : sfx;
  1485. String newCode = (base == null ? "" : base) + sfxReal;
  1486. if (newCode.isEmpty()) {
  1487. JOptionPane.showMessageDialog(this, "工位号不能为空", "错误", JOptionPane.ERROR_MESSAGE);
  1488. return;
  1489. }
  1490. if (newCode.equalsIgnoreCase(currentCode)) return;
  1491. pc.setStationOverride(newCode, null);
  1492. applyStationChange(newCode);
  1493. String des = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode);
  1494. JOptionPane.showMessageDialog(this,
  1495. "工位已更新为:" + newCode + (des.isEmpty() ? "" : (" - " + des)) + "\n已刷新界面并向MES重新同步",
  1496. "修改成功", JOptionPane.INFORMATION_MESSAGE);
  1497. }
  1498. /**
  1499. * 热更新工位号:更新 config/context/dispatcher/面板显示/窗口标题,并重新向MES同步
  1500. */
  1501. private void applyStationChange(String newCode) {
  1502. String oldCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode();
  1503. String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId();
  1504. // 1. 更新 config
  1505. if (!config.getStations().isEmpty()) {
  1506. config.getStations().get(0).setCode(newCode);
  1507. }
  1508. // 2. 更新 context(推导工位名)
  1509. String name = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode);
  1510. if (name == null || name.isEmpty()) name = newCode;
  1511. for (StationContext ctx : contexts) {
  1512. if (ctx.getStationIndex() == 0) {
  1513. ctx.updateStation(newCode, name, null);
  1514. }
  1515. }
  1516. // 3. dispatcher 注销旧的、注册新的
  1517. if (messageDispatcher != null && !oldCode.isEmpty() && !oldCode.equalsIgnoreCase(newCode)) {
  1518. messageDispatcher.unregisterStation(oldCode);
  1519. if (!engines.isEmpty() && !contexts.isEmpty()) {
  1520. messageDispatcher.registerStation(newCode, engines.get(0), contexts.get(0));
  1521. }
  1522. }
  1523. // 4. 刷新 WorkstationPanel 工位标签
  1524. for (WorkstationPanel p : panels) {
  1525. p.updateStationCode(newCode);
  1526. }
  1527. // 5. 重置工位并向 MES 重新同步
  1528. for (int i = 0; i < engines.size(); i++) {
  1529. engines.get(i).reset();
  1530. if (i < panels.size()) panels.get(i).reset();
  1531. contexts.get(i).setStatusMessage("工位已切换,请扫下一件", 0);
  1532. }
  1533. if (tcpClient != null && !contexts.isEmpty()) {
  1534. tcpClient.sendSync(contexts.get(0).getStationCode());
  1535. }
  1536. // 5b. 重建过程参数采集器(collector 以工位号为 key)
  1537. if (prodDataUploader != null && config.hasProdParams()) {
  1538. prodDataUploader.clearCollectors();
  1539. for (int i = 0; i < contexts.size(); i++) {
  1540. StationContext c = contexts.get(i);
  1541. ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig());
  1542. WorkflowEngine eng = engines.get(i);
  1543. for (com.mes.step.IWorkflowStep step : eng.getSteps()) {
  1544. if (step instanceof WaitCompleteStep) {
  1545. ((WaitCompleteStep) step).setProdDataCollector(collector);
  1546. }
  1547. // 注入到 ResetStationStep 供副机模式立即上传使用
  1548. stepFactory.injectProdParams(step, collector, prodDataUploader);
  1549. }
  1550. prodDataUploader.addCollector(c.getStationCode(), collector);
  1551. }
  1552. }
  1553. // 6. 窗口标题
  1554. String title = "MES系统客户端:" + newCode;
  1555. if (!name.isEmpty() && !name.equals(newCode)) title += " - " + name;
  1556. setTitle(title);
  1557. // 7. 刷新菜单文字
  1558. refreshMenuTexts();
  1559. log.info("工位已切换: {} -> {} ({})", oldCode, newCode, name);
  1560. }
  1561. /**
  1562. * 弹出工艺流程配置对话框
  1563. * 用户勾选启用的功能,保存后软重启所有工位流程
  1564. */
  1565. private void showFeaturesDialog() {
  1566. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1567. // yaml 定义的功能集合 + 当前有效值
  1568. Map<String, Boolean> yamlFeatures = config.getYamlFeatures();
  1569. if (yamlFeatures.isEmpty()) {
  1570. JOptionPane.showMessageDialog(this, "station.yaml 中未定义 workflow.features",
  1571. "提示", JOptionPane.WARNING_MESSAGE);
  1572. return;
  1573. }
  1574. // 功能显示名映射
  1575. Map<String, String> labels = new LinkedHashMap<>();
  1576. labels.put("check_quality", "质量查询(发AQDW问工件能否加工)");
  1577. labels.put("cold_plate", "扫+校验冷板码");
  1578. labels.put("bottom_plate", "扫+校验底护板码");
  1579. labels.put("riveting", "拉铆流程(心跳+计数+等待+停心跳)");
  1580. labels.put("prod_params", "拉铆过程参数采集");
  1581. labels.put("quality_submit", "提交工位结果(MBDW+MQDW;关闭=副机模式)");
  1582. // 构建复选框(按 yaml 声明顺序)
  1583. JPanel panel = new JPanel();
  1584. panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
  1585. panel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15));
  1586. Map<String, JCheckBox> checkBoxes = new LinkedHashMap<>();
  1587. for (String key : yamlFeatures.keySet()) {
  1588. String label = labels.getOrDefault(key, key);
  1589. JCheckBox cb = new JCheckBox(label, config.isFeatureEnabled(key));
  1590. cb.setFont(new Font("微软雅黑", Font.PLAIN, 18));
  1591. cb.setAlignmentX(Component.LEFT_ALIGNMENT);
  1592. panel.add(cb);
  1593. panel.add(Box.createVerticalStrut(6));
  1594. checkBoxes.put(key, cb);
  1595. }
  1596. // cold_plate 与 bottom_plate 互斥(勾一个自动取消另一个)
  1597. final JCheckBox cold = checkBoxes.get("cold_plate");
  1598. final JCheckBox bottom = checkBoxes.get("bottom_plate");
  1599. if (cold != null && bottom != null) {
  1600. cold.addActionListener(ev -> { if (cold.isSelected()) bottom.setSelected(false); });
  1601. bottom.addActionListener(ev -> { if (bottom.isSelected()) cold.setSelected(false); });
  1602. }
  1603. JLabel hint = new JLabel("<html><font color='gray' size='3'>冷板/底护板二选一;保存后将重建所有工位流程并重置</font></html>");
  1604. hint.setAlignmentX(Component.LEFT_ALIGNMENT);
  1605. panel.add(hint);
  1606. // 设备连接开关
  1607. panel.add(Box.createVerticalStrut(10));
  1608. JSeparator sep = new JSeparator();
  1609. sep.setMaximumSize(new java.awt.Dimension(Integer.MAX_VALUE, 1));
  1610. sep.setAlignmentX(Component.LEFT_ALIGNMENT);
  1611. panel.add(sep);
  1612. panel.add(Box.createVerticalStrut(6));
  1613. Boolean deviceOverride = pc.getDeviceEnabledOverride();
  1614. boolean deviceCurrently = config.isDeviceEnabled();
  1615. JCheckBox deviceEnabledCb = new JCheckBox("设备连接(PLC/Modbus)", deviceCurrently);
  1616. deviceEnabledCb.setFont(new Font("微软雅黑", Font.PLAIN, 18));
  1617. deviceEnabledCb.setAlignmentX(Component.LEFT_ALIGNMENT);
  1618. panel.add(deviceEnabledCb);
  1619. panel.add(Box.createVerticalStrut(4));
  1620. JLabel deviceHint = new JLabel("<html><font color='gray' size='3'>禁用后不连接PLC,上传结果不需要等待设备信号</font></html>");
  1621. deviceHint.setAlignmentX(Component.LEFT_ALIGNMENT);
  1622. panel.add(deviceHint);
  1623. // 单向联动:取消"拉铆流程"时自动取消"设备连接"(用户可再手动勾回)
  1624. final JCheckBox rivetingCb = checkBoxes.get("riveting");
  1625. if (rivetingCb != null) {
  1626. rivetingCb.addActionListener(ev -> {
  1627. if (!rivetingCb.isSelected()) {
  1628. deviceEnabledCb.setSelected(false);
  1629. }
  1630. });
  1631. }
  1632. // 记录旧的"设备连接"状态,用于保存后判断是否需要提示重启
  1633. boolean oldDeviceEnabled = deviceCurrently;
  1634. int result = JOptionPane.showConfirmDialog(this, panel,
  1635. "工艺流程配置", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1636. if (result != JOptionPane.OK_OPTION) return;
  1637. // 收集勾选结果
  1638. Map<String, Boolean> override = new LinkedHashMap<>();
  1639. for (Map.Entry<String, JCheckBox> e : checkBoxes.entrySet()) {
  1640. override.put(e.getKey(), e.getValue().isSelected());
  1641. }
  1642. // 二次兜底:如果用户绕过监听同时勾了两个,强制只保留冷板
  1643. if (Boolean.TRUE.equals(override.get("cold_plate"))
  1644. && Boolean.TRUE.equals(override.get("bottom_plate"))) {
  1645. override.put("bottom_plate", false);
  1646. }
  1647. pc.setFeaturesOverride(override);
  1648. // 保存设备连接覆盖
  1649. pc.setDeviceEnabledOverride(deviceEnabledCb.isSelected());
  1650. // 物料码输入框联动:勾了冷板或底护板 → 显示;都没勾 → 隐藏
  1651. boolean coldOn = Boolean.TRUE.equals(override.get("cold_plate"));
  1652. boolean bottomOn = Boolean.TRUE.equals(override.get("bottom_plate"));
  1653. // 拉铆流程联动:
  1654. // - 关闭 riveting: 两行(拉铆数量)都隐藏
  1655. // - 开启 riveting: 自动开启第1行;第2行保持用户设置
  1656. boolean rivetingOn = Boolean.TRUE.equals(override.get("riveting"));
  1657. Boolean row1Enabled = pc.getDeviceRow1EnabledOverride();
  1658. Boolean row2Enabled = pc.getDeviceRow2EnabledOverride();
  1659. if (!rivetingOn) {
  1660. row1Enabled = Boolean.FALSE;
  1661. row2Enabled = Boolean.FALSE;
  1662. } else {
  1663. row1Enabled = Boolean.TRUE;
  1664. }
  1665. pc.setDeviceInfoRowsOverride(
  1666. row1Enabled, pc.getDeviceRow1LabelOverride(),
  1667. row2Enabled, pc.getDeviceRow2LabelOverride(),
  1668. coldOn || bottomOn);
  1669. // 重新加载 yaml 配置(会按新 override 展开 steps)
  1670. StationConfig.reload();
  1671. this.config = StationConfig.getInstance();
  1672. // 重新应用当前设备IP覆盖
  1673. String ip = pc.getDeviceIp();
  1674. for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) {
  1675. conn.setIp(ip);
  1676. }
  1677. rebuildWorkflows();
  1678. // 按功能开关同步面板显示(物料码输入框显隐、label 文字)
  1679. for (WorkstationPanel p : panels) {
  1680. p.refreshDisplayFromConfig(config);
  1681. }
  1682. // 若行数变化涉及组件增删,强制重载布局(与"设备信息显示"对话框保持一致)
  1683. for (WorkstationPanel p : panels) {
  1684. try { p.reloadLayoutConfig(); } catch (Exception ex) { /* 忽略 */ }
  1685. }
  1686. JOptionPane.showMessageDialog(this,
  1687. "工艺流程已更新,所有工位已重建",
  1688. "配置成功", JOptionPane.INFORMATION_MESSAGE);
  1689. // 设备连接状态变更需要重启程序生效(PLC 连接在启动时初始化,软重建工位不会重连)
  1690. if (deviceEnabledCb.isSelected() != oldDeviceEnabled) {
  1691. JOptionPane.showMessageDialog(this,
  1692. "设备连接配置已变更,请重启程序使其生效",
  1693. "需要重启", JOptionPane.WARNING_MESSAGE);
  1694. }
  1695. }
  1696. /**
  1697. * 软重启工作流:重新构建所有工位的流程引擎、重新注入 panel 和过程参数采集器
  1698. * 不断开 TCP,不销毁设备驱动(IP变化由 showChangeDeviceIpDialog 另行处理)
  1699. */
  1700. private void rebuildWorkflows() {
  1701. log.info("开始重建工作流...");
  1702. // 先停止过程参数上传器并清空采集器
  1703. if (prodDataUploader != null) {
  1704. prodDataUploader.stop();
  1705. prodDataUploader.clearCollectors();
  1706. prodDataUploader = null;
  1707. }
  1708. // 若 features 启用了 prod_params 且 yaml 配置存在,重新创建 uploader
  1709. if (config.hasProdParams()) {
  1710. int uploadInterval = config.getProdParamsConfig().getUploadInterval();
  1711. prodDataUploader = new ProdDataUploader(
  1712. config.getServerIp(), config.getHttpPort(), uploadInterval);
  1713. }
  1714. // 逐个工位重建
  1715. for (int i = 0; i < engines.size(); i++) {
  1716. WorkflowEngine engine = engines.get(i);
  1717. StationContext ctx = contexts.get(i);
  1718. // 停止旧流程
  1719. engine.stop();
  1720. // 用新配置重建步骤
  1721. engine.buildFromConfig(config.getWorkflowSteps(), stepFactory);
  1722. // 重新注入 panel(扫码框弹出)
  1723. if (i < panels.size()) {
  1724. WorkstationPanel p = panels.get(i);
  1725. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  1726. stepFactory.injectPanel(step, p);
  1727. }
  1728. }
  1729. // 重新注入过程参数采集器
  1730. if (prodDataUploader != null && config.hasProdParams()) {
  1731. ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig());
  1732. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  1733. if (step instanceof WaitCompleteStep) {
  1734. ((WaitCompleteStep) step).setProdDataCollector(collector);
  1735. }
  1736. // 注入到 ResetStationStep 供副机模式立即上传使用
  1737. stepFactory.injectProdParams(step, collector, prodDataUploader);
  1738. }
  1739. prodDataUploader.addCollector(ctx.getStationCode(), collector);
  1740. }
  1741. // 重置并重新启动
  1742. engine.reset();
  1743. engine.start();
  1744. if (i < panels.size()) panels.get(i).reset();
  1745. ctx.setStatusMessage("工艺已更新,请扫下一件", 0);
  1746. }
  1747. // 刷新监控面板
  1748. if (workflowMonitorPanel != null) {
  1749. workflowMonitorPanel.setWorkstations(engines, contexts);
  1750. }
  1751. // 启动新的过程参数上传器
  1752. if (prodDataUploader != null) {
  1753. prodDataUploader.start();
  1754. }
  1755. log.info("工作流重建完成, 步骤数={}", config.getWorkflowSteps().size());
  1756. }
  1757. /**
  1758. * 弹出"设备信息显示"对话框
  1759. * 只允许勾选启用/禁用 + 改 label,寄存器地址不在UI改(留yaml)
  1760. */
  1761. private void showDeviceInfoUiDialog() {
  1762. com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
  1763. java.util.List<StationConfig.DeviceInfoRow> rows = config.getDeviceInfoRows();
  1764. // 第1行
  1765. StationConfig.DeviceInfoRow r1 = rows.size() > 0 ? rows.get(0) : null;
  1766. JCheckBox enableA = new JCheckBox("启用第1行", r1 != null && r1.isEnabled());
  1767. enableA.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1768. JTextField labelA = new JTextField(r1 != null && r1.getLabel() != null ? r1.getLabel() : "");
  1769. labelA.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1770. // 第2行
  1771. StationConfig.DeviceInfoRow r2 = rows.size() > 1 ? rows.get(1) : null;
  1772. JCheckBox enableB = new JCheckBox("启用第2行", r2 != null && r2.isEnabled());
  1773. enableB.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1774. JTextField labelB = new JTextField(r2 != null && r2.getLabel() != null ? r2.getLabel() : "");
  1775. labelB.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1776. // 物料码输入框显隐
  1777. JCheckBox showMaterial = new JCheckBox("显示物料码输入框", config.isShowMaterialInput());
  1778. showMaterial.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  1779. JPanel panel = new JPanel(new java.awt.GridBagLayout());
  1780. java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints();
  1781. gbc.insets = new Insets(4, 4, 4, 4);
  1782. gbc.anchor = java.awt.GridBagConstraints.WEST;
  1783. // 第1行
  1784. gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2;
  1785. panel.add(enableA, gbc);
  1786. gbc.gridy = 1; gbc.gridwidth = 1;
  1787. panel.add(new JLabel("标签:"), gbc);
  1788. gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; gbc.weightx = 1;
  1789. labelA.setPreferredSize(new Dimension(160, 26));
  1790. panel.add(labelA, gbc);
  1791. // 第2行
  1792. gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = java.awt.GridBagConstraints.NONE; gbc.weightx = 0;
  1793. panel.add(enableB, gbc);
  1794. gbc.gridy = 3; gbc.gridwidth = 1;
  1795. panel.add(new JLabel("标签:"), gbc);
  1796. gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; gbc.weightx = 1;
  1797. labelB.setPreferredSize(new Dimension(160, 26));
  1798. panel.add(labelB, gbc);
  1799. // 提示
  1800. gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.fill = java.awt.GridBagConstraints.NONE;
  1801. panel.add(showMaterial, gbc);
  1802. gbc.gridy = 5;
  1803. JLabel tip = new JLabel("<html><font color='gray' size='3'>寄存器地址需改 station.yaml</font></html>");
  1804. panel.add(tip, gbc);
  1805. int result = JOptionPane.showConfirmDialog(this, panel, "设备信息显示",
  1806. JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  1807. if (result != JOptionPane.OK_OPTION) return;
  1808. pc.setDeviceInfoRowsOverride(
  1809. enableA.isSelected(), labelA.getText(),
  1810. enableB.isSelected(), labelB.getText(),
  1811. showMaterial.isSelected());
  1812. // 重新加载 config 让 UI 覆盖生效
  1813. StationConfig.reload();
  1814. this.config = StationConfig.getInstance();
  1815. // 重新应用运行时IP
  1816. for (StationConfig.DeviceConnection conn : config.getDeviceConnections()) {
  1817. conn.setIp(pc.getDeviceIp());
  1818. }
  1819. // 刷新工位面板显示(启用/禁用行、label)
  1820. for (WorkstationPanel p : panels) {
  1821. p.refreshDisplayFromConfig(config);
  1822. }
  1823. // 若显示开关改变涉及组件增删,强制重载布局
  1824. for (WorkstationPanel p : panels) {
  1825. try { p.reloadLayoutConfig(); } catch (Exception ex) { /* 忽略 */ }
  1826. }
  1827. JOptionPane.showMessageDialog(this,
  1828. "设备信息显示配置已更新\n如行数变化布局未立即生效,请重启程序",
  1829. "配置成功", JOptionPane.INFORMATION_MESSAGE);
  1830. }
  1831. }