张世豪
6 天以前 b315a6943e6c0d6bdf0d5f7565c570d719154d6c
新增了障碍物管理页面
已添加3个文件
已修改9个文件
1330 ■■■■ 文件已修改
Obstacledge.properties 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
image/gecaojishijiao1.png 补丁 | 查看 | 原始文档 | blame | 历史
image/gecaojishijiao2.png 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/ObstacleManagementPage.java 759 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/addzhangaiwu.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/buttonset.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,5 +1,5 @@
# å‰²è‰æœºåœ°å—障碍物配置文件
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-17T12:03:32.881913900
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-17T13:28:00.386604500
# åæ ‡ç³»ï¼šWGS84(度分格式)
# ============ åœ°å—基准站配置 ============
@@ -12,3 +12,8 @@
# æ ¼å¼ï¼šplot.[地块编号].obstacle.[障碍物名称].originalCoords=[坐标串]
# æ ¼å¼ï¼šplot.[地块编号].obstacle.[障碍物名称].xyCoords=[坐标串]
# --- åœ°å—LAND1的障碍物 ---
plot.LAND1.obstacle.障碍物1.shape=1
plot.LAND1.obstacle.障碍物1.originalCoords=0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E
plot.LAND1.obstacle.障碍物1.xyCoords=81.22,-22.17;81.21,-22.17;81.19,-22.18;81.20,-22.19;81.18,-22.21;81.07,-22.54;80.94,-23.05;80.44,-23.54;79.84,-23.83;79.27,-24.19;78.63,-24.52;78.11,-24.79;77.69,-25.22;77.43,-25.75;77.15,-26.45;77.08,-27.23;77.10,-27.92;77.08,-28.67;77.08,-29.32;77.15,-29.97;77.27,-30.57;77.53,-31.19;77.92,-31.45;78.22,-31.73;78.92,-31.87;79.65,-31.77;80.35,-31.60;80.90,-31.17;81.27,-30.63;81.70,-30.11;81.90,-29.48;81.95,-28.92;81.90,-28.38;81.73,-27.83;81.73,-27.30;81.52,-26.81;81.42,-26.24;81.26,-25.76;81.27,-25.25;81.04,-25.07;81.11,-24.98;81.11,-25.00;81.12,-25.00
dikuai.properties
@@ -1,5 +1,5 @@
#Dikuai Properties
#Wed Dec 17 12:03:32 CST 2025
#Wed Dec 17 13:50:43 CST 2025
LAND1.angleThreshold=-1
LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E
LAND1.boundaryCoordinates=77.19,-32.68;80.71,-54.97;80.99,-55.90;83.54,-56.46;85.04,-55.55;85.94,-53.74;83.24,-35.82;84.55,-34.54;94.02,-31.92;94.10,-31.11;90.88,-20.39;90.35,-19.53;88.33,-19.00;84.12,-19.47;78.92,-22.36;76.63,-25.55;76.93,-29.84;77.06,-31.26;77.19,-32.68
@@ -13,7 +13,7 @@
LAND1.mowingPattern=平行线
LAND1.mowingTrack=
LAND1.mowingWidth=40
LAND1.plannedPath=77.17,-29.65;81.28,-55.71;81.70,-55.80;76.93,-25.57;77.26,-25.10;82.12,-55.89;82.54,-55.98;77.59,-24.64;77.92,-24.18;82.96,-56.08;83.38,-56.17;78.25,-23.72;78.59,-23.25;83.76,-56.03;84.13,-55.81;78.92,-22.79;79.27,-22.45;84.50,-55.58;84.87,-55.33;79.64,-22.24;80.01,-22.04;85.18,-54.72;85.48,-54.10;80.39,-21.83;80.76,-21.62;83.00,-35.83;83.34,-35.38;81.13,-21.42;81.50,-21.21;83.69,-35.03;84.04,-34.69;81.88,-21.00;82.25,-20.80;84.39,-34.35;84.77,-34.22;82.62,-20.59;82.99,-20.38;85.16,-34.11;85.55,-34.00;83.37,-20.18;83.74,-19.97;85.94,-33.90;86.32,-33.79;84.11,-19.76;84.50,-19.68;86.71,-33.68;87.10,-33.57;84.90,-19.63;85.30,-19.59;87.49,-33.47;87.88,-33.36;85.70,-19.55;86.09,-19.50;88.26,-33.25;88.65,-33.15;86.49,-19.46;86.89,-19.41;89.04,-33.04;89.43,-32.93;87.29,-19.37;87.69,-19.32;89.82,-32.82;90.20,-32.72;88.08,-19.28;88.49,-19.30;90.59,-32.61;90.98,-32.50;88.91,-19.41;89.34,-19.52;91.37,-32.39;91.76,-32.29;89.76,-19.63;90.18,-19.74;92.14,-32.18;92.53,-32.07;90.76,-20.88;91.62,-23.72;92.92,-31.96;93.31,-31.86;92.47,-26.56;93.33,-29.40;93.70,-31.75
LAND1.plannedPath=77.45,-31.44;81.28,-55.71;81.70,-55.80;77.91,-31.78;78.05,-31.91;78.17,-31.98;78.35,-32.01;82.12,-55.89;82.54,-55.98;78.77,-32.09;78.95,-32.12;79.17,-32.09;82.96,-56.08;83.38,-56.17;79.57,-32.03;79.96,-31.95;83.76,-56.03;84.13,-55.81;80.35,-31.86;80.50,-31.80;80.72,-31.63;84.50,-55.58;84.87,-55.33;81.08,-31.34;81.41,-30.87;85.18,-54.72;85.48,-54.10;81.75,-30.45;81.89,-30.27;81.94,-30.19;82.05,-29.82;83.00,-35.83;83.34,-35.38;81.43,-23.31;81.47,-22.18;81.43,-22.04;81.32,-21.94;81.21,-21.92;81.13,-21.42;81.50,-21.21;83.69,-35.03;84.04,-34.69;81.88,-21.00;82.25,-20.80;84.39,-34.35;84.77,-34.22;82.62,-20.59;82.99,-20.38;85.16,-34.11;85.55,-34.00;83.37,-20.18;83.74,-19.97;85.94,-33.90;86.32,-33.79;84.11,-19.76;84.50,-19.68;86.71,-33.68;87.10,-33.57;84.90,-19.63;85.30,-19.59;87.49,-33.47;87.88,-33.36;85.70,-19.55;86.09,-19.50;88.26,-33.25;88.65,-33.15;86.49,-19.46;86.89,-19.41;89.04,-33.04;89.43,-32.93;87.29,-19.37;87.69,-19.32;89.82,-32.82;90.20,-32.72;88.08,-19.28;88.49,-19.30;90.59,-32.61;90.98,-32.50;88.91,-19.41;89.34,-19.52;91.37,-32.39;91.76,-32.29;89.76,-19.63;90.18,-19.74;92.14,-32.18;92.53,-32.07;90.76,-20.88;91.62,-23.72;92.92,-31.96;93.31,-31.86;92.47,-26.56;93.33,-29.40;93.70,-31.75;81.40,-22.00;81.27,-21.93;81.21,-21.92;81.08,-21.96;80.98,-22.05;79.85,-23.55;79.64,-22.24;80.01,-22.04;80.22,-23.37;80.58,-23.05;80.39,-21.83;80.76,-21.62;80.87,-22.34;77.96,-24.59;77.63,-24.92;77.59,-24.64;77.92,-24.18;77.99,-24.57;78.36,-24.38;78.25,-23.72;78.59,-23.25;78.73,-24.19;79.11,-23.99;78.92,-22.79;79.27,-22.45;79.48,-23.76;77.99,-24.57;77.93,-24.62;77.51,-25.05;77.47,-25.11;77.20,-25.66;77.01,-26.12;76.93,-25.57;77.26,-25.10;77.31,-25.43
LAND1.returnPointCoordinates=-1
LAND1.updateTime=2025-12-17 12\:03\:32
LAND1.updateTime=2025-12-17 13\:50\:43
LAND1.userId=-1
image/gecaojishijiao1.png
image/gecaojishijiao2.png
set.properties
@@ -1,12 +1,12 @@
#Mower Configuration Properties - Updated
#Wed Dec 17 12:04:00 CST 2025
#Wed Dec 17 14:56:25 CST 2025
appVersion=-1
currentWorkLandNumber=LAND1
cuttingWidth=200
firmwareVersion=-1
handheldMarkerId=
idleTrailDurationSeconds=60
mapScale=15.31
mapScale=12.92
mowerId=1234
serialAutoConnect=true
serialBaudRate=115200
src/dikuai/Dikuaiguanli.java
@@ -226,12 +226,12 @@
        
        // åœ°å—编号
        contentPanel.add(createCardInfoItem("地块编号:", getDisplayValue(dikuai.getLandNumber(), "未知")));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // æ·»åŠ æ—¶é—´
        contentPanel.add(createCardInfoItem("添加时间:", getDisplayValue(dikuai.getCreateTime(), "未知")));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // åœ°å—面积
        String landArea = dikuai.getLandArea();
        if (landArea != null && !landArea.equals("-1")) {
@@ -240,14 +240,14 @@
            landArea = "未知";
        }
        contentPanel.add(createCardInfoItem("地块面积:", landArea));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // è¿”回点坐标(带修改按钮)
        contentPanel.add(createCardInfoItemWithButton("返回点坐标:",
            getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"),
        contentPanel.add(createCardInfoItemWithButton("返回点坐标:",
            getDisplayValue(dikuai.getReturnPointCoordinates(), "未设置"),
            "修改", e -> editReturnPoint(dikuai)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // åœ°å—边界坐标(带显示顶点按钮)
        JPanel boundaryPanel = createBoundaryInfoItem(dikuai,
            getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "未设置"));
@@ -256,27 +256,31 @@
            () -> editBoundaryCoordinates(dikuai),
            "点击查看/编辑地块边界坐标");
        contentPanel.add(boundaryPanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        ObstacleSummary obstacleSummary = getObstacleSummaryFromCache(dikuai.getLandNumber());
        JPanel obstaclePanel = createCardInfoItemWithButton("障碍物:",
            obstacleSummary.buildDisplayValue(),
            "新增",
            e -> addNewObstacle(dikuai));
        setInfoItemTooltip(obstaclePanel, obstacleSummary.buildTooltip());
        // è®©éšœç¢ç‰©æ ‡é¢˜å¯ç‚¹å‡»ï¼Œæ‰“开障碍物管理页面
        configureInteractiveLabel(getInfoItemTitleLabel(obstaclePanel),
            () -> showObstacleManagementPage(dikuai),
            "点击查看/管理障碍物");
        contentPanel.add(obstaclePanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // è·¯å¾„坐标(带查看按钮)
        JPanel pathPanel = createCardInfoItemWithButton("路径坐标:",
            getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"),
        JPanel pathPanel = createCardInfoItemWithButton("路径坐标:",
            getTruncatedValue(dikuai.getPlannedPath(), 12, "未设置"),
            "复制", e -> copyCoordinatesAction("路径坐标", dikuai.getPlannedPath()));
        setInfoItemTooltip(pathPanel, dikuai.getPlannedPath());
        configureInteractiveLabel(getInfoItemTitleLabel(pathPanel),
            () -> editPlannedPath(dikuai),
            "点击查看/编辑路径坐标");
        contentPanel.add(pathPanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        JPanel baseStationPanel = createCardInfoItemWithButton("基站坐标:",
            getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "未设置"),
@@ -286,7 +290,7 @@
            () -> editBaseStationCoordinates(dikuai),
            "点击查看/编辑基站坐标");
        contentPanel.add(baseStationPanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
    contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        JPanel boundaryOriginalPanel = createCardInfoItemWithButton("边界原始坐标:",
            getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "未设置"),
@@ -296,7 +300,7 @@
            () -> editBoundaryOriginalCoordinates(dikuai),
            "点击查看/编辑边界原始坐标");
        contentPanel.add(boundaryOriginalPanel);
        contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        JPanel mowingPatternPanel = createCardInfoItemWithButton("割草模式:",
            getTruncatedValue(dikuai.getMowingPattern(), 12, "未设置"),
@@ -306,7 +310,7 @@
            () -> editMowingPattern(dikuai),
            "点击查看/编辑割草模式");
        contentPanel.add(mowingPatternPanel);
        contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        String mowingWidthValue = dikuai.getMowingWidth();
        String widthSource = null;
@@ -319,7 +323,7 @@
            "编辑", e -> editMowingWidth(dikuai));
        setInfoItemTooltip(mowingWidthPanel, widthSource);
        contentPanel.add(mowingWidthPanel);
        contentPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        contentPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:",
            getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"),
@@ -371,7 +375,10 @@
    private JPanel createCardInfoItemWithButton(String label, String value, String buttonText, ActionListener listener) {
        JPanel itemPanel = new JPanel(new BorderLayout());
        itemPanel.setBackground(CARD_BACKGROUND);
        itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));
        // å¢žåŠ é«˜åº¦ä»¥ç¡®ä¿æŒ‰é’®å®Œæ•´æ˜¾ç¤ºï¼ˆæŒ‰é’®é«˜åº¦çº¦24-28像素,加上上下边距)
        itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35));
        itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30));
        itemPanel.setMinimumSize(new Dimension(0, 28));
        
        JLabel labelComp = new JLabel(label);
        labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
