From 631d1684ca5ba17ac68ed1d98f30cee384fbb867 Mon Sep 17 00:00:00 2001
From: 上海郢昱 <15922366+miu-haiqing@user.noreply.gitee.com>
Date: 星期三, 16 九月 2026 11:00:56 +0800
Subject: [PATCH] 运行新增托盘图标,新增查看串口状态、关闭串口监听、推出的右键菜单

---
 src/main/java/com/hwtd/mes/collect/DataAcquisitionApplication.java   |   55 ++++
 src/main/resources/tray.png                                          |    0 
 src/main/java/com/hwtd/mes/collect/handler/SerialPortListener.java   |    2 
 src/main/java/com/hwtd/mes/collect/config/SystemTrayInitializer.java |  676 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 731 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/hwtd/mes/collect/DataAcquisitionApplication.java b/src/main/java/com/hwtd/mes/collect/DataAcquisitionApplication.java
index 362e762..6ea359b 100644
--- a/src/main/java/com/hwtd/mes/collect/DataAcquisitionApplication.java
+++ b/src/main/java/com/hwtd/mes/collect/DataAcquisitionApplication.java
@@ -1,13 +1,66 @@
 package com.hwtd.mes.collect;
 
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
+import javax.swing.JOptionPane;
+import java.net.BindException;
+
+@Slf4j
 @SpringBootApplication
 public class DataAcquisitionApplication {
 
     public static void main(String[] args) {
-        SpringApplication.run(DataAcquisitionApplication.class, args);
+        SpringApplication application = new SpringApplication(DataAcquisitionApplication.class);
+        // 绯荤粺鎵樼洏鍥炬爣渚濊禆 AWT锛歋pring Boot 榛樿 java.awt.headless=true锛岃繖閲屽繀椤诲叧鎺夛紝鍚﹀垯鎵樼洏涓嶄細鏄剧ず
+        application.setHeadless(false);
+        try {
+            application.run(args);
+        } catch (Throwable e) {
+            // 鍙屽嚮 jar 鍚姩鐢ㄧ殑鏄� javaw锛屾病鏈夋帶鍒跺彴锛屽惎鍔ㄥけ璐ユ椂蹇呴』寮圭獥锛屽惁鍒欑敤鎴蜂粈涔堥兘鐪嬩笉鍒�
+            showStartupError(e);
+            System.exit(1);
+        }
     }
 
+    /**
+     * 鍚姩澶辫触鎻愮ず锛氬啓鏃ュ織 + 寮圭獥
+     */
+    private static void showStartupError(Throwable error) {
+        String message = buildStartupErrorMessage(error);
+        try {
+            log.error("鏁版嵁閲囬泦鍣ㄥ惎鍔ㄥけ璐ワ細{}", message, error);
+        } catch (Throwable ignored) {
+            // 鏃ュ織绯荤粺杩樻病鍒濆鍖栧ソ鏃跺拷鐣�
+        }
+        try {
+            JOptionPane.showMessageDialog(null, message, "鏁版嵁閲囬泦鍣ㄥ惎鍔ㄥけ璐�", JOptionPane.ERROR_MESSAGE);
+        } catch (Throwable t) {
+            // 娌℃湁妗岄潰浼氳瘽鏃跺脊涓嶄簡绐楋紝鍙兘闈犳棩蹇�
+            System.err.println(message);
+        }
+    }
+
+    /**
+     * 缁勮鍚姩澶辫触鎻愮ず锛氱鍙h鍗犵敤鏃剁粰鍑烘洿鏄庣‘鐨勫鐞嗗姙娉�
+     *
+     * @param error 鍚姩寮傚父
+     * @return 鎻愮ず鍐呭
+     */
+    public static String buildStartupErrorMessage(Throwable error) {
+        Throwable root = error;
+        while (root.getCause() != null && root.getCause() != root) {
+            root = root.getCause();
+        }
+        String detail = root.getMessage() == null ? root.getClass().getName() : root.getMessage();
+        if (root instanceof BindException
+                || detail.contains("already in use")
+                || detail.contains("Address already in use")) {
+            return "鏈嶅姟绔彛宸茶鍗犵敤锛岀▼搴忓彲鑳藉凡缁忓湪杩愯銆俓n"
+                    + "璇峰厛鐪嬩换鍔℃爮鍙充笅瑙掓湁娌℃湁閲囬泦鍣ㄥ浘鏍囷紙鍙抽敭 鈫� 閫�鍑猴級锛�"
+                    + "鎴栧湪浠诲姟绠$悊鍣ㄩ噷缁撴潫瀵瑰簲鐨� java 杩涚▼鍚庡啀鍚姩銆�";
+        }
+        return "鍚姩澶辫触锛�" + detail + "\n璇︾粏閿欒璇锋煡鐪嬫棩蹇楁枃浠躲��";
+    }
 }
