平台本身提供插件式开发,旨在将可变的功能,用户需求多变的功能,可以通过插件式开发的形式,来扩展平台的功能。

插件开发目前广泛使用的场景为过站界面,因为过站界面需要根据现场业务的逻辑发生变化,所以众多企业要求是不一样的。所以,我们可以通过一系列的插件,来定制客户需要的功能。另外,随着插件的增多,用户的可选择性也会变大,可以使平台的功能更加完善。

其实一切独立可变的功能都可以使用该系统来实现,如WMS的收料,IQA等,如各种系统的看板等功能。

1. 环境安装

1.1. 平台依赖

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

  3. Lumos 3.0 及以上

2. 定制项目中如何引用

在项目中使用插件非常简单,只需要引入对应的jar包即可。

2.1. 定制项目sdk项目依赖引入

在项目父模块中引入stage父模块依赖以确定版本号信息

    <dependencyManagenemnt>
        ......
        <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-workflow-parent</artifactId>
        <version>3.0.0</version>
        <type>pom</type>
        <scope>import</scope>
        </dependency>
        ......
    </dependencyManagenemnt>

将如下依赖加入定制项目的SDK(也就是接口定义项目)中。

        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-stage-sdk</artifactId>
        </dependency>

2.2. 定制项目后端依赖引入

将如下依赖加入定制项目的后端服务实现中。

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-stage-impl</artifactId>
    </dependency>

2.3. 定制项目前端依赖引入

将如下依赖加入定制项目的后端服务实现中。

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-stage-vaadin</artifactId>
    </dependency>
如果定制项目的项目接口没有这么详细的划分,比如说所有的项目实现都在一个项目中,那么将上述依赖全局加入这个项目的pom文件即可。

2.4. 配置界面进入

一旦加入上述的依赖,重新启动程序后,你将会看到如下界面

operationcenter

该界面用于插件的具体展示界面,是使用者的入口。该使用者包含产线操作工,仓库管理人员,以及看板等。

如果需要创建插件,可以进入插件定义窗口,在该界面,可以进行插件的增删改查等工作,也可以预览插件,如下图所示:

stageCRUD

2.5. 插件开发

2.5.1. 基类

插件开发相对比较简单,在引入具体的依赖后,你将会看到如下抽象类 “AbstractCustomizedComponent”,该抽象类是所有插件的基类,创建新的类,继承自该类即可。

其中,平台提供了两种类型的插件,一种为“操作中心”,一种为“看板”,不同类型的插件会在不同的界面展示。

该抽象类的插件类型默认为“操作中心”,若想改变插件类型,只要重写getStageCategory方法即可。
package com.ags.lumosframework.stage.web.webbase.ui.view.external;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import com.ags.lumosframework.stage.api.domain.Stage;
import com.ags.lumosframework.stage.api.enums.StageCategory;
import com.ags.lumosframework.web.vaadin.base.BaseComponent;

public abstract class AbstractCustomizedComponent extends BaseComponent {

    /**
     *
     */
    private static final long serialVersionUID = 3780261730528717346L;

    private String name;

    private Map<String, ParameterInstance> instanceMap = new HashMap<>();

    private boolean preview = false;

    public AbstractCustomizedComponent() {

    }

    public boolean isPreview() {
        return preview;
    }

    /**
     * 判定当前程序是否是预览界面,如果是预览界面,将不做数据的初始化。
     *
     * @param preview
     */
    public void setPreview(boolean preview) {
        this.preview = preview;
    }