@@ -379,13 +386,14 @@
        
        JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
        rightPanel.setBackground(CARD_BACKGROUND);
        // æ·»åŠ åž‚ç›´å†…è¾¹è·ä»¥ç¡®ä¿æŒ‰é’®ä¸è¢«è£å‰ª
        rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
        
        JLabel valueComp = new JLabel(value);
        valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        valueComp.setForeground(TEXT_COLOR);
        
        JButton button = createSmallButton(buttonText);
        button.addActionListener(listener);
        JButton button = createSmallLinkButton(buttonText, listener);
        
        rightPanel.add(valueComp);
        rightPanel.add(button);
@@ -401,17 +409,19 @@
        private JPanel createBoundaryInfoItem(Dikuai dikuai, String displayValue) {
            JPanel itemPanel = new JPanel(new BorderLayout());
            itemPanel.setBackground(CARD_BACKGROUND);
            int rowHeight = Math.max(36, BOUNDARY_TOGGLE_ICON_SIZE + 12);
            // å¢žåŠ é«˜åº¦ä»¥ç¡®ä¿æŒ‰é’®ä¸‹è¾¹ç¼˜å®Œæ•´æ˜¾ç¤ºï¼ˆæŒ‰é’®é«˜åº¦56,加上上下边距)
            int rowHeight = Math.max(60, BOUNDARY_TOGGLE_ICON_SIZE + 16);
            Dimension rowDimension = new Dimension(Integer.MAX_VALUE, rowHeight);
            itemPanel.setMaximumSize(rowDimension);
            itemPanel.setPreferredSize(rowDimension);
            itemPanel.setMinimumSize(new Dimension(0, 32));
            itemPanel.setMinimumSize(new Dimension(0, 56));
            JLabel labelComp = new JLabel("地块边界:");
            labelComp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
            labelComp.setForeground(LIGHT_TEXT);
            int verticalPadding = Math.max(0, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2);
            // ç¡®ä¿æŒ‰é’®æœ‰è¶³å¤Ÿçš„上下边距,避免下边缘被裁剪
            int verticalPadding = Math.max(2, (rowHeight - BOUNDARY_TOGGLE_ICON_SIZE) / 2);
            JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
            rightPanel.setBackground(CARD_BACKGROUND);
            rightPanel.setBorder(BorderFactory.createEmptyBorder(verticalPadding, 0, verticalPadding, 0));
@@ -838,6 +848,12 @@
        }
        Window owner = SwingUtilities.getWindowAncestor(this);
        // èŽ·å–åœ°å—ç®¡ç†å¯¹è¯æ¡†ï¼Œå‡†å¤‡åœ¨æ‰“å¼€è·¯å¾„è§„åˆ’é¡µé¢æ—¶å…³é—­
        Window managementWindow = null;
        if (owner instanceof JDialog) {
            managementWindow = owner;
        }
        // èŽ·å–åœ°å—åŸºæœ¬æ•°æ®
        String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates());
@@ -897,6 +913,11 @@
            callback
        );
        // å…³é—­åœ°å—管理页面
        if (managementWindow != null) {
            managementWindow.dispose();
        }
        dialog.setVisible(true);
    }
