Kaynağa Gözat

产线产品拆分配置

wangxichen 4 gün önce
ebeveyn
işleme
5f0b37177a

+ 10 - 8
config/project_config.json

@@ -1,20 +1,22 @@
 {
-	"currentProjectId":"P3",
+	"currentLineId":"P4",
+	"currentProductId":"AY7-520",
 	"deviceIp":"192.168.0.6",
-	"stationCodeOverride":"OP220A",
+	"stationCodeOverride":"OP300A",
 	"lineSnOverride":"XT",
 	"serverIpOverride":"192.168.113.99",
+	"deviceEnabledOverride":false,
 	"uiOverride":{
-		"deviceRow1Enabled":true,
+		"deviceRow1Enabled":false,
 		"deviceRow1Label":"M5",
 		"deviceRow2Enabled":false,
-		"showMaterialInput":false
+		"showMaterialInput":true
 	},
 	"featuresOverride":{
 		"cold_plate":false,
-		"riveting":true,
-		"prod_params":true,
-		"bottom_plate":false
+		"riveting":false,
+		"prod_params":false,
+		"bottom_plate":true
 	},
-	"lastModified":1778235188799
+	"lastModified":1783321584563
 }

+ 2 - 2
src/com/mes/App.java

@@ -64,8 +64,8 @@ public class App {
             }
             log.info("  - 服务器: {}:{}", config.getServerIp(), config.getTcpPort());
             log.info("  - 设备: {} ({})", config.isDeviceEnabled() ? "启用" : "禁用", config.getDeviceType());