    @PostConstruct
    private void initParameterInstance() {
        List<ParameterDefinition> parameterDefinitions = CustomizedComponentHelper.getDefinitions(this.getClass());
        parameterDefinitions.forEach(parameterDefinition -> {
            ParameterInstance parameterInstance =
                new ParameterInstance(parameterDefinition, parameterDefinition.readValue(this));
            instanceMap.put(parameterDefinition.getName(), parameterInstance);
        });
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 在插件页面添加插件时,该名称作为国际化的名称,展示给用户,所以一般情况下,你可以返回一个国际化的值。
     * <p>
     * 如在过站界面,系统将返回WorkStation.Runtime.Name。通过该Key,系统可以获得国际化的值,显示给用户
     *
     * @return
     */
    public abstract String getDisplayValue();

    @Override
    public String toString() {
        return getDisplayValue();
    }

    /**
     * 获得该插件所支持的所有参数,这些参数可以提供给用户在界面做配置,然后插件开发者,可以根据这些设定,来定义该插件的行为。
     *
     * @return
     */
    public Collection<ParameterInstance> getParameterInstances() {
        return instanceMap.values();
    }

    /**
     * internal use
     *
     * @param stage
     */
    public void initParameter(Stage stage) {
        Map<String, Object> parameters = stage.getParameters();
        parameters.forEach((key, value) -> {
            ParameterInstance parameterInstance = instanceMap.get(key);
            if (parameterInstance != null) {
                parameterInstance.setValue(value);
                parameterInstance.getParameterDefinition().writeValue(this, value);
            }
        });
    }

    public void _enter() {
        activateParameters();
        enter();
    }

    /**
     * 进入该插件时调用该方法,可以用于除页面之外的数据初始化操作
     */
    public abstract void enter();

    /**
     * 当用户在界面设定了插件开发者的参数,然后在打开界面(或者预览)的时候,如果系统加载到了参数信息,将调用这个方法。
     * <p>
     * 一个典型的应用,比如开发者提供了一个参数,允许用户选择工位,那么一旦工位在插件配置界面被选择后,当加载完这个参数后,会调用该方法,按照用户选择的工位进行动作。
     */
    public abstract void activateParameters();

    public StageCategory getStageCategory() {
        return StageCategory.OPERATION_CENTER;
    }

}

2.5.2. 参数指定

插件支持参数功能,参数主要是由插件开发者提供,允许用户在界面设定,然后插件开发者根据插件的参数,在插件中作出不同的反应。

如下代码片段,则是定义了一个插件,并且提供了一些参数:

@UIScope
@SpringComponent
public class RuntimeWorkStationView extends AbstractCustomizedComponent implements ClickListener {

    private static final long serialVersionUID = 2448127387625413009L;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.SOPShow", dataType = DataType.Boolean)
    private boolean isMpiShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.DataCollectionShow", dataType = DataType.Boolean)
    private boolean isDatacollectionShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.UploadPhotoShow", dataType = DataType.Boolean)
    private boolean isUploadPhotoShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.QTAGShow", dataType = DataType.Boolean)
    private boolean isQtagShow;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PrintSequenceShow", dataType = DataType.List, provider = PrinterProvider.class)
    private String printerName;
}

正如你所看到的那样,参数提供非常简单,只需要一个注解即可 “@ParameterDef”,具体内容如下。

package com.ags.lumosframework.stage.web.webbase.ui.view.external;

import java.lang.annotation.*;

import com.ags.lumosframework.sdk.base.entity.DataType;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParameterDef {

    /**
     * 国际化的参数的名称信息
     *
     * @return
     */
    String nameKey();

    /**
     * 该参数的数据类型
     *
     * @return
     */
    DataType dataType();

    /**
     * 在数据类型为List的情况下,系统会自动注入一个Combo Box,列出选项供用户选择,该参数提供下拉列表里的数据集合。
     *
     * @return
     */
    String[] options() default {};

    /**
     * 在数据类型为List的情况下,如果下拉列表是一个动态的值,需要实时到数据库中查询的,如列出所有的工位信息,那么可以实现一个ParameterValueProvider传入
     *
     * @return
     */
    @SuppressWarnings("rawtypes")
    Class<? extends ParameterValueProvider> provider() default ParameterValueProvider.class;
}

系统还为集成系统中的对象提供了便利,例如,可以在定义插件参数的时候,将系统中的全部工位信息提供出来,供用户选择,那么这个就不能在定义插件的时候,预先定义。因为这些事动态变化的值。 用户只需要从以下接口继承就好。

package com.ags.lumosframework.stage.web.webbase.ui.view.external;

import java.util.List;
import java.util.function.Supplier;

@FunctionalInterface
public interface ParameterValueProvider<T> extends Supplier<List<T>> {

}

如下是一个简单实现,供参考:

public class WorkStationProvider implements ParameterValueProvider<String> {