@@ -1292,112 +1313,104 @@
        return value;
    }
    /**
     * åˆ›å»ºç±»ä¼¼äºŽé“¾æŽ¥çš„小按钮
     */
    private JButton createSmallLinkButton(String text, ActionListener listener) {
        JButton btn = new JButton(text);
        btn.setFont(new Font("微软雅黑", Font.PLAIN, 11));
        btn.setForeground(PRIMARY_COLOR);
        btn.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(PRIMARY_COLOR, 1, true),
            BorderFactory.createEmptyBorder(2, 6, 2, 6)
        ));
        btn.setContentAreaFilled(false);
        btn.setFocusPainted(false);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) { btn.setOpaque(true); btn.setBackground(new Color(230, 250, 240)); }
            public void mouseExited(MouseEvent e) { btn.setOpaque(false); }
        });
        if (listener != null) {
            btn.addActionListener(listener);
        }
        return btn;
    }
    private JButton createSmallButton(String text) {
        return createSmallButton(text, PRIMARY_COLOR, PRIMARY_DARK);
        return createSmallLinkButton(text, null);
    }
    private JButton createSmallButton(String text, Color backgroundColor, Color hoverColor) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        // å¯¹äºŽéœ€è¦ä¸åŒé¢œè‰²çš„æŒ‰é’®ï¼Œä½¿ç”¨å®žå¿ƒé£Žæ ¼
        Color baseColor = backgroundColor == null ? PRIMARY_COLOR : backgroundColor;
        Color hover = hoverColor == null ? baseColor : hoverColor;
        button.setBackground(baseColor);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
        button.setMargin(new Insets(0, 0, 0, 0));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) {
                button.setBackground(hover);
            }
            public void mouseExited(MouseEvent e) {
                button.setBackground(baseColor);
            }
        });
        return button;
        return createStyledButton(text, baseColor, true);
    }
    private JButton createActionButton(String text, Color color) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        button.setBackground(color);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        return createStyledButton(text, color, true); // å®žå¿ƒé£Žæ ¼
    }
        // æ‚¬åœæ•ˆæžœ
        button.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) {
                if (color == RED_COLOR) {
                    button.setBackground(RED_DARK);
    /**
     * åˆ›å»ºçŽ°ä»£é£Žæ ¼æŒ‰é’® (实心/轮廓)
     */
    private JButton createStyledButton(String text, Color baseColor, boolean filled) {
        JButton btn = new JButton(text) {
            @Override
            protected void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D) g.create();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                boolean isPressed = getModel().isPressed();
                boolean isRollover = getModel().isRollover();
                if (filled) {
                    if (isPressed) g2.setColor(baseColor.darker());
                    else if (isRollover) g2.setColor(baseColor.brighter());
                    else g2.setColor(baseColor);
                    g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8);
                    g2.setColor(Color.WHITE);
                } else {
                    button.setBackground(PRIMARY_DARK);
                    g2.setColor(CARD_BACKGROUND); // èƒŒæ™¯
                    g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8);
                    if (isPressed) g2.setColor(baseColor.darker());
                    else if (isRollover) g2.setColor(baseColor);
                    else g2.setColor(new Color(200, 200, 200)); // é»˜è®¤è¾¹æ¡†ç°
                    g2.setStroke(new BasicStroke(1.2f));
                    g2.drawRoundRect(0, 0, getWidth()-1, getHeight()-1, 8, 8);
                    g2.setColor(isRollover ? baseColor : TEXT_COLOR);
                }
                FontMetrics fm = g2.getFontMetrics();
                int x = (getWidth() - fm.stringWidth(getText())) / 2;
                int y = (getHeight() - fm.getHeight()) / 2 + fm.getAscent();
                g2.drawString(getText(), x, y);
                g2.dispose();
            }
            public void mouseExited(MouseEvent e) {
                if (color == RED_COLOR) {
                    button.setBackground(RED_COLOR);
                } else {
                    button.setBackground(PRIMARY_COLOR);
                }
            }
        });
        return button;
        };
        btn.setFocusPainted(false);
        btn.setContentAreaFilled(false);
        btn.setBorderPainted(false);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setFont(new Font("微软雅黑", Font.BOLD, 12));
        return btn;
    }
    private JButton createDeleteButton() {
        JButton button = new JButton("删除");
        button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        button.setBackground(RED_COLOR);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        JButton button = createStyledButton("删除", RED_COLOR, false); // è½®å»“风格
        ImageIcon deleteIcon = loadIcon("image/delete.png", 16, 16);
        if (deleteIcon != null) {
            button.setIcon(deleteIcon);
            button.setIconTextGap(6);
        }
        // æ‚¬åœæ•ˆæžœ
        button.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) {
                button.setBackground(RED_DARK);
            }
            public void mouseExited(MouseEvent e) {
                button.setBackground(RED_COLOR);
            }
        });
        return button;
    }
    private JButton createPrimaryFooterButton(String text) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        button.setBackground(PRIMARY_COLOR);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) {
                button.setBackground(PRIMARY_DARK);
            }
            public void mouseExited(MouseEvent e) {
                button.setBackground(PRIMARY_COLOR);
            }
        });
        return button;
        return createStyledButton(text, PRIMARY_COLOR, true); // å®žå¿ƒé£Žæ ¼
    }
    private JButton createWorkToggleButton(Dikuai dikuai) {
@@ -1507,6 +1520,8 @@
                boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false);
                renderer.setBoundaryPointsVisible(showBoundaryPoints);
                renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d);
                // é€€å‡ºé¢„览后,不显示障碍物点(障碍物点只在预览时显示)
                renderer.setObstaclePointsVisible(false);
            }
            shouye.refreshMowingIndicators();
        }