-            log.info("  - 当前项目: {} ({})", projectConfig.getCurrentProjectId(),
-                    projectConfig.getCurrentProject() != null ? projectConfig.getCurrentProject().getName() : "?");
+            log.info("  - 当前产线位置: {}, 产品号: {}", projectConfig.getCurrentLineId(),
+                    projectConfig.getCurrentProductId());
         } catch (Exception e) {
             log.error("配置加载失败: {}", e.getMessage(), e);
             JOptionPane.showMessageDialog(null,

+ 12 - 12
src/com/mes/core/OprnoRegistry.java

@@ -6,8 +6,8 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 工位号注册表 —— 按项目(P1-P4)维护工位号到工位名称的映射
- * 切换项目后工位列表跟着切换
+ * 工位号注册表 —— 按产线位置(P1-P4)维护工位号到工位名称的映射
+ * 切换产线位置后工位列表跟着切换
  * 注:P1 和 P2 工位列表完全一致
  */
 public final class OprnoRegistry {
@@ -124,26 +124,26 @@ public final class OprnoRegistry {
     }
 
     /**
-     * 根据项目id查工位名称
+     * 根据产线位置id查工位名称
      * @return 匹配返回中文名,查不到返回空字符串
      */
-    public static String getGwDes(String projectId, String code) {
-        if (projectId == null || code == null) return "";
-        Map<String, String> table = tableOf(projectId);
+    public static String getGwDes(String lineId, String code) {
+        if (lineId == null || code == null) return "";
+        Map<String, String> table = tableOf(lineId);
         if (table == null) return "";
         return table.getOrDefault(formatOprno(code), "");
     }
 
-    /** 获取指定项目的工位码列表(UI下拉用) */
-    public static List<String> listCodes(String projectId) {
-        Map<String, String> table = tableOf(projectId);
+    /** 获取指定产线位置的工位码列表(UI下拉用) */
+    public static List<String> listCodes(String lineId) {
+        Map<String, String> table = tableOf(lineId);
         if (table == null) return Collections.emptyList();
         return new java.util.ArrayList<>(table.keySet());
     }
 
-    private static Map<String, String> tableOf(String projectId) {
-        if (projectId == null) return null;
-        switch (projectId.toUpperCase()) {
+    private static Map<String, String> tableOf(String lineId) {
+        if (lineId == null) return null;
+        switch (lineId.toUpperCase()) {
             case "P1":
             case "P2":
                 return P1P2;

+ 160 - 59
src/com/mes/core/ProjectConfigManager.java

@@ -20,10 +20,13 @@ import java.util.Map;
 
 /**
  * 项目配置管理器
- * 负责切换项目(不同项目有不同工件码/冷板码/底护板码前缀
+ * 负责切换"产线位置"和"产品号"(两者互相独立、可自由组合
  * 以及拉铆设备IP的运行时持久化
  *
- * 项目前缀对应表内置,避免误改;运行时可变的仅"当前项目id"和"设备IP"
+ * 产线位置(Line):决定工位号→工序名称的映射表(配合 OprnoRegistry 使用)
+ * 产品号(Product):决定工件码/冷板码/底护板码前缀
+ *
+ * 两张表内置,避免误改;运行时可变的仅"当前产线位置id"、"当前产品号id"和"设备IP"
  */
 public class ProjectConfigManager {
     private static final Logger log = LoggerFactory.getLogger(ProjectConfigManager.class);
@@ -31,48 +34,76 @@ public class ProjectConfigManager {
     private static final String CONFIG_DIR = "config";
     private static final String CONFIG_FILE = "project_config.json";
 
-    // ========== 项目定义 ==========
-    public static final class Project {
-        private final String id;          // P1/P2/P3/P4
-        private final String name;        // 显示名(如 AY7-610)
-        private final String productPrefix;      // 工件码(框架码) 前缀
-        private final String coldPlatePrefix;    // 冷板码 前缀
-        private final String bottomPlatePrefix;  // 底护板码 前缀
+    // ========== 产线位置定义 ==========
+    public static final class Line {
+        private final String id;   // P1/P2/P3/P4
+
+        public Line(String id) {
+            this.id = id;
+        }
+
+        public String getId() { return id; }
+
+        /** 供UI下拉显示 */
+        public String getDisplay() { return id; }
+    }
+
+    // ========== 产品号定义 ==========
+    public static final class Product {
+        private final String id;                 // P02-1/AY7-610/AY7-520/P04-1/650
+        private final String productPrefix;       // 工件码(框架码) 前缀
+        private final String coldPlatePrefix;     // 冷板码 前缀
+        private final String bottomPlatePrefix;   // 底护板码 前缀
 
-        public Project(String id, String name,
-                       String productPrefix, String coldPlatePrefix, String bottomPlatePrefix) {
+        public Product(String id, String productPrefix, String coldPlatePrefix, String bottomPlatePrefix) {
             this.id = id;
-            this.name = name;
             this.productPrefix = productPrefix;
             this.coldPlatePrefix = coldPlatePrefix;
             this.bottomPlatePrefix = bottomPlatePrefix;
         }
 
         public String getId() { return id; }
-        public String getName() { return name; }
         public String getProductPrefix() { return productPrefix; }
         public String getColdPlatePrefix() { return coldPlatePrefix; }
         public String getBottomPlatePrefix() { return bottomPlatePrefix; }
 
         /** 供UI下拉显示 */
-        public String getDisplay() { return id + " " + name; }
+        public String getDisplay() { return id; }
     }
 
-    // 内置项目列表(顺序即 UI 展示顺序)
-    private static final List<Project> PROJECTS = Collections.unmodifiableList(Arrays.asList(
-            new Project("P1", "P02-1",   "+KA99", "+KB004", "+KB77"),
-            new Project("P2", "AY7-610", "+KB24", "+KA94",  "+KB77"),
-            new Project("P3", "AY7-520", "+KB23", "+KB004", "+KB77"),
-            new Project("--", "P04-1",   "+KA93", "+KA94",  "+KB77"),
-//            new Project("P4", "650",   "+KA64IS", "+KA64IG",  "+KB78")
-            new Project("P4", "P04-1",   "+KA93", "+KA94",  "+KB77")
+    // 内置产线位置列表(顺序即 UI 展示顺序)
+    private static final List<Line> LINES = Collections.unmodifiableList(Arrays.asList(
+            new Line("P1"),
+            new Line("P2"),
+            new Line("P3"),
+            new Line("P4")
+    ));
 
+    // 内置产品号列表(顺序即 UI 展示顺序)
+    private static final List<Product> PRODUCTS = Collections.unmodifiableList(Arrays.asList(
+            new Product("P02-1",   "+KA99",   "+KB004",  "+KB77"),
+            new Product("AY7-610", "+KB24",   "+KA94",   "+KB77"),
+            new Product("AY7-520", "+KB23",   "+KB004",  "+KB77"),
+            new Product("P04-1",   "+KA93",   "+KA94",   "+KB77"),
+            new Product("650",     "+KA64IS", "+KA64IG", "+KB78")
     ));
 
     // 默认值
-    private static final String DEFAULT_PROJECT_ID = "P2";
+    private static final String DEFAULT_LINE_ID = "P2";
+    private static final String DEFAULT_PRODUCT_ID = "AY7-610";
     private static final String DEFAULT_DEVICE_IP = "192.168.0.6";
 
+    // 旧配置迁移:老版本 currentProjectId(P1~P4) → 产品号 的对应关系
+    private static final Map<String, String> LEGACY_PROJECT_TO_PRODUCT;
+    static {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put("P1", "P02-1");
+        m.put("P2", "AY7-610");
+        m.put("P3", "AY7-520");
+        m.put("P4", "P04-1");
+        LEGACY_PROJECT_TO_PRODUCT = Collections.unmodifiableMap(m);
+    }
+
     // ========== 单例 ==========
     private static volatile ProjectConfigManager instance;
 
@@ -89,7 +120,8 @@ public class ProjectConfigManager {
 
     // ========== 状态 ==========
     private final Path configPath;
-    private String currentProjectId;
+    private String currentLineId;
+    private String currentProductId;
     private String deviceIp;
     private String stationCodeOverride;   // 覆盖 yaml 的 stations[0].code
     private String lineSnOverride;        // 覆盖 yaml 的 line_sn
@@ -108,7 +140,8 @@ public class ProjectConfigManager {
 
     private ProjectConfigManager() {
         this.configPath = Paths.get(CONFIG_DIR, CONFIG_FILE);
-        this.currentProjectId = DEFAULT_PROJECT_ID;
+        this.currentLineId = DEFAULT_LINE_ID;
+        this.currentProductId = DEFAULT_PRODUCT_ID;
         this.deviceIp = DEFAULT_DEVICE_IP;
         load();
     }
@@ -117,7 +150,7 @@ public class ProjectConfigManager {
 
     private void load() {
         if (!Files.exists(configPath)) {
-            log.info("[ProjectConfig] 配置文件不存在,使用默认: project={}, ip={}", currentProjectId, deviceIp);
+            log.info("[ProjectConfig] 配置文件不存在,使用默认: line={}, product={}, ip={}", currentLineId, currentProductId, deviceIp);
             return;
         }
         try {
@@ -125,9 +158,30 @@ public class ProjectConfigManager {
             JSONObject json = JSON.parseObject(content);
             if (json == null) return;
 
-            String pid = json.getString("currentProjectId");
-            if (pid != null && findProject(pid) != null) {
-                currentProjectId = pid;
+            // 新字段:产线位置 + 产品号
+            String lineId = json.getString("currentLineId");
+            if (lineId != null && findLine(lineId) != null) {
+                currentLineId = lineId;
+            }
+            String productId = json.getString("currentProductId");
+            if (productId != null && findProduct(productId) != null) {
+                currentProductId = productId;
+            }
+            // 兼容旧配置:老版本只有 currentProjectId(如"P4",旧版项目id同时代表产线位置+产品号)
+            // 若新字段缺失,则按旧的 id→产品号 对应关系拆分迁移
+            if (lineId == null || productId == null) {
+                String oldPid = json.getString("currentProjectId");
+                if (oldPid != null) {
+                    if (lineId == null && findLine(oldPid) != null) {
+                        currentLineId = oldPid;
+                    }
+                    if (productId == null) {
+                        String legacyProduct = LEGACY_PROJECT_TO_PRODUCT.get(oldPid.toUpperCase());
+                        if (legacyProduct != null) {
+                            currentProductId = legacyProduct;
+                        }
+                    }
+                }
             }
             String ip = json.getString("deviceIp");
             if (ip != null && !ip.trim().isEmpty()) {
@@ -167,8 +221,8 @@ public class ProjectConfigManager {
                 deviceRow2LabelOverride = trimToNull(uiObj.getString("deviceRow2Label"));
                 showMaterialInputOverride = uiObj.getBoolean("showMaterialInput");
             }
-            log.info("[ProjectConfig] 加载成功: project={}, ip={}, station={}, lineSn={}, features={}",
-                    currentProjectId, deviceIp, stationCodeOverride, lineSnOverride, featuresOverride);
+            log.info("[ProjectConfig] 加载成功: line={}, product={}, ip={}, station={}, lineSn={}, features={}",
+                    currentLineId, currentProductId, deviceIp, stationCodeOverride, lineSnOverride, featuresOverride);
         } catch (Exception e) {
             log.error("[ProjectConfig] 加载失败: {}", e.getMessage(), e);
         }
@@ -180,7 +234,8 @@ public class ProjectConfigManager {
                 Files.createDirectories(configPath.getParent());
             }
             JSONObject root = new JSONObject();
-            root.put("currentProjectId", currentProjectId);
+            root.put("currentLineId", currentLineId);
+            root.put("currentProductId", currentProductId);
             root.put("deviceIp", deviceIp);
             if (stationCodeOverride != null) root.put("stationCodeOverride", stationCodeOverride);
             if (lineSnOverride != null) root.put("lineSnOverride", lineSnOverride);
@@ -205,46 +260,69 @@ public class ProjectConfigManager {
         }
     }
 
-    // ========== 项目查询 ==========
+    // ========== 产线位置查询 ==========
+
+    public List<Line> getLines() {
+        return LINES;
+    }
+
+    public Line findLine(String id) {
+        if (id == null) return null;
+        for (Line l : LINES) {
+            if (l.getId().equalsIgnoreCase(id)) return l;
+        }
+        return null;
+    }
+
+    public Line getCurrentLine() {
+        Line l = findLine(currentLineId);
+        return l != null ? l : LINES.get(0);
+    }
 
-    public List<Project> getProjects() {
-        return PROJECTS;
+    public String getCurrentLineId() {
+        return currentLineId;
     }
 
-    public Project findProject(String id) {
+    // ========== 产品号查询 ==========
+
+    public List<Product> getProducts() {
+        return PRODUCTS;
+    }
+
+    public Product findProduct(String id) {
         if (id == null) return null;
-        for (Project p : PROJECTS) {
+        for (Product p : PRODUCTS) {
             if (p.getId().equalsIgnoreCase(id)) return p;
         }
         return null;
     }
 
-    public Project getCurrentProject() {
-        Project p = findProject(currentProjectId);
-        return p != null ? p : PROJECTS.get(0);
+    public Product getCurrentProduct() {
+        Product p = findProduct(currentProductId);
+        return p != null ? p : PRODUCTS.get(0);
     }
 
-    public String getCurrentProjectId() {
-        return currentProjectId;
+    public String getCurrentProductId() {
+        return currentProductId;
     }
 
     // ========== 前缀快捷方法 ==========
 
-    /** 工件码(框架码)前缀,若当前项目找不到则返回null */
+    /** 工件码(框架码)前缀,若当前产品找不到则返回null */
     public String getProductPrefix() {
-        Project p = getCurrentProject();
+        Product p = getCurrentProduct();
         return p != null ? p.getProductPrefix() : null;
     }
 
     /** 冷板码前缀 */
     public String getColdPlatePrefix() {
-        Project p = getCurrentProject();
+        Product p = getCurrentProduct();
         return p != null ? p.getColdPlatePrefix() : null;
     }
 
     /** 底护板码前缀 */
     public String getBottomPlatePrefix() {
-        Project p = getCurrentProject();
+        Product p = getCurrentProduct();
         return p != null ? p.getBottomPlatePrefix() : null;
     }
 
@@ -259,20 +337,36 @@ public class ProjectConfigManager {
         return null;
     }
 
-    // ========== 切换项目 ==========
+    // ========== 切换产线位置 ==========
 
-    public void switchProject(String projectId) {
-        Project p = findProject(projectId);
+    public void switchLine(String lineId) {
+        Line l = findLine(lineId);
+        if (l == null) {
+            log.warn("[ProjectConfig] 无效的产线位置id: {}", lineId);
+            return;
+        }
+        if (l.getId().equalsIgnoreCase(currentLineId)) return;
+        String old = currentLineId;
+        currentLineId = l.getId();
+        save();
+        log.info("[ProjectConfig] 切换产线位置: {} -> {}", old, currentLineId);
+        fireLineChanged(l);
+    }
+
+    // ========== 切换产品号 ==========
+
+    public void switchProduct(String productId) {
+        Product p = findProduct(productId);
         if (p == null) {
-            log.warn("[ProjectConfig] 无效的项目id: {}", projectId);
+            log.warn("[ProjectConfig] 无效的产品号id: {}", productId);
             return;
         }
-        if (p.getId().equalsIgnoreCase(currentProjectId)) return;
-        String old = currentProjectId;
-        currentProjectId = p.getId();
+        if (p.getId().equalsIgnoreCase(currentProductId)) return;
+        String old = currentProductId;
+        currentProductId = p.getId();
         save();
-        log.info("[ProjectConfig] 切换项目: {} -> {}", old, currentProjectId);
-        fireProjectChanged(p);
+        log.info("[ProjectConfig] 切换产品号: {} -> {}", old, currentProductId);
+        fireProductChanged(p);
     }
 
     // ========== 设备IP ==========
@@ -413,7 +507,8 @@ public class ProjectConfigManager {
     // ========== 事件监听(UI订阅) ==========
 
     public interface Listener {
-        default void onProjectChanged(Project project) {}
+        default void onLineChanged(Line line) {}
+        default void onProductChanged(Product product) {}
         default void onDeviceIpChanged(String newIp) {}
         default void onServerIpChanged(String newIp) {}
         default void onFeaturesChanged() {}
@@ -429,9 +524,15 @@ public class ProjectConfigManager {
         listeners.remove(l);
     }
 
-    private void fireProjectChanged(Project p) {
-        for (Listener l : new ArrayList<>(listeners)) {
-            try { l.onProjectChanged(p); } catch (Exception e) { log.warn("listener error", e); }
+    private void fireLineChanged(Line l) {
+        for (Listener listener : new ArrayList<>(listeners)) {
+            try { listener.onLineChanged(l); } catch (Exception e) { log.warn("listener error", e); }
+        }
+    }
+
+    private void fireProductChanged(Product p) {
+        for (Listener listener : new ArrayList<>(listeners)) {
+            try { listener.onProductChanged(p); } catch (Exception e) { log.warn("listener error", e); }
         }
     }
 

+ 3 - 3
src/com/mes/core/StationContext.java

@@ -155,9 +155,9 @@ public class StationContext {
      * 从配置创建上下文
      */
     public static StationContext fromConfig(StationConfig.StationInfo stationInfo, String lineSn) {
-        // 工位名:按当前项目从 OprnoRegistry 查;查不到 fallback 到工位号
-        String projectId = ProjectConfigManager.getInstance().getCurrentProjectId();
-        String name = OprnoRegistry.getGwDes(projectId, stationInfo.getCode());
+        // 工位名:按当前产线位置从 OprnoRegistry 查;查不到 fallback 到工位号
+        String lineId = ProjectConfigManager.getInstance().getCurrentLineId();
+        String name = OprnoRegistry.getGwDes(lineId, stationInfo.getCode());
         if (name == null || name.isEmpty()) {
             name = stationInfo.getCode();
         }

+ 115 - 53
src/com/mes/ui/MainFrame.java

@@ -80,7 +80,8 @@ public class MainFrame extends JFrame {
     private JMenuItem importLayoutItem;
     // 项目/设备子菜单(mes123解锁后显示)
     private JMenu projectDeviceMenu;
-    private JMenuItem switchProjectItem;
+    private JMenuItem switchProjectItem;   // 切换产线位置
+    private JMenuItem switchProductItem;   // 切换产品号
     private JMenuItem switchStationItem;
     private JMenuItem deviceIpItem;
     private JMenuItem serverIpItem;
@@ -130,11 +131,11 @@ public class MainFrame extends JFrame {
      */
     private void initFrame() {
         String codes = getStationCodesString();
-        // 标题优先用 OprnoRegistry 按当前项目推导的工位名称,其次 yaml 的 name
-        String projectId = com.mes.core.ProjectConfigManager.getInstance().getCurrentProjectId();
+        // 标题优先用 OprnoRegistry 按当前产线位置推导的工位名称,其次 yaml 的 name
+        String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId();
         String name = "";
         if (!config.getStations().isEmpty()) {
-            name = com.mes.core.OprnoRegistry.getGwDes(projectId, config.getStations().get(0).getCode());
+            name = com.mes.core.OprnoRegistry.getGwDes(lineId, config.getStations().get(0).getCode());
         }
         if ((name == null || name.isEmpty()) && config.getStationName() != null) {
             name = config.getStationName();
@@ -238,9 +239,15 @@ public class MainFrame extends JFrame {
         switchProjectItem = new JMenuItem();
         switchProjectItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
         switchProjectItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        switchProjectItem.addActionListener(e -> showSwitchProjectDialog());
+        switchProjectItem.addActionListener(e -> showSwitchLineDialog());
         projectDeviceMenu.add(switchProjectItem);
 
+        switchProductItem = new JMenuItem();
+        switchProductItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
+        switchProductItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        switchProductItem.addActionListener(e -> showSwitchProductDialog());
+        projectDeviceMenu.add(switchProductItem);
+
         switchStationItem = new JMenuItem();
         switchStationItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
         switchStationItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
@@ -426,10 +433,15 @@ public class MainFrame extends JFrame {
         toolBar.add(projectMenu);
         updateProjectDisplay();
 
-        // 订阅项目切换 → 刷新工具栏显示
+        // 订阅产线位置/产品号切换 → 刷新工具栏显示
         com.mes.core.ProjectConfigManager.getInstance().addListener(new com.mes.core.ProjectConfigManager.Listener() {
             @Override
-            public void onProjectChanged(com.mes.core.ProjectConfigManager.Project project) {
+            public void onLineChanged(com.mes.core.ProjectConfigManager.Line line) {
+                SwingUtilities.invokeLater(() -> updateProjectDisplay());
+            }
+
+            @Override
+            public void onProductChanged(com.mes.core.ProjectConfigManager.Product product) {
                 SwingUtilities.invokeLater(() -> updateProjectDisplay());
             }
         });
@@ -1318,37 +1330,38 @@ public class MainFrame extends JFrame {
         }
     }
 
-    // ========== 项目切换/设备IP修改 ==========
+    // ========== 产线位置/产品号切换/设备IP修改 ==========
 
     /**
-     * 刷新工具栏上的项目显示
+     * 刷新工具栏上的产线位置/产品号显示
      */
     private void updateProjectDisplay() {
         if (projectMenu == null) return;
         com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
-        com.mes.core.ProjectConfigManager.Project p = pc.getCurrentProject();
-        if (p != null) {
-            projectMenu.setText(p.getDisplay());
+        com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine();
+        com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct();
+        if (line != null && product != null) {
+            projectMenu.setText(line.getDisplay() + " / " + product.getDisplay());
             projectMenu.setToolTipText(String.format(
-                    "<html>当前项目:%s<br>工件码前缀:%s<br>冷板码前缀:%s<br>底护板码前缀:%s</html>",
-                    p.getDisplay(), p.getProductPrefix(), p.getColdPlatePrefix(), p.getBottomPlatePrefix()));
+                    "<html>产线位置:%s<br>产品号:%s<br>工件码前缀:%s<br>冷板码前缀:%s<br>底护板码前缀:%s</html>",
+                    line.getDisplay(), product.getDisplay(),
+                    product.getProductPrefix(), product.getColdPlatePrefix(), product.getBottomPlatePrefix()));
         }
     }
 
     /**
-     * 弹出切换项目对话框
+     * 弹出切换产线位置对话框(只影响工位号→工序名称的映射)
      */
-    private void showSwitchProjectDialog() {
+    private void showSwitchLineDialog() {
         com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
-        java.util.List<com.mes.core.ProjectConfigManager.Project> projects = pc.getProjects();
+        java.util.List<com.mes.core.ProjectConfigManager.Line> lines = pc.getLines();
 
-        String[] options = new String[projects.size()];
+        String[] options = new String[lines.size()];
         int currentIndex = 0;
-        for (int i = 0; i < projects.size(); i++) {
-            com.mes.core.ProjectConfigManager.Project p = projects.get(i);
-            options[i] = String.format("%s  (工件:%s 冷板:%s 底护板:%s)",
-                    p.getDisplay(), p.getProductPrefix(), p.getColdPlatePrefix(), p.getBottomPlatePrefix());
-            if (p.getId().equalsIgnoreCase(pc.getCurrentProjectId())) {
+        for (int i = 0; i < lines.size(); i++) {
+            com.mes.core.ProjectConfigManager.Line l = lines.get(i);
+            options[i] = l.getDisplay();
+            if (l.getId().equalsIgnoreCase(pc.getCurrentLineId())) {
                 currentIndex = i;
             }
         }
@@ -1358,37 +1371,31 @@ public class MainFrame extends JFrame {
         combo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
 
         JPanel panel = new JPanel(new BorderLayout(5, 5));
-        panel.add(new JLabel("选择项目:"), BorderLayout.NORTH);
+        panel.add(new JLabel("选择产线位置:"), BorderLayout.NORTH);
         panel.add(combo, BorderLayout.CENTER);
 
         int result = JOptionPane.showConfirmDialog(this, panel,
-                "切换项目", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+                "切换产线位置", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
         if (result != JOptionPane.OK_OPTION) return;
 
         int idx = combo.getSelectedIndex();
-        if (idx < 0 || idx >= projects.size()) return;
-        com.mes.core.ProjectConfigManager.Project target = projects.get(idx);
-        if (target.getId().equalsIgnoreCase(pc.getCurrentProjectId())) {
+        if (idx < 0 || idx >= lines.size()) return;
+        com.mes.core.ProjectConfigManager.Line target = lines.get(idx);
+        if (target.getId().equalsIgnoreCase(pc.getCurrentLineId())) {
             return;
         }
-        pc.switchProject(target.getId());
+        pc.switchLine(target.getId());
 
-        // 重置所有工位,避免沿用旧项目扫到一半的码
-        for (int i = 0; i < engines.size(); i++) {
-            engines.get(i).reset();
-            if (i < panels.size()) panels.get(i).reset();
-        }
-
-        // 切换项目后同步工位名(标题 + context 的 stationName)
-        String newProjectId = target.getId();
+        // 切换产线位置后同步工位名(标题 + context 的 stationName)
+        String newLineId = target.getId();
         for (StationContext ctx : contexts) {
-            String newName = com.mes.core.OprnoRegistry.getGwDes(newProjectId, ctx.getStationCode());
+            String newName = com.mes.core.OprnoRegistry.getGwDes(newLineId, ctx.getStationCode());
             if (newName == null || newName.isEmpty()) newName = ctx.getStationCode();
             ctx.updateStation(null, newName, null);
         }
         if (!config.getStations().isEmpty()) {
             String code = config.getStations().get(0).getCode();
-            String name = com.mes.core.OprnoRegistry.getGwDes(newProjectId, code);
+            String name = com.mes.core.OprnoRegistry.getGwDes(newLineId, code);
             String title = "MES系统客户端:" + code;
             if (name != null && !name.isEmpty() && !name.equals(code)) title += " - " + name;
             setTitle(title);
@@ -1397,6 +1404,58 @@ public class MainFrame extends JFrame {
         updateProjectDisplay();
 
         JOptionPane.showMessageDialog(this,
+                "已切换到:" + target.getDisplay() + "\n工位名称已刷新",
+                "切换成功", JOptionPane.INFORMATION_MESSAGE);
+    }
+
+    /**
+     * 弹出切换产品号对话框(只影响工件码/冷板码/底护板码前缀)
+     */
+    private void showSwitchProductDialog() {
+        com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
+        java.util.List<com.mes.core.ProjectConfigManager.Product> products = pc.getProducts();
+
+        String[] options = new String[products.size()];
+        int currentIndex = 0;
+        for (int i = 0; i < products.size(); i++) {
+            com.mes.core.ProjectConfigManager.Product p = products.get(i);
+            options[i] = String.format("%s  (工件:%s 冷板:%s 底护板:%s)",
+                    p.getDisplay(), p.getProductPrefix(), p.getColdPlatePrefix(), p.getBottomPlatePrefix());
+            if (p.getId().equalsIgnoreCase(pc.getCurrentProductId())) {
+                currentIndex = i;
+            }
+        }
+
+        JComboBox<String> combo = new JComboBox<>(options);
+        combo.setSelectedIndex(currentIndex);
+        combo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+
+        JPanel panel = new JPanel(new BorderLayout(5, 5));
+        panel.add(new JLabel("选择产品号:"), BorderLayout.NORTH);
+        panel.add(combo, BorderLayout.CENTER);
+
+        int result = JOptionPane.showConfirmDialog(this, panel,
+                "切换产品号", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+        if (result != JOptionPane.OK_OPTION) return;
+
+        int idx = combo.getSelectedIndex();
+        if (idx < 0 || idx >= products.size()) return;
+        com.mes.core.ProjectConfigManager.Product target = products.get(idx);
+        if (target.getId().equalsIgnoreCase(pc.getCurrentProductId())) {
+            return;
+        }
+        pc.switchProduct(target.getId());
+
+        // 重置所有工位,避免沿用旧产品号扫到一半的码
+        for (int i = 0; i < engines.size(); i++) {
+            engines.get(i).reset();
+            if (i < panels.size()) panels.get(i).reset();
+        }
+
+        refreshMenuTexts();
+        updateProjectDisplay();
+
+        JOptionPane.showMessageDialog(this,
                 "已切换到:" + target.getDisplay() + "\n前缀校验立即生效,所有工位已重置",
                 "切换成功", JOptionPane.INFORMATION_MESSAGE);
     }
@@ -1527,12 +1586,16 @@ public class MainFrame extends JFrame {
     private void refreshMenuTexts() {
         com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
         if (switchProjectItem != null) {
-            com.mes.core.ProjectConfigManager.Project p = pc.getCurrentProject();
-            switchProjectItem.setText("切换项目 (当前: " + (p != null ? p.getDisplay() : "?") + ")");
+            com.mes.core.ProjectConfigManager.Line line = pc.getCurrentLine();
+            switchProjectItem.setText("切换产线位置 (当前: " + (line != null ? line.getDisplay() : "?") + ")");
+        }
+        if (switchProductItem != null) {
+            com.mes.core.ProjectConfigManager.Product product = pc.getCurrentProduct();
+            switchProductItem.setText("切换产品号 (当前: " + (product != null ? product.getDisplay() : "?") + ")");
         }
         if (switchStationItem != null && !config.getStations().isEmpty()) {
             String code = config.getStations().get(0).getCode();
-            String des = com.mes.core.OprnoRegistry.getGwDes(pc.getCurrentProjectId(), code);
+            String des = com.mes.core.OprnoRegistry.getGwDes(pc.getCurrentLineId(), code);
             if (des == null || des.isEmpty()) des = "未知工位";
             switchStationItem.setText("修改工位号 (当前: " + code + " - " + des + ")");
         }
@@ -1546,12 +1609,11 @@ public class MainFrame extends JFrame {
 
     /**
      * 弹出修改工位号对话框
-     * 工位列表按当前项目(P1-P4)展示;工位名称由 OprnoRegistry 自动推导
+     * 工位列表按当前产线位置(P1-P4)展示;工位名称由 OprnoRegistry 自动推导
      */
     private void showSwitchStationDialog() {
         com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
-        String projectId = pc.getCurrentProjectId();
-        com.mes.core.ProjectConfigManager.Project project = pc.getCurrentProject();
+        String lineId = pc.getCurrentLineId();
         String currentCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode();
 
         // 拆分当前code为 "基础码" + "后缀"
@@ -1563,10 +1625,10 @@ public class MainFrame extends JFrame {
         JComboBox<String> codeCombo = new JComboBox<>();
         codeCombo.setEditable(false);
         codeCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
-        for (String c : com.mes.core.OprnoRegistry.listCodes(projectId)) {
+        for (String c : com.mes.core.OprnoRegistry.listCodes(lineId)) {
             codeCombo.addItem(c);
         }
-        if (com.mes.core.OprnoRegistry.listCodes(projectId).contains(currentBase)) {
+        if (com.mes.core.OprnoRegistry.listCodes(lineId).contains(currentBase)) {
             codeCombo.setSelectedItem(currentBase);
         }
 
@@ -1593,7 +1655,7 @@ public class MainFrame extends JFrame {
             String sfxReal = ("(无)".equals(sfx) || sfx == null) ? "" : sfx;
             String full = base + sfxReal;
             previewLabel.setText("最终工位号: " + (full.isEmpty() ? "-" : full));
-            String des = com.mes.core.OprnoRegistry.getGwDes(projectId, full);
+            String des = com.mes.core.OprnoRegistry.getGwDes(lineId, full);
             desLabel.setText("工位名称: " + (des.isEmpty() ? "(未知工位)" : des));
         };
         refresh.run();
@@ -1606,9 +1668,9 @@ public class MainFrame extends JFrame {
         gbc.anchor = java.awt.GridBagConstraints.WEST;
 
         gbc.gridx = 0; gbc.gridy = 0;
-        panel.add(new JLabel("当前项目:"), gbc);
+        panel.add(new JLabel("当前产线位置:"), gbc);
         gbc.gridx = 1; gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
-        JLabel projectLbl = new JLabel(project != null ? project.getDisplay() : projectId);
+        JLabel projectLbl = new JLabel(lineId);
         projectLbl.setFont(new Font("微软雅黑", Font.PLAIN, 16));
         panel.add(projectLbl, gbc);
 
@@ -1644,7 +1706,7 @@ public class MainFrame extends JFrame {
         pc.setStationOverride(newCode, null);
         applyStationChange(newCode);
 
-        String des = com.mes.core.OprnoRegistry.getGwDes(projectId, newCode);
+        String des = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode);
         JOptionPane.showMessageDialog(this,
                 "工位已更新为:" + newCode + (des.isEmpty() ? "" : (" - " + des)) + "\n已刷新界面并向MES重新同步",
                 "修改成功", JOptionPane.INFORMATION_MESSAGE);
@@ -1655,7 +1717,7 @@ public class MainFrame extends JFrame {
      */
     private void applyStationChange(String newCode) {
         String oldCode = config.getStations().isEmpty() ? "" : config.getStations().get(0).getCode();
-        String projectId = com.mes.core.ProjectConfigManager.getInstance().getCurrentProjectId();
+        String lineId = com.mes.core.ProjectConfigManager.getInstance().getCurrentLineId();
 
         // 1. 更新 config
         if (!config.getStations().isEmpty()) {
@@ -1663,7 +1725,7 @@ public class MainFrame extends JFrame {
         }
 
         // 2. 更新 context(推导工位名)
-        String name = com.mes.core.OprnoRegistry.getGwDes(projectId, newCode);
+        String name = com.mes.core.OprnoRegistry.getGwDes(lineId, newCode);
         if (name == null || name.isEmpty()) name = newCode;
         for (StationContext ctx : contexts) {
             if (ctx.getStationIndex() == 0) {

+ 1 - 1
src/resources/config/station.yaml

@@ -9,7 +9,7 @@ station:
   line_sn: XT
 
 server:
-  ip: 192.168.112.99
+  ip: 192.168.113.99
   tcp_port: 3000
   http_port: 8980
   heart_beat_cycle: 60