    @Override
    public List<String> get() {
        IWorkStationService workStationService = BeanManager.getService(IWorkStationService.class);
        List<WorkStation> workstation = workStationService.list(0, Integer.MAX_VALUE);
        return workstation.stream().map(WorkStation::getName).collect(Collectors.toList());
    }
}

一旦用户定义了如上的参数,那么在插件配置界面,就可以看到如下参数配置信息:

stageParameters

2.5.3. 开发示例

@UIScope
@SpringComponent
public class PackingStationView extends AbstractCustomizedComponent implements ClickListener {

    private static final long serialVersionUID = -7578836084244167502L;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.IsPrintNeeded", dataType = DataType.Boolean)
    private boolean isPrintNeeded = false;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PackagePrinter", dataType = DataType.List, provider = PrinterProvider.class)
    private String packagePrinter;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PackageType", dataType = DataType.List, provider = PackageTypeProvider.class)
    private String packageType;

    @Inject
    private PackingStationViewPresenter presenter;

    @I18Support(caption = "Barcode:", captionKey = "WorkStation.Barcode")
    private TextField tfBarcode = new TextField();

    @I18Support(caption = "Scan the SN", captionKey = "WorkStation.InputTips")
    private InputLabel lblInputTips = new InputLabel();

    @I18Support(caption = "Seal", captionKey = "WorkStation.Package.Seal")
    private Button btnSeal = new Button();

    @I18Support(caption = "Complete", captionKey = "WorkStation.Complete")
    private Button btnComplete = new Button();

    @I18Support(caption = "Print", captionKey = "WorkStation.Print")
    private Button btnPrint = new Button();

    private Button[] topTools = { btnSeal, btnComplete, btnPrint };

    private HorizontalLayout hlTips = new HorizontalLayout();

    private TabSheet tabSheet = new TabSheet();

    private Tab packingTab;

    @Inject
    private IStationMPITab mpiTabContent;

    @Inject
    private IPackingScheduleTab scheduleTab;

    @Inject
    private IPackingTab packingTabContent;

    @Inject
    private IApproveConfirmDialog approveConfirmDialog;

    private void setElementsId() {
        tfBarcode.setId("tf_barcode");
        lblInputTips.setId("ilbl_inputtips");
        btnSeal.setId("btn_seal");
        btnComplete.setId("btn_complete");
        btnPrint.setId("btn_print");
        hlTips.setId("hl_tips");
    }

    public PackingStationView() {
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.setSpacing(false);
        vlRoot.setMargin(false);
        vlRoot.setSizeFull();

        HorizontalLayout hlToolBox = new HorizontalLayout();
        hlToolBox.setWidth("100%");
        hlToolBox.setSpacing(true);
        hlToolBox.setMargin(true);
        hlToolBox.addStyleName(CoreTheme.TOOLBOX);
        vlRoot.addComponent(hlToolBox);

        HorizontalLayout hlTempToolBox = new HorizontalLayout();
        hlToolBox.addComponent(hlTempToolBox);
        hlTempToolBox.addStyleName(CoreTheme.INPUT_DISPLAY_INLINE);
        hlTempToolBox.addComponent(tfBarcode);
        tfBarcode.addStyleName(CoreTheme.BACKGROUND_YELLOW);
        hlTempToolBox.addComponent(hlTips);
        hlTips.addStyleName(CoreTheme.STAGE_LAYOUT_TIPS + " " + CoreTheme.BACKGROUND_ORANGE);

        hlTips.addComponent(lblInputTips);
        for (Button btn : topTools) {
            hlTempToolBox.addComponent(btn);
            //btn.setWidth(120, Unit.PIXELS);
            btn.addClickListener(this);
            btn.setDisableOnClick(true);
        }

        btnSeal.setIcon(VaadinIcons.PACKAGE);
        btnPrint.setIcon(VaadinIcons.PRINT);
        btnComplete.setIcon(VaadinIcons.CHECK);

        // TabSheet
        tabSheet.setSizeFull();
        tabSheet.addStyleNames(ValoTheme.TABSHEET_FRAMED, CoreTheme.JASPER_TABSHEET);
        vlRoot.addComponent(tabSheet);
        vlRoot.setExpandRatio(tabSheet, 1);

        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
        setElementsId();
    }

