| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644 |
- 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<String, DraggableComponentWrapper> 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<UILayoutConfig.RowLayout> visibleRows = uiLayoutConfig.getVisibleRowsForMode(isSingleMode);
-
- if (visibleRows.isEmpty()) {
- log.warn("[{}] Flex布局没有可见行,回退到Free布局", stationCode);
- applyFreeLayout(insets, contentWidth, contentHeight);
- return;
- }
-
- // 先计算每行内组件的实际高度(从配置读取)
- Map<String, Integer> 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<UILayoutConfig.RowComponentLayout> 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<String, Rectangle> captureCurrentLayout() {
- Map<String, Rectangle> 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<String, UILayoutConfig.ComponentLayout> components = uiLayoutConfig.getComponents();
-
- // 应用每个组件的布局
- for (Map.Entry<String, UILayoutConfig.ComponentLayout> 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<UILayoutConfig.RowLayout> visibleRows = uiLayoutConfig.getVisibleRowsForMode(isSingleMode);
- if (visibleRows.isEmpty()) {
- applyEditModeFreeLayout(insets, contentWidth, contentHeight, isSingleMode);
- return;
- }
-
- // 获取网格大小
- int gridSize = uiLayoutConfig.getGlobalSettings().getGridSize();
-
- // 计算每行的实际高度(对齐到网格)
- Map<String, Integer> 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<String, Integer> 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<String, DraggableComponentWrapper> 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<String, Rectangle> capturedBounds) {
- for (Map.Entry<String, Rectangle> 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<String, DraggableComponentWrapper> 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<UILayoutConfig.RowLayout> 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<String, Integer> 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<String, DraggableComponentWrapper> 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<String, DraggableComponentWrapper> 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;
- }
|