MainFrame.java 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  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 JSeparator layoutSeparator;
  66. private JMenuItem saveLayoutItem;
  67. private JMenuItem exportLayoutItem;
  68. private JMenuItem importLayoutItem;
  69. // 三击解锁相关
  70. private int userMenuClickCount = 0;
  71. private long lastUserMenuClickTime = 0;
  72. private static final int TRIPLE_CLICK_INTERVAL = 1500; // 1.5秒内完成3次点击
  73. private static final String WORKFLOW_PASSWORD = "mes123";
  74. public MainFrame() {
  75. this(StationConfig.getInstance());
  76. }
  77. public MainFrame(StationConfig config) {
  78. this.config = config;
  79. this.contexts = new ArrayList<>();
  80. this.engines = new ArrayList<>();
  81. this.panels = new ArrayList<>();
  82. this.deviceDrivers = new HashMap<>();
  83. initFrame();
  84. initComponents();
  85. initTcpClient();
  86. initWorkstations();
  87. }
  88. /**
  89. * 初始化窗口
  90. */
  91. private void initFrame() {
  92. String title = "MES系统客户端:" + getStationCodesString();
  93. if (config.getStationName() != null && !config.getStationName().isEmpty()) {
  94. title += " - " + config.getStationName();
  95. }
  96. setTitle(title);
  97. setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/resources/image/bg/logo.png")));
  98. setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
  99. // 根据工位数量设置窗口初始宽度:双工位宽度是单工位的两倍
  100. int stationCount = config.getStations() != null ? config.getStations().size() : 1;
  101. int baseWidth = 1024;
  102. int windowWidth = stationCount > 1 ? (int)(baseWidth * 1) : baseWidth;
  103. setBounds(0, 0, windowWidth, 768);
  104. addWindowListener(new WindowAdapter() {
  105. @Override
  106. public void windowClosing(WindowEvent e) {
  107. onWindowClosing();
  108. }
  109. });
  110. }
  111. /**
  112. * 初始化UI组件 - 参照OP150版本
  113. */
  114. private void initComponents() {
  115. // 菜单栏
  116. JMenuBar menuBar = new JMenuBar();
  117. menuBar.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 26));
  118. setJMenuBar(menuBar);
  119. // 用户菜单
  120. JMenu fileMenu = new JMenu("用户");
  121. fileMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/user.png"))));
  122. fileMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  123. menuBar.add(fileMenu);
  124. JMenuItem exitMenuItem = new JMenuItem("退出");
  125. exitMenuItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/logoff.png"))));
  126. exitMenuItem.setFont(new Font("微软雅黑", Font.PLAIN, 22));
  127. exitMenuItem.addMouseListener(new MouseAdapter() {
  128. @Override
  129. public void mousePressed(MouseEvent e) {
  130. logoff();
  131. }
  132. });
  133. fileMenu.add(exitMenuItem);
  134. // 设置菜单
  135. JMenu settingMenu = new JMenu("设置");
  136. settingMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
  137. settingMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  138. menuBar.add(settingMenu);
  139. // 重连MES
  140. JMenuItem resetTcpMenu = new JMenuItem("重连MES");
  141. resetTcpMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  142. resetTcpMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  143. resetTcpMenu.addMouseListener(new MouseAdapter() {
  144. @Override
  145. public void mousePressed(MouseEvent e) {
  146. reconnectMes();
  147. }
  148. });
  149. settingMenu.add(resetTcpMenu);
  150. // 为每个工位添加刷新菜单
  151. for (int i = 0; i < config.getStationCount(); i++) {
  152. final int stationIndex = i;
  153. StationConfig.StationInfo station = config.getStation(i);
  154. if (station != null) {
  155. JMenuItem refreshMenu = new JMenuItem("刷新" + station.getCode());
  156. refreshMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/reset_logo.png"))));
  157. refreshMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  158. refreshMenu.addMouseListener(new MouseAdapter() {
  159. @Override
  160. public void mousePressed(MouseEvent e) {
  161. resetStation(stationIndex);
  162. }
  163. });
  164. settingMenu.add(refreshMenu);
  165. }
  166. }
  167. editModeSeparator = new JSeparator();
  168. editModeSeparator.setVisible(false);
  169. settingMenu.add(editModeSeparator);
  170. // UI编辑模式(默认隐藏,密码解锁后显示)
  171. editModeItem = new JCheckBoxMenuItem("UI编辑模式", false);
  172. editModeItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  173. editModeItem.addItemListener(e -> {
  174. boolean selected = editModeItem.isSelected();
  175. toggleEditMode(selected);
  176. });
  177. editModeItem.setVisible(false);
  178. settingMenu.add(editModeItem);
  179. layoutSeparator = new JSeparator();
  180. layoutSeparator.setVisible(false);
  181. settingMenu.add(layoutSeparator);
  182. // 保存UI布局(默认隐藏,密码解锁后显示)
  183. saveLayoutItem = new JMenuItem("保存UI布局");
  184. saveLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/save_bg.png"))));
  185. saveLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  186. saveLayoutItem.addActionListener(e -> saveUILayout());
  187. saveLayoutItem.setVisible(false);
  188. settingMenu.add(saveLayoutItem);
  189. // 导出布局配置(默认隐藏,密码解锁后显示)
  190. exportLayoutItem = new JMenuItem("导出布局配置");
  191. exportLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/download.png"))));
  192. exportLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  193. exportLayoutItem.addActionListener(e -> exportUILayout());
  194. exportLayoutItem.setVisible(false);
  195. settingMenu.add(exportLayoutItem);
  196. // 导入布局配置(默认隐藏,密码解锁后显示)
  197. importLayoutItem = new JMenuItem("导入布局配置");
  198. importLayoutItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/open_file.png"))));
  199. importLayoutItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  200. importLayoutItem.addActionListener(e -> importUILayout());
  201. importLayoutItem.setVisible(false);
  202. settingMenu.add(importLayoutItem);
  203. // 内容面板
  204. contentPane = new JPanel();
  205. contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
  206. setContentPane(contentPane);
  207. contentPane.setLayout(new BorderLayout(0, 0));
  208. // 工具栏 - 完全参考OP40
  209. JToolBar toolBar = new JToolBar();
  210. contentPane.add(toolBar, BorderLayout.NORTH);
  211. // 状态标签
  212. JLabel statusLabel = new JLabel("状态:");
  213. statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
  214. statusLabel.setForeground(Color.BLACK);
  215. statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  216. statusLabel.setBackground(Color.LIGHT_GRAY);
  217. toolBar.add(statusLabel);
  218. toolbarStatusMenu = new JButton("等待加工信号");
  219. toolbarStatusMenu.setForeground(Color.GREEN);
  220. toolbarStatusMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  221. toolbarStatusMenu.setBackground(Color.BLACK);
  222. toolBar.add(toolbarStatusMenu);
  223. toolBar.add(new JLabel(" "));
  224. // 心跳标签
  225. JLabel heartBeatLabel = new JLabel("心跳:");
  226. heartBeatLabel.setHorizontalAlignment(SwingConstants.CENTER);
  227. heartBeatLabel.setForeground(Color.BLACK);
  228. heartBeatLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  229. heartBeatLabel.setBackground(Color.LIGHT_GRAY);
  230. toolBar.add(heartBeatLabel);
  231. // 心跳显示(带时间)- 完全参考OP40
  232. heartBeatMenu = new JButton("");
  233. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/green_dot.png"))));
  234. heartBeatMenu.setForeground(Color.GREEN);
  235. heartBeatMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  236. heartBeatMenu.setBackground(Color.BLACK);
  237. toolBar.add(heartBeatMenu);
  238. toolBar.add(new JLabel(" "));
  239. // 用户标签
  240. JLabel userLabel = new JLabel("登录用户:");
  241. userLabel.setHorizontalAlignment(SwingConstants.CENTER);
  242. userLabel.setForeground(Color.BLACK);
  243. userLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
  244. userLabel.setBackground(Color.LIGHT_GRAY);
  245. toolBar.add(userLabel);
  246. userMenu = new JButton("");
  247. userMenu.setForeground(Color.GREEN);
  248. userMenu.setFont(new Font("微软雅黑", Font.PLAIN, 22));
  249. userMenu.setBackground(Color.BLACK);
  250. userMenu.addActionListener(e -> onUserMenuClicked());
  251. toolBar.add(userMenu);
  252. // 标签页
  253. tabbedPane = new JTabbedPane(JTabbedPane.TOP);
  254. tabbedPane.setMinimumSize(new Dimension(400, 50));
  255. tabbedPane.setFont(new Font("宋体", Font.BOLD, 22));
  256. contentPane.add(tabbedPane);
  257. // 工作面板 - 直接添加,支持等比例缩放
  258. JPanel workPanel = createWorkPanel();
  259. tabbedPane.addTab("工作面板",
  260. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/a_side.png"))),
  261. workPanel, null);
  262. tabbedPane.setEnabledAt(0, true);
  263. // 开班点检Tab - 使用WebView(参考op150)
  264. // URL格式: http://{server_ip}:8980/js/a/mes/mesProcessCheckRecord/ulist?oprno={oprno}&lineSn={line_sn}
  265. String shiftCheckOprno = config.getStation(0) != null ? config.getStation(0).getCode() : "";
  266. String shiftCheckUrl = String.format("http://%s:%d/js/a/mes/mesProcessCheckRecord/ulist?oprno=%s&lineSn=%s",
  267. config.getServerIp(), config.getHttpPort(), shiftCheckOprno, config.getLineSn());
  268. shiftCheckWebView = new com.mes.ui.component.MesWebView(shiftCheckUrl);
  269. tabbedPane.addTab("开班点检",
  270. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))),
  271. shiftCheckWebView, null);
  272. // 工作记录Tab - 使用WebView(参考op150)
  273. // URL格式: http://{server_ip}:8980/js/a/mes/mesProductRecord/work?oprno={oprno}&lineSn={line_sn}
  274. String workRecordOprno = config.getStation(0) != null ? config.getStation(0).getCode() : "";
  275. String workRecordUrl = String.format("http://%s:%d/js/a/mes/mesProductRecord/work?oprno=%s&lineSn=%s",
  276. config.getServerIp(), config.getHttpPort(), workRecordOprno, config.getLineSn());
  277. workRecordWebView = new com.mes.ui.component.MesWebView(workRecordUrl);
  278. tabbedPane.addTab("工作记录",
  279. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_data_preprocess.png"))),
  280. workRecordWebView, null);
  281. // 流程监控Tab - 默认隐藏,需要三击用户按钮并输入密码解锁
  282. workflowMonitorPanel = new WorkflowMonitorPanel();
  283. // 切换Tab时自动刷新
  284. tabbedPane.addChangeListener(e -> {
  285. int selectedIndex = tabbedPane.getSelectedIndex();
  286. if (selectedIndex < 0) return;
  287. String tabTitle = tabbedPane.getTitleAt(selectedIndex);
  288. if ("开班点检".equals(tabTitle) && shiftCheckWebView != null) {
  289. shiftCheckWebView.reload();
  290. } else if ("工作记录".equals(tabTitle) && workRecordWebView != null) {
  291. workRecordWebView.reload();
  292. } else if ("流程".equals(tabTitle) && workflowMonitorPanel != null) {
  293. workflowMonitorPanel.refresh();
  294. }
  295. });
  296. log.info("UI组件初始化完成");
  297. }
  298. /**
  299. * 创建工作面板
  300. */
  301. private JPanel createWorkPanel() {
  302. workPanel = new JPanel();
  303. if (config.isDualMode()) {
  304. // 双工位布局 - 使用GridLayout自适应,中间有间距
  305. workPanel.setLayout(new GridLayout(1, 2, 20, 0));
  306. workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
  307. } else {
  308. // 单工位布局 - 使用 BorderLayout 让面板填满整个工作区域
  309. // 这样面板大小固定,不会随模式切换而变化
  310. workPanel.setLayout(new BorderLayout());
  311. workPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
  312. }
  313. // 创建工位面板
  314. for (int i = 0; i < config.getStationCount(); i++) {
  315. StationConfig.StationInfo stationInfo = config.getStation(i);
  316. if (stationInfo != null) {
  317. // 单工位不显示header(工位号和状态栏),双工位显示
  318. boolean showHeader = config.isDualMode();
  319. WorkstationPanel panel = new WorkstationPanel(
  320. stationInfo.getCode(),
  321. stationInfo.getIndex(),
  322. config,
  323. showHeader
  324. );
  325. // 单工位时,把工具栏状态按钮绑定到面板
  326. if (!showHeader) {
  327. panel.setExternalStatusMenu(toolbarStatusMenu);
  328. }
  329. panels.add(panel);
  330. if (config.isDualMode()) {
  331. workPanel.add(panel);
  332. } else {
  333. // 单工位:面板填满整个工作区域
  334. workPanel.add(panel, BorderLayout.CENTER);
  335. }
  336. }
  337. }
  338. return workPanel;
  339. }
  340. /**
  341. * 初始化TCP客户端
  342. */
  343. private void initTcpClient() {
  344. messageDispatcher = new MessageDispatcher();
  345. tcpClient = new MesTcpClient(config);
  346. tcpClient.setDispatcher(messageDispatcher);
  347. tcpClient.setConnectionListener(new MesTcpClient.ConnectionListener() {
  348. @Override
  349. public void onConnected() {
  350. SwingUtilities.invokeLater(() -> updateConnectionStatus(true));
  351. for (StationContext ctx : contexts) {
  352. tcpClient.sendSync(ctx.getStationCode());
  353. }
  354. }
  355. @Override
  356. public void onDisconnected() {
  357. SwingUtilities.invokeLater(() -> updateConnectionStatus(false));
  358. }
  359. @Override
  360. public void onReconnecting() {
  361. SwingUtilities.invokeLater(() -> {
  362. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  363. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  364. });
  365. }
  366. });
  367. stepFactory = new StepFactory(tcpClient);
  368. log.info("TCP客户端初始化完成");
  369. }
  370. /**
  371. * 初始化工位
  372. */
  373. private void initWorkstations() {
  374. if (config.isDeviceEnabled()) {
  375. deviceDrivers = DeviceDriverFactory.createDrivers(config);
  376. }
  377. // 初始化过程参数上传器
  378. if (config.hasProdParams()) {
  379. int uploadInterval = config.getProdParamsConfig().getUploadInterval();
  380. prodDataUploader = new ProdDataUploader(
  381. config.getServerIp(), config.getHttpPort(), uploadInterval);
  382. }
  383. for (int i = 0; i < config.getStationCount(); i++) {
  384. StationConfig.StationInfo stationInfo = config.getStation(i);
  385. if (stationInfo == null) continue;
  386. // 创建上下文
  387. StationContext context = StationContext.fromConfig(stationInfo, config.getLineSn());
  388. context.setDeviceEnabled(config.isDeviceEnabled());
  389. if (deviceDrivers.containsKey(i)) {
  390. context.setDeviceDriver(deviceDrivers.get(i));
  391. }
  392. // 设置第二行设备驱动(可能来自不同IP的设备)
  393. StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
  394. if (row2 != null) {
  395. int connIdx = row2.getConnectionIndex();
  396. if (deviceDrivers.containsKey(connIdx)) {
  397. context.setDeviceDriver2(deviceDrivers.get(connIdx));
  398. }
  399. }
  400. contexts.add(context);
  401. // 创建流程引擎
  402. WorkflowEngine engine = new WorkflowEngine(context);
  403. engine.buildFromConfig(config.getWorkflowSteps(), stepFactory);
  404. // 注入过程参数采集器到WaitCompleteStep
  405. if (config.hasProdParams()) {
  406. ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig());
  407. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  408. if (step instanceof WaitCompleteStep) {
  409. ((WaitCompleteStep) step).setProdDataCollector(collector);
  410. log.info("[{}] 过程参数采集器已注入", stationInfo.getCode());
  411. }
  412. }
  413. prodDataUploader.addCollector(stationInfo.getCode(), collector);
  414. }
  415. engine.setListener(new WorkflowEngine.WorkflowListener() {
  416. @Override
  417. public void onStepStarted(WorkflowEngine eng, com.mes.step.IWorkflowStep step) {
  418. log.debug("[{}] 步骤开始: {}", context.getStationCode(), step.getStepName());
  419. }
  420. @Override
  421. public void onStepCompleted(WorkflowEngine eng, com.mes.step.IWorkflowStep step, boolean success) {
  422. log.debug("[{}] 步骤完成: {} (成功={})", context.getStationCode(), step.getStepName(), success);
  423. }
  424. @Override
  425. public void onWorkflowCompleted(WorkflowEngine eng) {
  426. log.info("[{}] 流程完成", context.getStationCode());
  427. eng.reset();
  428. context.setStatusMessage("结果已提交,请扫下一件", 0);
  429. }
  430. @Override
  431. public void onWorkflowError(WorkflowEngine eng, String error) {
  432. log.error("[{}] 流程错误: {}", context.getStationCode(), error);
  433. }
  434. });
  435. engines.add(engine);
  436. // 注册到消息分发器
  437. messageDispatcher.registerStation(stationInfo.getCode(), engine, context);
  438. // 绑定UI面板
  439. if (i < panels.size()) {
  440. WorkstationPanel panel = panels.get(i);
  441. context.setUiPanel(panel);
  442. // 注入panel引用到需要弹出扫码框的步骤
  443. for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
  444. stepFactory.injectPanel(step, panel);
  445. }
  446. bindPanelEvents(panel, context, engine);
  447. log.info("[{}] UI面板绑定完成", stationInfo.getCode());
  448. }
  449. log.info("工位初始化完成: {}", stationInfo.getCode());
  450. }
  451. // 设置流程监控面板的工位列表
  452. if (workflowMonitorPanel != null) {
  453. workflowMonitorPanel.setWorkstations(engines, contexts);
  454. log.info("流程监控面板初始化完成");
  455. }
  456. // 启动过程参数上传器
  457. if (prodDataUploader != null) {
  458. prodDataUploader.start();
  459. }
  460. }
  461. /**
  462. * 绑定面板事件
  463. */
  464. private void bindPanelEvents(WorkstationPanel panel, StationContext context, WorkflowEngine engine) {
  465. panel.setListener(new WorkstationPanel.WorkstationPanelListener() {
  466. @Override
  467. public void onProductScanned(WorkstationPanel p, String productSn) {
  468. context.setProductSn(productSn);
  469. context.setUser(currentUser);
  470. engine.triggerCurrentStep();
  471. }
  472. @Override
  473. public void onMaterialScanned(WorkstationPanel p, String materialSn) {
  474. context.setMaterialSn(materialSn);
  475. engine.triggerCurrentStep();
  476. }
  477. @Override
  478. public void onOkClicked(WorkstationPanel p) {
  479. submitResult(context, engine, "OK");
  480. }
  481. @Override
  482. public void onNgClicked(WorkstationPanel p) {
  483. submitResult(context, engine, "NG");
  484. }
  485. @Override
  486. public void onUnbindClicked(WorkstationPanel p) {
  487. log.info("[{}] 解绑操作", context.getStationCode());
  488. // TODO: 实现解绑逻辑
  489. }
  490. });
  491. }
  492. /**
  493. * 提交结果
  494. * 手动模式下:用户点击OK/NG后,按顺序执行 StartWorkStep -> UploadResultStep
  495. */
  496. private void submitResult(StationContext context, WorkflowEngine engine, String result) {
  497. log.info("[{}] 用户提交结果: {}, 当前状态: qualityPassed={}, workStarted={}, waitingForUserAction={}",
  498. context.getStationCode(), result,
  499. context.isQualityPassed(), context.isWorkStarted(), context.isWaitingForUserAction());
  500. com.mes.step.IWorkflowStep currentStep = engine.getCurrentStep();
  501. log.info("[{}] 当前步骤: {}", context.getStationCode(),
  502. currentStep != null ? currentStep.getStepId() : "null");
  503. // 设置结果到 UploadResultStep(不管当前在哪一步)
  504. com.mes.step.IWorkflowStep uploadStep = engine.getStep("upload_result");
  505. if (uploadStep instanceof UploadResultStep) {
  506. ((UploadResultStep) uploadStep).setResult(result);
  507. }
  508. if (currentStep instanceof UploadResultStep) {
  509. // 当前步骤已经是上传结果,直接触发
  510. log.info("[{}] 当前步骤是 UploadResultStep,直接触发", context.getStationCode());
  511. engine.triggerCurrentStep();
  512. } else if (currentStep instanceof StartWorkStep) {
  513. // 当前步骤是开始工作(手动模式),标记用户已确认并触发
  514. log.info("[{}] 手动模式:触发 StartWorkStep", context.getStationCode());
  515. context.setWaitingForUserAction(false);
  516. engine.triggerCurrentStep();
  517. } else {
  518. // 其他情况:直接发送(兼容旧逻辑)
  519. log.warn("[{}] 当前步骤非预期: {}, 直接发送结果",
  520. context.getStationCode(),
  521. currentStep != null ? currentStep.getStepId() : "null");
  522. // 先发送 MKSW,再发送 MQDW
  523. tcpClient.sendStartWork(
  524. context.getProcessedProductSn(),
  525. context.getUser(),
  526. context.getStationCode()
  527. );
  528. if (uploadStep instanceof UploadResultStep) {
  529. tcpClient.sendQuality(
  530. context.getProcessedProductSn(),
  531. result,
  532. context.getUser(),
  533. context.getStationCode()
  534. );
  535. }
  536. }
  537. }
  538. /**
  539. * 更新连接状态
  540. */
  541. private void updateConnectionStatus(boolean connected) {
  542. if (connected) {
  543. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  544. getClass().getResource("/resources/image/bg/green_dot.png"))));
  545. startHeartBeatDisplay();
  546. } else {
  547. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  548. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  549. stopHeartBeatDisplay();
  550. }
  551. }
  552. /**
  553. * 启动心跳显示
  554. */
  555. private void startHeartBeatDisplay() {
  556. stopHeartBeatDisplay();
  557. heartBeatDisplayTimer = new Timer("HeartBeatDisplay", true);
  558. heartBeatDisplayTimer.scheduleAtFixedRate(new TimerTask() {
  559. private boolean green = true;
  560. @Override
  561. public void run() {
  562. SwingUtilities.invokeLater(() -> {
  563. // 更新时间显示
  564. String timeStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
  565. heartBeatMenu.setText(timeStr);
  566. if (green) {
  567. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  568. getClass().getResource("/resources/image/bg/green_dot.png"))));
  569. } else {
  570. heartBeatMenu.setIcon(new ImageIcon(Objects.requireNonNull(
  571. getClass().getResource("/resources/image/bg/grey_dot.png"))));
  572. }
  573. green = !green;
  574. });
  575. }
  576. }, 0, 1000);
  577. }
  578. /**
  579. * 停止心跳显示
  580. */
  581. private void stopHeartBeatDisplay() {
  582. if (heartBeatDisplayTimer != null) {
  583. heartBeatDisplayTimer.cancel();
  584. heartBeatDisplayTimer = null;
  585. }
  586. }
  587. /**
  588. * 获取工位号字符串
  589. */
  590. private String getStationCodesString() {
  591. StringBuilder sb = new StringBuilder();
  592. for (int i = 0; i < config.getStationCount(); i++) {
  593. if (i > 0) sb.append("/");
  594. StationConfig.StationInfo station = config.getStation(i);
  595. if (station != null) {
  596. sb.append(station.getCode());
  597. }
  598. }
  599. return sb.toString();
  600. }
  601. /**
  602. * 重连MES
  603. */
  604. private void reconnectMes() {
  605. log.info("手动重连MES");
  606. tcpClient.disconnect();
  607. tcpClient.connect();
  608. }
  609. /**
  610. * 重置工位
  611. */
  612. private void resetStation(int stationIndex) {
  613. if (stationIndex >= 0 && stationIndex < contexts.size()) {
  614. StationContext context = contexts.get(stationIndex);
  615. WorkflowEngine engine = engines.get(stationIndex);
  616. log.info("[{}] 手动刷新工位", context.getStationCode());
  617. context.setStatusMessage("已手动刷新,请扫码", 0);
  618. engine.reset();
  619. if (stationIndex < panels.size()) {
  620. panels.get(stationIndex).reset();
  621. }
  622. }
  623. }
  624. /**
  625. * 退出登录
  626. */
  627. private void logoff() {
  628. int result = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "确认", JOptionPane.YES_NO_OPTION);
  629. if (result == JOptionPane.YES_OPTION) {
  630. shutdown();
  631. System.exit(0);
  632. }
  633. }
  634. /**
  635. * 窗口关闭处理
  636. */
  637. private void onWindowClosing() {
  638. int result = JOptionPane.showConfirmDialog(this, "确定要退出程序吗?", "确认退出", JOptionPane.YES_NO_OPTION);
  639. if (result == JOptionPane.YES_OPTION) {
  640. shutdown();
  641. System.exit(0);
  642. }
  643. }
  644. /**
  645. * 关闭资源
  646. */
  647. public void shutdown() {
  648. log.info("正在关闭...");
  649. stopHeartBeatDisplay();
  650. // 停止过程参数上传器(会尝试上传剩余数据)
  651. if (prodDataUploader != null) {
  652. prodDataUploader.stop();
  653. }
  654. // 停止流程监控面板的刷新
  655. if (workflowMonitorPanel != null) {
  656. workflowMonitorPanel.stopRefresh();
  657. }
  658. for (WorkflowEngine engine : engines) {
  659. engine.destroy();
  660. }
  661. for (IDeviceDriver driver : deviceDrivers.values()) {
  662. driver.disconnect();
  663. }
  664. if (tcpClient != null) {
  665. tcpClient.disconnect();
  666. }
  667. log.info("关闭完成");
  668. }
  669. /**
  670. * 启动
  671. */
  672. public void start() {
  673. tcpClient.connect();
  674. for (WorkflowEngine engine : engines) {
  675. engine.start();
  676. }
  677. log.info("主窗口启动完成");
  678. }
  679. // ========== Getters & Setters ==========
  680. public void setCurrentUser(String user) {
  681. this.currentUser = user;
  682. userMenu.setText(user);
  683. for (StationContext ctx : contexts) {
  684. ctx.setUser(user);
  685. }
  686. }
  687. public void setSessionId(String sessionId) {
  688. this.sessionId = sessionId;
  689. }
  690. public String getSessionId() {
  691. return sessionId;
  692. }
  693. public void setUserAuth(int auth) {
  694. this.userAuth = auth;
  695. }
  696. public int getUserAuth() {
  697. return userAuth;
  698. }
  699. public void setLoginHour(int hour) {
  700. this.loginHour = hour;
  701. }
  702. public int getLoginHour() {
  703. return loginHour;
  704. }
  705. public boolean isAdmin() {
  706. return userAuth == 2;
  707. }
  708. public StationConfig getConfig() {
  709. return config;
  710. }
  711. public MesTcpClient getTcpClient() {
  712. return tcpClient;
  713. }
  714. public MessageDispatcher getMessageDispatcher() {
  715. return messageDispatcher;
  716. }
  717. public List<StationContext> getContexts() {
  718. return contexts;
  719. }
  720. public List<WorkflowEngine> getEngines() {
  721. return engines;
  722. }
  723. public List<WorkstationPanel> getPanels() {
  724. return panels;
  725. }
  726. // ========== UI编辑模式相关方法 ==========
  727. /**
  728. * 切换编辑模式
  729. */
  730. private void toggleEditMode(boolean editMode) {
  731. log.info("切换UI编辑模式: {}", editMode);
  732. // 双工位模式下,编辑模式只显示一个面板(因为布局是同步的)
  733. if (config.isDualMode() && panels.size() > 1) {
  734. if (editMode) {
  735. // 进入编辑模式:隐藏第二个面板,调整布局
  736. panels.get(1).setVisible(false);
  737. workPanel.setLayout(new GridLayout(1, 1, 0, 0));
  738. // 只对第一个面板启用编辑模式
  739. panels.get(0).setEditMode(true);
  740. } else {
  741. // 退出编辑模式:先退出编辑状态
  742. panels.get(0).setEditMode(false);
  743. // 同步布局到第二个面板(重新加载配置)
  744. panels.get(1).reloadLayoutConfig();
  745. // 恢复双面板布局
  746. workPanel.setLayout(new GridLayout(1, 2, 20, 0));
  747. panels.get(1).setVisible(true);
  748. }
  749. workPanel.revalidate();
  750. workPanel.repaint();
  751. } else {
  752. // 单工位模式:直接切换
  753. for (WorkstationPanel panel : panels) {
  754. panel.setEditMode(editMode);
  755. }
  756. }
  757. if (editMode) {
  758. String modeHint = config.isDualMode() ?
  759. "\n注意:双工位模式下只显示一个编辑面板,布局会同步到两个工位" : "";
  760. JOptionPane.showMessageDialog(this,
  761. "已进入UI编辑模式\n\n" +
  762. "操作说明:\n" +
  763. "• 拖动组件可调整位置\n" +
  764. "• 拖动边缘和角落可调整大小\n" +
  765. "• 右键组件可访问更多选项\n" +
  766. "• 组件会自动吸附到网格\n" +
  767. "• 编辑完成后记得保存布局\n\n" +
  768. "注意:编辑模式下工位功能将被禁用" + modeHint,
  769. "UI编辑模式",
  770. JOptionPane.INFORMATION_MESSAGE);
  771. }
  772. }
  773. /**
  774. * 切换网格显示
  775. */
  776. private void toggleGrid(boolean show) {
  777. for (WorkstationPanel panel : panels) {
  778. panel.toggleGrid(show);
  779. }
  780. }
  781. /**
  782. * 保存UI布局
  783. */
  784. private void saveUILayout() {
  785. if (panels.isEmpty()) {
  786. JOptionPane.showMessageDialog(this, "没有可保存的工位面板", "提示",
  787. JOptionPane.WARNING_MESSAGE);
  788. return;
  789. }
  790. // 保存第一个面板的布局(单/双工位共享布局)
  791. panels.get(0).saveLayoutConfig();
  792. // 保存后自动退出编辑模式(找到菜单项并取消选中)
  793. exitEditModeAfterSave();
  794. log.info("UI布局已保存");
  795. }
  796. /**
  797. * 保存后退出编辑模式
  798. */
  799. private void exitEditModeAfterSave() {
  800. // 查找设置菜单中的 UI编辑模式 选项并取消勾选
  801. JMenuBar menuBar = getJMenuBar();
  802. if (menuBar != null) {
  803. for (int i = 0; i < menuBar.getMenuCount(); i++) {
  804. JMenu menu = menuBar.getMenu(i);
  805. if (menu != null && "设置".equals(menu.getText())) {
  806. for (int j = 0; j < menu.getItemCount(); j++) {
  807. JMenuItem item = menu.getItem(j);
  808. if (item instanceof JCheckBoxMenuItem) {
  809. JCheckBoxMenuItem checkItem = (JCheckBoxMenuItem) item;
  810. if ("UI编辑模式".equals(checkItem.getText()) && checkItem.isSelected()) {
  811. checkItem.setSelected(false);
  812. // 触发 itemStateChanged 事件
  813. toggleEditMode(false);
  814. log.info("保存后自动退出编辑模式");
  815. break;
  816. }
  817. }
  818. }
  819. break;
  820. }
  821. }
  822. }
  823. }
  824. /**
  825. * 导出UI布局
  826. */
  827. private void exportUILayout() {
  828. if (panels.isEmpty()) {
  829. JOptionPane.showMessageDialog(this, "没有可导出的工位面板", "提示",
  830. JOptionPane.WARNING_MESSAGE);
  831. return;
  832. }
  833. panels.get(0).exportLayoutConfig();
  834. }
  835. /**
  836. * 导入UI布局
  837. */
  838. private void importUILayout() {
  839. if (panels.isEmpty()) {
  840. JOptionPane.showMessageDialog(this, "没有可导入布局的工位面板", "提示",
  841. JOptionPane.WARNING_MESSAGE);
  842. return;
  843. }
  844. panels.get(0).importLayoutConfig();
  845. // 询问是否应用到所有工位
  846. if (panels.size() > 1) {
  847. int result = JOptionPane.showConfirmDialog(this,
  848. "是否将导入的布局应用到所有工位?",
  849. "应用布局",
  850. JOptionPane.YES_NO_OPTION);
  851. if (result == JOptionPane.YES_OPTION) {
  852. // TODO: 如果需要,可以同步布局到其他工位面板
  853. log.info("布局将应用到所有工位");
  854. }
  855. }
  856. }
  857. // ========== 流程Tab解锁相关方法 ==========
  858. /**
  859. * 用户按钮点击处理 - 三击解锁流程Tab
  860. */
  861. private void onUserMenuClicked() {
  862. long now = System.currentTimeMillis();
  863. if (now - lastUserMenuClickTime > TRIPLE_CLICK_INTERVAL) {
  864. // 超时,重新计数
  865. userMenuClickCount = 1;
  866. } else {
  867. userMenuClickCount++;
  868. }
  869. lastUserMenuClickTime = now;
  870. if (userMenuClickCount >= 3) {
  871. userMenuClickCount = 0;
  872. if (workflowTabVisible) {
  873. // 已显示,再次三击则隐藏
  874. hideWorkflowTab();
  875. } else {
  876. // 弹出密码输入框
  877. showWorkflowPasswordDialog();
  878. }
  879. }
  880. }
  881. /**
  882. * 显示密码输入对话框
  883. */
  884. private void showWorkflowPasswordDialog() {
  885. JPasswordField passwordField = new JPasswordField();
  886. passwordField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
  887. int result = JOptionPane.showConfirmDialog(this,
  888. passwordField, "请输入密码",
  889. JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
  890. if (result == JOptionPane.OK_OPTION) {
  891. String input = new String(passwordField.getPassword());
  892. if (WORKFLOW_PASSWORD.equals(input)) {
  893. showWorkflowTab();
  894. } else {
  895. JOptionPane.showMessageDialog(this, "密码错误", "提示", JOptionPane.ERROR_MESSAGE);
  896. }
  897. }
  898. }
  899. /**
  900. * 显示流程Tab
  901. */
  902. private void showWorkflowTab() {
  903. if (!workflowTabVisible) {
  904. tabbedPane.addTab("流程",
  905. new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))),
  906. workflowMonitorPanel, null);
  907. workflowTabVisible = true;
  908. tabbedPane.setSelectedComponent(workflowMonitorPanel);
  909. workflowMonitorPanel.refresh();
  910. log.info("流程监控Tab已解锁显示");
  911. }
  912. // 同时显示隐藏的设置菜单项
  913. editModeSeparator.setVisible(true);
  914. editModeItem.setVisible(true);
  915. layoutSeparator.setVisible(true);
  916. saveLayoutItem.setVisible(true);
  917. exportLayoutItem.setVisible(true);
  918. importLayoutItem.setVisible(true);
  919. }
  920. /**
  921. * 隐藏流程Tab
  922. */
  923. private void hideWorkflowTab() {
  924. if (workflowTabVisible) {
  925. tabbedPane.remove(workflowMonitorPanel);
  926. workflowTabVisible = false;
  927. log.info("流程监控Tab已隐藏");
  928. }
  929. // 同时隐藏设置菜单项
  930. editModeSeparator.setVisible(false);
  931. editModeItem.setVisible(false);
  932. layoutSeparator.setVisible(false);
  933. saveLayoutItem.setVisible(false);
  934. exportLayoutItem.setVisible(false);
  935. importLayoutItem.setVisible(false);
  936. }
  937. }