    @Override
    public void init() {
        tabSheet.addSelectedTabChangeListener(new SelectedTabChangeListener() {
            private static final long serialVersionUID = -3265758415179285552L;

            @Override
            public void selectedTabChange(SelectedTabChangeEvent event) {
                if (event.getComponent() != null && event.getTabSheet().getSelectedTab() instanceof IPackingTab) {
                    tfBarcode.focus();
                }
            }
        });
        tabSheet.addTab((Component) mpiTabContent, I18NUtility.getValue("WorkStation.MPI", "MPI")).setId("tab_mpi");
        packingTab = tabSheet.addTab((Component) packingTabContent,
                I18NUtility.getValue("WorkStation.Packing", "Packing"));
        packingTab.setId("tab_packing");
        tabSheet.addTab((Component) scheduleTab, I18NUtility.getValue("WorkStation.Schedule", "Schedule"))
                .setId("tab_schedule");
        tabSheet.setSelectedTab((Component) scheduleTab);
        tfBarcode.addShortcutListener(new ShortcutListener(null, KeyCode.ENTER, null) {
            private static final long serialVersionUID = 1L;

            @Override
            public void handleAction(Object sender, Object target) {
                try {
                    String value = tfBarcode.getValue();
                    processSNInput(value);
                } catch (Exception e) {
                    handlingException(e);
                } finally {
                    tfBarcode.setValue("");
                }
            }

        });
        scheduleTab.addScheduleItemClickListener(new ScheduleItemClickListener() {

            @Override
            public void scheduleItemClick(WIP<?> wip) {
                try {
                    processSNInput(wip.getSerialNumber());
                } catch (Exception e) {
                    tfBarcode.setValue("");
                    handlingException(e);
                }
            }
        });
    }

    private void processSNInput(String serialNumber) {
        if (serialNumber != null && !serialNumber.equals("")) {
            if (btnComplete.isEnabled() && serialNumber.equals(presenter.getCompleteBarcode())) {
                clickCompleteBtn();
                return;
            }
            tabSheet.setSelectedTab(packingTab);
            packingTabContent.checkPackageDefSelect();
            if (packingTabContent.isSealed()) {

                throw new PlatformException(
                        I18NUtility.getValue("WorkStation.Packing.BoxSealed", "The box is sealed!"));
            }
            if (isStartedPackage(serialNumber)) {
                setEnable(btnPrint, true);
                presenter.setPacking(true);
                updateButtonStatus();
                return;
            }
            packingTabContent.addSubInstance(serialNumber);
            if (packingTabContent.isSealed()) {
                presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                        packingTabContent.getSubPackageInstance());
                updateButtonStatus();
            }
            presenter.setPacking(true);
            updateButtonStatus();
            NotificationUtils.notificationInfo(I18NUtility.getValue("Common.Success", "Success"));
        }
    }

    private boolean isStartedPackage(String value) {
        List<PackageInstance> startedPackage = scheduleTab.getStartedPackage();
        PackageInstance parentPackage = presenter.getParentPackage(value, startedPackage);
        if (Objects.isNull(parentPackage)) {
            return false;
        } else {
            packingTabContent.setPackageInstance(parentPackage);
            return true;
        }
    }