@@ -1614,6 +1629,31 @@
        }
    }
    /**
     * æ˜¾ç¤ºéšœç¢ç‰©ç®¡ç†é¡µé¢
     */
    private void showObstacleManagementPage(Dikuai dikuai) {
        if (dikuai == null) {
            return;
        }
        Window owner = SwingUtilities.getWindowAncestor(this);
        // èŽ·å–åœ°å—ç®¡ç†å¯¹è¯æ¡†ï¼Œå‡†å¤‡åœ¨æ‰“å¼€éšœç¢ç‰©ç®¡ç†é¡µé¢æ—¶å…³é—­
        Window managementWindow = null;
        if (owner instanceof JDialog) {
            managementWindow = owner;
        }
        ObstacleManagementPage managementPage = new ObstacleManagementPage(owner, dikuai);
        // å…³é—­åœ°å—管理页面
        if (managementWindow != null) {
            managementWindow.dispose();
        }
        managementPage.setVisible(true);
    }
    private void addNewObstacle(Dikuai dikuai) {
        if (dikuai == null) {
            JOptionPane.showMessageDialog(this, "未找到当前地块,无法新增障碍物", "提示", JOptionPane.WARNING_MESSAGE);
src/dikuai/ObstacleManagementPage.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,759 @@
package dikuai;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import zhangaiwu.Obstacledge;
import zhuye.Shouye;
/**
 * éšœç¢ç‰©ç®¡ç†é¡µé¢ - UI优化版
 */
public class ObstacleManagementPage extends JDialog {
    private static final long serialVersionUID = 1L;
    // --- å°ºå¯¸å¸¸é‡ (保持不变) ---
    private static final int SCREEN_WIDTH = 400;
    private static final int SCREEN_HEIGHT = 800;
    // --- é…è‰²æ–¹æ¡ˆ (更现代的莫兰迪/扁平风格) ---
    private static final Color PRIMARY_COLOR = new Color(46, 139, 87);     // æµ·è—»ç»¿ (主色)
    private static final Color PRIMARY_HOVER = new Color(60, 179, 113);    // æ‚¬åœç»¿
    private static final Color DANGER_COLOR = new Color(220, 53, 69);      // è­¦å‘Šçº¢
    private static final Color DANGER_HOVER = new Color(200, 35, 51);
    private static final Color TEXT_PRIMARY = new Color(33, 37, 41);       // ä¸»è¦æ–‡å­—
    private static final Color TEXT_SECONDARY = new Color(108, 117, 125);  // æ¬¡è¦æ–‡å­—
    private static final Color BG_MAIN = new Color(248, 249, 250);         // æ•´ä½“背景 (灰白)
    private static final Color BG_CARD = Color.WHITE;                      // å¡ç‰‡èƒŒæ™¯
    private static final Color BG_INPUT = new Color(241, 243, 245);        // è¾“入框背景
    private static final Color BORDER_LIGHT = new Color(233, 236, 239);    // æµ…边框
    private final Dikuai dikuai;
    private JPanel cardsPanel;
    private JScrollPane scrollPane;
    public ObstacleManagementPage(Window owner, Dikuai dikuai) {
        super(owner, "障碍物管理", Dialog.ModalityType.APPLICATION_MODAL);
        this.dikuai = dikuai;
        initializeWindow();
        initializeUI();
        // ç¡®ä¿å¸ƒå±€å®ŒæˆåŽå†æ˜¾ç¤ºæ•°æ®
        SwingUtilities.invokeLater(this::loadAndDisplayObstacles);
    }
    private void initializeWindow() {
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());
        getContentPane().setBackground(BG_MAIN);
        setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        setResizable(false);
        setLocationRelativeTo(getOwner());
    }
    private void initializeUI() {
        // 1. é¡¶éƒ¨å¯¼èˆªæ 
        add(createHeader(), BorderLayout.NORTH);
        // 2. ä¸­é—´æ»šåŠ¨åŒºåŸŸ
        cardsPanel = new JPanel();
        cardsPanel.setLayout(new BoxLayout(cardsPanel, BoxLayout.Y_AXIS));
        cardsPanel.setBackground(BG_MAIN);
        cardsPanel.setBorder(new EmptyBorder(15, 15, 15, 15)); // å¤–边距
        scrollPane = new JScrollPane(cardsPanel);
        scrollPane.setBorder(null); // æ— è¾¹æ¡†
        scrollPane.setBackground(BG_MAIN);
        scrollPane.getVerticalScrollBar().setUnitIncrement(20);
        // è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼
        customizeScrollBar(scrollPane);
        add(scrollPane, BorderLayout.CENTER);
    }
    /**
     * åˆ›å»ºé¡¶éƒ¨æ ‡é¢˜æ 
     */
    private JPanel createHeader() {
        JPanel header = new JPanel(new BorderLayout());
        header.setBackground(Color.WHITE);
        header.setPreferredSize(new Dimension(SCREEN_WIDTH, 60));
        header.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, BORDER_LIGHT)); // åº•部细线
        // å·¦ä¾§ä¿¡æ¯
        JPanel infoPanel = new JPanel(new GridLayout(2, 1));
        infoPanel.setBackground(Color.WHITE);
        infoPanel.setBorder(new EmptyBorder(8, 20, 8, 0));
        String landName = getDisplayValue(dikuai.getLandName(), "未知地块");
        String landNumber = getDisplayValue(dikuai.getLandNumber(), "--");
        JLabel titleLabel = new JLabel(landName);
        titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        titleLabel.setForeground(TEXT_PRIMARY);
        JLabel subTitleLabel = new JLabel("编号: " + landNumber);
        subTitleLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        subTitleLabel.setForeground(TEXT_SECONDARY);
        infoPanel.add(titleLabel);
        infoPanel.add(subTitleLabel);
        header.add(infoPanel, BorderLayout.CENTER);
        // å³ä¾§å…³é—­æŒ‰é’®
        JButton closeBtn = new JButton("✕");
        closeBtn.setFont(new Font("Arial", Font.BOLD, 18));
        closeBtn.setForeground(TEXT_SECONDARY);
        closeBtn.setBorder(new EmptyBorder(0, 15, 0, 20));
        closeBtn.setContentAreaFilled(false);
        closeBtn.setFocusPainted(false);
        closeBtn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        closeBtn.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) { closeBtn.setForeground(TEXT_PRIMARY); }
            public void mouseExited(MouseEvent e) { closeBtn.setForeground(TEXT_SECONDARY); }
        });
        closeBtn.addActionListener(e -> dispose());
        header.add(closeBtn, BorderLayout.EAST);
        return header;
    }
    /**
     * è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ï¼ˆæ‰å¹³åŒ–ã€ç»†æ¡ï¼‰
     */
    private void customizeScrollBar(JScrollPane scrollPane) {
        scrollPane.getVerticalScrollBar().setUI(new BasicScrollBarUI() {
            @Override
            protected void configureScrollBarColors() {
                this.thumbColor = new Color(200, 200, 200);
                this.trackColor = BG_MAIN;
            }
            @Override
            protected JButton createDecreaseButton(int orientation) {
                return createZeroButton();
            }
            @Override
            protected JButton createIncreaseButton(int orientation) {
                return createZeroButton();
            }
            private JButton createZeroButton() {
                JButton jbutton = new JButton();
                jbutton.setPreferredSize(new Dimension(0, 0));
                jbutton.setMinimumSize(new Dimension(0, 0));
                jbutton.setMaximumSize(new Dimension(0, 0));
                return jbutton;
            }
            @Override
            protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
                Graphics2D g2 = (Graphics2D) g.create();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setColor(thumbColor);
                g2.fillRoundRect(thumbBounds.x + 2, thumbBounds.y, thumbBounds.width - 4, thumbBounds.height, 6, 6);
                g2.dispose();
            }
        });
        scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(8, 0));
    }
    /**
     * åŠ è½½æ•°æ®
     */
    private void loadAndDisplayObstacles() {
        cardsPanel.removeAll();
        List<Obstacledge.Obstacle> obstacles = loadObstacles();
        if (obstacles.isEmpty()) {
            showEmptyState();
        } else {
            for (Obstacledge.Obstacle obstacle : obstacles) {
                cardsPanel.add(createObstacleCard(obstacle));
                cardsPanel.add(Box.createVerticalStrut(15)); // å¡ç‰‡é—´è·
            }
        }
        cardsPanel.revalidate();
        cardsPanel.repaint();
    }
    private void showEmptyState() {
        JPanel emptyPanel = new JPanel();
        emptyPanel.setLayout(new BoxLayout(emptyPanel, BoxLayout.Y_AXIS));
        emptyPanel.setBackground(BG_MAIN);
        emptyPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
        JLabel iconLabel = new JLabel("🔲"); // ç®€å•的占位符,如果有图片更好
        iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 48));
        iconLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        JLabel textLabel = new JLabel("暂无障碍物数据");
        textLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        textLabel.setForeground(TEXT_SECONDARY);
        textLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        emptyPanel.add(Box.createVerticalStrut(50));
        emptyPanel.add(iconLabel);
        emptyPanel.add(Box.createVerticalStrut(10));
        emptyPanel.add(textLabel);
        cardsPanel.add(emptyPanel);
    }
    /**
     * åˆ›å»ºå•个障碍物卡片
     */
    private JPanel createObstacleCard(Obstacledge.Obstacle obstacle) {
        // å¡ç‰‡å®¹å™¨ - ä½¿ç”¨åœ†è§’矩形背景
        JPanel card = new JPanel() {
            @Override
            protected void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D) g.create();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setColor(BG_CARD);
                // ç»˜åˆ¶åœ†è§’矩形背景
                g2.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
                // ç»˜åˆ¶æ·¡æ·¡çš„边框
                g2.setColor(BORDER_LIGHT);
                g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
                g2.dispose();
            }
        };
        card.setLayout(new BoxLayout(card, BoxLayout.Y_AXIS));
        card.setOpaque(false); // é€æ˜Žä»¥æ˜¾ç¤º paintComponent æ•ˆæžœ
        card.setBorder(new EmptyBorder(15, 15, 15, 15)); // å¡ç‰‡å†…部内边距
        card.setMaximumSize(new Dimension(Integer.MAX_VALUE, 380)); // é™åˆ¶é«˜åº¦ï¼Œé˜²æ­¢æ‹‰ä¼¸
        // 1. å¡ç‰‡æ ‡é¢˜è¡Œ (名称 + å½¢çж + åˆ é™¤)
        JPanel titleRow = new JPanel(new BorderLayout());
        titleRow.setOpaque(false);
        String name = (obstacle.getObstacleName() == null || obstacle.getObstacleName().isEmpty())
                      ? "未命名" : obstacle.getObstacleName();
        JLabel nameLabel = new JLabel(name);
        nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 15));
        nameLabel.setForeground(TEXT_PRIMARY);
        // æ·»åŠ ä¸€ä¸ªå°å›¾æ ‡è£…é¥°
        nameLabel.setIcon(new Icon() {
            public void paintIcon(Component c, Graphics g, int x, int y) {
                g.setColor(PRIMARY_COLOR);
                g.fillOval(x, y + 4, 8, 8);
            }
            public int getIconWidth() { return 12; }
            public int getIconHeight() { return 16; }
        });
        // æ·»åŠ å½¢çŠ¶æ˜¾ç¤ºï¼ˆåœ¨åç§°å³è¾¹ï¼‰
        JLabel shapeLabel = createShapeLabel(obstacle.getShape());
        // å·¦ä¾§é¢æ¿ï¼šåç§° + å½¢çж
        JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
        leftPanel.setOpaque(false);
        leftPanel.add(nameLabel);
        leftPanel.add(shapeLabel);
        JButton deleteBtn = createIconButton("🗑", DANGER_COLOR); // ä½¿ç”¨Unicode垃圾桶
        deleteBtn.addActionListener(e -> deleteObstacle(obstacle));
        titleRow.add(leftPanel, BorderLayout.CENTER);
        titleRow.add(deleteBtn, BorderLayout.EAST);
        card.add(titleRow);
        card.add(Box.createVerticalStrut(12));
        // åˆ†å‰²çº¿
        JSeparator sep = new JSeparator();
        sep.setForeground(BORDER_LIGHT);
        sep.setMaximumSize(new Dimension(Integer.MAX_VALUE, 1));
        card.add(sep);
        card.add(Box.createVerticalStrut(12));
        // 2. åŽŸå§‹åæ ‡åŒºåŸŸ
        String originalCoords = extractOriginalCoordinates(obstacle);
        card.add(createDataField("原始经纬度数据", originalCoords, 2));
        card.add(Box.createVerticalStrut(10));
        // 3. ç”Ÿæˆåæ ‡åŒºåŸŸ
        String genCoords = extractObstacleCoordinates(obstacle);
        JTextArea xyArea = createDataTextArea(genCoords, 3); // å¼•用以便更新
        JScrollPane scrollXY = new JScrollPane(xyArea);
        scrollXY.setBorder(BorderFactory.createEmptyBorder()); // å¤–部由Panel提供边框
        JPanel xyWrapper = createWrapperPanel("生成坐标 (XYç±³)", scrollXY);
        card.add(xyWrapper);
        card.add(Box.createVerticalStrut(15));
        // 4. æ“ä½œæŒ‰é’®è¡Œ
        JPanel actionPanel = new JPanel();
        actionPanel.setLayout(new BoxLayout(actionPanel, BoxLayout.X_AXIS));
        actionPanel.setOpaque(false);
        JButton generateBtn = createStyledButton("重新生成坐标", PRIMARY_COLOR, true);
        generateBtn.addActionListener(e -> generateObstacleCoordinates(obstacle, xyArea));
        JButton previewBtn = createStyledButton("预览", TEXT_SECONDARY, false);
        previewBtn.setPreferredSize(new Dimension(70, 36)); // ç¨å¾®çª„一点
        previewBtn.addActionListener(e -> previewObstacle(obstacle));
        actionPanel.add(generateBtn);
        actionPanel.add(Box.createHorizontalStrut(10));
        actionPanel.add(previewBtn);
        card.add(actionPanel);
        return card;
    }
    /**
     * è¾…助方法:创建带标题的数据展示块
     */
    private JPanel createDataField(String title, String content, int rows) {
        JTextArea area = createDataTextArea(content, rows);
        JScrollPane scroll = new JScrollPane(area);
        scroll.setBorder(BorderFactory.createEmptyBorder());
        return createWrapperPanel(title, scroll);
    }
    private JPanel createWrapperPanel(String title, JComponent content) {
        JPanel panel = new JPanel(new BorderLayout(0, 5));
        panel.setOpaque(false);
        JLabel label = new JLabel(title);
        label.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        label.setForeground(TEXT_SECONDARY);
        JPanel contentBox = new JPanel(new BorderLayout());
        contentBox.setBackground(BG_INPUT);
        contentBox.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(new Color(230, 230, 230), 1),
            BorderFactory.createEmptyBorder(5, 5, 5, 5)
        ));
        contentBox.add(content, BorderLayout.CENTER);
        panel.add(label, BorderLayout.NORTH);
        panel.add(contentBox, BorderLayout.CENTER);
        return panel;
    }
    private JTextArea createDataTextArea(String text, int rows) {
        JTextArea area = new JTextArea(text);
        area.setRows(rows);
        area.setEditable(false);
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        area.setBackground(BG_INPUT);
        area.setForeground(new Color(50, 50, 50));
        // ä½¿ç”¨ç­‰å®½å­—体显示数据,看起来更专业
        area.setFont(new Font("Monospaced", Font.PLAIN, 12));
        return area;
    }
    /**
     * åˆ›å»ºçŽ°ä»£é£Žæ ¼æŒ‰é’® (实心或轮廓)
     */
    private JButton createStyledButton(String text, Color baseColor, boolean filled) {
        JButton btn = new JButton(text) {
            @Override
            protected void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D) g.create();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                boolean isPressed = getModel().isPressed();
                boolean isRollover = getModel().isRollover();
                if (filled) {
                    if (isPressed) g2.setColor(baseColor.darker());
                    else if (isRollover) g2.setColor(new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), 230)); // å¾®é€æ˜Ž
                    else g2.setColor(baseColor);
                    g2.fill(new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), 8, 8));
                    g2.setColor(Color.WHITE);
                } else {
                    g2.setColor(BG_CARD); // èƒŒæ™¯ç™½
                    g2.fill(new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), 8, 8));
                    if (isPressed) g2.setColor(baseColor.darker());
                    else if (isRollover) g2.setColor(baseColor);
                    else g2.setColor(new Color(200, 200, 200));
                    g2.setStroke(new BasicStroke(1.2f));
                    g2.draw(new RoundRectangle2D.Double(0, 0, getWidth()-1, getHeight()-1, 8, 8));
                    g2.setColor(isRollover ? baseColor : TEXT_SECONDARY);
                }
                FontMetrics fm = g2.getFontMetrics();
                int x = (getWidth() - fm.stringWidth(getText())) / 2;
                int y = (getHeight() - fm.getHeight()) / 2 + fm.getAscent();
                g2.drawString(getText(), x, y);
                g2.dispose();
            }
        };
        btn.setFont(new Font("微软雅黑", Font.BOLD, 12));
        btn.setFocusPainted(false);
        btn.setContentAreaFilled(false);
        btn.setBorderPainted(false);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
        return btn;
    }
    /**
     * åˆ›å»ºå½¢çŠ¶æ ‡ç­¾ï¼ˆæ˜¾ç¤ºåœ†å½¢æˆ–å¤šè¾¹å½¢å›¾æ ‡ï¼‰
     */
    private JLabel createShapeLabel(Obstacledge.ObstacleShape shape) {
        JLabel shapeLabel = new JLabel();
        shapeLabel.setPreferredSize(new Dimension(24, 24));
        if (shape == null) {
            return shapeLabel;
        }
        // æ ¹æ®å½¢çŠ¶åˆ›å»ºä¸åŒçš„å›¾æ ‡
        Icon shapeIcon;
        if (shape == Obstacledge.ObstacleShape.CIRCLE) {
            // åœ†å½¢å›¾æ ‡
            shapeIcon = new Icon() {
                @Override
                public void paintIcon(Component c, Graphics g, int x, int y) {
                    Graphics2D g2 = (Graphics2D) g.create();
                    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                    g2.setColor(PRIMARY_COLOR);
                    g2.setStroke(new BasicStroke(2.0f));
                    g2.drawOval(x + 2, y + 2, getIconWidth() - 4, getIconHeight() - 4);
                    g2.dispose();
                }
                @Override
                public int getIconWidth() {
                    return 20;
                }
                @Override
                public int getIconHeight() {
                    return 20;
                }
            };
            shapeLabel.setToolTipText("圆形");
        } else if (shape == Obstacledge.ObstacleShape.POLYGON) {
            // å¤šè¾¹å½¢å›¾æ ‡ï¼ˆå…­è¾¹å½¢ï¼‰
            shapeIcon = new Icon() {
                @Override
                public void paintIcon(Component c, Graphics g, int x, int y) {
                    Graphics2D g2 = (Graphics2D) g.create();
                    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                    g2.setColor(PRIMARY_COLOR);
                    g2.setStroke(new BasicStroke(2.0f));
                    // ç»˜åˆ¶å…­è¾¹å½¢
                    int centerX = x + getIconWidth() / 2;
                    int centerY = y + getIconHeight() / 2;
                    int radius = 8;
                    int[] xPoints = new int[6];
                    int[] yPoints = new int[6];
                    for (int i = 0; i < 6; i++) {
                        double angle = Math.PI / 3.0 * i - Math.PI / 6.0; // ä»Žé¡¶éƒ¨å¼€å§‹
                        xPoints[i] = centerX + (int) (radius * Math.cos(angle));
                        yPoints[i] = centerY + (int) (radius * Math.sin(angle));
                    }
                    g2.drawPolygon(xPoints, yPoints, 6);
                    g2.dispose();
                }
                @Override
                public int getIconWidth() {
                    return 20;
                }
                @Override
                public int getIconHeight() {
                    return 20;
                }
            };
            shapeLabel.setToolTipText("多边形");
        } else {
            return shapeLabel; // æœªçŸ¥å½¢çŠ¶ï¼Œè¿”å›žç©ºæ ‡ç­¾
        }
        shapeLabel.setIcon(shapeIcon);
        return shapeLabel;
    }
    /**
     * åˆ›å»ºçº¯å›¾æ ‡æŒ‰é’®ï¼ˆç”¨äºŽåˆ é™¤ï¼‰
     */
    private JButton createIconButton(String iconText, Color hoverColor) {
        JButton btn = new JButton(iconText);
        btn.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 16));
        btn.setForeground(TEXT_SECONDARY);
        btn.setBorder(null);
        btn.setContentAreaFilled(false);
        btn.setFocusPainted(false);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setPreferredSize(new Dimension(30, 30));
        btn.addMouseListener(new MouseAdapter() {
            public void mouseEntered(MouseEvent e) { btn.setForeground(hoverColor); }
            public void mouseExited(MouseEvent e) { btn.setForeground(TEXT_SECONDARY); }
        });
        return btn;
    }
    // =================================================================================
    // ä»¥ä¸‹ä¸ºåŽŸæœ‰ä¸šåŠ¡é€»è¾‘ä»£ç ï¼Œä¿æŒä¸å˜ï¼Œä»…è°ƒæ•´éƒ¨åˆ†ç©ºå€¼åˆ¤æ–­ä»¥é€‚é…æ–°UI
    // =================================================================================
    private void previewObstacle(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) return;
        String landNumber = dikuai.getLandNumber();
        String landName = dikuai.getLandName();
        String boundary = dikuai.getBoundaryCoordinates();
        String obstacleCoords = extractObstacleCoordinates(obstacle);
        if (obstacleCoords == null || obstacleCoords.trim().isEmpty()) {
            JOptionPane.showMessageDialog(this, "该障碍物没有坐标数据", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        List<Obstacledge.Obstacle> allObstacles = loadObstacles();
        String allObstaclesCoords = buildAllObstaclesCoordinates(allObstacles);
        setVisible(false);
        SwingUtilities.invokeLater(() -> {
            Shouye shouye = Shouye.getInstance();
            if (shouye != null) {
                shouye.startMowingPathPreview(
                    landNumber,
                    landName,
                    boundary,
                    allObstaclesCoords,
                    null,
                    () -> SwingUtilities.invokeLater(() -> setVisible(true))
                );
            } else {
                JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE);
                setVisible(true);
            }
        });
    }
    private String buildAllObstaclesCoordinates(List<Obstacledge.Obstacle> obstacles) {
        if (obstacles == null || obstacles.isEmpty()) return null;
        List<String> coordStrings = new ArrayList<>();
        for (Obstacledge.Obstacle obstacle : obstacles) {
            String coords = extractObstacleCoordinates(obstacle);
            if (coords != null && !coords.trim().isEmpty()) {
                coordStrings.add(coords.trim());
            }
        }
        return coordStrings.isEmpty() ? null : String.join(" ", coordStrings);
    }
    private String extractObstacleCoordinates(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) return "";
        String xy = obstacle.getXyCoordsString();
        return isMeaningfulValue(xy) ? xy.trim() : "";
    }
    private String extractOriginalCoordinates(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) return "";
        String original = obstacle.getOriginalCoordsString();
        return isMeaningfulValue(original) ? original.trim() : "";
    }
    private void generateObstacleCoordinates(Obstacledge.Obstacle obstacle, JTextArea coordArea) {
        if (obstacle == null || coordArea == null) return;
        String originalCoords = extractOriginalCoordinates(obstacle);
        if (!isMeaningfulValue(originalCoords)) {
            JOptionPane.showMessageDialog(this, "无原始坐标数据,无法生成", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        String baseStation = dikuai.getBaseStationCoordinates();
        if (!isMeaningfulValue(baseStation)) {
            JOptionPane.showMessageDialog(this, "地块未设置基站坐标", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        try {
            List<Obstacledge.DMCoordinate> originalCoordsList = obstacle.getOriginalCoordinates();
            if (originalCoordsList == null || originalCoordsList.isEmpty()) {
                JOptionPane.showMessageDialog(this, "原始坐标数据无效", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            String[] baseParts = baseStation.split(",");
            if (baseParts.length < 4) {
                JOptionPane.showMessageDialog(this, "基站坐标格式不正确", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            double baseLat = parseDMToDecimal(baseParts[0].trim(), baseParts[1].trim());
            double baseLon = parseDMToDecimal(baseParts[2].trim(), baseParts[3].trim());
            List<Obstacledge.XYCoordinate> xyCoords = new ArrayList<>();
            for (int i = 0; i < originalCoordsList.size(); i += 2) {
                if (i + 1 >= originalCoordsList.size()) break;
                double lat = originalCoordsList.get(i).toDecimalDegree();
                double lon = originalCoordsList.get(i + 1).toDecimalDegree();
                if (Double.isFinite(lat) && Double.isFinite(lon)) {
                    double[] localXY = convertLatLonToLocal(lat, lon, baseLat, baseLon);
                    xyCoords.add(new Obstacledge.XYCoordinate(localXY[0], localXY[1]));
                }
            }
            if (xyCoords.isEmpty()) {
                JOptionPane.showMessageDialog(this, "坐标转换失败", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            obstacle.setXyCoordinates(xyCoords);
            saveObstacleUpdate(obstacle, coordArea);
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "错误: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    private void saveObstacleUpdate(Obstacledge.Obstacle obstacle, JTextArea coordArea) {
        String landNumber = dikuai.getLandNumber();
        try {
            File configFile = new File("Obstacledge.properties");
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (configFile.exists()) manager.loadFromFile(configFile.getAbsolutePath());
            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
            if (plot != null) {
                plot.removeObstacleByName(obstacle.getObstacleName());
                plot.addObstacle(obstacle);
                manager.saveToFile(configFile.getAbsolutePath());
                Dikuai.updateField(landNumber.trim(), "updateTime", getCurrentTime());
                Dikuai.saveToProperties();
                coordArea.setText(obstacle.getXyCoordsString());
                JOptionPane.showMessageDialog(this, "坐标已生成并保存", "成功", JOptionPane.INFORMATION_MESSAGE);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    private double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) return Double.NaN;
        try {
            String trimmed = dmm.trim();
            int dotIndex = trimmed.indexOf('.');
            if (dotIndex < 2) return Double.NaN;
            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) decimal = -decimal;
            return decimal;
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
    }
    private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double METERS_PER_DEGREE_LAT = 111320.0;
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
    }
    private void deleteObstacle(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) return;
        String name = obstacle.getObstacleName();
        int choice = JOptionPane.showConfirmDialog(this,
            "确定要删除障碍物 \"" + name + "\" å—?",
            "删除确认", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
        if (choice != JOptionPane.YES_OPTION) return;
        String landNumber = dikuai.getLandNumber();
        try {
            File configFile = new File("Obstacledge.properties");
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (configFile.exists()) manager.loadFromFile(configFile.getAbsolutePath());
            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
            if (plot != null && plot.removeObstacleByName(name)) {
                manager.saveToFile(configFile.getAbsolutePath());
                Dikuai.updateField(landNumber.trim(), "updateTime", getCurrentTime());
                Dikuai.saveToProperties();
                Dikuaiguanli.notifyExternalCreation(landNumber.trim());
                JOptionPane.showMessageDialog(this, "删除成功");
                loadAndDisplayObstacles();
            } else {
                JOptionPane.showMessageDialog(this, "删除失败:未找到记录", "错误", JOptionPane.ERROR_MESSAGE);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    private String getCurrentTime() {
        return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
    }
    private List<Obstacledge.Obstacle> loadObstacles() {
        if (dikuai == null) return Collections.emptyList();
        String landNumber = dikuai.getLandNumber();
        if (landNumber == null || landNumber.trim().isEmpty()) return Collections.emptyList();
        try {
            File configFile = new File("Obstacledge.properties");
            if (!configFile.exists()) return Collections.emptyList();
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (!manager.loadFromFile(configFile.getAbsolutePath())) return Collections.emptyList();
            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
            return plot != null ? new ArrayList<>(plot.getObstacles()) : Collections.emptyList();
        } catch (Exception ex) {
            ex.printStackTrace();
            return Collections.emptyList();
        }
    }
    private String getDisplayValue(String value, String defaultValue) {
        return (value != null && !value.equals("-1") && !value.trim().isEmpty()) ? value : defaultValue;
    }
    private boolean isMeaningfulValue(String value) {
        return value != null && !value.trim().isEmpty() && !"-1".equals(value.trim());
    }
}
src/dikuai/addzhangaiwu.java
@@ -564,6 +564,9 @@
        prevButton = createSecondaryButton("上一步");
        nextButton = createPrimaryButton("下一步", 16);
        // è®¾ç½®ä¸‹ä¸€æ­¥æŒ‰é’®å®½åº¦ä¸º300像素
        nextButton.setPreferredSize(new Dimension(300, nextButton.getPreferredSize().height));
        nextButton.setMaximumSize(new Dimension(300, nextButton.getPreferredSize().height));
        saveButton = createPrimaryButton("保存", 16);
        saveButton.setVisible(false);
src/lujing/MowingPathGenerationPage.java
@@ -1,6 +1,7 @@
package lujing;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -143,14 +144,17 @@
        buttonPanel.setBackground(BACKGROUND_COLOR);
        
        JButton generateBtn = createPrimaryFooterButton("生成割草路径");
        JButton previewBtn = createPrimaryFooterButton("预览");
        JButton saveBtn = createPrimaryFooterButton("保存路径");
        JButton cancelBtn = createPrimaryFooterButton("取消");
        
        generateBtn.addActionListener(e -> generatePath(modeValue));
        previewBtn.addActionListener(e -> previewPath());
        saveBtn.addActionListener(e -> savePath());
        cancelBtn.addActionListener(e -> dispose());
        
        buttonPanel.add(generateBtn);
        buttonPanel.add(previewBtn);
        buttonPanel.add(saveBtn);
        buttonPanel.add(cancelBtn);
        add(buttonPanel, BorderLayout.SOUTH);
@@ -187,6 +191,123 @@
    }
    
    /**
     * é¢„览路径
     */
    private void previewPath() {
        // å…ˆä¿å­˜å½“前路径到地块(临时保存,用于预览)
        String pathNormalized = normalizeCoordinateInput(pathArea.getText());
        if (!"-1".equals(pathNormalized)) {
            pathNormalized = pathNormalized
                .replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll(";+", ";")
                .replaceAll("\\s*;\\s*", ";")
                .trim();
            if (pathNormalized.isEmpty()) {
                pathNormalized = "-1";
            }
        }
        if ("-1".equals(pathNormalized)) {
            JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        // ä¸´æ—¶ä¿å­˜è·¯å¾„到地块对象(不持久化)
        if (saveCallback != null) {
            saveCallback.savePlannedPath(dikuai, pathNormalized);
        }
        // ä¿å­˜å½“前页面状态,用于返回时恢复
        String currentBaseStation = baseStationField.getText();
        String currentBoundary = boundaryArea.getText();
        String currentObstacle = obstacleArea.getText();
        String currentWidth = widthField.getText();
        String currentPath = pathArea.getText();
        // èŽ·å–åœ°å—ä¿¡æ¯
        String landNumber = dikuai.getLandNumber();
        String landName = dikuai.getLandName();
        // å¤„理边界坐标,确保变量是 effectively final
        String boundaryInput = normalizeCoordinateInput(boundaryArea.getText());
        final String boundary;
        if (!"-1".equals(boundaryInput)) {
            String processed = boundaryInput.replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll(";+", ";")
                .replaceAll("\\s*;\\s*", ";")
                .trim();
            if (processed.isEmpty()) {
                boundary = dikuai.getBoundaryCoordinates();
            } else {
                boundary = processed;
            }
        } else {
            boundary = dikuai.getBoundaryCoordinates();
        }
        // å¤„理障碍物坐标,确保变量是 effectively final
        String obstaclesInput = normalizeCoordinateInput(obstacleArea.getText());
        final String obstacles;
        if (!"-1".equals(obstaclesInput)) {
            String processed = obstaclesInput.replace("\r\n", " ")
                .replace('\r', ' ')
                .replace('\n', ' ')
                .replaceAll("\\s{2,}", " ")
                .trim();
            if (processed.isEmpty()) {
                obstacles = null;
            } else {
                obstacles = processed;
            }
        } else {
            obstacles = null;
        }
        // ä¿å­˜æœ€ç»ˆå€¼åˆ° final å˜é‡ï¼Œä»¥ä¾¿åœ¨ lambda ä¸­ä½¿ç”¨
        final String finalPathNormalized = pathNormalized;
        final String finalLandNumber = landNumber;
        final String finalLandName = landName;
        // å…³é—­è·¯å¾„规划页面
        setVisible(false);
        // æ‰“开主页面并显示路径预览
        SwingUtilities.invokeLater(() -> {
            zhuye.Shouye shouye = zhuye.Shouye.getInstance();
            if (shouye != null) {
                // æ˜¾ç¤ºè·¯å¾„预览,并设置返回回调
                shouye.startMowingPathPreview(
                    finalLandNumber,
                    finalLandName,
                    boundary,
                    obstacles,
                    finalPathNormalized,
                    () -> {
                        // è¿”回回调:重新打开路径规划页面
                        SwingUtilities.invokeLater(() -> {
                            setVisible(true);
                            // æ¢å¤ä¹‹å‰çš„状态
                            baseStationField.setText(currentBaseStation);
                            boundaryArea.setText(currentBoundary);
                            obstacleArea.setText(currentObstacle);
                            widthField.setText(currentWidth);
                            pathArea.setText(currentPath);
                        });
                    }
                );
            } else {
                // å¦‚果主页面不存在,提示用户并重新显示路径规划页面
                JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE);
                setVisible(true);
            }
        });
    }
    /**
     * ä¿å­˜è·¯å¾„
     */
    private void savePath() {
src/zhuye/MapRenderer.java
@@ -52,6 +52,7 @@
    private static final Color GRASS_FILL_COLOR = new Color(144, 238, 144, 120);
    private static final Color GRASS_BORDER_COLOR = new Color(60, 179, 113);
    private static final Color BOUNDARY_POINT_COLOR = new Color(128, 0, 128);
    private static final Color OBSTACLE_POINT_COLOR = new Color(255, 140, 0); // æ©™è‰²ï¼Œç”¨äºŽåŒºåˆ†éšœç¢ç‰©ç‚¹
    private static final Color CIRCLE_SAMPLE_COLOR = new Color(220, 20, 60, 230);
    private static final double CIRCLE_SAMPLE_SIZE = 0.54d;
    private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05;
@@ -71,6 +72,7 @@
    private String currentObstacleLandNumber;
    private String boundaryName;
    private boolean boundaryPointsVisible;
    private boolean obstaclePointsVisible;
    private double boundaryPointSizeScale = 1.0d;
    private boolean previewSizingEnabled;
    private String currentBoundaryLandNumber;
@@ -385,6 +387,22 @@
            );
        }
        // ç»˜åˆ¶éšœç¢ç‰©åæ ‡ç‚¹
        if (obstaclePointsVisible && hasObstacles) {
            List<Point2D.Double> obstaclePoints = Obstacledraw.getObstaclePoints(currentObstacles);
            if (obstaclePoints != null && !obstaclePoints.isEmpty()) {
                double markerScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d);
                pointandnumber.drawBoundaryPoints(
                    g2d,
                    obstaclePoints,
                    scale,
                    BOUNDARY_POINT_MERGE_THRESHOLD,
                    OBSTACLE_POINT_COLOR,
                    markerScale
                );
            }
        }
        if (shouldRenderIdleTrail()) {
            tuowei.draw(g2d, idleMowerTrail, scale);
        }
@@ -1746,6 +1764,7 @@
        obstacleBounds = null;
        selectedObstacleName = null;
        currentObstacleLandNumber = null;
        obstaclePointsVisible = false;
    }
    private List<Obstacledge.Obstacle> parseObstacles(String obstaclesData, String landNumber) {
@@ -2033,6 +2052,11 @@
        visualizationPanel.repaint();
    }
    public void setObstaclePointsVisible(boolean visible) {
        this.obstaclePointsVisible = visible;
        visualizationPanel.repaint();
    }
    public void setBoundaryPointSizeScale(double sizeScale) {
        double normalized = (Double.isFinite(sizeScale) && sizeScale > 0.0d) ? sizeScale : 1.0d;
        if (Math.abs(boundaryPointSizeScale - normalized) < 1e-6) {
src/zhuye/Shouye.java
@@ -320,7 +320,7 @@
        }
        // å…³é—­æ¨¡å¼æ—¶ä¸éœ€è¦åšä»»ä½•操作,用户已经可以自由移动地图
        
        // æ›´æ–°å·¥å…·æç¤º
        // æ›´æ–°å›¾æ ‡æ˜¾ç¤ºï¼ˆé‡ç»˜ä»¥åˆ‡æ¢å›¾æ ‡ï¼‰
        if (visualizationPanel != null) {
            visualizationPanel.repaint();
        }
@@ -564,14 +564,16 @@
        // å¯è§†åŒ–区域 - ä½¿ç”¨MapRenderer进行绘制
        visualizationPanel = new JPanel() {
            private ImageIcon gecaojiIcon = null;
            private ImageIcon gecaojiIcon1 = null;  // é»˜è®¤å›¾æ ‡
            private ImageIcon gecaojiIcon2 = null;  // ä»¥å‰²è‰æœºä¸ºä¸­å¿ƒæ¨¡å¼å›¾æ ‡
            private static final int GECAOJI_ICON_X = 37;
            private static final int GECAOJI_ICON_Y = 10;
            private static final int GECAOJI_ICON_SIZE = 20;
            
            {
                // åŠ è½½å‰²è‰æœºå›¾æ ‡ï¼Œå¤§å°20x20像素
                gecaojiIcon = loadScaledIcon("image/gecaoji.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE);
                gecaojiIcon1 = loadScaledIcon("image/gecaojishijiao1.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE);
                gecaojiIcon2 = loadScaledIcon("image/gecaojishijiao2.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE);
            }
            
            /**
@@ -607,11 +609,15 @@
                if (isMowerOutsideBoundary && warningIconVisible) {
                    // ç»˜åˆ¶çº¢è‰²ä¸‰è§’形警告图标(带叹号)
                    drawWarningIcon(g, GECAOJI_ICON_X, GECAOJI_ICON_Y, GECAOJI_ICON_SIZE);
                } else if (gecaojiIcon != null) {
                    // ç»˜åˆ¶æ­£å¸¸çš„割草机图标
                    // æ°´å¹³æ–¹å‘与速度指示器对齐(x=37)
                    // åž‚直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐)
                    g.drawImage(gecaojiIcon.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null);
                } else {
                    // æ ¹æ®æ¨¡å¼é€‰æ‹©ä¸åŒçš„图标
                    ImageIcon iconToDraw = centerOnMowerMode ? gecaojiIcon2 : gecaojiIcon1;
                    if (iconToDraw != null) {
                        // ç»˜åˆ¶å‰²è‰æœºå›¾æ ‡
                        // æ°´å¹³æ–¹å‘与速度指示器对齐(x=37)
                        // åž‚直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐)
                        g.drawImage(iconToDraw.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null);
                    }
                }
            }
            
src/zhuye/buttonset.java
@@ -1,70 +1,118 @@
package zhuye;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import javax.swing.BorderFactory;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JButton;
import javax.swing.border.EmptyBorder;
/**
 * æä¾›ç»Ÿä¸€æŒ‰é’®æ ·å¼çš„工厂方法。
 * æä¾›ç»Ÿä¸€æŒ‰é’®æ ·å¼çš„工厂方法(美化版)。
 * é£Žæ ¼ä¸Ž ObstacleManagementPage ä¿æŒä¸€è‡´ï¼šåœ†è§’、扁平化、抗锯齿。
 */
public final class buttonset {
    // é»˜è®¤æŒ‰é’®å°ºå¯¸
    private static final Dimension DEFAULT_SIZE = new Dimension(90, 36);
    // åœ†è§’半径
    private static final int CORNER_RADIUS = 8;
    private buttonset() {
        // å·¥å…·ç±»ä¸éœ€è¦å®žä¾‹åŒ–
    }
    /**
     * åˆ›å»ºå¸¦æœ‰ç»Ÿä¸€æ ·å¼çš„æŒ‰é’®ï¼Œä¾›å¤šä¸ªç•Œé¢å¤ç”¨ã€‚
     * åˆ›å»ºå¸¦æœ‰çŽ°ä»£åŒ–ç»Ÿä¸€æ ·å¼çš„æŒ‰é’®ï¼ˆåœ†è§’ã€æ‰å¹³é£Žæ ¼ï¼‰ã€‚
     *
     * @param text æŒ‰é’®æ˜¾ç¤ºæ–‡æœ¬
     * @param backgroundColor æŒ‰é’®èƒŒæ™¯è‰²
     * @param backgroundColor æŒ‰é’®ä¸»è‰²è°ƒï¼ˆå¦‚果为null,默认使用灰蓝色)
     * @return å·²åº”用样式的 JButton
     */
    public static JButton createStyledButton(String text, Color backgroundColor) {
        // ç¡®å®šåŸºç¡€é¢œè‰²ï¼Œå¦‚果未指定则使用默认的钢蓝色
        final Color baseColor = backgroundColor != null ? backgroundColor : new Color(70, 130, 180);
        JButton button = new JButton(text);
        // åˆ›å»ºè‡ªå®šä¹‰ç»˜åˆ¶çš„æŒ‰é’®
        JButton button = new JButton(text) {
            private static final long serialVersionUID = 1L;
            @Override
            protected void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D) g.create();
                // å¼€å¯æŠ—锯齿,使圆角和文字更平滑
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                // èŽ·å–æŒ‰é’®çŠ¶æ€
                boolean isPressed = getModel().isPressed();
                boolean isRollover = getModel().isRollover();
                int w = getWidth();
                int h = getHeight();
                // è®¡ç®—当前背景色
                if (isPressed) {
                    g2.setColor(baseColor.darker()); // æŒ‰ä¸‹å˜æ·±
                } else if (isRollover) {
                    // æ‚¬åœæ—¶ç¨å¾®é€æ˜Žæˆ–变亮,增加交互感
                    g2.setColor(new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), 220));
                } else {
                    g2.setColor(baseColor); // æ­£å¸¸é¢œè‰²
                }
                // ç»˜åˆ¶åœ†è§’矩形背景
                g2.fill(new RoundRectangle2D.Double(0, 0, w, h, CORNER_RADIUS, CORNER_RADIUS));
                // å¦‚果需要在按下时有轻微的边框效果(可选,增加立体感)
                if (isPressed) {
                    g2.setColor(new Color(0, 0, 0, 30));
                    g2.draw(new RoundRectangle2D.Double(0.5, 0.5, w - 1, h - 1, CORNER_RADIUS, CORNER_RADIUS));
                }
                // ç»˜åˆ¶æ–‡å­—
                g2.setColor(Color.WHITE);
                g2.setFont(getFont());
                FontMetrics fm = g2.getFontMetrics();
                // ç²¾ç¡®è®¡ç®—文字居中位置
                int textX = (w - fm.stringWidth(getText())) / 2;
                // æ³¨æ„ï¼šy坐标是基线位置,需要加上 Ascent å¹¶å‡åŽ»é«˜åº¦çš„ä¸€åŠ
                int textY = (h - fm.getHeight()) / 2 + fm.getAscent();
                g2.drawString(getText(), textX, textY);
                g2.dispose();
            }
        };
        // åŸºç¡€å±žæ€§è®¾ç½®
        button.setFont(new Font("微软雅黑", Font.BOLD, 14));
        button.setBackground(baseColor);
        button.setForeground(Color.WHITE);
        button.setFocusPainted(false);
        button.setBorder(BorderFactory.createEmptyBorder(8, 18, 8, 18));
        button.setFocusPainted(false);      // åŽ»é™¤ç„¦ç‚¹è™šçº¿æ¡†
        button.setContentAreaFilled(false); // åŽ»é™¤é»˜è®¤èƒŒæ™¯ç»˜åˆ¶ï¼ˆç”±paintComponent接管)
        button.setBorderPainted(false);     // åŽ»é™¤é»˜è®¤è¾¹æ¡†
        button.setCursor(new Cursor(Cursor.HAND_CURSOR)); // é¼ æ ‡å˜æˆæ‰‹åž‹
        // è®¾ç½®å†…边距(虽然主要由paintComponent控制,但对布局计算有帮助)
        button.setBorder(new EmptyBorder(8, 18, 8, 18));
    Dimension preferred = button.getPreferredSize();
    int width = Math.max(preferred.width, DEFAULT_SIZE.width);
    int height = Math.max(preferred.height, DEFAULT_SIZE.height);
    Dimension adjustedSize = new Dimension(width, height);
    button.setPreferredSize(adjustedSize);
    button.setMinimumSize(new Dimension(DEFAULT_SIZE.width, DEFAULT_SIZE.height));
    button.setMaximumSize(adjustedSize);
        button.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseEntered(java.awt.event.MouseEvent event) {
                button.setBackground(brightenColor(baseColor));
            }
            @Override
            public void mouseExited(java.awt.event.MouseEvent event) {
                button.setBackground(baseColor);
            }
        });
        // å°ºå¯¸è®¡ç®—逻辑
        Dimension preferred = button.getPreferredSize();
        // ç¡®ä¿å®½åº¦ä¸å°äºŽé»˜è®¤å€¼ï¼Œä¸”根据文字长度自适应
        int width = Math.max(preferred.width + 20, DEFAULT_SIZE.width);
        int height = Math.max(preferred.height, DEFAULT_SIZE.height);
        Dimension finalSize = new Dimension(width, height);
        button.setPreferredSize(finalSize);
        button.setMinimumSize(DEFAULT_SIZE);
        button.setMaximumSize(finalSize); // é˜²æ­¢åœ¨æŸäº›å¸ƒå±€ä¸­è¢«è¿‡åº¦æ‹‰ä¼¸
        return button;
    }
    private static Color brightenColor(Color color) {
        if (color == null) {
            return new Color(200, 200, 200);
        }
        int r = Math.min(255, color.getRed() + 30);
        int g = Math.min(255, color.getGreen() + 30);
        int b = Math.min(255, color.getBlue() + 30);
        return new Color(r, g, b);
    }
}
}