package com.mes.ui.component; import com.mes.core.StationContext; import com.mes.core.StationConfig; import com.mes.ui.layout.DraggableComponentWrapper; import com.mes.ui.layout.UILayoutConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.swing.filechooser.FileNameExtensionFilter; /** * 工位面板组件 - 与OP150版本UI保持一致 */ public class WorkstationPanel extends JPanel { private static final Logger log = LoggerFactory.getLogger(WorkstationPanel.class); // 工位信息 private String stationCode; private String stationName; private final int stationIndex; // 配置 private boolean showMaterialInput = false; private boolean showDeviceInfo = false; private String materialLabel = "底护板码"; private boolean showHeader = true; // 是否显示工位号和状态栏(单工位时不显示) // UI组件 - 工件码 private JTextField productSnInput; private JButton scanProductBtn; // UI组件 - 物料码 private JTextField materialSnInput; private JButton scanMaterialBtn; private JButton unbindMaterialBtn; // UI组件 - 状态显示 private JButton statusMenu; private JButton externalStatusMenu; // 外部状态组件引用(单工位模式使用) private JLabel stationLabel; // UI组件 - OK/NG按钮 private JButton okButton; private JButton ngButton; private Image okIconImage; private Image ngIconImage; // UI组件 - 设备信息 private JLabel presetCountLabel; private JTextField presetCountField; private JLabel finishedCountLabel; private JTextField finishedCountField; private JLabel deviceInfoLabel; // 第一行可选前置标签 // UI组件 - 设备信息第二行 private JLabel presetCountLabel2; private JTextField presetCountField2; private JLabel finishedCountLabel2; private JTextField finishedCountField2; private JLabel deviceInfoLabel2; // 第二行可选前置标签 private boolean showDeviceInfo2 = false; // UI组件 - 物料标签 private JLabel materialLabelUI; // 回调 private WorkstationPanelListener listener; // 基准尺寸(用于计算缩放比例) private static final int BASE_WIDTH = 800; private static final int BASE_HEIGHT = 600; private double currentScale = 1.0; // 默认布局配置(相对坐标 0-1) // 单工位模式(showHeader=false)的默认布局 private static final double[][] DEFAULT_LAYOUT_SINGLE = { // {x, y, width, height} - 相对于内容区域 {0.005, 0.10, 0.75, 0.20}, // product_sn_input: 工件码输入框 {0.76, 0.10, 0.23, 0.20}, // scan_product_btn: 扫码按钮 {0.15, 0.55, 0.30, 0.25}, // ok_button: OK按钮 {0.55, 0.55, 0.30, 0.25}, // ng_button: NG按钮 }; // 组件ID顺序(与DEFAULT_LAYOUT_SINGLE对应) private static final String[] COMPONENT_IDS = { "product_sn_input", "scan_product_btn", "ok_button", "ng_button" }; // 编辑模式相关 private boolean editMode = false; private UILayoutConfig uiLayoutConfig; private Map componentWrappers = new HashMap<>(); private JPanel gridOverlay; // 网格叠加层 private Dimension capturedPanelSize; // 捕获时的面板大小 private ComponentAdapter resizeListener; // 窗口大小变化监听器 /** * 面板事件监听器 */ public interface WorkstationPanelListener { void onProductScanned(WorkstationPanel panel, String productSn); void onMaterialScanned(WorkstationPanel panel, String materialSn); void onOkClicked(WorkstationPanel panel); void onNgClicked(WorkstationPanel panel); void onUnbindClicked(WorkstationPanel panel); } public WorkstationPanel(String stationCode, int stationIndex, StationConfig config) { this(stationCode, stationIndex, config, true); } public WorkstationPanel(String stationCode, int stationIndex, StationConfig config, boolean showHeader) { this.stationCode = stationCode; this.stationName = stationCode; this.stationIndex = stationIndex; this.showHeader = showHeader; // 从配置读取显示选项 if (config != null) { this.showMaterialInput = config.isEffectiveShowMaterialInput(); this.showDeviceInfo = config.isShowDeviceInfo(); this.showDeviceInfo2 = config.isShowDeviceInfo2(); String effLabel = config.getEffectiveMaterialLabel(); if (effLabel != null && !effLabel.isEmpty()) { this.materialLabel = effLabel; } } initUI(); } /** * 从配置创建工位面板 * @param stationInfo 工位信息 * @param config 全局配置 * @return 工位面板实例 */ public static WorkstationPanel fromConfig(StationConfig.StationInfo stationInfo, StationConfig config) { return new WorkstationPanel( stationInfo.getCode(), stationInfo.getIndex(), config ); } /** * 初始化UI - 使用绝对定位 + 相对坐标,支持窗口缩放 */ private void initUI() { // 先确保所有组件已创建(统一入口) ensureComponentsCreated(); // 使用绝对定位 setLayout(null); // 设置边框 if (showHeader) { setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(10, 10, 10, 10) )); } else { setBorder(BorderFactory.createEmptyBorder(40, 10, 10, 10)); } // 加载布局配置 loadLayoutConfig(); // 添加组件到面板(不使用嵌套容器) add(productSnInput); add(scanProductBtn); add(okButton); add(ngButton); // 可选组件 if (showHeader) { add(stationLabel); add(statusMenu); } if (showMaterialInput) { add(materialLabelUI); add(materialSnInput); add(scanMaterialBtn); add(unbindMaterialBtn); } if (showDeviceInfo) { if (deviceInfoLabel != null) { add(deviceInfoLabel); } add(presetCountLabel); add(presetCountField); add(finishedCountLabel); add(finishedCountField); } if (showDeviceInfo2) { if (deviceInfoLabel2 != null) { add(deviceInfoLabel2); } add(presetCountLabel2); add(presetCountField2); add(finishedCountLabel2); add(finishedCountField2); } // 添加窗口大小变化监听器,实现按比例缩放 resizeListener = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (!editMode) { applyRuntimeLayout(); } } }; addComponentListener(resizeListener); // 确保面板显示后再应用布局 addHierarchyListener(e -> { if ((e.getChangeFlags() & java.awt.event.HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() && !editMode) { SwingUtilities.invokeLater(this::applyRuntimeLayout); } }); // 初始应用布局(如果面板已有尺寸) if (getWidth() > 0 && getHeight() > 0) { applyRuntimeLayout(); } } /** * 应用运行模式布局(相对坐标转绝对坐标) */ private void applyRuntimeLayout() { int panelWidth = getWidth(); int panelHeight = getHeight(); // 首次有效调用时保存面板尺寸 if (capturedPanelSize == null && panelWidth > 100 && panelHeight > 100) { capturedPanelSize = new Dimension(panelWidth, panelHeight); setPreferredSize(capturedPanelSize); setMinimumSize(capturedPanelSize); log.info("[{}] 首次保存面板尺寸: {}x{}", stationCode, panelWidth, panelHeight); } Insets insets = getInsets(); int contentWidth = panelWidth - insets.left - insets.right; int contentHeight = panelHeight - insets.top - insets.bottom; if (contentWidth <= 0 || contentHeight <= 0) { return; } log.debug("[{}] applyRuntimeLayout: 面板={}x{}, 内容区域={}x{}", stationCode, panelWidth, panelHeight, contentWidth, contentHeight); // 根据布局模式选择布局方式 if (uiLayoutConfig != null && uiLayoutConfig.isFlexMode()) { applyFlexLayout(insets, contentWidth, contentHeight); } else { applyFreeLayout(insets, contentWidth, contentHeight); } revalidate(); repaint(); } /** * 应用自由定位布局(Free模式) */ private void applyFreeLayout(Insets insets, int contentWidth, int contentHeight) { // 应用每个组件的布局 applyComponentLayout("product_sn_input", productSnInput, insets, contentWidth, contentHeight); applyComponentLayout("scan_product_btn", scanProductBtn, insets, contentWidth, contentHeight); applyComponentLayout("ok_button", okButton, insets, contentWidth, contentHeight); applyComponentLayout("ng_button", ngButton, insets, contentWidth, contentHeight); // 可选组件 if (showHeader && stationLabel != null) { applyComponentLayout("station_label", stationLabel, insets, contentWidth, contentHeight); } if (showHeader && statusMenu != null) { applyComponentLayout("status_menu", statusMenu, insets, contentWidth, contentHeight); } if (showMaterialInput && materialLabelUI != null) { applyComponentLayout("material_label", materialLabelUI, insets, contentWidth, contentHeight); } if (showMaterialInput && materialSnInput != null) { applyComponentLayout("material_sn_input", materialSnInput, insets, contentWidth, contentHeight); } if (showMaterialInput && scanMaterialBtn != null) { applyComponentLayout("scan_material_btn", scanMaterialBtn, insets, contentWidth, contentHeight); } if (showMaterialInput && unbindMaterialBtn != null) { applyComponentLayout("unbind_material_btn", unbindMaterialBtn, insets, contentWidth, contentHeight); } if (showDeviceInfo && deviceInfoLabel != null) { applyComponentLayout("device_info_label", deviceInfoLabel, insets, contentWidth, contentHeight); } if (showDeviceInfo && presetCountLabel != null) { applyComponentLayout("preset_count_label", presetCountLabel, insets, contentWidth, contentHeight); } if (showDeviceInfo && presetCountField != null) { applyComponentLayout("preset_count_field", presetCountField, insets, contentWidth, contentHeight); } if (showDeviceInfo && finishedCountLabel != null) { applyComponentLayout("finished_count_label", finishedCountLabel, insets, contentWidth, contentHeight); } if (showDeviceInfo && finishedCountField != null) { applyComponentLayout("finished_count_field", finishedCountField, insets, contentWidth, contentHeight); } if (showDeviceInfo2 && deviceInfoLabel2 != null) { applyComponentLayout("device_info_label2", deviceInfoLabel2, insets, contentWidth, contentHeight); } if (showDeviceInfo2 && presetCountLabel2 != null) { applyComponentLayout("preset_count_label2", presetCountLabel2, insets, contentWidth, contentHeight); } if (showDeviceInfo2 && presetCountField2 != null) { applyComponentLayout("preset_count_field2", presetCountField2, insets, contentWidth, contentHeight); } if (showDeviceInfo2 && finishedCountLabel2 != null) { applyComponentLayout("finished_count_label2", finishedCountLabel2, insets, contentWidth, contentHeight); } if (showDeviceInfo2 && finishedCountField2 != null) { applyComponentLayout("finished_count_field2", finishedCountField2, insets, contentWidth, contentHeight); } } /** * 应用弹性行布局(Flex模式) * 行与行之间按 flex 比例分配高度 * 隐藏某行时,其他行自动向中间靠拢填补空间 * 组件大小保持不变,只调整 Y 坐标 */ private void applyFlexLayout(Insets insets, int contentWidth, int contentHeight) { boolean isSingleMode = !showHeader; List visibleRows = uiLayoutConfig.getVisibleRowsForMode(isSingleMode); if (visibleRows.isEmpty()) { log.warn("[{}] Flex布局没有可见行,回退到Free布局", stationCode); applyFreeLayout(insets, contentWidth, contentHeight); return; } // 先计算每行内组件的实际高度(从配置读取) Map rowHeights = new HashMap<>(); for (UILayoutConfig.RowLayout row : visibleRows) { int maxHeight = 0; for (UILayoutConfig.RowComponentLayout compLayout : row.getVisibleComponents()) { String compId = compLayout.getId(); UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(compId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(compId); // 第二行组件回退到第一行组件布局 if (layout == null && compId.endsWith("2")) { String fallbackId = compId.substring(0, compId.length() - 1); layout = uiLayoutConfig.getComponentForMode(fallbackId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(fallbackId); } if (layout != null) { int h = (int)(layout.getSize().getHeight() * contentHeight); maxHeight = Math.max(maxHeight, h); } } rowHeights.put(row.getId(), Math.max(maxHeight, 50)); } // 计算总高度和剩余空间 int totalRowHeight = 0; for (Integer h : rowHeights.values()) { totalRowHeight += h; } // 剩余空间均匀分配给行间距 int remainingSpace = contentHeight - totalRowHeight; int gapCount = visibleRows.size() + 1; // 上边距 + 行间距 + 下边距 int gap = Math.max(remainingSpace / gapCount, 10); // 按顺序布局每行,均匀分布 int currentY = insets.top + gap; for (UILayoutConfig.RowLayout row : visibleRows) { int rowHeight = rowHeights.get(row.getId()); // 布局该行内的组件 applyRowLayoutFromComponents(row, insets, contentWidth, currentY, rowHeight, isSingleMode); currentY += rowHeight + gap; } log.debug("[{}] Flex布局完成: {}行, 间距={}", stationCode, visibleRows.size(), gap); } /** * 布局单行内的组件 * X 和宽度从 singleComponents/dualComponents 读取(保持编辑模式效果) * Y 坐标使用传入的行起始位置 */ private void applyRowLayoutFromComponents(UILayoutConfig.RowLayout row, Insets insets, int contentWidth, int rowStartY, int rowHeight, boolean isSingleMode) { List rowComponents = row.getVisibleComponents(); if (rowComponents.isEmpty()) return; int panelContentHeight = getHeight() - insets.top - insets.bottom; for (UILayoutConfig.RowComponentLayout compLayout : rowComponents) { String componentId = compLayout.getId(); JComponent component = getComponentById(componentId); if (component == null) continue; // 从 singleComponents/dualComponents 读取组件配置 UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(componentId, isSingleMode); if (layout == null) { layout = uiLayoutConfig.getComponent(componentId); } // 第二行组件没有独立布局时,回退到对应的第一行组件布局 if (layout == null && componentId.endsWith("2")) { String fallbackId = componentId.substring(0, componentId.length() - 1); layout = uiLayoutConfig.getComponentForMode(fallbackId, isSingleMode); if (layout == null) { layout = uiLayoutConfig.getComponent(fallbackId); } } int compX, compWidth, compHeight; if (layout != null) { // 从配置读取 X 和宽度(相对坐标转绝对坐标) compX = insets.left + (int)(layout.getPosition().getX() * contentWidth); compWidth = (int)(layout.getSize().getWidth() * contentWidth); compHeight = (int)(layout.getSize().getHeight() * panelContentHeight); } else { // 使用默认布局 double[] defaultLayout = getDefaultLayout(componentId); compX = insets.left + (int)(defaultLayout[0] * contentWidth); compWidth = (int)(defaultLayout[2] * contentWidth); compHeight = (int)(defaultLayout[3] * panelContentHeight); } // 确保最小尺寸 compWidth = Math.max(compWidth, 50); compHeight = Math.max(compHeight, 30); // Y 坐标:在行内垂直居中 int compY = rowStartY + (rowHeight - compHeight) / 2; component.setBounds(compX, compY, compWidth, compHeight); component.setVisible(true); } } /** * 根据ID获取组件 */ private JComponent getComponentById(String id) { switch (id) { case "product_sn_input": return productSnInput; case "scan_product_btn": return scanProductBtn; case "ok_button": return okButton; case "ng_button": return ngButton; case "station_label": return stationLabel; case "status_menu": return statusMenu; case "material_label": return materialLabelUI; case "material_sn_input": return materialSnInput; case "scan_material_btn": return scanMaterialBtn; case "unbind_material_btn": return unbindMaterialBtn; case "preset_count_label": return presetCountLabel; case "preset_count_field": return presetCountField; case "finished_count_label": return finishedCountLabel; case "finished_count_field": return finishedCountField; case "device_info_label": return deviceInfoLabel; case "device_info_label2": return deviceInfoLabel2; case "preset_count_label2": return presetCountLabel2; case "preset_count_field2": return presetCountField2; case "finished_count_label2": return finishedCountLabel2; case "finished_count_field2": return finishedCountField2; default: return null; } } /** * 设置行可见性(运行时动态调整) * @param rowId 行ID * @param visible 是否可见 */ public void setRowVisible(String rowId, boolean visible) { if (uiLayoutConfig == null || !uiLayoutConfig.isFlexMode()) return; boolean isSingleMode = !showHeader; uiLayoutConfig.setRowVisibleForMode(rowId, visible, isSingleMode); // 重新应用布局 if (getWidth() > 0 && getHeight() > 0) { applyRuntimeLayout(); } } /** * 应用单个组件的布局 */ private void applyComponentLayout(String componentId, JComponent component, Insets insets, int contentWidth, int contentHeight) { if (component == null) return; // 从配置获取相对坐标 double relX, relY, relW, relH; if (uiLayoutConfig != null) { // 根据模式获取组件配置 (showHeader=false 是单工位模式) boolean isSingleMode = !showHeader; UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(componentId, isSingleMode); if (layout == null) { // 回退到通用配置 layout = uiLayoutConfig.getComponent(componentId); } if (layout != null) { relX = layout.getPosition().getX(); relY = layout.getPosition().getY(); relW = layout.getSize().getWidth(); relH = layout.getSize().getHeight(); } else { // 使用默认布局 double[] defaultLayout = getDefaultLayout(componentId); relX = defaultLayout[0]; relY = defaultLayout[1]; relW = defaultLayout[2]; relH = defaultLayout[3]; } } else { // 使用默认布局 double[] defaultLayout = getDefaultLayout(componentId); relX = defaultLayout[0]; relY = defaultLayout[1]; relW = defaultLayout[2]; relH = defaultLayout[3]; } // 相对坐标转绝对坐标 int x = insets.left + (int)(relX * contentWidth); int y = insets.top + (int)(relY * contentHeight); int width = (int)(relW * contentWidth); int height = (int)(relH * contentHeight); // 确保最小尺寸 width = Math.max(width, 50); height = Math.max(height, 30); component.setBounds(x, y, width, height); } /** * 获取组件的默认布局 */ private double[] getDefaultLayout(String componentId) { switch (componentId) { case "product_sn_input": return new double[]{0.005, 0.10, 0.75, 0.18}; case "scan_product_btn": return new double[]{0.76, 0.10, 0.23, 0.18}; case "ok_button": return new double[]{0.15, 0.55, 0.30, 0.22}; case "ng_button": return new double[]{0.55, 0.55, 0.30, 0.22}; case "station_label": return new double[]{0.01, 0.02, 0.15, 0.08}; case "status_menu": return new double[]{0.20, 0.02, 0.78, 0.08}; case "material_label": return new double[]{0.005, 0.32, 0.10, 0.08}; case "material_sn_input": return new double[]{0.12, 0.32, 0.50, 0.12}; case "scan_material_btn": return new double[]{0.63, 0.32, 0.17, 0.12}; case "unbind_material_btn": return new double[]{0.81, 0.32, 0.17, 0.12}; case "preset_count_label": return new double[]{0.10, 0.75, 0.35, 0.08}; case "preset_count_field": return new double[]{0.10, 0.83, 0.35, 0.10}; case "finished_count_label": return new double[]{0.55, 0.75, 0.35, 0.08}; case "finished_count_field": return new double[]{0.55, 0.83, 0.35, 0.10}; default: return new double[]{0.0, 0.0, 0.1, 0.1}; } } /** * 根据面板大小更新缩放比例 */ private void updateScale() { int width = getWidth(); int height = getHeight(); if (width <= 0 || height <= 0) return; // 计算缩放比例(取宽高中较小的缩放比) double scaleX = (double) width / BASE_WIDTH; double scaleY = (double) height / BASE_HEIGHT; double newScale = Math.min(scaleX, scaleY); // 限制缩放范围 newScale = Math.max(0.8, Math.min(2.0, newScale)); if (Math.abs(newScale - currentScale) > 0.05) { currentScale = newScale; applyScale(); } } /** * 应用缩放到所有组件 */ private void applyScale() { // 计算缩放后的字体大小和尺寸,设置较小的最小值 int productFontSize = Math.max(14, (int) (28 * currentScale)); int normalFontSize = Math.max(12, (int) (20 * currentScale)); int buttonFontSize = Math.max(10, (int) (32 * currentScale)); int productHeight = (int) (70 * currentScale); int materialHeight = (int) (50 * currentScale); int deviceHeight = (int) (40 * currentScale); int buttonHeight = (int) (80 * currentScale); int statusHeight = Math.max(30, (int) (40 * currentScale)); // 更新工位标签和状态栏(双工位时) if (showHeader && stationLabel != null) { stationLabel.setFont(new Font("微软雅黑", Font.PLAIN, normalFontSize)); } if (showHeader && statusMenu != null) { statusMenu.setFont(new Font("微软雅黑", Font.PLAIN, normalFontSize)); statusMenu.setPreferredSize(new Dimension((int)(400 * currentScale), statusHeight)); } // 更新工件码输入框 productSnInput.setFont(new Font("微软雅黑", Font.PLAIN, productFontSize)); productSnInput.setPreferredSize(new Dimension((int)(602 * currentScale), productHeight)); // 更新扫码按钮 scanProductBtn.setFont(new Font("微软雅黑", Font.PLAIN, buttonFontSize)); scanProductBtn.setPreferredSize(new Dimension((int)(198 * currentScale), productHeight)); // 更新物料码区域 - 只设置高度,宽度由GridBagLayout控制 if (materialSnInput != null) { materialSnInput.setFont(new Font("微软雅黑", Font.PLAIN, normalFontSize)); // 不设置固定宽度,让weightx控制 } if (scanMaterialBtn != null) { scanMaterialBtn.setPreferredSize(new Dimension((int)(50 * currentScale), materialHeight)); } if (unbindMaterialBtn != null) { unbindMaterialBtn.setPreferredSize(new Dimension((int)(50 * currentScale), materialHeight)); } // 更新设备信息 if (presetCountField != null) { presetCountField.setFont(new Font("微软雅黑", Font.PLAIN, normalFontSize)); presetCountField.setPreferredSize(new Dimension((int)(150 * currentScale), deviceHeight)); } if (finishedCountField != null) { finishedCountField.setFont(new Font("微软雅黑", Font.PLAIN, normalFontSize)); finishedCountField.setPreferredSize(new Dimension((int)(150 * currentScale), deviceHeight)); } // 更新OK/NG按钮 - 图标大小根据缩放比例调整 int iconSize = Math.max(16, (int)(32 * currentScale)); okButton.setIcon(new ImageIcon(okIconImage.getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH))); okButton.setFont(new Font("微软雅黑", Font.PLAIN, buttonFontSize)); ngButton.setIcon(new ImageIcon(ngIconImage.getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH))); ngButton.setFont(new Font("微软雅黑", Font.PLAIN, buttonFontSize)); // 更新按钮面板大小 Container buttonPanel = okButton.getParent(); if (buttonPanel != null) { buttonPanel.setPreferredSize(new Dimension((int)(600 * currentScale), buttonHeight)); } // 刷新布局 revalidate(); repaint(); } // ========== 事件处理 ========== /** * 弹出工件码扫码对话框(供外部调用) */ public void requestScan() { javax.swing.SwingUtilities.invokeLater(this::onScanProductClick); } /** * 弹出物料码扫码对话框(供外部调用) */ public void requestMaterialScan() { javax.swing.SwingUtilities.invokeLater(this::onScanMaterialClick); } private void onScanProductClick() { // 防重入保护:如果对话框已经在显示,直接返回 if (isScanDialogShowing) { log.warn("[{}] 扫码对话框已在显示中,忽略重复调用", stationCode); return; } try { isScanDialogShowing = true; log.info("[{}] 点击扫码按钮", stationCode); String sn = JOptionPane.showInputDialog(null, "请扫描工件二维码", "扫描工件码", JOptionPane.PLAIN_MESSAGE); if (sn != null && !sn.trim().isEmpty()) { productSnInput.setText(sn.trim()); if (listener != null) { listener.onProductScanned(this, sn.trim()); } } } finally { // 确保标志位被重置 isScanDialogShowing = false; } } private void onScanMaterialClick() { // 防重入保护:如果对话框已经在显示,直接返回 if (isMaterialScanDialogShowing) { log.warn("[{}] {}扫码对话框已在显示中,忽略重复调用", stationCode, materialLabel); return; } try { isMaterialScanDialogShowing = true; log.info("[{}] 点击{}扫码按钮", stationCode, materialLabel); String sn = JOptionPane.showInputDialog(null, "请扫描" + materialLabel + "二维码", "扫描" + materialLabel, JOptionPane.PLAIN_MESSAGE); if (sn != null && !sn.trim().isEmpty()) { materialSnInput.setText(sn.trim()); if (listener != null) { listener.onMaterialScanned(this, sn.trim()); } } } finally { // 确保标志位被重置 isMaterialScanDialogShowing = false; } } private void onOkClick() { log.info("[{}] 点击OK", stationCode); if (listener != null) { listener.onOkClicked(this); } } private void onNgClick() { log.info("[{}] 点击NG", stationCode); if (listener != null) { listener.onNgClicked(this); } } private void onUnbindClick() { log.info("[{}] 点击解绑", stationCode); if (listener != null) { listener.onUnbindClicked(this); } } // ========== 公共方法 ========== /** * 更新状态显示 */ public void updateStatus(String message, int level) { if (statusMenu == null) return; // 单工位时statusMenu可能为null statusMenu.setText(message); switch (level) { case 0: // 正常 statusMenu.setForeground(Color.GREEN); break; case 1: // 警告 statusMenu.setForeground(Color.GREEN); break; case -1: // 错误 statusMenu.setForeground(Color.RED); break; default: statusMenu.setForeground(Color.GREEN); } } /** * 获取状态菜单(供外部更新状态) */ public JButton getStatusMenu() { return statusMenu; } /** * 设置外部状态组件(单工位时使用工具栏状态) */ public void setExternalStatusMenu(JButton externalStatus) { this.statusMenu = externalStatus; this.externalStatusMenu = externalStatus; // 保存外部引用,编辑模式切换后恢复 } /** * 从上下文刷新UI */ public void refresh(StationContext context) { if (context == null) return; // 更新工件码 productSnInput.setText(context.getProductSn() != null ? context.getProductSn() : ""); // 更新物料码 if (materialSnInput != null) { materialSnInput.setText(context.getMaterialSn() != null ? context.getMaterialSn() : ""); } // 更新设备信息 if (showDeviceInfo && presetCountField != null) { presetCountField.setText(String.valueOf(context.getPresetCount())); finishedCountField.setText(String.valueOf(context.getFinishedCount())); } if (showDeviceInfo2 && presetCountField2 != null) { // 检查第二行设备连接状态 if (context.getPresetCount2() < 0 || context.getFinishedCount2() < 0) { // 设备未连接,显示"--" presetCountField2.setText("--"); finishedCountField2.setText("--"); } else { // 设备已连接,显示数值 presetCountField2.setText(String.valueOf(context.getPresetCount2())); finishedCountField2.setText(String.valueOf(context.getFinishedCount2())); } } // 更新按钮状态 // 手动模式:等待用户操作时启用OK/NG按钮 // 自动模式:始终禁用,由流程自动提交 boolean canSubmit; if (StationConfig.getInstance().isAutoSubmitMode()) { // 自动模式:始终禁用 canSubmit = false; } else if (context.isWaitingForUserAction()) { // 手动模式:等待用户点击OK/NG canSubmit = context.isQualityPassed() && !context.isResultUploaded(); } else { canSubmit = false; } // 手动提交菜单启用标志:无视模式强制亮起 OK/NG Boolean forceSubmit = context.getExtra("forceSubmitEnabled", Boolean.FALSE); if (Boolean.TRUE.equals(forceSubmit) && !context.isResultUploaded()) { canSubmit = true; } okButton.setEnabled(canSubmit); ngButton.setEnabled(canSubmit); // 更新物料扫码状态 if (scanMaterialBtn != null) { boolean canScanMaterial = context.isQualityPassed() && !context.isMaterialValidated(); scanMaterialBtn.setEnabled(canScanMaterial); if (unbindMaterialBtn != null) { unbindMaterialBtn.setEnabled(context.isMaterialBound()); } } // 更新状态消息 updateStatus(context.getStatusMessage(), context.getStatusLevel()); } /** * 重置面板 */ public void reset() { productSnInput.setText(""); if (materialSnInput != null) { materialSnInput.setText(""); } if (scanMaterialBtn != null) { scanMaterialBtn.setEnabled(false); } if (unbindMaterialBtn != null) { unbindMaterialBtn.setEnabled(false); } okButton.setEnabled(false); ngButton.setEnabled(false); if (presetCountField != null) { presetCountField.setText("0"); } if (finishedCountField != null) { finishedCountField.setText("0"); } if (presetCountField2 != null) { presetCountField2.setText("0"); } if (finishedCountField2 != null) { finishedCountField2.setText("0"); } updateStatus("等待加工信号", 0); } // ========== Getters & Setters ========== public String getStationCode() { return stationCode; } /** * 运行时更新工位号(修改工位号菜单使用) * 会同步刷新 stationLabel 显示 */ public void updateStationCode(String newCode) { if (newCode == null || newCode.equals(this.stationCode)) return; this.stationCode = newCode; this.stationName = newCode; if (stationLabel != null) { stationLabel.setText(newCode); } } /** * 运行时更新物料码标签(冷板码 ↔ 底护板码切换时调用) */ public void updateMaterialLabel(String newLabel) { if (newLabel == null || newLabel.isEmpty()) return; this.materialLabel = newLabel; if (materialLabelUI != null) { materialLabelUI.setText(newLabel + ":"); } } /** * 重新从 StationConfig 读取 showDeviceInfo/showMaterialInput/label 等显示选项 * 用于"设备信息显示""工艺流程配置"菜单改动后的热刷新 */ public void refreshDisplayFromConfig(StationConfig cfg) { if (cfg == null) return; this.showDeviceInfo = cfg.isShowDeviceInfo(); this.showDeviceInfo2 = cfg.isShowDeviceInfo2(); this.showMaterialInput = cfg.isEffectiveShowMaterialInput(); String effLabel = cfg.getEffectiveMaterialLabel(); if (effLabel != null && !effLabel.isEmpty()) { this.materialLabel = effLabel; } // 物料码相关组件:按 showMaterialInput 直接控制可见性 if (materialLabelUI != null) materialLabelUI.setVisible(showMaterialInput); if (materialSnInput != null) materialSnInput.setVisible(showMaterialInput); if (scanMaterialBtn != null) scanMaterialBtn.setVisible(showMaterialInput); if (unbindMaterialBtn != null) unbindMaterialBtn.setVisible(showMaterialInput); if (showMaterialInput && materialLabelUI != null) { materialLabelUI.setText(this.materialLabel + ":"); } // 设备信息第1行 StationConfig.DeviceInfoRow row0 = cfg.getDeviceInfoRow(0); if (deviceInfoLabel != null) { if (row0 != null && row0.hasLabel()) { deviceInfoLabel.setText(row0.getLabel()); deviceInfoLabel.setVisible(showDeviceInfo); } else { deviceInfoLabel.setVisible(false); } } if (presetCountLabel != null) presetCountLabel.setVisible(showDeviceInfo); if (presetCountField != null) presetCountField.setVisible(showDeviceInfo); if (finishedCountLabel != null) finishedCountLabel.setVisible(showDeviceInfo); if (finishedCountField != null) finishedCountField.setVisible(showDeviceInfo); // 设备信息第2行 StationConfig.DeviceInfoRow row1 = cfg.getDeviceInfoRow(1); if (deviceInfoLabel2 != null) { if (row1 != null && row1.hasLabel()) { deviceInfoLabel2.setText(row1.getLabel()); deviceInfoLabel2.setVisible(showDeviceInfo2); } else { deviceInfoLabel2.setVisible(false); } } if (presetCountLabel2 != null) presetCountLabel2.setVisible(showDeviceInfo2); if (presetCountField2 != null) presetCountField2.setVisible(showDeviceInfo2); if (finishedCountLabel2 != null) finishedCountLabel2.setVisible(showDeviceInfo2); if (finishedCountField2 != null) finishedCountField2.setVisible(showDeviceInfo2); revalidate(); repaint(); } public String getStationName() { return stationName; } public int getStationIndex() { return stationIndex; } public String getProductSn() { return productSnInput.getText().trim(); } public void setProductSn(String sn) { productSnInput.setText(sn); } public String getMaterialSn() { return materialSnInput != null ? materialSnInput.getText().trim() : null; } public void setMaterialSn(String sn) { if (materialSnInput != null) { materialSnInput.setText(sn); } } public WorkstationPanelListener getListener() { return listener; } public void setListener(WorkstationPanelListener listener) { this.listener = listener; } public void setOkButtonEnabled(boolean enabled) { okButton.setEnabled(enabled); } public void setNgButtonEnabled(boolean enabled) { ngButton.setEnabled(enabled); } public void setMaterialInputEnabled(boolean enabled) { if (scanMaterialBtn != null) { scanMaterialBtn.setEnabled(enabled); } } // ========== 编辑模式相关方法 ========== /** * 切换编辑模式 */ public void setEditMode(boolean editMode) { if (this.editMode == editMode) return; // 保存当前面板大小 if (editMode) { capturedPanelSize = new Dimension(getWidth(), getHeight()); } this.editMode = editMode; // 清理当前状态 cleanupCurrentMode(); // 移除所有组件 removeAll(); // 重新初始化UI if (editMode) { initEditModeUI(); } else { initRuntimeUI(); } revalidate(); repaint(); log.info("[{}] 切换到{}模式", stationCode, editMode ? "编辑" : "运行"); } /** * 捕获当前运行模式下的组件布局 */ private Map captureCurrentLayout() { Map bounds = new HashMap<>(); // 保存当前面板大小(用于编辑模式) capturedPanelSize = new Dimension(getWidth(), getHeight()); // 获取内容区域大小 Insets insets = getInsets(); int contentWidth = getWidth() - insets.left - insets.right; int contentHeight = getHeight() - insets.top - insets.bottom; log.info("[{}] 捕获面板大小: {}x{}, 内容区域: {}x{}, insets: top={},left={},bottom={},right={}", stationCode, getWidth(), getHeight(), contentWidth, contentHeight, insets.top, insets.left, insets.bottom, insets.right); if (contentWidth <= 0 || contentHeight <= 0) { return bounds; } // 捕获各组件的相对位置 if (productSnInput != null && productSnInput.isVisible()) { bounds.put("product_sn_input", getRelativeBounds(productSnInput, insets, contentWidth, contentHeight)); } if (scanProductBtn != null && scanProductBtn.isVisible()) { bounds.put("scan_product_btn", getRelativeBounds(scanProductBtn, insets, contentWidth, contentHeight)); } if (okButton != null && okButton.isVisible()) { // OK按钮可能在buttonPanel中 Container parent = okButton.getParent(); if (parent != null && parent != this) { // 计算相对于面板的位置 Point loc = SwingUtilities.convertPoint(parent, okButton.getLocation(), this); int x = loc.x - insets.left; int y = loc.y - insets.top; bounds.put("ok_button", new Rectangle( (int)((double)x / contentWidth * 1000), (int)((double)y / contentHeight * 1000), (int)((double)okButton.getWidth() / contentWidth * 1000), (int)((double)okButton.getHeight() / contentHeight * 1000) )); } else { bounds.put("ok_button", getRelativeBounds(okButton, insets, contentWidth, contentHeight)); } } if (ngButton != null && ngButton.isVisible()) { Container parent = ngButton.getParent(); if (parent != null && parent != this) { Point loc = SwingUtilities.convertPoint(parent, ngButton.getLocation(), this); int x = loc.x - insets.left; int y = loc.y - insets.top; bounds.put("ng_button", new Rectangle( (int)((double)x / contentWidth * 1000), (int)((double)y / contentHeight * 1000), (int)((double)ngButton.getWidth() / contentWidth * 1000), (int)((double)ngButton.getHeight() / contentHeight * 1000) )); } else { bounds.put("ng_button", getRelativeBounds(ngButton, insets, contentWidth, contentHeight)); } } log.info("[{}] 捕获运行模式布局: {}", stationCode, bounds); return bounds; } /** * 获取组件相对于内容区域的边界(放大1000倍以保留精度) */ private Rectangle getRelativeBounds(JComponent comp, Insets insets, int contentWidth, int contentHeight) { int x = comp.getX() - insets.left; int y = comp.getY() - insets.top; return new Rectangle( (int)((double)x / contentWidth * 1000), (int)((double)y / contentHeight * 1000), (int)((double)comp.getWidth() / contentWidth * 1000), (int)((double)comp.getHeight() / contentHeight * 1000) ); } /** * 清理当前模式的资源 */ private void cleanupCurrentMode() { // 移除旧的监听器 if (resizeListener != null) { removeComponentListener(resizeListener); resizeListener = null; } // 清理组件包装器 if (componentWrappers != null) { for (DraggableComponentWrapper wrapper : componentWrappers.values()) { wrapper.setEditMode(false); } componentWrappers.clear(); } // 清理网格叠加层 gridOverlay = null; // 注意:不清除 preferredSize 和 capturedPanelSize // 保持面板尺寸一致,避免布局跳动 // 重置组件引用(强制重新创建) productSnInput = null; scanProductBtn = null; materialLabelUI = null; materialSnInput = null; scanMaterialBtn = null; unbindMaterialBtn = null; presetCountLabel = null; presetCountField = null; finishedCountLabel = null; finishedCountField = null; deviceInfoLabel = null; presetCountLabel2 = null; presetCountField2 = null; finishedCountLabel2 = null; finishedCountField2 = null; deviceInfoLabel2 = null; okButton = null; ngButton = null; statusMenu = null; stationLabel = null; } /** * 初始化运行模式UI - 使用绝对定位 + 相对坐标,支持窗口缩放 * 运行模式和编辑模式使用相同的布局逻辑,保证一致性 */ private void initRuntimeUI() { log.info("[{}] 运行模式使用绝对定位", stationCode); // 保持面板大小(从编辑模式切换回来时) if (capturedPanelSize != null) { setPreferredSize(capturedPanelSize); setMinimumSize(capturedPanelSize); } // 先确保所有组件已创建(统一入口) ensureComponentsCreated(); // 恢复外部状态组件引用(单工位模式) if (!showHeader && externalStatusMenu != null) { statusMenu = externalStatusMenu; log.debug("[{}] 恢复外部状态组件引用", stationCode); } // 使用绝对定位 setLayout(null); // 设置边框 if (showHeader) { setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(10, 10, 10, 10) )); } else { setBorder(BorderFactory.createEmptyBorder(40, 10, 10, 10)); } // 加载布局配置 loadLayoutConfig(); // 添加组件到面板(不使用嵌套容器) add(productSnInput); add(scanProductBtn); add(okButton); add(ngButton); // 可选组件 if (showHeader) { add(stationLabel); add(statusMenu); } if (showMaterialInput) { add(materialLabelUI); add(materialSnInput); add(scanMaterialBtn); add(unbindMaterialBtn); } if (showDeviceInfo) { if (deviceInfoLabel != null) { add(deviceInfoLabel); } add(presetCountLabel); add(presetCountField); add(finishedCountLabel); add(finishedCountField); } if (showDeviceInfo2) { if (deviceInfoLabel2 != null) { add(deviceInfoLabel2); } add(presetCountLabel2); add(presetCountField2); add(finishedCountLabel2); add(finishedCountField2); } // 添加窗口大小变化监听器,实现按比例缩放 resizeListener = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (!editMode) { applyRuntimeLayout(); } } }; addComponentListener(resizeListener); // 确保面板显示后再应用布局 addHierarchyListener(e -> { if ((e.getChangeFlags() & java.awt.event.HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() && !editMode) { SwingUtilities.invokeLater(this::applyRuntimeLayout); } }); // 初始应用布局(如果面板已有尺寸) if (getWidth() > 0 && getHeight() > 0) { applyRuntimeLayout(); } } /** * 初始化自定义布局UI(运行模式使用编辑器保存的配置) * 注意:此方法已被 initCustomRuntimeUI() 替代,保留以备后用 */ private void initCustomLayoutUI() { setLayout(null); // 单工位不显示外边框,双工位显示边框 if (showHeader) { setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(10, 10, 10, 10) )); } else { setBorder(BorderFactory.createEmptyBorder(40, 10, 10, 10)); } // 确保组件已创建(统一入口) ensureComponentsCreated(); // 添加组件到面板 add(productSnInput); add(scanProductBtn); if (showMaterialInput) { add(materialLabelUI); add(materialSnInput); add(scanMaterialBtn); add(unbindMaterialBtn); } if (showDeviceInfo) { if (deviceInfoLabel != null) { add(deviceInfoLabel); } add(presetCountLabel); add(presetCountField); add(finishedCountLabel); add(finishedCountField); } if (showDeviceInfo2) { if (deviceInfoLabel2 != null) { add(deviceInfoLabel2); } add(presetCountLabel2); add(presetCountField2); add(finishedCountLabel2); add(finishedCountField2); } add(okButton); add(ngButton); // 添加尺寸变化监听器,应用自定义布局 addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (!editMode) { applyCustomLayout(); } } }); // 立即应用布局 applyCustomLayout(); } /** * 应用自定义布局配置(运行模式) */ private void applyCustomLayout() { if (uiLayoutConfig == null) return; int panelWidth = getWidth(); int panelHeight = getHeight(); if (panelWidth == 0 || panelHeight == 0) { // 使用基准尺寸 panelWidth = uiLayoutConfig.getGlobalSettings().getBaseWidth(); panelHeight = uiLayoutConfig.getGlobalSettings().getBaseHeight(); } // 应用配置到各个组件 applyLayoutToComponent(productSnInput, "product_sn_input", panelWidth, panelHeight); applyLayoutToComponent(scanProductBtn, "scan_product_btn", panelWidth, panelHeight); if (showMaterialInput) { applyLayoutToComponent(materialLabelUI, "material_label", panelWidth, panelHeight); applyLayoutToComponent(materialSnInput, "material_sn_input", panelWidth, panelHeight); applyLayoutToComponent(scanMaterialBtn, "scan_material_btn", panelWidth, panelHeight); applyLayoutToComponent(unbindMaterialBtn, "unbind_material_btn", panelWidth, panelHeight); } if (showDeviceInfo) { if (deviceInfoLabel != null) { applyLayoutToComponent(deviceInfoLabel, "device_info_label", panelWidth, panelHeight); } applyLayoutToComponent(presetCountLabel, "preset_count_label", panelWidth, panelHeight); applyLayoutToComponent(presetCountField, "preset_count_field", panelWidth, panelHeight); applyLayoutToComponent(finishedCountLabel, "finished_count_label", panelWidth, panelHeight); applyLayoutToComponent(finishedCountField, "finished_count_field", panelWidth, panelHeight); } if (showDeviceInfo2) { if (deviceInfoLabel2 != null) { applyLayoutToComponent(deviceInfoLabel2, "device_info_label2", panelWidth, panelHeight); } applyLayoutToComponent(presetCountLabel2, "preset_count_label2", panelWidth, panelHeight); applyLayoutToComponent(presetCountField2, "preset_count_field2", panelWidth, panelHeight); applyLayoutToComponent(finishedCountLabel2, "finished_count_label2", panelWidth, panelHeight); applyLayoutToComponent(finishedCountField2, "finished_count_field2", panelWidth, panelHeight); } applyLayoutToComponent(okButton, "ok_button", panelWidth, panelHeight); applyLayoutToComponent(ngButton, "ng_button", panelWidth, panelHeight); } /** * 应用布局配置到单个组件 */ private void applyLayoutToComponent(JComponent component, String componentId, int panelWidth, int panelHeight) { if (component == null) return; UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponent(componentId); if (layout == null || !layout.isVisible()) { component.setVisible(false); return; } // 计算绝对位置和尺寸 int x = (int)(layout.getPosition().getX() * panelWidth); int y = (int)(layout.getPosition().getY() * panelHeight); int width = (int)(layout.getSize().getWidth() * panelWidth); int height = (int)(layout.getSize().getHeight() * panelHeight); // 应用最小尺寸限制 width = Math.max(width, layout.getSize().getMinWidth()); height = Math.max(height, layout.getSize().getMinHeight()); // 应用最大尺寸限制 width = Math.min(width, layout.getSize().getMaxWidth()); height = Math.min(height, layout.getSize().getMaxHeight()); // 设置组件位置和大小 component.setBounds(x, y, width, height); component.setVisible(layout.isVisible()); // 应用样式配置 UILayoutConfig.Style style = layout.getStyle(); if (style != null) { // 应用字体 Font currentFont = component.getFont(); Font newFont = new Font( style.getFontName(), style.getFontStyle(), style.getFontSize() ); component.setFont(newFont); // 应用颜色(如果组件支持) try { if (style.getForeground() != null) { component.setForeground(Color.decode(style.getForeground())); } if (style.getBackground() != null && component.isOpaque()) { component.setBackground(Color.decode(style.getBackground())); } } catch (NumberFormatException e) { log.warn("颜色格式错误: {}", e.getMessage()); } } } /** * 使用自定义布局初始化运行模式UI */ private void initCustomRuntimeUI() { setLayout(null); // 设置边框(和默认运行模式一致) if (showHeader) { setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(10, 10, 10, 10) )); } else { setBorder(BorderFactory.createEmptyBorder(40, 10, 10, 10)); } // 确保组件已创建 ensureComponentsCreated(); // 等待面板大小确定后应用布局 SwingUtilities.invokeLater(() -> { applyLayoutConfigToRuntime(); }); } /** * 确保所有组件已创建(统一的组件创建入口) * 所有组件的样式在这里统一定义,其他地方只做布局 */ private void ensureComponentsCreated() { // === 工件码输入框 === if (productSnInput == null) { productSnInput = new JTextField(); productSnInput.setHorizontalAlignment(SwingConstants.CENTER); productSnInput.setEditable(false); productSnInput.setFont(new Font("微软雅黑", Font.PLAIN, 28)); productSnInput.setPreferredSize(new Dimension(640, 70)); } // === 扫码按钮 === if (scanProductBtn == null) { scanProductBtn = new JButton("扫码"); scanProductBtn.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/scan_barcode.png")))); scanProductBtn.setFont(new Font("微软雅黑", Font.PLAIN, 32)); scanProductBtn.setPreferredSize(new Dimension(160, 70)); scanProductBtn.addActionListener(e -> onScanProductClick()); } // === OK按钮 === if (okButton == null) { okIconImage = new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/ok_bg.png"))).getImage(); okButton = new JButton("OK"); okButton.setIcon(new ImageIcon(okIconImage.getScaledInstance(24, 24, Image.SCALE_SMOOTH))); okButton.setFont(new Font("微软雅黑", Font.PLAIN, 32)); okButton.setEnabled(false); okButton.addActionListener(e -> onOkClick()); } // === NG按钮 === if (ngButton == null) { ngIconImage = new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/ng_bg.png"))).getImage(); ngButton = new JButton("NG"); ngButton.setIcon(new ImageIcon(ngIconImage.getScaledInstance(24, 24, Image.SCALE_SMOOTH))); ngButton.setFont(new Font("微软雅黑", Font.PLAIN, 32)); ngButton.setEnabled(false); ngButton.addActionListener(e -> onNgClick()); } // === 物料码标签(可选)=== if (showMaterialInput && materialLabelUI == null) { materialLabelUI = new JLabel(this.materialLabel + ":"); materialLabelUI.setFont(new Font("微软雅黑", Font.PLAIN, 20)); materialLabelUI.setForeground(Color.BLACK); } // === 物料码输入框(可选)=== if (showMaterialInput && materialSnInput == null) { materialSnInput = new JTextField(); materialSnInput.setHorizontalAlignment(SwingConstants.CENTER); materialSnInput.setEditable(false); materialSnInput.setFont(new Font("微软雅黑", Font.PLAIN, 28)); materialSnInput.setPreferredSize(new Dimension(400, 70)); } // === 物料扫码按钮(可选)=== if (showMaterialInput && scanMaterialBtn == null) { scanMaterialBtn = new JButton(""); scanMaterialBtn.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/scan_barcode.png")))); scanMaterialBtn.setEnabled(false); scanMaterialBtn.addActionListener(e -> onScanMaterialClick()); } // === 解绑按钮(可选)=== if (showMaterialInput && unbindMaterialBtn == null) { unbindMaterialBtn = new JButton(""); unbindMaterialBtn.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/delete.png")))); unbindMaterialBtn.setEnabled(false); unbindMaterialBtn.addActionListener(e -> onUnbindClick()); } // === 工位标签(双工位时显示)=== if (showHeader && stationLabel == null) { stationLabel = new JLabel(stationCode); stationLabel.setForeground(Color.BLACK); stationLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); } // === 状态显示(双工位时显示)=== if (showHeader && statusMenu == null) { statusMenu = new JButton("等待加工信号"); statusMenu.setForeground(Color.GREEN); statusMenu.setFont(new Font("微软雅黑", Font.PLAIN, 20)); statusMenu.setBackground(Color.BLACK); statusMenu.setPreferredSize(new Dimension(400, 40)); } // === 预设数量标签(可选)=== if (showDeviceInfo && presetCountLabel == null) { presetCountLabel = new JLabel("预设数量"); presetCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); presetCountLabel.setForeground(Color.BLACK); presetCountLabel.setHorizontalAlignment(SwingConstants.CENTER); } // === 预设数量(可选)=== if (showDeviceInfo && presetCountField == null) { presetCountField = new JTextField("0"); presetCountField.setHorizontalAlignment(SwingConstants.CENTER); presetCountField.setFont(new Font("微软雅黑", Font.PLAIN, 20)); presetCountField.setEditable(false); presetCountField.setPreferredSize(new Dimension(150, 40)); } // === 完成数量标签(可选)=== if (showDeviceInfo && finishedCountLabel == null) { finishedCountLabel = new JLabel("完成数量"); finishedCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); finishedCountLabel.setForeground(Color.BLACK); finishedCountLabel.setHorizontalAlignment(SwingConstants.CENTER); } // === 完成数量(可选)=== if (showDeviceInfo && finishedCountField == null) { finishedCountField = new JTextField("0"); finishedCountField.setHorizontalAlignment(SwingConstants.CENTER); finishedCountField.setFont(new Font("微软雅黑", Font.PLAIN, 20)); finishedCountField.setEditable(false); finishedCountField.setPreferredSize(new Dimension(150, 40)); } // === 第一行可选前置标签 === if (showDeviceInfo && deviceInfoLabel == null) { StationConfig cfg = StationConfig.getInstance(); StationConfig.DeviceInfoRow row0 = cfg.getDeviceInfoRow(0); if (row0 != null && row0.hasLabel()) { deviceInfoLabel = new JLabel(row0.getLabel()); deviceInfoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); deviceInfoLabel.setForeground(Color.BLACK); deviceInfoLabel.setHorizontalAlignment(SwingConstants.CENTER); } } // === 第二行设备信息(可选)=== if (showDeviceInfo2 && presetCountLabel2 == null) { StationConfig cfg = StationConfig.getInstance(); StationConfig.DeviceInfoRow row1 = cfg.getDeviceInfoRow(1); // 第二行可选前置标签 if (row1 != null && row1.hasLabel()) { deviceInfoLabel2 = new JLabel(row1.getLabel()); deviceInfoLabel2.setFont(new Font("微软雅黑", Font.PLAIN, 18)); deviceInfoLabel2.setForeground(Color.BLACK); deviceInfoLabel2.setHorizontalAlignment(SwingConstants.CENTER); } presetCountLabel2 = new JLabel("预设数量"); presetCountLabel2.setFont(new Font("微软雅黑", Font.PLAIN, 18)); presetCountLabel2.setForeground(Color.BLACK); presetCountLabel2.setHorizontalAlignment(SwingConstants.CENTER); presetCountField2 = new JTextField("0"); presetCountField2.setHorizontalAlignment(SwingConstants.CENTER); presetCountField2.setFont(new Font("微软雅黑", Font.PLAIN, 20)); presetCountField2.setEditable(false); presetCountField2.setPreferredSize(new Dimension(150, 40)); finishedCountLabel2 = new JLabel("完成数量"); finishedCountLabel2.setFont(new Font("微软雅黑", Font.PLAIN, 18)); finishedCountLabel2.setForeground(Color.BLACK); finishedCountLabel2.setHorizontalAlignment(SwingConstants.CENTER); finishedCountField2 = new JTextField("0"); finishedCountField2.setHorizontalAlignment(SwingConstants.CENTER); finishedCountField2.setFont(new Font("微软雅黑", Font.PLAIN, 20)); finishedCountField2.setEditable(false); finishedCountField2.setPreferredSize(new Dimension(150, 40)); } } /** * 将布局配置应用到运行模式 */ private void applyLayoutConfigToRuntime() { Insets insets = getInsets(); int contentWidth = getWidth() - insets.left - insets.right; int contentHeight = getHeight() - insets.top - insets.bottom; if (contentWidth <= 0 || contentHeight <= 0) { // 面板还没有大小,稍后重试 SwingUtilities.invokeLater(this::applyLayoutConfigToRuntime); return; } log.debug("[{}] applyLayoutConfigToRuntime: 面板={}x{}, 内容={}x{}, insets=({},{},{},{})", stationCode, getWidth(), getHeight(), contentWidth, contentHeight, insets.top, insets.left, insets.bottom, insets.right); Map components = uiLayoutConfig.getComponents(); // 应用每个组件的布局 for (Map.Entry entry : components.entrySet()) { String id = entry.getKey(); UILayoutConfig.ComponentLayout layout = entry.getValue(); JComponent comp = getComponentById(id); if (comp == null) continue; // 计算绝对位置(相对坐标 * 内容区域大小 + 边距偏移) int x = insets.left + (int)(layout.getPosition().getX() * contentWidth); int y = insets.top + (int)(layout.getPosition().getY() * contentHeight); int width = (int)(layout.getSize().getWidth() * contentWidth); int height = (int)(layout.getSize().getHeight() * contentHeight); comp.setBounds(x, y, width, height); add(comp); log.debug("[{}] 运行模式组件 {} 布局: ({},{},{},{})", stationCode, id, x, y, width, height); } revalidate(); repaint(); } /** * 初始化编辑模式UI - 使用与运行模式相同的布局配置 */ private void initEditModeUI() { log.info("[{}] 初始化编辑模式", stationCode); setLayout(null); // 保持面板大小 if (capturedPanelSize != null) { setPreferredSize(capturedPanelSize); setMinimumSize(capturedPanelSize); setSize(capturedPanelSize); } // 编辑模式不使用边框,橙色边框通过gridOverlay绘制 setBorder(null); // 加载布局配置(与运行模式相同) loadLayoutConfig(); // 确保组件已创建 ensureComponentsCreated(); // 添加网格叠加层(带橙色边框标识编辑模式) gridOverlay = new JPanel() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (uiLayoutConfig != null && uiLayoutConfig.getGlobalSettings().isShowGrid()) { drawGrid(g); } // 绘制 Flex 行边界(如果是 flex 模式) if (uiLayoutConfig != null && uiLayoutConfig.isFlexMode()) { drawFlexRowBounds(g); } // 绘制橙色边框标识编辑模式 Graphics2D g2 = (Graphics2D) g; g2.setColor(new Color(255, 152, 0)); g2.setStroke(new BasicStroke(3)); g2.drawRect(1, 1, getWidth() - 3, getHeight() - 3); } }; gridOverlay.setOpaque(false); gridOverlay.setLayout(null); // 包装所有组件为可拖拽版本 wrapComponent("product_sn_input", productSnInput); wrapComponent("scan_product_btn", scanProductBtn); wrapComponent("ok_button", okButton); wrapComponent("ng_button", ngButton); if (showMaterialInput) { wrapComponent("material_label", materialLabelUI); wrapComponent("material_sn_input", materialSnInput); wrapComponent("scan_material_btn", scanMaterialBtn); wrapComponent("unbind_material_btn", unbindMaterialBtn); } if (showDeviceInfo) { if (deviceInfoLabel != null) { wrapComponent("device_info_label", deviceInfoLabel); } wrapComponent("preset_count_label", presetCountLabel); wrapComponent("preset_count_field", presetCountField); wrapComponent("finished_count_label", finishedCountLabel); wrapComponent("finished_count_field", finishedCountField); } // 包装工位标签和状态栏(双工位模式或有这些组件时) if (stationLabel != null) { wrapComponent("station_label", stationLabel); } if (statusMenu != null) { wrapComponent("status_menu", statusMenu); } add(gridOverlay); gridOverlay.setBounds(0, 0, getWidth(), getHeight()); // 添加尺寸变化监听器 addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (editMode && gridOverlay != null) { gridOverlay.setBounds(0, 0, getWidth(), getHeight()); applyEditModeLayout(); } } }); // 应用布局 SwingUtilities.invokeLater(this::applyEditModeLayout); log.info("[{}] 编辑模式初始化完成,组件数: {}", stationCode, componentWrappers.size()); } /** * 应用编辑模式布局 * 如果是 flex 模式,组件会被约束在对应的行内 */ private void applyEditModeLayout() { if (gridOverlay == null) return; int panelWidth = getWidth(); int panelHeight = getHeight(); if (panelWidth <= 0 || panelHeight <= 0) return; Insets insets = getInsets(); int contentWidth = panelWidth - insets.left - insets.right; int contentHeight = panelHeight - insets.top - insets.bottom; if (contentWidth <= 0 || contentHeight <= 0) return; boolean isSingleMode = !showHeader; log.debug("[{}] applyEditModeLayout: 面板={}x{}, 内容区域={}x{}, flex={}", stationCode, panelWidth, panelHeight, contentWidth, contentHeight, uiLayoutConfig != null && uiLayoutConfig.isFlexMode()); // 如果是 flex 模式,使用 flex 布局定位组件 if (uiLayoutConfig != null && uiLayoutConfig.isFlexMode()) { applyEditModeFlexLayout(insets, contentWidth, contentHeight, isSingleMode); } else { applyEditModeFreeLayout(insets, contentWidth, contentHeight, isSingleMode); } gridOverlay.revalidate(); gridOverlay.repaint(); } /** * 编辑模式下应用 Flex 布局(组件约束在行内) */ private void applyEditModeFlexLayout(Insets insets, int contentWidth, int contentHeight, boolean isSingleMode) { List visibleRows = uiLayoutConfig.getVisibleRowsForMode(isSingleMode); if (visibleRows.isEmpty()) { applyEditModeFreeLayout(insets, contentWidth, contentHeight, isSingleMode); return; } // 获取网格大小 int gridSize = uiLayoutConfig.getGlobalSettings().getGridSize(); // 计算每行的实际高度(对齐到网格) Map rowHeights = new HashMap<>(); for (UILayoutConfig.RowLayout row : visibleRows) { int maxHeight = 0; for (UILayoutConfig.RowComponentLayout compLayout : row.getVisibleComponents()) { String compId = compLayout.getId(); UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(compId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(compId); if (layout == null && compId.endsWith("2")) { String fallbackId = compId.substring(0, compId.length() - 1); layout = uiLayoutConfig.getComponentForMode(fallbackId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(fallbackId); } if (layout != null) { int h = (int)(layout.getSize().getHeight() * contentHeight); h = snapToGrid(h, gridSize); maxHeight = Math.max(maxHeight, h); } } rowHeights.put(row.getId(), Math.max(maxHeight, snapToGrid(50, gridSize))); } // 计算总高度和间距 int totalRowHeight = 0; for (Integer h : rowHeights.values()) { totalRowHeight += h; } int remainingSpace = contentHeight - totalRowHeight; int gapCount = visibleRows.size() + 1; int gap = snapToGrid(Math.max(remainingSpace / gapCount, 10), gridSize); // 计算每行的 Y 起始位置(对齐到网格) Map rowStartY = new HashMap<>(); int currentY = snapToGrid(insets.top + gap, gridSize); for (UILayoutConfig.RowLayout row : visibleRows) { rowStartY.put(row.getId(), currentY); currentY += rowHeights.get(row.getId()) + gap; } // 布局每个组件,约束在对应的行内 for (UILayoutConfig.RowLayout row : visibleRows) { int rowY = rowStartY.get(row.getId()); int rowHeight = rowHeights.get(row.getId()); for (UILayoutConfig.RowComponentLayout compLayout : row.getVisibleComponents()) { String componentId = compLayout.getId(); DraggableComponentWrapper wrapper = componentWrappers.get(componentId); if (wrapper == null) continue; // 从配置读取 X 和宽度 UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(componentId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(componentId); if (layout == null && componentId.endsWith("2")) { String fallbackId = componentId.substring(0, componentId.length() - 1); layout = uiLayoutConfig.getComponentForMode(fallbackId, isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(fallbackId); } int compX, compWidth, compHeight; if (layout != null) { compX = insets.left + (int)(layout.getPosition().getX() * contentWidth); compWidth = (int)(layout.getSize().getWidth() * contentWidth); compHeight = (int)(layout.getSize().getHeight() * contentHeight); } else { double[] defaultLayout = getDefaultLayout(componentId); compX = insets.left + (int)(defaultLayout[0] * contentWidth); compWidth = (int)(defaultLayout[2] * contentWidth); compHeight = (int)(defaultLayout[3] * contentHeight); } // 对齐到网格 compX = snapToGrid(compX, gridSize); compWidth = snapToGrid(Math.max(compWidth, 50), gridSize); compHeight = snapToGrid(Math.max(compHeight, 30), gridSize); // Y 坐标:在行内垂直居中,对齐到网格 int compY = snapToGrid(rowY + (rowHeight - compHeight) / 2, gridSize); wrapper.setBounds(compX, compY, compWidth, compHeight); } } } /** * 编辑模式下应用 Free 布局(自由定位) */ private void applyEditModeFreeLayout(Insets insets, int contentWidth, int contentHeight, boolean isSingleMode) { // 获取网格大小 int gridSize = uiLayoutConfig.getGlobalSettings().getGridSize(); for (Map.Entry entry : componentWrappers.entrySet()) { String componentId = entry.getKey(); DraggableComponentWrapper wrapper = entry.getValue(); // 获取相对坐标 double relX, relY, relW, relH; UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(componentId, isSingleMode); if (layout == null) { layout = wrapper.getLayoutConfig(); } if (layout != null) { relX = layout.getPosition().getX(); relY = layout.getPosition().getY(); relW = layout.getSize().getWidth(); relH = layout.getSize().getHeight(); } else { double[] defaultLayout = getDefaultLayout(componentId); relX = defaultLayout[0]; relY = defaultLayout[1]; relW = defaultLayout[2]; relH = defaultLayout[3]; } // 相对坐标转绝对坐标 int x = insets.left + (int)(relX * contentWidth); int y = insets.top + (int)(relY * contentHeight); int width = Math.max((int)(relW * contentWidth), 50); int height = Math.max((int)(relH * contentHeight), 30); // 对齐到网格 x = snapToGrid(x, gridSize); y = snapToGrid(y, gridSize); width = snapToGrid(width, gridSize); height = snapToGrid(height, gridSize); wrapper.setBounds(x, y, width, height); } } /** * 将值对齐到网格 */ private int snapToGrid(int value, int gridSize) { if (gridSize <= 0) return value; return Math.round(value / (float) gridSize) * gridSize; } /** * 从捕获的运行模式布局更新配置 */ private void updateLayoutFromCaptured(Map capturedBounds) { for (Map.Entry entry : capturedBounds.entrySet()) { String componentId = entry.getKey(); Rectangle bounds = entry.getValue(); UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponent(componentId); if (layout == null) { layout = new UILayoutConfig.ComponentLayout(); layout.setId(componentId); uiLayoutConfig.addComponent(componentId, layout); } // 将放大1000倍的相对坐标转换回0-1范围 double relX = bounds.x / 1000.0; double relY = bounds.y / 1000.0; double relW = bounds.width / 1000.0; double relH = bounds.height / 1000.0; layout.getPosition().setX(relX); layout.getPosition().setY(relY); layout.getSize().setWidth(relW); layout.getSize().setHeight(relH); log.debug("[{}] 更新组件 {} 配置: raw=({},{},{},{}) -> rel=({},{},{},{})", stationCode, componentId, bounds.x, bounds.y, bounds.width, bounds.height, String.format("%.3f", relX), String.format("%.3f", relY), String.format("%.3f", relW), String.format("%.3f", relH)); } log.info("[{}] 已从运行模式布局更新配置,共{}个组件", stationCode, capturedBounds.size()); } /** * 获取布局配置文件名 * 统一使用 ui_layout_flex.yaml,单双工位差异由 station.yaml 控制 */ private String getLayoutConfigFileName() { return "ui_layout_flex.yaml"; } /** * 加载布局配置 * @return true 如果从文件加载成功,false 如果使用默认布局 */ private boolean loadLayoutConfig() { String configFileName = getLayoutConfigFileName(); try { // 优先从文件系统加载(开发环境,可以实时更新) Path filePath = Paths.get("src/resources/config/" + configFileName); if (Files.exists(filePath)) { log.info("从文件系统加载UI布局配置: {}", filePath); StringBuilder sb = new StringBuilder(); java.io.BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8); String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } reader.close(); String yaml = sb.toString(); uiLayoutConfig = UILayoutConfig.fromYaml(yaml); log.info("UI布局配置从文件系统加载成功 ({})", configFileName); // 根据 station.yaml 同步行可见性 syncLayoutWithStationConfig(); return true; // 从文件加载成功 } // 从classpath加载(生产环境) String configPath = "resources/config/" + configFileName; InputStream is = getClass().getClassLoader().getResourceAsStream(configPath); if (is != null) { log.info("从classpath加载UI布局配置: {}", configFileName); // Java 8 兼容方式读取流 StringBuilder sb = new StringBuilder(); java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(is, StandardCharsets.UTF_8)); String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } reader.close(); String yaml = sb.toString(); uiLayoutConfig = UILayoutConfig.fromYaml(yaml); log.info("UI布局配置从classpath加载成功 ({})", configFileName); // 根据 station.yaml 同步行可见性 syncLayoutWithStationConfig(); return true; // 从classpath加载成功 } else { // 使用默认布局 uiLayoutConfig = createDefaultLayout(); log.info("使用默认UI布局 ({})", configFileName); return false; // 使用默认布局 } } catch (Exception e) { log.error("加载UI布局配置失败 ({}): {}", configFileName, e.getMessage()); uiLayoutConfig = createDefaultLayout(); return false; // 加载失败,使用默认布局 } } /** * 根据 station.yaml 配置同步布局的行可见性 * 这样只需要在 station.yaml 中配置开关,布局会自动调整 */ private void syncLayoutWithStationConfig() { if (uiLayoutConfig == null || !uiLayoutConfig.isFlexMode()) { return; } StationConfig stationConfig = StationConfig.getInstance(); boolean isSingleMode = !showHeader; // 同步工位头行:单工位(showHeader=false)时隐藏 uiLayoutConfig.setRowVisibleForMode("row_header", showHeader, isSingleMode); log.debug("[{}] 同步行可见性: row_header={}", stationCode, showHeader); // 同步物料行:由 show_material_input 控制 boolean showMaterial = stationConfig.isShowMaterialInput(); uiLayoutConfig.setRowVisibleForMode("row_material", showMaterial, isSingleMode); log.debug("[{}] 同步行可见性: row_material={}", stationCode, showMaterial); // 同步设备信息行:由 show_device_info 控制 boolean showDevice = stationConfig.isShowDeviceInfo(); uiLayoutConfig.setRowVisibleForMode("row_device", showDevice, isSingleMode); log.debug("[{}] 同步行可见性: row_device={}", stationCode, showDevice); // 同步设备信息第二行:由 show_device_info2 控制 boolean showDevice2 = stationConfig.isShowDeviceInfo2(); uiLayoutConfig.setRowVisibleForMode("row_device2", showDevice2, isSingleMode); log.debug("[{}] 同步行可见性: row_device2={}", stationCode, showDevice2); log.info("[{}] Flex布局已同步 station.yaml 配置: mode={}, header={}, material={}, device={}, device2={}", stationCode, isSingleMode ? "单工位" : "双工位", showHeader, showMaterial, showDevice, showDevice2); } /** * 重新加载布局配置并刷新界面 * 用于双工位模式下,编辑完成后同步布局到另一个工位 * 同时支持 show_device_info / show_material_input 切换后的动态组件创建与添加 */ public void reloadLayoutConfig() { log.info("[{}] 重新加载布局配置", stationCode); // 1) 按当前 show_* 开关创建缺失的组件 ensureComponentsCreated(); // 2) 把尚未添加到面板的可选组件补加进去(幂等:已添加的不重复加) ensureComponentsAdded(); loadLayoutConfig(); if (!editMode && getWidth() > 0 && getHeight() > 0) { applyRuntimeLayout(); revalidate(); repaint(); } } /** * 把当前 show_* 开关需要的可选组件补加到面板 * 幂等:若组件已在本面板中则跳过 */ private void ensureComponentsAdded() { if (showDeviceInfo) { addIfAbsent(deviceInfoLabel); addIfAbsent(presetCountLabel); addIfAbsent(presetCountField); addIfAbsent(finishedCountLabel); addIfAbsent(finishedCountField); } if (showDeviceInfo2) { addIfAbsent(deviceInfoLabel2); addIfAbsent(presetCountLabel2); addIfAbsent(presetCountField2); addIfAbsent(finishedCountLabel2); addIfAbsent(finishedCountField2); } if (showMaterialInput) { addIfAbsent(materialLabelUI); addIfAbsent(materialSnInput); addIfAbsent(scanMaterialBtn); addIfAbsent(unbindMaterialBtn); } } private void addIfAbsent(java.awt.Component c) { if (c == null) return; if (c.getParent() == this) return; add(c); } /** * 创建默认布局配置 */ private UILayoutConfig createDefaultLayout() { UILayoutConfig config = new UILayoutConfig(); // 工件码输入框 UILayoutConfig.ComponentLayout productInput = new UILayoutConfig.ComponentLayout(); productInput.setId("product_sn_input"); productInput.setType("text_field"); productInput.getPosition().setX(0.0); productInput.getPosition().setY(0.18); productInput.getSize().setWidth(0.75); productInput.getSize().setHeight(0.12); productInput.getSize().setMinWidth(400); productInput.getSize().setMinHeight(50); productInput.getStyle().setFontSize(28); config.addComponent("product_sn_input", productInput); // 扫码按钮 UILayoutConfig.ComponentLayout scanBtn = new UILayoutConfig.ComponentLayout(); scanBtn.setId("scan_product_btn"); scanBtn.setType("button"); scanBtn.getPosition().setX(0.78); scanBtn.getPosition().setY(0.18); scanBtn.getSize().setWidth(0.20); scanBtn.getSize().setHeight(0.12); scanBtn.getSize().setMinWidth(100); scanBtn.getSize().setMinHeight(50); scanBtn.getStyle().setFontSize(32); config.addComponent("scan_product_btn", scanBtn); // OK按钮 UILayoutConfig.ComponentLayout okBtn = new UILayoutConfig.ComponentLayout(); okBtn.setId("ok_button"); okBtn.setType("button"); okBtn.getPosition().setX(0.25); okBtn.getPosition().setY(0.70); okBtn.getSize().setWidth(0.20); okBtn.getSize().setHeight(0.13); okBtn.getSize().setMinWidth(120); okBtn.getSize().setMinHeight(60); okBtn.getStyle().setFontSize(32); config.addComponent("ok_button", okBtn); // NG按钮 UILayoutConfig.ComponentLayout ngBtn = new UILayoutConfig.ComponentLayout(); ngBtn.setId("ng_button"); ngBtn.setType("button"); ngBtn.getPosition().setX(0.55); ngBtn.getPosition().setY(0.70); ngBtn.getSize().setWidth(0.20); ngBtn.getSize().setHeight(0.13); ngBtn.getSize().setMinWidth(120); ngBtn.getSize().setMinHeight(60); ngBtn.getStyle().setFontSize(32); config.addComponent("ng_button", ngBtn); return config; } /** * 获取布局偏移量(供 DraggableComponentWrapper 使用) * 返回值与运行模式下的 Insets 保持一致 */ public int[] getLayoutOffsets() { // 使用与运行模式相同的边框 Insets 计算 // showHeader=true: CompoundBorder(EtchedBorder(2,2,2,2) + EmptyBorder(10,10,10,10)) = (12,12,12,12) // showHeader=false: EmptyBorder(40,10,10,10) = top=40, left=10, bottom=10, right=10 int offsetLeft, offsetTop, offsetRight, offsetBottom; if (showHeader) { // EtchedBorder ~= 2 + EmptyBorder = 10, total = 12 offsetLeft = 12; offsetTop = 12; offsetRight = 12; offsetBottom = 12; } else { // EmptyBorder(top=40, left=10, bottom=10, right=10) offsetLeft = 10; offsetTop = 40; offsetRight = 10; offsetBottom = 10; } return new int[]{offsetLeft, offsetTop, offsetRight, offsetBottom}; } /** * 包装组件为可拖拽版本 */ private void wrapComponent(String componentId, JComponent component) { if (component == null) return; // 根据模式获取组件配置 (showHeader=false 是单工位模式) boolean isSingleMode = !showHeader; UILayoutConfig.ComponentLayout layoutConfig = uiLayoutConfig.getComponentForMode(componentId, isSingleMode); if (layoutConfig == null) { // 尝试从通用配置获取 layoutConfig = uiLayoutConfig.getComponent(componentId); } if (layoutConfig == null) { // 创建默认配置 layoutConfig = new UILayoutConfig.ComponentLayout(); layoutConfig.setId(componentId); layoutConfig.setType(getComponentType(component)); // 保存到模式专用配置 uiLayoutConfig.setComponentForMode(componentId, layoutConfig, isSingleMode); } DraggableComponentWrapper wrapper = new DraggableComponentWrapper( component, componentId, layoutConfig, this ); wrapper.setEditMode(editMode); wrapper.setSnapToGrid(uiLayoutConfig.getGlobalSettings().isEnableSnap()); wrapper.setGridSize(uiLayoutConfig.getGlobalSettings().getGridSize()); // 监听布局变更 wrapper.setLayoutChangeListener((id, layout) -> { log.debug("组件 {} 布局已更改", id); }); componentWrappers.put(componentId, wrapper); gridOverlay.add(wrapper); } /** * 应用布局配置(将相对坐标转换为绝对坐标) */ private void applyLayoutConfig() { // 使用捕获的面板大小(如果有),否则使用当前大小 int panelWidth = (capturedPanelSize != null) ? capturedPanelSize.width : getWidth(); int panelHeight = (capturedPanelSize != null) ? capturedPanelSize.height : getHeight(); log.debug("[{}] applyLayoutConfig: 使用大小={}x{} (捕获={}, 当前={}x{})", stationCode, panelWidth, panelHeight, capturedPanelSize != null, getWidth(), getHeight()); if (panelWidth <= 0 || panelHeight <= 0) { return; } // 同时更新 gridOverlay 大小 if (gridOverlay != null) { gridOverlay.setBounds(0, 0, panelWidth, panelHeight); } // 使用 getLayoutOffsets() 获取与运行模式一致的偏移量 int[] offsets = getLayoutOffsets(); int offsetLeft = offsets[0]; int offsetTop = offsets[1]; int offsetRight = offsets[2]; int offsetBottom = offsets[3]; // 计算模拟的内容区域大小 int contentWidth = panelWidth - offsetLeft - offsetRight; int contentHeight = panelHeight - offsetTop - offsetBottom; log.debug("[{}] 内容区域: offset=({},{}) size={}x{}", stationCode, offsetLeft, offsetTop, contentWidth, contentHeight); if (contentWidth <= 0 || contentHeight <= 0) { return; } for (Map.Entry entry : componentWrappers.entrySet()) { DraggableComponentWrapper wrapper = entry.getValue(); UILayoutConfig.ComponentLayout layout = wrapper.getLayoutConfig(); if (!layout.isVisible()) continue; // 计算绝对位置和尺寸(基于模拟的内容区域) int x = offsetLeft + (int)(layout.getPosition().getX() * contentWidth); int y = offsetTop + (int)(layout.getPosition().getY() * contentHeight); int width = (int)(layout.getSize().getWidth() * contentWidth); int height = (int)(layout.getSize().getHeight() * contentHeight); // 应用最小尺寸限制 width = Math.max(width, layout.getSize().getMinWidth()); height = Math.max(height, layout.getSize().getMinHeight()); log.debug("[{}] 组件 {} 布局: 相对pos=({},{}) size=({},{}) -> 绝对bounds=({},{},{},{})", stationCode, entry.getKey(), String.format("%.3f", layout.getPosition().getX()), String.format("%.3f", layout.getPosition().getY()), String.format("%.3f", layout.getSize().getWidth()), String.format("%.3f", layout.getSize().getHeight()), x, y, width, height); wrapper.setBounds(x, y, width, height); wrapper.setVisible(layout.isVisible()); } } /** * 绘制网格 */ private void drawGrid(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setColor(Color.decode(uiLayoutConfig.getGlobalSettings().getGridColor())); g2.setStroke(new BasicStroke(1)); int gridSize = uiLayoutConfig.getGlobalSettings().getGridSize(); int width = getWidth(); int height = getHeight(); // 绘制垂直线 for (int x = 0; x <= width; x += gridSize) { g2.drawLine(x, 0, x, height); } // 绘制水平线 for (int y = 0; y <= height; y += gridSize) { g2.drawLine(0, y, width, y); } } /** * 绘制 Flex 行边界(编辑模式显示) */ private void drawFlexRowBounds(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); boolean isSingleMode = !showHeader; List visibleRows = uiLayoutConfig.getVisibleRowsForMode(isSingleMode); if (visibleRows.isEmpty()) return; Insets insets = getInsets(); int contentWidth = getWidth() - insets.left - insets.right; int contentHeight = getHeight() - insets.top - insets.bottom; // 计算每行的实际高度 Map rowHeights = new HashMap<>(); for (UILayoutConfig.RowLayout row : visibleRows) { int maxHeight = 0; for (UILayoutConfig.RowComponentLayout compLayout : row.getVisibleComponents()) { UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponentForMode(compLayout.getId(), isSingleMode); if (layout == null) layout = uiLayoutConfig.getComponent(compLayout.getId()); if (layout != null) { int h = (int)(layout.getSize().getHeight() * contentHeight); maxHeight = Math.max(maxHeight, h); } } rowHeights.put(row.getId(), Math.max(maxHeight, 50)); } // 计算总高度和间距 int totalRowHeight = 0; for (Integer h : rowHeights.values()) { totalRowHeight += h; } int remainingSpace = contentHeight - totalRowHeight; int gapCount = visibleRows.size() + 1; int gap = Math.max(remainingSpace / gapCount, 10); // 定义行的颜色 Color[] rowColors = { new Color(255, 100, 100, 60), // 红色半透明 new Color(100, 255, 100, 60), // 绿色半透明 new Color(100, 100, 255, 60), // 蓝色半透明 new Color(255, 255, 100, 60), // 黄色半透明 new Color(255, 100, 255, 60), // 紫色半透明 }; // 绘制每行的边界 int currentY = insets.top + gap; int colorIndex = 0; for (UILayoutConfig.RowLayout row : visibleRows) { int rowHeight = rowHeights.get(row.getId()); // 填充行背景 g2.setColor(rowColors[colorIndex % rowColors.length]); g2.fillRect(insets.left + 5, currentY, contentWidth - 10, rowHeight); // 绘制行边框 g2.setColor(rowColors[colorIndex % rowColors.length].darker()); g2.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{5, 5}, 0)); g2.drawRect(insets.left + 5, currentY, contentWidth - 10, rowHeight); // 绘制行标签 g2.setColor(new Color(50, 50, 50)); g2.setFont(new Font("微软雅黑", Font.BOLD, 12)); String label = row.getId() + " (h=" + rowHeight + ")"; g2.drawString(label, insets.left + 10, currentY + 15); currentY += rowHeight + gap; colorIndex++; } } /** * 获取组件类型 */ private String getComponentType(JComponent component) { if (component instanceof JTextField) return "text_field"; if (component instanceof JButton) return "button"; if (component instanceof JLabel) return "label"; if (component instanceof JPanel) return "panel"; return "unknown"; } /** * 保存布局配置 */ public void saveLayoutConfig() { String configFileName = getLayoutConfigFileName(); try { // 先把当前编辑的组件位置更新到模式专用配置 boolean isSingleMode = !showHeader; updateModeLayoutFromWrappers(isSingleMode); String yaml = uiLayoutConfig.toYaml(); // 保存到文件 (Java 8 兼容方式) Path configPath = Paths.get("src/resources/config/" + configFileName); java.io.BufferedWriter writer = Files.newBufferedWriter(configPath, StandardCharsets.UTF_8); writer.write(yaml); writer.close(); String modeDesc = showHeader ? "双工位" : "单工位"; log.info("UI布局配置已保存 ({}, {})", modeDesc, configFileName); JOptionPane.showMessageDialog(this, modeDesc + "布局配置保存成功!", "保存", JOptionPane.INFORMATION_MESSAGE); } catch (Exception e) { log.error("保存UI布局配置失败 ({}): {}", configFileName, e.getMessage(), e); JOptionPane.showMessageDialog(this, "布局配置保存失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } /** * 把当前wrapper的位置更新到模式专用配置 */ private void updateModeLayoutFromWrappers(boolean isSingleMode) { for (Map.Entry entry : componentWrappers.entrySet()) { String componentId = entry.getKey(); DraggableComponentWrapper wrapper = entry.getValue(); UILayoutConfig.ComponentLayout layout = wrapper.getLayoutConfig(); if (layout != null) { uiLayoutConfig.setComponentForMode(componentId, layout, isSingleMode); } } log.debug("已更新{}布局到配置", isSingleMode ? "单工位" : "双工位"); } /** * 导出布局配置 */ public void exportLayoutConfig() { JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle("导出布局配置"); fileChooser.setFileFilter(new FileNameExtensionFilter("YAML文件 (*.yaml)", "yaml")); if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { try { java.io.File file = fileChooser.getSelectedFile(); if (!file.getName().endsWith(".yaml")) { file = new java.io.File(file.getAbsolutePath() + ".yaml"); } String yaml = uiLayoutConfig.toYaml(); // Java 8 兼容方式写入文件 java.io.BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8); writer.write(yaml); writer.close(); JOptionPane.showMessageDialog(this, "导出成功!", "导出", JOptionPane.INFORMATION_MESSAGE); } catch (Exception e) { log.error("导出失败: {}", e.getMessage(), e); JOptionPane.showMessageDialog(this, "导出失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } } /** * 导入布局配置 */ public void importLayoutConfig() { JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle("导入布局配置"); fileChooser.setFileFilter(new FileNameExtensionFilter("YAML文件 (*.yaml)", "yaml")); if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { try { java.io.File file = fileChooser.getSelectedFile(); // Java 8 兼容方式读取文件 StringBuilder sb = new StringBuilder(); java.io.BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8); String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } reader.close(); String yaml = sb.toString(); uiLayoutConfig = UILayoutConfig.fromYaml(yaml); // 更新组件 wrapper 的配置引用并重新应用布局 if (editMode) { for (Map.Entry entry : componentWrappers.entrySet()) { String id = entry.getKey(); DraggableComponentWrapper wrapper = entry.getValue(); UILayoutConfig.ComponentLayout layout = uiLayoutConfig.getComponent(id); if (layout != null) { wrapper.setLayoutConfig(layout); } } applyEditModeLayout(); } JOptionPane.showMessageDialog(this, "导入成功!", "导入", JOptionPane.INFORMATION_MESSAGE); } catch (Exception e) { log.error("导入失败: {}", e.getMessage(), e); JOptionPane.showMessageDialog(this, "导入失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } } /** * 切换网格显示 */ public void toggleGrid(boolean show) { if (uiLayoutConfig != null) { uiLayoutConfig.getGlobalSettings().setShowGrid(show); if (gridOverlay != null) { gridOverlay.repaint(); } } } // 防重入保护标志 private volatile boolean isScanDialogShowing = false; private volatile boolean isMaterialScanDialogShowing = false; }