    private void updateButtonStatus() {
        btnSeal.setEnabled(presenter.isPacking() && !packingTabContent.isSealed());
        btnComplete.setEnabled(presenter.isPacking());
        setEnable(btnPrint, presenter.isPacking());
    }

    @Override
    public void buttonClick(ClickEvent event) {
        event.getButton().setEnabled(true);
        try {
            if (event.getButton().equals(btnSeal)) {
                packingTabContent.sealPackage();
                presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                        packingTabContent.getSubPackageInstance());
                updateButtonStatus();
            } else if (event.getButton().equals(btnComplete)) {
                packingTabContent.checkPackageSealed();
                PackageInstance packageInstance = packingTabContent.getPackageInstance();
                presenter.complete(packageInstance);
                presenter.setPacking(false);
                scheduleTab.refresh();
                clearAll();
            } else if (event.getButton().equals(btnPrint)) {
                approveConfirmDialog.setPrivalege(MesPrivilegeConstants.PACKING_STAGE_PRINT_ALLOWED);
                approveConfirmDialog.show(getUI(), new DialogCallBack() {

                    @Override
                    public void done(ConfirmResult result) {
                        if (ConfirmResult.Result.OK.equals(result.getResult())) {
                            packingTabContent.checkPackageSealed();
                            presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                                    packingTabContent.getSubPackageInstance());
                        }
                    }
                });
            }
            updateButtonStatus();
        } catch (Exception e) {
            handlingException(e);
        }
    }

    private void clickCompleteBtn() {
        btnComplete.click();
    }

    private void clearAll() {
        presenter.setPacking(false);
        mpiTabContent.refresh(null, null);
        scheduleTab.refresh();
        packingTabContent.clear();
        tfBarcode.setValue("");
    }

    @Override
    public String getDisplayValue() {
        return "WorkStation.Packingstation.Packing";
    }

    @Override
    public void enter() {
        try {
            if (!isPreview()) {
                packingTabContent.clear();
                clearAll();
                updateButtonStatus();
                scheduleTab.refresh();
            } else {
                btnSeal.setEnabled(false);
                btnPrint.setEnabled(false);
                btnComplete.setEnabled(false);
                tfBarcode.setEnabled(false);
            }
        } catch (Exception e) {
            handlingException(e);
        }
    }

    @Override
    public void activateParameters() {
        btnPrint.setVisible(isPrintNeeded);

        if (Strings.isNullOrEmpty(packageType)) {
            NotificationUtils.notificationError(
                    I18NUtility.getValue("WorkStation.Packing.PackageTypeNotSet", "Package type is not set!"));
            return;
        } else {
            packingTabContent.setPackageType(packageType);
            scheduleTab.setPackageType(packageType);
        }

        if (Strings.isNullOrEmpty(packagePrinter)) {
            NotificationUtils.notificationError(
                    I18NUtility.getValue("WorkStation.Packing.PrinterNotSet", "Package printer is not set!"));
        }
        packingTabContent.setPreview(isPreview());

    }

    @Override
    public BasePresenter<?> getPresenter() {
        return presenter;
    }

2.6. 看板插件

看板以插件的显示展示,展示在BoardUI中。

平台提供了两种类型的插件,一种为“操作中心”一种为“看板”,不同类型的插件在不同页面展示。

看板操作步骤:

  • 新建一个插件,类别选择“Board”即看板,选择应用类型(平台提供了一个测试的插件应用,仅供测试使用,无实际用途)。

  • 为插件绑定工作站,工作站需要绑定IP,然后用此IP对应的设备访问看板页面。

  • 如果没有绑定IP,也可以通过浏览器传参数的方式,如:http://localhost:8080/Jasper/Board#!/kanban,其中kanban即工作站的名称。

2.6.1. 看板插件样式

LumosChartPanel为展示Chart的容器,平台对此做了样式优化,另外,Chart样式的优化,只需调用ChartThemeUtils工具类中的optimizeStyle方法即可。

BarChartConfig config = new BarChartConfig();
ChartThemeUtils.optimizeStyle(config);
ChartJs chart = new ChartJs(config);