diff --git a/src/main/java/com/hwtd/mes/collect/config/SystemTrayInitializer.java b/src/main/java/com/hwtd/mes/collect/config/SystemTrayInitializer.java
new file mode 100644
index 0000000..2e492ee
--- /dev/null
+++ b/src/main/java/com/hwtd/mes/collect/config/SystemTrayInitializer.java
@@ -0,0 +1,676 @@
+package com.hwtd.mes.collect.config;
+
+import com.hwtd.mes.collect.handler.SerialPortListener;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.PreDestroy;
+import javax.imageio.ImageIO;
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JWindow;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.Timer;
+import javax.swing.WindowConstants;
+import java.awt.AWTException;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsDevice;
+import java.awt.GraphicsEnvironment;
+import java.awt.Image;
+import java.awt.MouseInfo;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.SystemTray;
+import java.awt.TrayIcon;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 绯荤粺鎵樼洏鍥炬爣锛歟xe锛堟垨 jar锛夊惎鍔ㄥ悗鍦ㄤ换鍔℃爮鍙充笅瑙掓墭鐩樺父椹讳竴涓皬鍥炬爣锛屽彸閿彍鍗曞彲浠ュ揩閫熼��鍑恒��
+ * <p>
+ * 璇存槑锛�
+ * <ul>
+ *     <li>鎵樼洏渚濊禆 AWT锛岃�� Spring Boot 榛樿 {@code java.awt.headless=true}锛堟鏃� {@link SystemTray#isSupported()} 涓� false锛夛紝
+ *         鎵�浠ュ惎鍔ㄧ被閲屽凡缁忕敤 {@code setHeadless(false)} 鍏虫帀锛�</li>
+ *     <li>鍙抽敭鑿滃崟鐢� Swing 鑷繁鐢讳竴涓棤杈规绐楀彛锛坽@link JWindow}锛夛紝涓嶇敤 AWT 鐨� PopupMenu锛�
+ *         AWT 鑿滃崟鏄� Windows 鍘熺敓鑿滃崟锛屾枃瀛楃敤绯荤粺鑿滃崟瀛椾綋缁樺埗锛岄儴鍒嗘満鍣ㄤ笂涓嶅惈涓枃瀛楀舰浼氭樉绀烘垚鏂瑰潡锛岃�屼笖 {@code setFont} 涓嶇敓鏁堬紱</li>
+ *     <li>鑿滃崟寮瑰嚭浣嶇疆鍜岀郴缁熸墭鐩樿彍鍗曚竴鑷达細鏄剧ず鍦ㄥ厜鏍囦笂鏂广�佸彸杈圭紭涓庡厜鏍囧榻愶紝骞朵繚璇佷笉瓒呭嚭灞忓箷锛堝鏄剧ず鍣ㄦ椂鎸夊厜鏍囨墍鍦ㄥ睆骞曡绠楋級锛�</li>
+ *     <li>鑿滃崟鏀惰捣鏂瑰紡锛氱偣鍑昏彍鍗曞闈紙绐楀彛澶辩劍锛夈�佹寜 ESC銆佸啀鍙抽敭涓�娆°�佺偣浜嗘煇涓�椤逛箣鍚庯紝
+ *         鍙﹀杩樻湁涓�灞傚厹搴曗�斺�旈紶鏍囩寮�鑿滃崟闄勮繎涔熶細鑷姩鏀惰捣锛堥伩鍏嶄釜鍒幆澧冪獥鍙f姠涓嶅埌鐒︾偣瀵艰嚧鑿滃崟鍏充笉鎺夛級锛�</li>
+ *     <li>鑿滃崟閲岀殑鈥滄煡鐪嬩覆鍙g姸鎬佲�濓紙鎴栧弻鍑绘墭鐩樺浘鏍囷級浼氬脊鍑轰竴涓姸鎬佺獥鍙o細涓插彛鍚嶇О銆佺洃鍚姸鎬併�佷覆鍙h繛鎺ャ�佺紦瀛樻潯鏁帮紝姣忕鑷姩鍒锋柊锛�</li>
+ *     <li>鑿滃崟閲岀殑鈥滃叧闂覆鍙b�濆彧鍏抽棴涓插彛鐩戝惉锛堜笉閫�鍑虹▼搴忥級锛屾柟渚跨幇鍦烘妸涓插彛閲婃斁缁欏埆鐨勭▼搴忕敤锛岀粨鏋滅敤鎵樼洏姘旀场鎻愮ず锛�</li>
+ *     <li>鍥炬爣浼樺厛璇诲彇 classpath 涓嬬殑 {@code tray.png}锛堟斁涓�寮� 32x32 鐨� png 鍗冲彲鎹㈡垚鑷繁鐨勫浘鏍囷級锛屾病鏈夊氨鐢ㄤ唬鐮佺敾鐨勯粯璁ゅ浘鏍囷紱</li>
+ *     <li>鐐光�滈��鍑衡�濅細鍏堝叧闂� Spring 瀹瑰櫒锛堣Е鍙� {@code @PreDestroy}锛屼緥濡傚叧闂覆鍙g洃鍚�佸仠姝㈣ˉ甯х嚎绋嬶級锛屽啀缁撴潫杩涚▼锛�
+ *         鐢ㄤ换鍔$鐞嗗櫒缁撴潫杩涚▼銆佹垨鐩存帴鍏虫帀 exe 鐨勬帶鍒跺彴绐楀彛锛屽垯涓嶄細璧拌繖濂楁竻鐞嗘祦绋嬶紱</li>
+ *     <li>浠� Windows 鏈嶅姟锛圫ession 0锛屾棤妗岄潰锛夋柟寮忚繍琛屾椂鎵樼洏涓嶄細鏄剧ず锛屼唬鐮佷細鑷姩璺宠繃锛屼笉褰卞搷鎺ュ彛浣跨敤銆�</li>
+ * </ul>
+ */
+@Slf4j
+@Component
+public class SystemTrayInitializer implements ApplicationRunner {
+
+    /** 榛樿鍥炬爣灏哄 */
+    private static final int DEFAULT_ICON_SIZE = 32;
+    /** 鑿滃崟鑳屾櫙鑹� */
+    private static final Color MENU_BACKGROUND = Color.WHITE;
+    /** 鑿滃崟椤归紶鏍囨偓鍋滆儗鏅壊 */
+    private static final Color MENU_HOVER_BACKGROUND = new Color(0xE8F0FE);
+    /** 鑿滃崟杈规棰滆壊 */
+    private static final Color MENU_BORDER = new Color(0xC8C8C8);
+
+    @Value("${server.port:9527}")
+    private int serverPort;
+
+    @Value("${logging.file-location:logs}")
+    private String logLocation;
+
+    private final ApplicationContext applicationContext;
+
+    /** 鎵樼洏鍥炬爣锛岄��鍑烘椂鍏堢Щ闄� */
+    private TrayIcon trayIcon;
+
+    /** 鍙抽敭鑿滃崟绐楀彛锛岀涓�娆″彸閿椂鍒涘缓 */
+    private JWindow popupWindow;
+
+    /** 鐘舵�佺獥鍙o紝绗竴娆$偣鈥滄煡鐪嬮噰闆嗙姸鎬佲�濇椂鍒涘缓 */
+    private JDialog statusDialog;
+
+    /** 鐘舵�佺獥鍙g殑鑷姩鍒锋柊瀹氭椂鍣紙姣忕鍒锋柊涓�娆★級 */
+    private Timer statusRefreshTimer;
+
+    /** 鐘舵�佺獥鍙i噷鐨勫�� */
+    private JLabel serialPortNameValue;
+    private JLabel listeningValue;
+    private JLabel portOpenValue;
+    private JLabel dataSizeValue;
+
+    /** 鍏滃簳鏀惰捣妫�鏌ワ細涓囦竴鑿滃崟绐楀彛娌℃姠鍒扮劍鐐癸紝榧犳爣绂诲紑鑿滃崟鍚庝篃鑳借嚜鍔ㄦ敹璧� */
+    private Timer popupWatchdog;
+
+    /** 寮瑰嚭鑿滃崟鏃剁殑鍏夋爣浣嶇疆锛堟墭鐩樺浘鏍囦綅缃級 */
+    private Point popupAnchor;
+
+    /** 鍏夋爣杩炵画鍑犳涓嶅湪鑿滃崟闄勮繎 */
+    private int outsideCount;
+
+    public SystemTrayInitializer(ApplicationContext applicationContext) {
+        this.applicationContext = applicationContext;
+    }
+
+    @Override
+    public void run(ApplicationArguments args) {
+        if (!SystemTray.isSupported()) {
+            log.warn("褰撳墠鐜涓嶆敮鎸佺郴缁熸墭鐩橈紙headless 妯″紡銆佹棤妗岄潰浼氳瘽鎴栫己灏� java.desktop 妯″潡锛夛紝璺宠繃鎵樼洏鍥炬爣");
+            return;
+        }
+        try {
+            // AWT/Swing 缁勪欢缁熶竴鍦ㄤ簨浠跺垎鍙戠嚎绋嬶紙EDT锛夐噷鍒涘缓
+            EventQueue.invokeAndWait(this::createTrayIcon);
+        } catch (Exception e) {
+            log.error("鍒涘缓绯荤粺鎵樼洏鍥炬爣澶辫触锛歿}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 鍒涘缓鎵樼洏鍥炬爣锛屽苟娉ㄥ唽榧犳爣浜嬩欢锛氬彸閿脊鑿滃崟銆佸乏閿弻鍑绘墦寮�閲囬泦鐘舵��
+     */
+    private void createTrayIcon() {
+        trayIcon = new TrayIcon(loadTrayImage(), "浜ㄦ椇鐗瑰MES鏁版嵁閲囬泦鍣�");
+        trayIcon.setImageAutoSize(true);
+        trayIcon.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseReleased(MouseEvent event) {
+                if (event.isPopupTrigger() || event.getButton() == MouseEvent.BUTTON3) {
+                    showPopupWindow();
+                }
+            }
+
+            @Override
+            public void mouseClicked(MouseEvent event) {
+                if (event.getButton() == MouseEvent.BUTTON1 && event.getClickCount() >= 2) {
+                    showStatusDialog();
+                }
+            }
+        });
+
+        try {
+            SystemTray.getSystemTray().add(trayIcon);
+            log.info("绯荤粺鎵樼洏鍥炬爣宸插惎鍔紝鍙抽敭鎵樼洏鍥炬爣鍙互閫�鍑虹▼搴忥紙绔彛 {}锛�", serverPort);
+        } catch (AWTException e) {
+            log.error("娣诲姞绯荤粺鎵樼洏鍥炬爣澶辫触锛歿}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 鍦ㄥ厜鏍囦綅缃脊鍑哄彸閿彍鍗曪紱宸茬粡寮圭潃灏辨敹璧锋潵
+     */
+    private void showPopupWindow() {
+        Point mouse = MouseInfo.getPointerInfo() == null ? null : MouseInfo.getPointerInfo().getLocation();
+        if (mouse == null) {
+            return;
+        }
+        JWindow window = ensurePopupWindow(mouse);
+        if (window.isVisible()) {
+            hidePopupWindow();
+            return;
+        }
+        Rectangle screen = screenConfigurationOf(mouse).getBounds();
+        Dimension size = window.getSize();
+        // 鍜岀郴缁熸墭鐩樿彍鍗曚竴鏍凤細鏄剧ず鍦ㄥ厜鏍囦笂鏂癸紝鍙宠竟缂樹笌鍏夋爣瀵归綈
+        int x = mouse.x - size.width;
+        int y = mouse.y - size.height - 2;
+        if (y < screen.y) {
+            // 涓婃柟鏀句笉涓嬶紙渚嬪浠诲姟鏍忓湪椤堕儴锛夊氨鏀惧埌鍏夋爣涓嬫柟
+            y = mouse.y + 12;
+        }
+        // 淇濊瘉鑿滃崟瀹屾暣鏄剧ず鍦ㄥ睆骞曞唴
+        x = Math.max(screen.x, Math.min(x, screen.x + screen.width - size.width));
+        y = Math.max(screen.y, Math.min(y, screen.y + screen.height - size.height));
+
+        window.setLocation(x, y);
+        window.setVisible(true);
+        window.toFront();
+        window.requestFocus();
+        startPopupWatchdog(mouse);
+    }
+
+    /**
+     * 鏀惰捣鍙抽敭鑿滃崟
+     */
+    private void hidePopupWindow() {
+        stopPopupWatchdog();
+        JWindow window = popupWindow;
+        if (window != null && window.isVisible()) {
+            window.setVisible(false);
+        }
+    }
+
+    /**
+     * 鍚姩鍏滃簳鏀惰捣妫�鏌ワ細姝e父鎯呭喌涓嬬偣鍑诲埆澶勭獥鍙eけ鐒﹀氨浼氭敹璧凤紝
+     * 涓囦竴绐楀彛娌℃姠鍒扮劍鐐癸紙閮ㄥ垎鐜浼氳繖鏍凤級锛岄潬榧犳爣绂诲紑鑿滃崟闄勮繎涔熻兘鏀惰捣銆�
+     */
+    private void startPopupWatchdog(Point anchor) {
+        stopPopupWatchdog();
+        popupAnchor = anchor;
+        outsideCount = 0;
+        popupWatchdog = new Timer(250, event -> checkPopupDismiss());
+        // 鐣欎竴鐐规椂闂寸粰鐢ㄦ埛浠庢墭鐩樺浘鏍囩Щ鍔ㄥ埌鑿滃崟涓�
+        popupWatchdog.setInitialDelay(600);
+        popupWatchdog.start();
+    }
+
+    /**
+     * 鍏滃簳妫�鏌ワ細鍏夋爣鏃笉鍦ㄨ彍鍗曚笂銆佷篃涓嶅湪鎵樼洏鍥炬爣闄勮繎鏃讹紝杩炵画涓ゆ灏辨敹璧疯彍鍗�
+     */
+    private void checkPopupDismiss() {
+        JWindow window = popupWindow;
+        if (window == null || !window.isVisible()) {
+            stopPopupWatchdog();
+            return;
+        }
+        Point mouse = MouseInfo.getPointerInfo() == null ? null : MouseInfo.getPointerInfo().getLocation();
+        if (mouse == null) {
+            return;
+        }
+        boolean nearMenu = window.getBounds().contains(mouse);
+        boolean nearAnchor = popupAnchor != null && popupAnchor.distance(mouse) < 80;
+        if (nearMenu || nearAnchor) {
+            outsideCount = 0;
+            return;
+        }
+        if (++outsideCount >= 2) {
+            hidePopupWindow();
+        }
+    }
+
+    private void stopPopupWatchdog() {
+        if (popupWatchdog != null) {
+            popupWatchdog.stop();
+            popupWatchdog = null;
+        }
+        outsideCount = 0;
+    }
+
+    /**
+     * 鑿滃崟绐楀彛鎸夊睆骞曞垱寤猴細澶氭樉绀哄櫒鏃跺垱寤哄湪鍏夋爣鎵�鍦ㄧ殑閭e潡灞忓箷涓�
+     */
+    private JWindow ensurePopupWindow(Point mouse) {
+        GraphicsConfiguration configuration = screenConfigurationOf(mouse);
+        if (popupWindow == null || popupWindow.getGraphicsConfiguration() != configuration) {
+            if (popupWindow != null) {
+                popupWindow.dispose();
+            }
+            popupWindow = createPopupWindow(configuration);
+        }
+        return popupWindow;
+    }
+
+    /**
+     * 鍒涘缓鑿滃崟绐楀彛锛氭棤杈规銆佺疆椤躲�佸彲鑾峰彇鐒︾偣锛堢偣鍒澶辩劍灏辫兘鑷姩鏀惰捣锛�
+     */
+    private JWindow createPopupWindow(GraphicsConfiguration configuration) {
+        Font menuFont = resolveMenuFont();
+
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+        panel.setBackground(MENU_BACKGROUND);
+        panel.setBorder(BorderFactory.createCompoundBorder(
+                BorderFactory.createLineBorder(MENU_BORDER),
+                BorderFactory.createEmptyBorder(4, 0, 4, 0)));
+        panel.add(createMenuItem("鏌ョ湅涓插彛鐘舵��", menuFont, e -> showStatusDialog()));
+        panel.add(createMenuItem("鍏抽棴涓插彛鐩戝惉", menuFont, e -> closeSerialPort()));
+        panel.add(createMenuItem("鎵撳紑鏃ュ織鐩綍", menuFont, e -> openLogDirectory()));
+        panel.add(createSeparator());
+        panel.add(createMenuItem("閫�鍑�", menuFont, e -> exit()));
+
+        // 鎸� ESC 鏀惰捣鑿滃崟
+        panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
+                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "hidePopupWindow");
+        panel.getActionMap().put("hidePopupWindow", new AbstractAction() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public void actionPerformed(ActionEvent event) {
+                hidePopupWindow();
+            }
+        });
+
+        JWindow window = new JWindow(configuration);
+        window.setContentPane(panel);
+        window.setAlwaysOnTop(true);
+        window.setFocusable(true);
+        // 鍏抽敭锛氱獥鍙e繀椤昏兘鑾峰緱鐒︾偣锛岀偣鑿滃崟澶栭潰鏃舵墠浼氳Е鍙戝け鐒︺�佽彍鍗曟墠浼氳嚜鍔ㄦ敹璧�
+        window.setFocusableWindowState(true);
+        window.addWindowFocusListener(new WindowAdapter() {
+            @Override
+            public void windowLostFocus(WindowEvent event) {
+                hidePopupWindow();
+            }
+        });
+        window.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowDeactivated(WindowEvent event) {
+                hidePopupWindow();
+            }
+        });
+        window.pack();
+        return window;
+    }
+
+    /**
+     * 鑿滃崟椤癸細鎵佸钩鎸夐挳 + 鎮仠楂樹寒
+     */
+    private JButton createMenuItem(String text, Font font, ActionListener actionListener) {
+        JButton item = new JButton(text);
+        item.setFont(font);
+        item.setHorizontalAlignment(SwingConstants.LEFT);
+        item.setBorder(BorderFactory.createEmptyBorder(6, 18, 6, 24));
+        item.setBorderPainted(false);
+        item.setFocusPainted(false);
+        item.setContentAreaFilled(false);
+        item.setOpaque(true);
+        item.setBackground(MENU_BACKGROUND);
+        item.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        item.setMaximumSize(new Dimension(Integer.MAX_VALUE, item.getPreferredSize().height));
+        item.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseEntered(MouseEvent event) {
+                item.setBackground(MENU_HOVER_BACKGROUND);
+            }
+
+            @Override
+            public void mouseExited(MouseEvent event) {
+                item.setBackground(MENU_BACKGROUND);
+            }
+        });
+        item.addActionListener(event -> {
+            // 鍏堟敹璧疯彍鍗曪紝鍐嶆墽琛屽姩浣�
+            hidePopupWindow();
+            actionListener.actionPerformed(event);
+        });
+        return item;
+    }
+
+    /**
+     * 鑿滃崟鍒嗛殧绾�
+     */
+    private JSeparator createSeparator() {
+        JSeparator separator = new JSeparator();
+        separator.setForeground(MENU_BORDER);
+        separator.setMaximumSize(new Dimension(Integer.MAX_VALUE, 1));
+        return separator;
+    }
+
+    /**
+     * @return 鍏夋爣鎵�鍦ㄥ睆骞曠殑鏄剧ず閰嶇疆锛屾壘涓嶅埌灏辩敤涓诲睆骞�
+     */
+    private GraphicsConfiguration screenConfigurationOf(Point point) {
+        GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+        for (GraphicsDevice device : environment.getScreenDevices()) {
+            GraphicsConfiguration configuration = device.getDefaultConfiguration();
+            if (configuration.getBounds().contains(point)) {
+                return configuration;
+            }
+        }
+        return environment.getDefaultScreenDevice().getDefaultConfiguration();
+    }
+
+    /**
+     * 閫�鍑虹▼搴忥細鍏堝叧闂� Spring 瀹瑰櫒锛堣Е鍙� @PreDestroy锛屽叧闂覆鍙c�佸仠姝㈣ˉ甯х嚎绋嬶級锛屽啀缁撴潫杩涚▼
+     */
+    private void exit() {
+        log.info("浠庣郴缁熸墭鐩橀��鍑虹▼搴�");
+        removeTrayIcon();
+        int exitCode = SpringApplication.exit(applicationContext, () -> 0);
+        System.exit(exitCode);
+    }
+
+    /**
+     * 椤圭洰鍋滄鏃剁Щ闄ゆ墭鐩樺浘鏍囥�侀攢姣佽彍鍗曠獥鍙�
+     */
+    @PreDestroy
+    public void destroy() {
+        removeTrayIcon();
+        stopPopupWatchdog();
+        stopStatusRefreshTimer();
+        if (statusDialog != null) {
+            statusDialog.dispose();
+            statusDialog = null;
+        }
+        if (popupWindow != null) {
+            popupWindow.dispose();
+            popupWindow = null;
+        }
+    }
+
+    private void removeTrayIcon() {
+        TrayIcon icon = trayIcon;
+        trayIcon = null;
+        if (icon != null) {
+            SystemTray.getSystemTray().remove(icon);
+        }
+    }
+
+    /**
+     * 寮瑰嚭閲囬泦鐘舵�佺獥鍙o紙涓嶆槸鎵撳紑娴忚鍣級锛岀獥鍙i噷鐨勭姸鎬佹瘡绉掕嚜鍔ㄥ埛鏂�
+     */
+    private void showStatusDialog() {
+        if (statusDialog == null) {
+            statusDialog = createStatusDialog();
+        }
+        refreshStatus();
+        statusDialog.setLocationRelativeTo(null);
+        statusDialog.setVisible(true);
+        statusDialog.toFront();
+    }
+
+    /**
+     * 鍒涘缓涓插彛鐘舵�佺獥鍙o細涓插彛鍚嶇О / 鐩戝惉鐘舵�� / 涓插彛杩炴帴 / 缂撳瓨鏉℃暟 + 鍒锋柊銆佸叧闂寜閽�
+     */
+    private JDialog createStatusDialog() {
+        Font font = resolveMenuFont();
+
+        JPanel fields = new JPanel(new GridLayout(4, 2, 12, 10));
+        fields.setBackground(MENU_BACKGROUND);
+        serialPortNameValue = addStatusRow(fields, "涓插彛鍚嶇О", font);
+        listeningValue = addStatusRow(fields, "鐩戝惉鐘舵��", font);
+        portOpenValue = addStatusRow(fields, "涓插彛杩炴帴", font);
+        dataSizeValue = addStatusRow(fields, "缂撳瓨鏁版嵁", font);
+
+        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
+        buttons.setBackground(MENU_BACKGROUND);
+        JButton refreshButton = new JButton("鍒锋柊");
+        refreshButton.setFont(font);
+        refreshButton.addActionListener(event -> refreshStatus());
+        JButton closeButton = new JButton("鍏抽棴");
+        closeButton.setFont(font);
+        closeButton.addActionListener(event -> statusDialog.setVisible(false));
+        buttons.add(refreshButton);
+        buttons.add(closeButton);
+
+        JPanel content = new JPanel(new BorderLayout(0, 14));
+        content.setBackground(MENU_BACKGROUND);
+        content.setBorder(BorderFactory.createEmptyBorder(18, 22, 14, 22));
+        content.add(fields, BorderLayout.CENTER);
+        content.add(buttons, BorderLayout.SOUTH);
+
+        JDialog dialog = new JDialog();
+        dialog.setTitle("涓插彛鐘舵��");
+        dialog.setModal(false);
+        dialog.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
+        dialog.setContentPane(content);
+        dialog.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowOpened(WindowEvent event) {
+                refreshStatus();
+                startStatusRefreshTimer();
+            }
+
+            @Override
+            public void windowClosing(WindowEvent event) {
+                // 鍏抽棴锛堝叾瀹炴槸闅愯棌锛夋椂鍋滄帀鑷姩鍒锋柊
+                stopStatusRefreshTimer();
+            }
+        });
+        dialog.pack();
+        dialog.setLocationRelativeTo(null);
+        return dialog;
+    }
+
+    /**
+     * 鐘舵�佺獥鍙i噷鐨勪竴琛岋細宸﹁竟鏄爣棰橈紝鍙宠竟鏄��
+     *
+     * @return 鍊奸偅涓�鍒楃殑鏍囩锛屽埛鏂版椂鐩存帴 setText
+     */
+    private JLabel addStatusRow(JPanel parent, String title, Font font) {
+        JLabel titleLabel = new JLabel(title + "锛�", SwingConstants.RIGHT);
+        titleLabel.setFont(font);
+        titleLabel.setForeground(new Color(0x666666));
+        JLabel valueLabel = new JLabel("-");
+        valueLabel.setFont(font);
+        valueLabel.setForeground(new Color(0x222222));
+        parent.add(titleLabel);
+        parent.add(valueLabel);
+        return valueLabel;
+    }
+
+    /**
+     * 鍒锋柊鐘舵�佺獥鍙i噷鐨勫唴瀹�
+     */
+    private void refreshStatus() {
+        if (serialPortNameValue == null) {
+            return;
+        }
+        SerialPortListener listener = serialPortListener();
+        if (listener == null) {
+            serialPortNameValue.setText("-");
+            listeningValue.setText("鑾峰彇涓插彛鐩戝惉鍣ㄥけ璐�");
+            portOpenValue.setText("-");
+            dataSizeValue.setText("-");
+            return;
+        }
+        String serialPortName = listener.getListenName();
+        serialPortNameValue.setText(StringUtils.hasText(serialPortName) ? serialPortName : "-");
+        listeningValue.setText(listener.isListening() ? "鐩戝惉涓�" : "鏈洃鍚�");
+        portOpenValue.setText(listener.isPortOpen() ? "宸叉墦寮�" : "鏈墦寮�");
+        dataSizeValue.setText(listener.dataSize() + " 鏉�");
+    }
+
+    /**
+     * @return 涓插彛鐩戝惉鍣紝鍙栦笉鍒版椂杩斿洖 null
+     */
+    private SerialPortListener serialPortListener() {
+        if (applicationContext == null) {
+            return null;
+        }
+        try {
+            return applicationContext.getBean(SerialPortListener.class);
+        } catch (Exception e) {
+            log.warn("鑾峰彇涓插彛鐩戝惉鍣ㄥけ璐ワ細{}", e.getMessage());
+            return null;
+        }
+    }
+
+    private void startStatusRefreshTimer() {
+        if (statusRefreshTimer == null) {
+            statusRefreshTimer = new Timer(1000, event -> refreshStatus());
+        }
+        statusRefreshTimer.start();
+    }
+
+    private void stopStatusRefreshTimer() {
+        if (statusRefreshTimer != null) {
+            statusRefreshTimer.stop();
+        }
+    }
+
+    /**
+     * 鍏抽棴涓插彛鐩戝惉锛堝彧鍏充覆鍙o紝涓嶉��鍑虹▼搴忥級锛屾柟渚跨幇鍦虹洿鎺ユ妸涓插彛閲婃斁缁欏埆鐨勭▼搴忕敤
+     */
+    private void closeSerialPort() {
+        SerialPortListener listener = serialPortListener();
+        if (listener == null) {
+            showError("鑾峰彇涓插彛鐩戝惉鍣ㄥけ璐ワ紝鏃犳硶鍏抽棴涓插彛");
+            return;
+        }
+        String serialPortName = listener.getListenName();
+        serialPortName = StringUtils.hasText(serialPortName) ? serialPortName : "-";
+        if (!listener.isListening() && !listener.isPortOpen()) {
+            notifyMessage("涓插彛 " + serialPortName + " 褰撳墠鏈紑鍚洃鍚�");
+            return;
+        }
+        if (listener.closeListening()) {
+            log.info("浠庣郴缁熸墭鐩樺叧闂覆鍙� {} 鐩戝惉", serialPortName);
+            notifyMessage("涓插彛 " + serialPortName + " 鐩戝惉宸插叧闂�");
+        } else {
+            showError("涓插彛 " + serialPortName + " 鍏抽棴澶辫触锛岃妫�鏌ヤ覆鍙g姸鎬�");
+        }
+    }
+
+    /**
+     * 鎵樼洏姘旀场鎻愮ず锛屼笉鎵撴柇褰撳墠鎿嶄綔
+     */
+    private void notifyMessage(String message) {
+        if (trayIcon == null) {
+            log.info(message);
+            return;
+        }
+        trayIcon.displayMessage("浜ㄦ椇鐗瑰MES鏁版嵁閲囬泦鍣�", message, TrayIcon.MessageType.INFO);
+    }
+
+    /**
+     * 鍑洪敊鏃跺脊绐楁彁绀猴紙姘旀场鎻愮ず鍙兘琚郴缁熻缃睆钄斤紝澶辫触蹇呴』璁╀汉鐪嬭锛�
+     */
+    private void showError(String message) {
+        log.warn(message);
+        try {
+            JOptionPane.showMessageDialog(null, message, "鏁版嵁閲囬泦鍣�", JOptionPane.ERROR_MESSAGE);
+        } catch (Exception e) {
+            log.warn("寮瑰嚭閿欒鎻愮ず澶辫触锛歿}", e.getMessage());
+        }
+    }
+
+    /**
+     * 鎵撳紑鏃ュ織鐩綍锛屾柟渚跨幇鍦烘帓鏌ラ棶棰�
+     */
+    private void openLogDirectory() {
+        try {
+            File directory = new File(StringUtils.hasText(logLocation) ? logLocation : "logs");
+            if (!directory.exists() && !directory.mkdirs()) {
+                log.warn("鏃ュ織鐩綍涓嶅瓨鍦ㄤ笖鍒涘缓澶辫触锛歿}", directory.getAbsolutePath());
+            }
+            Desktop.getDesktop().open(directory);
+        } catch (Exception e) {
+            log.warn("鎵撳紑鏃ュ織鐩綍澶辫触锛歿}", e.getMessage());
+        }
+    }
+
+    /**
+     * 鑿滃崟瀛椾綋锛氫紭鍏堟寫涓�涓郴缁熼噷瀛樺湪鐨勪腑鏂囧瓧浣�
+     *
+     * @return 鑿滃崟瀛椾綋
+     */
+    private Font resolveMenuFont() {
+        Set<String> fontFamilies = new HashSet<>(Arrays.asList(
+                GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()));
+        for (String name : new String[]{"Microsoft YaHei", "寰蒋闆呴粦", "SimSun", "瀹嬩綋"}) {
+            if (fontFamilies.contains(name)) {
+                return new Font(name, Font.PLAIN, 12);
+            }
+        }
+        return new Font(Font.DIALOG, Font.PLAIN, 12);
+    }
+
+    /**
+     * 鎵樼洏鍥炬爣锛氫紭鍏堢敤 classpath 涓嬬殑 tray.png锛屾病鏈夊氨鐜扮敾涓�涓紝閬垮厤灏戜竴涓浘鐗囨枃浠跺氨璺戜笉璧锋潵
+     */
+    private Image loadTrayImage() {
+        try (InputStream in = getClass().getClassLoader().getResourceAsStream("tray.png")) {
+            if (in != null) {
+                BufferedImage image = ImageIO.read(in);
+                if (image != null) {
+                    return image;
+                }
+            }
+        } catch (Exception e) {
+            log.warn("璇诲彇鎵樼洏鍥炬爣 tray.png 澶辫触锛屾敼鐢ㄩ粯璁ゅ浘鏍囷細{}", e.getMessage());
+        }
+        return createDefaultTrayImage();
+    }
+
+    /**
+     * 榛樿鍥炬爣锛氳摑搴曠櫧瀛� S
+     */
+    private Image createDefaultTrayImage() {
+        BufferedImage image = new BufferedImage(DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, BufferedImage.TYPE_INT_ARGB);
+        Graphics2D graphics = image.createGraphics();
+        try {
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            graphics.setColor(new Color(0x2D8CF0));
+            graphics.fillRoundRect(0, 0, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, 8, 8);
+            graphics.setColor(Color.WHITE);
+            graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20));
+            graphics.drawString("S", 10, 24);
+        } finally {
+            graphics.dispose();
+        }
+        return image;
+    }
+}
diff --git a/src/main/java/com/hwtd/mes/collect/handler/SerialPortListener.java b/src/main/java/com/hwtd/mes/collect/handler/SerialPortListener.java
index 84409ee..da22b9b 100644
--- a/src/main/java/com/hwtd/mes/collect/handler/SerialPortListener.java
+++ b/src/main/java/com/hwtd/mes/collect/handler/SerialPortListener.java
@@ -425,7 +425,7 @@
                     log.debug("涓插彛 {} 缂撳瓨宸叉弧 {} 鏉★紝绉婚櫎鏈�鏃х殑涓�鏉℃暟鎹� {}",
                             setting.getSerialPortName(), setting.getMaxCount(), removed);
                 }
-                log.info("涓插彛 {} 鎺ユ敹鍒版暟鎹�-->{}", setting.getSerialPortName(), value);
+//                log.info("涓插彛 {} 鎺ユ敹鍒版暟鎹�-->{}", setting.getSerialPortName(), value);
             }
         }
     }
diff --git a/src/main/resources/tray.png b/src/main/resources/tray.png
new file mode 100644
index 0000000..8ce5c7b
--- /dev/null
+++ b/src/main/resources/tray.png
Binary files differ

--
Gitblit v1.9.3